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

180 lines
5.1 KiB
Go

package settlement
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- Mock HSM provider ---
type mockHSM struct {
signResult []byte
signErr error
verifyOK bool
}
func (m *mockHSM) Sign(_ context.Context, _ string, _ []byte) ([]byte, error) {
if m.signErr != nil {
return nil, m.signErr
}
return m.signResult, nil
}
func (m *mockHSM) Verify(_ context.Context, _ string, _, _ []byte) (bool, error) {
return m.verifyOK, nil
}
func makeMatchedIntent(t *testing.T) *Intent {
t.Helper()
i := NewIntent("intent-1", "org-1", "wallet-1", IntentBuy, "lux", "0xabc", "100", "LUX")
require.NoError(t, i.SetSignature("sig", "user"))
require.NoError(t, i.SetCoSignature("cosig", "key", "hsm"))
require.NoError(t, i.RecordOnChain("0xtx", 100))
require.NoError(t, i.SetMatched("match-1"))
return i
}
func TestSettlementLifecycle(t *testing.T) {
settler := NewSettler(2) // require 2 HSM sigs
intent := makeMatchedIntent(t)
// Create settlement
s, err := settler.CreateSettlement("settle-1", "org-1", intent, "match-1")
require.NoError(t, err)
assert.Equal(t, SettlementPending, s.Status)
assert.Equal(t, "intent-1", s.IntentID)
assert.NotNil(t, s.MatchedAt)
// First HSM signature
hsm := &mockHSM{signResult: []byte("sig1"), verifyOK: true}
err = settler.AddHSMSignature(context.Background(), s, "signer-1", "key-1", hsm, []byte("msg"))
require.NoError(t, err)
assert.Equal(t, SettlementHSMSigning, s.Status)
assert.Len(t, s.HSMSignatures, 1)
// Duplicate signer rejected
err = settler.AddHSMSignature(context.Background(), s, "signer-1", "key-1", hsm, []byte("msg"))
assert.Error(t, err)
// Second HSM signature → transitions to broadcast
hsm2 := &mockHSM{signResult: []byte("sig2"), verifyOK: true}
err = settler.AddHSMSignature(context.Background(), s, "signer-2", "key-2", hsm2, []byte("msg"))
require.NoError(t, err)
assert.Equal(t, SettlementBroadcast, s.Status)
assert.Len(t, s.HSMSignatures, 2)
assert.NotNil(t, s.SignedAt)
// Broadcast
err = settler.MarkBroadcast(s, "0xsettle_tx")
require.NoError(t, err)
assert.Equal(t, SettlementConfirming, s.Status)
assert.Equal(t, "0xsettle_tx", s.SettlementTxHash)
assert.NotNil(t, s.BroadcastAt)
// Finalize
err = settler.MarkFinalized(s, "0xfinalize_tx", 200)
require.NoError(t, err)
assert.Equal(t, SettlementFinalized, s.Status)
assert.Equal(t, int64(200), *s.FinalizedBlockNumber)
assert.NotNil(t, s.FinalizedAt)
// Verify with transfer agency
err = settler.MarkVerified(s, "hash-abc-123")
require.NoError(t, err)
assert.Equal(t, SettlementVerified, s.Status)
assert.True(t, s.TransferAgencyVerified)
assert.NotNil(t, s.VerifiedAt)
// Full history recorded
assert.True(t, len(s.History) >= 5) // pending → hsm_signing → broadcast → confirming → finalized → verified
}
func TestSettlementFromUnmatchedIntent(t *testing.T) {
settler := NewSettler(1)
intent := NewIntent("i1", "o1", "w1", IntentBuy, "eth", "0x", "1", "ETH")
_, err := settler.CreateSettlement("s1", "o1", intent, "m1")
assert.Error(t, err, "should reject intent that isn't matched")
}
func TestSettlementSingleHSMSig(t *testing.T) {
settler := NewSettler(1) // only 1 sig needed
intent := makeMatchedIntent(t)
s, err := settler.CreateSettlement("s1", "org-1", intent, "match-1")
require.NoError(t, err)
hsm := &mockHSM{signResult: []byte("sig"), verifyOK: true}
err = settler.AddHSMSignature(context.Background(), s, "signer-1", "key-1", hsm, []byte("msg"))
require.NoError(t, err)
// With only 1 required, should go straight to broadcast status
assert.Equal(t, SettlementBroadcast, s.Status)
}
func TestSettlementFailure(t *testing.T) {
settler := NewSettler(1)
intent := makeMatchedIntent(t)
s, _ := settler.CreateSettlement("s1", "org-1", intent, "match-1")
settler.MarkFailed(s, "network timeout")
assert.Equal(t, SettlementFailed, s.Status)
}
func TestTransferAgencyVerification(t *testing.T) {
ta := NewTransferAgency()
record := &TransferRecord{
TxHash: "0xabc",
IntentHash: "intent-hash",
SettlementHash: "settle-hash",
Chain: "lux",
FromAddress: "0x111",
ToAddress: "0x222",
Amount: "1000",
Token: "LUX",
}
// Register
hash, err := ta.Register(record)
require.NoError(t, err)
assert.NotEmpty(t, hash)
// Verify with matching record
result := ta.Verify("0xabc", record)
assert.True(t, result.Verified)
assert.Empty(t, result.Discrepancies)
// Verify with tampered record
tampered := *record
tampered.Amount = "9999"
result = ta.Verify("0xabc", &tampered)
assert.False(t, result.Verified)
assert.NotEmpty(t, result.Discrepancies)
// Verify unknown tx
result = ta.Verify("0xunknown", record)
assert.False(t, result.Verified)
}
func TestTransferHashDeterministic(t *testing.T) {
r := &TransferRecord{
TxHash: "0xabc",
IntentHash: "ihash",
SettlementHash: "shash",
Chain: "ethereum",
FromAddress: "0x1",
ToAddress: "0x2",
Amount: "100",
Token: "ETH",
}
h1 := ComputeTransferHash(r)
h2 := ComputeTransferHash(r)
assert.Equal(t, h1, h2)
assert.Len(t, h1, 64) // hex-encoded SHA-256
}