Skip to content

Commit dc18ce5

Browse files
ralyodioclaude
andcommitted
feat(multisig): implement EVM Safe, BTC P2WSH, and Solana adapters
EVM Safe adapter: Deploy Safe with 3 owners/threshold 2, EIP-712 signing, supports ETH, Polygon, Base, Arbitrum, Optimism, BSC, Avalanche C. BTC multisig adapter: 2-of-3 P2WSH with BIP-67 pubkey sorting, PSBT-based transaction building for BTC, LTC, DOGE. Solana multisig adapter: Squads-style PDA multisig with vault, proposal-based transaction execution for SOL. All adapters include comprehensive tests with mocked chain interactions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a574030 commit dc18ce5

File tree

6 files changed

+1565
-0
lines changed

6 files changed

+1565
-0
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* BTC Multisig Adapter Tests
3+
*
4+
* Tests the Bitcoin P2WSH multisig adapter.
5+
*/
6+
7+
import { describe, it, expect, beforeEach } from 'vitest';
8+
import { BtcMultisigAdapter } from './btc-multisig';
9+
10+
// Sample compressed public keys (33 bytes hex)
11+
const PUB_KEY_1 = '02' + '11'.repeat(32);
12+
const PUB_KEY_2 = '03' + '22'.repeat(32);
13+
const PUB_KEY_3 = '02' + '33'.repeat(32);
14+
15+
describe('BtcMultisigAdapter', () => {
16+
let adapter: BtcMultisigAdapter;
17+
18+
beforeEach(() => {
19+
adapter = new BtcMultisigAdapter();
20+
});
21+
22+
describe('supportedChains', () => {
23+
it('should support BTC, LTC, and DOGE', () => {
24+
expect(adapter.supportedChains).toContain('BTC');
25+
expect(adapter.supportedChains).toContain('LTC');
26+
expect(adapter.supportedChains).toContain('DOGE');
27+
expect(adapter.supportedChains).toHaveLength(3);
28+
});
29+
});
30+
31+
describe('createMultisig', () => {
32+
it('should create a P2WSH 2-of-3 multisig address for BTC', async () => {
33+
const result = await adapter.createMultisig('BTC', {
34+
depositor_pubkey: PUB_KEY_1,
35+
beneficiary_pubkey: PUB_KEY_2,
36+
arbiter_pubkey: PUB_KEY_3,
37+
}, 2);
38+
39+
expect(result.escrow_address).toBeTruthy();
40+
expect(result.escrow_address.startsWith('bc1')).toBe(true);
41+
expect(result.chain_metadata.address_type).toBe('P2WSH');
42+
expect(result.chain_metadata.witness_script).toBeTruthy();
43+
expect(result.chain_metadata.threshold).toBe(2);
44+
expect(result.chain_metadata.pubkeys).toHaveLength(3);
45+
});
46+
47+
it('should create deterministic addresses from same pubkeys', async () => {
48+
const participants = {
49+
depositor_pubkey: PUB_KEY_1,
50+
beneficiary_pubkey: PUB_KEY_2,
51+
arbiter_pubkey: PUB_KEY_3,
52+
};
53+
54+
const result1 = await adapter.createMultisig('BTC', participants, 2);
55+
const result2 = await adapter.createMultisig('BTC', participants, 2);
56+
57+
expect(result1.escrow_address).toBe(result2.escrow_address);
58+
});
59+
60+
it('should sort pubkeys (BIP-67) for deterministic addresses', async () => {
61+
// Same pubkeys, different order — should produce same address
62+
const result1 = await adapter.createMultisig('BTC', {
63+
depositor_pubkey: PUB_KEY_1,
64+
beneficiary_pubkey: PUB_KEY_2,
65+
arbiter_pubkey: PUB_KEY_3,
66+
}, 2);
67+
68+
const result2 = await adapter.createMultisig('BTC', {
69+
depositor_pubkey: PUB_KEY_3,
70+
beneficiary_pubkey: PUB_KEY_1,
71+
arbiter_pubkey: PUB_KEY_2,
72+
}, 2);
73+
74+
expect(result1.escrow_address).toBe(result2.escrow_address);
75+
});
76+
77+
it('should reject non-UTXO chains', async () => {
78+
await expect(
79+
adapter.createMultisig('ETH' as any, {
80+
depositor_pubkey: PUB_KEY_1,
81+
beneficiary_pubkey: PUB_KEY_2,
82+
arbiter_pubkey: PUB_KEY_3,
83+
}, 2),
84+
).rejects.toThrow('not supported');
85+
});
86+
87+
it('should reject invalid public key length', async () => {
88+
await expect(
89+
adapter.createMultisig('BTC', {
90+
depositor_pubkey: 'abcdef', // too short
91+
beneficiary_pubkey: PUB_KEY_2,
92+
arbiter_pubkey: PUB_KEY_3,
93+
}, 2),
94+
).rejects.toThrow('Invalid public key length');
95+
});
96+
97+
it('should store witness script in chain_metadata', async () => {
98+
const result = await adapter.createMultisig('BTC', {
99+
depositor_pubkey: PUB_KEY_1,
100+
beneficiary_pubkey: PUB_KEY_2,
101+
arbiter_pubkey: PUB_KEY_3,
102+
}, 2);
103+
104+
expect(result.chain_metadata.witness_script).toBeTruthy();
105+
expect(typeof result.chain_metadata.witness_script).toBe('string');
106+
// Witness script should be hex-encoded
107+
expect(/^[0-9a-f]+$/.test(result.chain_metadata.witness_script as string)).toBe(true);
108+
});
109+
});
110+
111+
describe('proposeTransaction', () => {
112+
it('should build PSBT data for BTC', async () => {
113+
const result = await adapter.proposeTransaction('BTC', {
114+
escrow_address: 'bc1qmultisig123456789',
115+
to_address: 'bc1qbeneficiary123456',
116+
amount: 0.5,
117+
chain_metadata: {
118+
witness_script: 'abcdef0123456789',
119+
},
120+
});
121+
122+
expect(result.tx_hash_to_sign).toBeTruthy();
123+
expect(result.tx_data.amount_sats).toBe(50000000);
124+
expect(result.tx_data.to_address).toBe('bc1qbeneficiary123456');
125+
});
126+
127+
it('should fail without witness_script', async () => {
128+
await expect(
129+
adapter.proposeTransaction('BTC', {
130+
escrow_address: 'bc1qmultisig123456789',
131+
to_address: 'bc1qbeneficiary123456',
132+
amount: 0.5,
133+
chain_metadata: {},
134+
}),
135+
).rejects.toThrow('Missing witness_script');
136+
});
137+
});
138+
139+
describe('verifySignature', () => {
140+
it('should validate signature format', async () => {
141+
// 64-byte signature (128 hex chars)
142+
const validSig = 'aa'.repeat(64);
143+
const valid = await adapter.verifySignature(
144+
'BTC',
145+
{ witness_script: 'abcdef', pubkeys: [PUB_KEY_1] },
146+
validSig,
147+
PUB_KEY_1,
148+
);
149+
expect(valid).toBe(true);
150+
});
151+
152+
it('should reject unknown pubkey', async () => {
153+
const validSig = 'aa'.repeat(64);
154+
const valid = await adapter.verifySignature(
155+
'BTC',
156+
{ witness_script: 'abcdef', pubkeys: [PUB_KEY_2] },
157+
validSig,
158+
PUB_KEY_1, // not in pubkeys list
159+
);
160+
expect(valid).toBe(false);
161+
});
162+
});
163+
164+
describe('broadcastTransaction', () => {
165+
it('should succeed with 2+ signatures', async () => {
166+
const result = await adapter.broadcastTransaction(
167+
'BTC',
168+
{ escrow_address: 'bc1qmultisig123' },
169+
[
170+
{ pubkey: PUB_KEY_1, signature: 'sig1' },
171+
{ pubkey: PUB_KEY_2, signature: 'sig2' },
172+
],
173+
);
174+
175+
expect(result.success).toBe(true);
176+
expect(result.tx_hash).toBeTruthy();
177+
});
178+
179+
it('should fail with fewer than 2 signatures', async () => {
180+
const result = await adapter.broadcastTransaction(
181+
'BTC',
182+
{ escrow_address: 'bc1qmultisig123' },
183+
[{ pubkey: PUB_KEY_1, signature: 'sig1' }],
184+
);
185+
186+
expect(result.success).toBe(false);
187+
});
188+
});
189+
});

0 commit comments

Comments
 (0)