|
1 | | -import 'should'; |
2 | 1 | import * as assert from 'assert'; |
3 | 2 |
|
4 | | -import * as utxolib from '@bitgo/utxo-lib'; |
5 | | -const { chainCodes } = utxolib.bitgo; |
| 3 | +import { InvalidAddressDerivationPropertyError, UnexpectedAddressError } from '@bitgo/sdk-core'; |
6 | 4 |
|
7 | | -import { AbstractUtxoCoin, GenerateFixedScriptAddressOptions, generateAddress } from '../../src'; |
| 5 | +import { assertFixedScriptWalletAddress, generateAddress } from '../../src'; |
8 | 6 |
|
9 | | -import { utxoCoins, keychains as keychainsBip32, getFixture, shouldEqualJSON } from './util'; |
| 7 | +import { keychainsBase58 } from './util'; |
10 | 8 |
|
11 | | -// TODO (@rushilbg): Delete these tests because they are redundant (similar tests are in utxo-lib) |
12 | | -function isCompatibleAddress(a: AbstractUtxoCoin, b: AbstractUtxoCoin): boolean { |
13 | | - if (a === b) { |
14 | | - return true; |
15 | | - } |
16 | | - switch (a.getChain()) { |
17 | | - case 'btc': |
18 | | - case 'bsv': |
19 | | - case 'bch': |
20 | | - case 'bcha': |
21 | | - return ['btc', 'bsv', 'bch', 'bcha'].includes(b.getChain()); |
22 | | - case 'tbtc': |
23 | | - case 'tbtcsig': |
24 | | - case 'tbtc4': |
25 | | - case 'tbtcbgsig': |
26 | | - case 'tbsv': |
27 | | - case 'tbch': |
28 | | - case 'tdoge': |
29 | | - case 'tbcha': |
30 | | - return ['tbtc', 'tbtcsig', 'tbtc4', 'tbtcbgsig', 'tbsv', 'tbch', 'tbcha', 'tdoge'].includes(b.getChain()); |
31 | | - default: |
32 | | - return false; |
33 | | - } |
34 | | -} |
| 9 | +const keychains = keychainsBase58.map((k) => ({ pub: k.pub })); |
35 | 10 |
|
36 | | -function run(coin: AbstractUtxoCoin) { |
37 | | - const keychains = keychainsBip32.map((k) => ({ pub: k.neutered().toBase58() })); |
38 | | - |
39 | | - function getParameters(): GenerateFixedScriptAddressOptions[] { |
40 | | - return [undefined, ...chainCodes].map((chain) => ({ keychains, chain })); |
41 | | - } |
42 | | - |
43 | | - describe(`UTXO Addresses ${coin.getChain()}`, function () { |
44 | | - it('address support', function () { |
45 | | - const supportedAddressTypes = utxolib.bitgo.outputScripts.scriptTypes2Of3.filter((t) => |
46 | | - coin.supportsAddressType(t) |
| 11 | +describe('assertFixedScriptWalletAddress', function () { |
| 12 | + describe('input validation', function () { |
| 13 | + it('throws InvalidAddressDerivationPropertyError when both chain and index are undefined', function () { |
| 14 | + assert.throws( |
| 15 | + () => |
| 16 | + assertFixedScriptWalletAddress('btc', { |
| 17 | + chain: undefined, |
| 18 | + index: undefined as unknown as number, |
| 19 | + keychains, |
| 20 | + format: 'base58', |
| 21 | + address: 'anything', |
| 22 | + }), |
| 23 | + InvalidAddressDerivationPropertyError |
47 | 24 | ); |
48 | | - switch (coin.getChain()) { |
49 | | - case 'btc': |
50 | | - case 'tbtc': |
51 | | - case 'tbtcsig': |
52 | | - case 'tbtc4': |
53 | | - case 'tbtcbgsig': |
54 | | - supportedAddressTypes.should.eql(['p2sh', 'p2shP2wsh', 'p2wsh', 'p2tr', 'p2trMusig2']); |
55 | | - break; |
56 | | - case 'btg': |
57 | | - case 'tbtg': |
58 | | - case 'ltc': |
59 | | - case 'tltc': |
60 | | - supportedAddressTypes.should.eql(['p2sh', 'p2shP2wsh', 'p2wsh']); |
61 | | - break; |
62 | | - case 'bch': |
63 | | - case 'tbch': |
64 | | - case 'bcha': |
65 | | - case 'tbcha': |
66 | | - case 'bsv': |
67 | | - case 'tbsv': |
68 | | - case 'dash': |
69 | | - case 'tdash': |
70 | | - case 'doge': |
71 | | - case 'tdoge': |
72 | | - case 'zec': |
73 | | - case 'tzec': |
74 | | - supportedAddressTypes.should.eql(['p2sh']); |
75 | | - break; |
76 | | - default: |
77 | | - throw new Error(`unexpected coin ${coin.getChain()}`); |
78 | | - } |
79 | 25 | }); |
80 | 26 |
|
81 | | - it('generates address matching the fixtures', async function () { |
82 | | - const addresses = getParameters().map((p) => { |
83 | | - const label = { chain: p.chain === undefined ? 'default' : p.chain }; |
84 | | - try { |
85 | | - return [label, generateAddress(coin.name, p)]; |
86 | | - } catch (e) { |
87 | | - return [label, { error: e.message }]; |
88 | | - } |
89 | | - }); |
90 | | - |
91 | | - shouldEqualJSON(addresses, await getFixture(coin, 'addresses-by-chain', addresses)); |
| 27 | + it('throws InvalidAddressDerivationPropertyError when chain is non-finite', function () { |
| 28 | + assert.throws( |
| 29 | + () => |
| 30 | + assertFixedScriptWalletAddress('btc', { |
| 31 | + chain: Infinity, |
| 32 | + index: 0, |
| 33 | + keychains, |
| 34 | + format: 'base58', |
| 35 | + address: 'anything', |
| 36 | + }), |
| 37 | + InvalidAddressDerivationPropertyError |
| 38 | + ); |
92 | 39 | }); |
93 | 40 |
|
94 | | - it('validates and verifies generated addresses', function () { |
95 | | - getParameters().forEach((p) => { |
96 | | - if (p.chain && !coin.supportsAddressChain(p.chain)) { |
97 | | - assert.throws(() => generateAddress(coin.name, p)); |
98 | | - return; |
99 | | - } |
100 | | - |
101 | | - const address = generateAddress(coin.name, p); |
102 | | - coin.isValidAddress(address).should.eql(true); |
103 | | - if (address !== address.toUpperCase()) { |
104 | | - coin.isValidAddress(address.toUpperCase()).should.eql(false); |
105 | | - } |
106 | | - coin.verifyAddress({ address, keychains }); |
107 | | - }); |
| 41 | + it('throws InvalidAddressDerivationPropertyError when index is non-finite', function () { |
| 42 | + assert.throws( |
| 43 | + () => |
| 44 | + assertFixedScriptWalletAddress('btc', { |
| 45 | + chain: 0, |
| 46 | + index: NaN, |
| 47 | + keychains, |
| 48 | + format: 'base58', |
| 49 | + address: 'anything', |
| 50 | + }), |
| 51 | + InvalidAddressDerivationPropertyError |
| 52 | + ); |
108 | 53 | }); |
109 | 54 |
|
110 | | - it('defaults to canonical address', function () { |
111 | | - getParameters().forEach((p) => { |
112 | | - if (!p.chain || coin.supportsAddressChain(p.chain)) { |
113 | | - const address = generateAddress(coin.name, p); |
114 | | - coin.canonicalAddress(address).should.eql(address); |
115 | | - } |
116 | | - }); |
| 55 | + it('throws when keychains is missing', function () { |
| 56 | + assert.throws( |
| 57 | + () => |
| 58 | + assertFixedScriptWalletAddress('btc', { |
| 59 | + chain: 0, |
| 60 | + index: 0, |
| 61 | + keychains: undefined as unknown as { pub: string }[], |
| 62 | + format: 'base58', |
| 63 | + address: 'anything', |
| 64 | + }), |
| 65 | + /missing required param keychains/ |
| 66 | + ); |
117 | 67 | }); |
| 68 | + }); |
118 | 69 |
|
119 | | - it('respects format parameter', function () { |
120 | | - // Only test coins that actually support multiple address formats (BCH/BCHA) |
121 | | - // These are the only coins where the format parameter matters |
122 | | - const cashaddrPrefixes: Record<string, string> = { |
123 | | - bch: 'bitcoincash:', |
124 | | - tbch: 'bchtest:', |
125 | | - bcha: 'ecash:', |
126 | | - tbcha: 'ectest:', |
127 | | - }; |
| 70 | + describe('address matching', function () { |
| 71 | + it('throws UnexpectedAddressError when address does not match derived address', function () { |
| 72 | + assert.throws( |
| 73 | + () => |
| 74 | + assertFixedScriptWalletAddress('btc', { |
| 75 | + chain: 0, |
| 76 | + index: 0, |
| 77 | + keychains, |
| 78 | + format: 'base58', |
| 79 | + address: '3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', |
| 80 | + }), |
| 81 | + UnexpectedAddressError |
| 82 | + ); |
| 83 | + }); |
128 | 84 |
|
129 | | - const expectedPrefix = cashaddrPrefixes[coin.getChain()]; |
130 | | - if (!expectedPrefix) { |
131 | | - this.skip(); |
132 | | - } |
| 85 | + it('succeeds for btc p2sh (chain 0)', function () { |
| 86 | + const address = generateAddress('btc', { keychains, chain: 0 }); |
| 87 | + assert.doesNotThrow(() => |
| 88 | + assertFixedScriptWalletAddress('btc', { |
| 89 | + chain: 0, |
| 90 | + index: 0, |
| 91 | + keychains, |
| 92 | + format: 'base58', |
| 93 | + address, |
| 94 | + }) |
| 95 | + ); |
| 96 | + }); |
133 | 97 |
|
134 | | - const chain = chainCodes[0]; |
135 | | - const params = { keychains, chain }; |
| 98 | + it('succeeds for btc p2wsh (chain 20)', function () { |
| 99 | + const address = generateAddress('btc', { keychains, chain: 20 }); |
| 100 | + assert.doesNotThrow(() => |
| 101 | + assertFixedScriptWalletAddress('btc', { |
| 102 | + chain: 20, |
| 103 | + index: 0, |
| 104 | + keychains, |
| 105 | + format: 'base58', |
| 106 | + address, |
| 107 | + }) |
| 108 | + ); |
| 109 | + }); |
136 | 110 |
|
137 | | - // Generate with cashaddr format |
138 | | - const addressCashaddr = generateAddress(coin.name, { ...params, format: 'cashaddr' }); |
139 | | - coin.isValidAddress(addressCashaddr).should.eql(true); |
140 | | - addressCashaddr.should.startWith(expectedPrefix, `cashaddr should start with ${expectedPrefix}`); |
| 111 | + it('succeeds for btc p2tr (chain 40)', function () { |
| 112 | + const address = generateAddress('btc', { keychains, chain: 40 }); |
| 113 | + assert.doesNotThrow(() => |
| 114 | + assertFixedScriptWalletAddress('btc', { |
| 115 | + chain: 40, |
| 116 | + index: 0, |
| 117 | + keychains, |
| 118 | + format: 'base58', |
| 119 | + address, |
| 120 | + }) |
| 121 | + ); |
| 122 | + }); |
141 | 123 |
|
142 | | - // Generate with base58 format explicitly |
143 | | - const addressBase58 = generateAddress(coin.name, { ...params, format: 'base58' }); |
144 | | - coin.isValidAddress(addressBase58).should.eql(true); |
145 | | - addressBase58.should.not.match(/.*:.*/, 'base58 should not contain colon separator'); |
| 124 | + it('succeeds for bch cashaddr (chain 0)', function () { |
| 125 | + const address = generateAddress('bch', { keychains, chain: 0, format: 'cashaddr' }); |
| 126 | + assert.doesNotThrow(() => |
| 127 | + assertFixedScriptWalletAddress('bch', { |
| 128 | + chain: 0, |
| 129 | + index: 0, |
| 130 | + keychains, |
| 131 | + format: 'cashaddr', |
| 132 | + address, |
| 133 | + }) |
| 134 | + ); |
| 135 | + }); |
146 | 136 |
|
147 | | - // Verify formats produce different strings |
148 | | - addressCashaddr.should.not.equal(addressBase58, 'cashaddr and base58 should produce different address strings'); |
| 137 | + it('succeeds for a non-zero index', function () { |
| 138 | + const address = generateAddress('btc', { keychains, chain: 0, index: 5 }); |
| 139 | + assert.doesNotThrow(() => |
| 140 | + assertFixedScriptWalletAddress('btc', { |
| 141 | + chain: 0, |
| 142 | + index: 5, |
| 143 | + keychains, |
| 144 | + format: 'base58', |
| 145 | + address, |
| 146 | + }) |
| 147 | + ); |
149 | 148 | }); |
150 | 149 |
|
151 | | - utxoCoins.forEach((otherCoin) => { |
152 | | - it(`has expected address compatability with ${otherCoin.getChain()}`, async function () { |
153 | | - getParameters().forEach((p) => { |
154 | | - if (p.chain && (!coin.supportsAddressChain(p.chain) || !otherCoin.supportsAddressChain(p.chain))) { |
155 | | - return; |
156 | | - } |
157 | | - const address = generateAddress(coin.name, p); |
158 | | - const otherAddress = generateAddress(otherCoin.name, p); |
159 | | - (address === otherAddress).should.eql(isCompatibleAddress(coin, otherCoin)); |
160 | | - coin.isValidAddress(otherAddress).should.eql(isCompatibleAddress(coin, otherCoin)); |
161 | | - }); |
162 | | - }); |
| 150 | + it('throws UnexpectedAddressError when index does not match', function () { |
| 151 | + const address = generateAddress('btc', { keychains, chain: 0, index: 0 }); |
| 152 | + assert.throws( |
| 153 | + () => |
| 154 | + assertFixedScriptWalletAddress('btc', { |
| 155 | + chain: 0, |
| 156 | + index: 1, |
| 157 | + keychains, |
| 158 | + format: 'base58', |
| 159 | + address, |
| 160 | + }), |
| 161 | + UnexpectedAddressError |
| 162 | + ); |
163 | 163 | }); |
164 | 164 | }); |
165 | | -} |
166 | | - |
167 | | -utxoCoins.forEach((c) => run(c)); |
| 165 | +}); |
0 commit comments