13. Transaction — 17 Primary Status State Machine
Stage 9 정식 명세 · color-coded · 시간 제약 · chain-specific quirk
17 Primary Status (color-coded)
| Color | Stage | Status (API code) |
| 🟡 Yellow | 입력/검증 | SUBMITTED / PENDING_AML_SCREENING / PENDING_ENRICHMENT / PENDING_AUTHORIZATION / QUEUED |
| 🟡 Yellow | 서명 | PENDING_SIGNATURE / SIGNED (Solana-only) / CANCELLING |
| 🔵 Blue | 외부 처리 (3rd party) | PENDING_3RD_PARTY_MANUAL_APPROVAL / PENDING_3RD_PARTY |
| 🔵 Blue | blockchain | BROADCASTING / CONFIRMING |
| 🟢 Green | 종결 (성공) | COMPLETED |
| 🔴 Red | 종결 (실패) | CANCELLED / BLOCKED / REJECTED / FAILED |
Outgoing Transaction Flow
stateDiagram-v2
[*] --> SUBMITTED
SUBMITTED --> PENDING_AML_SCREENING : AML/Travel Rule enabled
PENDING_AML_SCREENING --> PENDING_ENRICHMENT
SUBMITTED --> PENDING_ENRICHMENT : AML disabled
PENDING_ENRICHMENT --> PENDING_AUTHORIZATION : dApp Protection done
PENDING_AUTHORIZATION --> QUEUED : approvals collected
PENDING_AUTHORIZATION --> FAILED : 2h timeout
QUEUED --> PENDING_SIGNATURE
PENDING_SIGNATURE --> SIGNED : Solana sign-only
PENDING_SIGNATURE --> BROADCASTING
PENDING_SIGNATURE --> CANCELLED : co-signer reject
PENDING_SIGNATURE --> FAILED : 2h timeout
BROADCASTING --> CONFIRMING : ~1 min typical
CONFIRMING --> COMPLETED
CONFIRMING --> FAILED
CANCELLING --> CANCELLED
CANCELLING --> COMPLETED : already broadcast
Figure 4. Outgoing transaction state machine — 17 status. AML/Travel Rule optional, 2h timeout on Pending Authorization + Pending Signature.
Incoming Transaction Flow
stateDiagram-v2
[*] --> START
START --> PENDING_AML_SCREENING : AML/KYC enabled
START --> BROADCASTING : network conn / exchange / gas station
START --> CONFIRMING : AML/KYC disabled
PENDING_AML_SCREENING --> REJECTED : reject
PENDING_AML_SCREENING --> PENDING_3RD_PARTY
PENDING_3RD_PARTY --> CONFIRMING
CONFIRMING --> COMPLETED
CONFIRMING --> FAILED
CONFIRMING --> REJECTED : user-initiated freeze via API
Figure 5. Incoming transaction state machine — rejected incoming 의 asset 은 Admin unfreeze 까지 사용 불가.
시간 제약
| 제약 | 적용 상태 | 동작 |
| 2 hours | Pending Authorization | timeout → fail |
| 2 hours | Pending Signature | timeout → fail |
| 30 seconds | Cancelling (typical) | 길어지면 Status page 확인 |
| 1 minute | Broadcasting (typical) | 길어지면 Status page 확인 |
| 2 hours | Solana 6번째 이상 tx Submitted | 만료 → terminated |
Chain-Specific 처리 모델
- EVM-compatible: 동일 vault account 의 EVM tx 들은 blockchain-standard 단위 직렬화 (Ethereum + Polygon → 순차, BTC + Solana → 병렬)
- Solana: vault account 당 동시 5 tx queue (6번째 이상은 Submitted max 2h 대기)
- EVM withdrawal: nonce 충돌 위험 → multiple withdrawal vault 권장 round-robin
- Bitcoin withdrawal: unconfirmed input 25-tx chain limit (Bitcoin Core default) → multiple withdrawal vault
Special Branches
- Approvers Unanimous-Veto Rule: "If at least one person chooses to reject, the transaction is rejected." Approvers N명 → reject 1표 = tx 즉시 rejected
- Outgoing rejected: 자산 즉시 사용 가능 (re-tx 가능)
- Incoming rejected: 자산이 Admin 가 unfreeze 할 때까지 사용 불가
- Raw Signing (cached signature): 동일 raw data 재서명 시 Submitted → Completed (전체 ceremony 우회)
- Solana Sign-Only: signed payload client 에 반환, Fireblocks 가 broadcast 안 함,
NOT BROADCAST BY FIREBLOCKS 태그
DB Schema — Transaction State
CREATE TABLE transactions (
id BINARY(16) PRIMARY KEY,
workspace_id BINARY(16) NOT NULL,
source_vault_id BINARY(16),
destination_type ENUM('vault', 'exchange', 'whitelisted', 'one-time', 'network'),
asset_id VARCHAR(32),
amount_decimal DECIMAL(38, 18),
chain VARCHAR(32),
status ENUM('SUBMITTED', 'PENDING_AML_SCREENING', ...) NOT NULL,
status_changed_at DATETIME(6) NOT NULL,
created_at DATETIME(6) NOT NULL,
raw_payload_cbor BLOB, -- ★ append-only (TEE-signed)
tx_hash VARCHAR(128), -- ★ set-once (broadcast 시점)
pending_auth_at DATETIME(6), -- 2h timeout 산정
pending_sign_at DATETIME(6),
KEY (workspace_id, status)
);
-- 상태 전이는 별도 테이블 (append-only)
CREATE TABLE transaction_events (
id BINARY(16) PRIMARY KEY,
transaction_id BINARY(16) NOT NULL,
from_status VARCHAR(32),
to_status VARCHAR(32) NOT NULL,
reason VARCHAR(255),
occurred_at DATETIME(6) NOT NULL,
KEY (transaction_id, occurred_at)
);