Files
zeekay 6f83e39ab0 feat: tx lifecycle tracking, Shamir backup sharding, settlement engine
Transaction Lifecycle ("did the tx land?"):
- TxTracker polls eth_getTransactionReceipt for block inclusion
- Receipt fields: blockNumber, blockHash, receiptStatus, gasUsed, revertReason
- Confirmation counter with configurable target (default 12)
- StatusHistory records every state transition with timestamp and actor
- Stuck/dropped tx detection with webhook alerts

Backup Key Sharding (Secure Recovery Flow):
- GF(2^8) Shamir Secret Sharing (ShamirSplit/ShamirCombine)
- WalletBackup: AES-256-GCM encrypt key share, Shamir-split the key
- Labeled shard destinations: icloud, hsm, offline, custody, device
- Default 2-of-2 (iCloud + Platform HSM), institutional 3-of-5

Settlement & Intent Engine:
- Intent lifecycle: pending_sign → signed → co_signed → recorded → matched
- HSM co-signing as second signer in multisig flow
- Settlement with HSM multisig attestation collection
- TransferAgency hash registration and verification
- WalletBackup, Intent, Settlement DB models

API Routes:
- POST/GET /intents, /intents/:id/sign, /intents/:id/co-sign
- GET /settlements, /settlements/:id
- POST/GET /wallets/:id/backup
- New webhooks: tx.included, tx.finalized, tx.reverted, intent.*

40 tests passing across backup, settlement, txtracker, api packages.
2025-12-27 18:09:37 -08:00

123 lines
3.7 KiB
Go

package settlement
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIntentHashDeterministic(t *testing.T) {
i1 := NewIntent("id1", "org1", "wallet1", IntentBuy, "ethereum", "0xabc", "1000", "USDC")
i2 := NewIntent("id2", "org1", "wallet1", IntentBuy, "ethereum", "0xabc", "1000", "USDC")
// Same fields → same hash, regardless of ID or creation time
assert.Equal(t, i1.IntentHash, i2.IntentHash)
assert.NotEmpty(t, i1.IntentHash)
}
func TestIntentHashChangesWithFields(t *testing.T) {
i1 := NewIntent("id1", "org1", "wallet1", IntentBuy, "ethereum", "0xabc", "1000", "USDC")
i2 := NewIntent("id2", "org1", "wallet1", IntentBuy, "ethereum", "0xabc", "2000", "USDC")
// Different amount → different hash
assert.NotEqual(t, i1.IntentHash, i2.IntentHash)
}
func TestIntentVerify(t *testing.T) {
i := NewIntent("id1", "org1", "wallet1", IntentSell, "lux", "0xdef", "500", "LUX")
require.NoError(t, i.Verify())
// Tamper with a field
i.Amount = "999"
assert.Error(t, i.Verify())
}
func TestIntentSignAndCoSign(t *testing.T) {
i := NewIntent("id1", "org1", "wallet1", IntentTransfer, "ethereum", "0xabc", "100", "ETH")
// Can't co-sign before signing
err := i.SetCoSignature("cosig", "key1", "hsm")
assert.Error(t, err)
// Sign
err = i.SetSignature("sig123", "user1")
require.NoError(t, err)
assert.Equal(t, IntentSigned, i.Status)
assert.Equal(t, "sig123", i.Signature)
require.Len(t, i.History, 1)
// Can't sign twice
err = i.SetSignature("sig456", "user1")
assert.Error(t, err)
// Co-sign
err = i.SetCoSignature("cosig456", "hsm-key-1", "hsm")
require.NoError(t, err)
assert.Equal(t, IntentCoSigned, i.Status)
assert.Equal(t, "cosig456", i.CoSignature)
assert.Equal(t, "hsm-key-1", i.CoSignerKeyID)
require.Len(t, i.History, 2)
}
func TestIntentRecordOnChain(t *testing.T) {
i := NewIntent("id1", "org1", "wallet1", IntentBuy, "lux", "0xabc", "50", "ZOO")
// Can't record without co-signing
err := i.RecordOnChain("0xtx1", 100)
assert.Error(t, err)
i.SetSignature("sig", "user")
i.SetCoSignature("cosig", "key", "hsm")
err = i.RecordOnChain("0xtx1", 100)
require.NoError(t, err)
assert.Equal(t, IntentRecorded, i.Status)
assert.Equal(t, "0xtx1", i.OnChainTxHash)
assert.NotNil(t, i.RecordedAt)
assert.Equal(t, int64(100), *i.RecordedBlock)
}
func TestIntentMatching(t *testing.T) {
i := NewIntent("id1", "org1", "wallet1", IntentSell, "ethereum", "0xabc", "1000", "USDC")
i.SetSignature("sig", "user")
i.SetCoSignature("cosig", "key", "hsm")
i.RecordOnChain("0xtx1", 50)
err := i.SetMatched("match-001")
require.NoError(t, err)
assert.Equal(t, IntentMatched, i.Status)
assert.Equal(t, "match-001", i.MatchID)
assert.NotNil(t, i.MatchedAt)
}
func TestIntentFullLifecycle(t *testing.T) {
i := NewIntent("id1", "org1", "wallet1", IntentBuy, "lux", "0xabc", "100", "LUX")
i.SetSignature("sig", "user-1")
i.SetCoSignature("cosig", "hsm-key-1", "hsm-node-0")
i.RecordOnChain("0xabc123", 1000)
i.SetMatched("match-42")
assert.Equal(t, IntentMatched, i.Status)
require.Len(t, i.History, 4)
// Verify full transition chain
assert.Equal(t, "pending_sign", i.History[0].From)
assert.Equal(t, "signed", i.History[0].To)
assert.Equal(t, "signed", i.History[1].From)
assert.Equal(t, "co_signed", i.History[1].To)
assert.Equal(t, "co_signed", i.History[2].From)
assert.Equal(t, "recorded", i.History[2].To)
assert.Equal(t, "recorded", i.History[3].From)
assert.Equal(t, "matched", i.History[3].To)
}
func TestIntentExpiry(t *testing.T) {
i := NewIntent("id1", "org1", "wallet1", IntentBuy, "ethereum", "0xabc", "100", "ETH")
assert.False(t, i.IsExpired()) // just created, 24h expiry
i.ExpiresAt = nil
assert.False(t, i.IsExpired()) // no expiry
}