Skip to content

Commit a574030

Browse files
ralyodioclaude
andcommitted
feat(multisig): add domain types, adapter interface, and validation schemas
Define MultisigChain, MultisigEscrow, MultisigProposal types. Create ChainAdapter interface with createMultisig, proposeTransaction, verifySignature, and broadcastTransaction methods. Add Zod validation for all multisig operations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5e99b92 commit a574030

File tree

5 files changed

+614
-0
lines changed

5 files changed

+614
-0
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Chain Adapter Interface Tests
3+
*
4+
* Tests the adapter type resolution utility.
5+
*/
6+
7+
import { describe, it, expect } from 'vitest';
8+
import { getAdapterType } from './interface';
9+
10+
describe('getAdapterType', () => {
11+
it('should return "evm" for EVM chains', () => {
12+
expect(getAdapterType('ETH')).toBe('evm');
13+
expect(getAdapterType('POL')).toBe('evm');
14+
expect(getAdapterType('BASE')).toBe('evm');
15+
expect(getAdapterType('ARB')).toBe('evm');
16+
expect(getAdapterType('OP')).toBe('evm');
17+
expect(getAdapterType('BNB')).toBe('evm');
18+
expect(getAdapterType('AVAX')).toBe('evm');
19+
});
20+
21+
it('should return "utxo" for UTXO chains', () => {
22+
expect(getAdapterType('BTC')).toBe('utxo');
23+
expect(getAdapterType('LTC')).toBe('utxo');
24+
expect(getAdapterType('DOGE')).toBe('utxo');
25+
});
26+
27+
it('should return "solana" for SOL', () => {
28+
expect(getAdapterType('SOL')).toBe('solana');
29+
});
30+
31+
it('should throw for unsupported chains', () => {
32+
expect(() => getAdapterType('XRP' as any)).toThrow('Unsupported multisig chain');
33+
expect(() => getAdapterType('ADA' as any)).toThrow('Unsupported multisig chain');
34+
});
35+
});
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* Chain Adapter Interface
3+
*
4+
* Abstract interface for all multisig chain adapters.
5+
* Each adapter implements chain-specific multisig operations:
6+
* - EVM: Safe (Gnosis Safe) protocol
7+
* - BTC/UTXO: P2WSH 2-of-3 multisig with PSBT
8+
* - Solana: Squads-style multisig PDA
9+
*/
10+
11+
import type {
12+
MultisigChain,
13+
MultisigParticipants,
14+
CreateMultisigResult,
15+
ProposeTransactionInput,
16+
ProposeTransactionResult,
17+
BroadcastResult,
18+
} from '../types';
19+
20+
export interface ChainAdapter {
21+
/** Chain identifier(s) this adapter supports */
22+
readonly supportedChains: readonly MultisigChain[];
23+
24+
/**
25+
* Create a new 2-of-3 multisig wallet/account on-chain.
26+
* Returns the escrow address and any chain-specific metadata.
27+
*/
28+
createMultisig(
29+
chain: MultisigChain,
30+
participants: MultisigParticipants,
31+
threshold: number,
32+
): Promise<CreateMultisigResult>;
33+
34+
/**
35+
* Build a transaction proposal for the multisig.
36+
* Returns transaction data that signers need to sign.
37+
*/
38+
proposeTransaction(
39+
chain: MultisigChain,
40+
input: ProposeTransactionInput,
41+
): Promise<ProposeTransactionResult>;
42+
43+
/**
44+
* Verify that a signature is valid for the given transaction data.
45+
* Returns true if the signature is valid and from an authorized signer.
46+
*/
47+
verifySignature(
48+
chain: MultisigChain,
49+
txData: Record<string, unknown>,
50+
signature: string,
51+
signerPubkey: string,
52+
): Promise<boolean>;
53+
54+
/**
55+
* Combine signatures and broadcast the transaction on-chain.
56+
* Requires at least `threshold` valid signatures.
57+
*/
58+
broadcastTransaction(
59+
chain: MultisigChain,
60+
txData: Record<string, unknown>,
61+
signatures: Array<{ pubkey: string; signature: string }>,
62+
): Promise<BroadcastResult>;
63+
}
64+
65+
/**
66+
* Determine adapter type from chain identifier
67+
*/
68+
export function getAdapterType(chain: MultisigChain): 'evm' | 'utxo' | 'solana' {
69+
const evmChains: MultisigChain[] = ['ETH', 'POL', 'BASE', 'ARB', 'OP', 'BNB', 'AVAX'];
70+
const utxoChains: MultisigChain[] = ['BTC', 'LTC', 'DOGE'];
71+
72+
if (evmChains.includes(chain)) return 'evm';
73+
if (utxoChains.includes(chain)) return 'utxo';
74+
if (chain === 'SOL') return 'solana';
75+
76+
throw new Error(`Unsupported multisig chain: ${chain}`);
77+
}

src/lib/multisig/types.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/**
2+
* Multisig Escrow Types
3+
*
4+
* 2-of-3 multisig escrow: Depositor, Beneficiary, CoinPay (Arbiter).
5+
* CoinPay can never move funds alone — always requires 2 of 3 signers.
6+
*/
7+
8+
// ── Chain Types ─────────────────────────────────────────────
9+
10+
/** EVM chains supported by Safe adapter */
11+
export type EvmChain = 'ETH' | 'POL' | 'BASE' | 'ARB' | 'OP' | 'BNB' | 'AVAX';
12+
13+
/** UTXO chains supported by BTC multisig adapter */
14+
export type UtxoChain = 'BTC' | 'LTC' | 'DOGE';
15+
16+
/** Solana chain */
17+
export type SolanaChain = 'SOL';
18+
19+
/** All chains supported by multisig */
20+
export type MultisigChain = EvmChain | UtxoChain | SolanaChain;
21+
22+
// ── Escrow Model ────────────────────────────────────────────
23+
24+
export type EscrowModel = 'custodial' | 'multisig_2of3';
25+
26+
export type MultisigEscrowStatus =
27+
| 'pending'
28+
| 'funded'
29+
| 'released'
30+
| 'settled'
31+
| 'disputed'
32+
| 'refunded'
33+
| 'expired';
34+
35+
export type DisputeStatus =
36+
| 'open'
37+
| 'under_review'
38+
| 'resolved_release'
39+
| 'resolved_refund';
40+
41+
export type ProposalType = 'release' | 'refund';
42+
43+
export type ProposalStatus = 'pending' | 'approved' | 'executed' | 'cancelled';
44+
45+
export type SignerRole = 'depositor' | 'beneficiary' | 'arbiter';
46+
47+
// ── Core Interfaces ─────────────────────────────────────────
48+
49+
export interface MultisigParticipants {
50+
depositor_pubkey: string;
51+
beneficiary_pubkey: string;
52+
arbiter_pubkey: string;
53+
}
54+
55+
export interface CreateMultisigEscrowInput {
56+
chain: MultisigChain;
57+
amount: number;
58+
depositor_pubkey: string;
59+
beneficiary_pubkey: string;
60+
arbiter_pubkey: string;
61+
metadata?: Record<string, unknown>;
62+
business_id?: string;
63+
expires_in_hours?: number;
64+
}
65+
66+
export interface MultisigEscrow {
67+
id: string;
68+
escrow_model: 'multisig_2of3';
69+
chain: MultisigChain;
70+
threshold: 2;
71+
depositor_pubkey: string;
72+
beneficiary_pubkey: string;
73+
arbiter_pubkey: string;
74+
escrow_address: string;
75+
chain_metadata: Record<string, unknown>;
76+
amount: number;
77+
amount_usd: number | null;
78+
status: MultisigEscrowStatus;
79+
dispute_status: DisputeStatus | null;
80+
dispute_reason: string | null;
81+
metadata: Record<string, unknown>;
82+
business_id: string | null;
83+
funded_at: string | null;
84+
settled_at: string | null;
85+
created_at: string;
86+
expires_at: string;
87+
}
88+
89+
export interface MultisigProposal {
90+
id: string;
91+
escrow_id: string;
92+
proposal_type: ProposalType;
93+
to_address: string;
94+
amount: number;
95+
chain_tx_data: Record<string, unknown>;
96+
status: ProposalStatus;
97+
created_by: string;
98+
created_at: string;
99+
executed_at: string | null;
100+
tx_hash: string | null;
101+
}
102+
103+
export interface MultisigSignature {
104+
id: string;
105+
proposal_id: string;
106+
signer_role: SignerRole;
107+
signer_pubkey: string;
108+
signature: string;
109+
signed_at: string;
110+
}
111+
112+
// ── Adapter Interfaces ──────────────────────────────────────
113+
114+
/** Result of creating a multisig wallet on-chain */
115+
export interface CreateMultisigResult {
116+
escrow_address: string;
117+
chain_metadata: Record<string, unknown>;
118+
}
119+
120+
/** Data needed to propose a transaction */
121+
export interface ProposeTransactionInput {
122+
escrow_address: string;
123+
to_address: string;
124+
amount: number;
125+
chain_metadata: Record<string, unknown>;
126+
}
127+
128+
/** Result of proposing a transaction */
129+
export interface ProposeTransactionResult {
130+
tx_data: Record<string, unknown>;
131+
tx_hash_to_sign: string;
132+
}
133+
134+
/** Result of adding a signature */
135+
export interface AddSignatureResult {
136+
signatures_collected: number;
137+
threshold_met: boolean;
138+
}
139+
140+
/** Result of broadcasting */
141+
export interface BroadcastResult {
142+
tx_hash: string;
143+
success: boolean;
144+
}
145+
146+
// ── API Response Types ──────────────────────────────────────
147+
148+
export interface CreateMultisigEscrowResult {
149+
success: boolean;
150+
escrow?: MultisigEscrow;
151+
error?: string;
152+
}
153+
154+
export interface ProposeResult {
155+
success: boolean;
156+
proposal?: MultisigProposal;
157+
tx_data?: Record<string, unknown>;
158+
error?: string;
159+
}
160+
161+
export interface SignResult {
162+
success: boolean;
163+
signature?: MultisigSignature;
164+
signatures_collected?: number;
165+
threshold_met?: boolean;
166+
error?: string;
167+
}
168+
169+
export interface BroadcastResultResponse {
170+
success: boolean;
171+
tx_hash?: string;
172+
proposal?: MultisigProposal;
173+
error?: string;
174+
}
175+
176+
export interface DisputeResult {
177+
success: boolean;
178+
escrow?: MultisigEscrow;
179+
error?: string;
180+
}

0 commit comments

Comments
 (0)