Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 141 additions & 143 deletions modules/abstract-utxo/test/unit/address.ts
Original file line number Diff line number Diff line change
@@ -1,167 +1,165 @@
import 'should';
import * as assert from 'assert';

import * as utxolib from '@bitgo/utxo-lib';
const { chainCodes } = utxolib.bitgo;
import { InvalidAddressDerivationPropertyError, UnexpectedAddressError } from '@bitgo/sdk-core';

import { AbstractUtxoCoin, GenerateFixedScriptAddressOptions, generateAddress } from '../../src';
import { assertFixedScriptWalletAddress, generateAddress } from '../../src';

import { utxoCoins, keychains as keychainsBip32, getFixture, shouldEqualJSON } from './util';
import { keychainsBase58 } from './util';

// TODO (@rushilbg): Delete these tests because they are redundant (similar tests are in utxo-lib)
function isCompatibleAddress(a: AbstractUtxoCoin, b: AbstractUtxoCoin): boolean {
if (a === b) {
return true;
}
switch (a.getChain()) {
case 'btc':
case 'bsv':
case 'bch':
case 'bcha':
return ['btc', 'bsv', 'bch', 'bcha'].includes(b.getChain());
case 'tbtc':
case 'tbtcsig':
case 'tbtc4':
case 'tbtcbgsig':
case 'tbsv':
case 'tbch':
case 'tdoge':
case 'tbcha':
return ['tbtc', 'tbtcsig', 'tbtc4', 'tbtcbgsig', 'tbsv', 'tbch', 'tbcha', 'tdoge'].includes(b.getChain());
default:
return false;
}
}
const keychains = keychainsBase58.map((k) => ({ pub: k.pub }));

function run(coin: AbstractUtxoCoin) {
const keychains = keychainsBip32.map((k) => ({ pub: k.neutered().toBase58() }));

function getParameters(): GenerateFixedScriptAddressOptions[] {
return [undefined, ...chainCodes].map((chain) => ({ keychains, chain }));
}

describe(`UTXO Addresses ${coin.getChain()}`, function () {
it('address support', function () {
const supportedAddressTypes = utxolib.bitgo.outputScripts.scriptTypes2Of3.filter((t) =>
coin.supportsAddressType(t)
describe('assertFixedScriptWalletAddress', function () {
describe('input validation', function () {
it('throws InvalidAddressDerivationPropertyError when both chain and index are undefined', function () {
assert.throws(
() =>
assertFixedScriptWalletAddress('btc', {
chain: undefined,
index: undefined as unknown as number,
keychains,
format: 'base58',
address: 'anything',
}),
InvalidAddressDerivationPropertyError
);
switch (coin.getChain()) {
case 'btc':
case 'tbtc':
case 'tbtcsig':
case 'tbtc4':
case 'tbtcbgsig':
supportedAddressTypes.should.eql(['p2sh', 'p2shP2wsh', 'p2wsh', 'p2tr', 'p2trMusig2']);
break;
case 'btg':
case 'tbtg':
case 'ltc':
case 'tltc':
supportedAddressTypes.should.eql(['p2sh', 'p2shP2wsh', 'p2wsh']);
break;
case 'bch':
case 'tbch':
case 'bcha':
case 'tbcha':
case 'bsv':
case 'tbsv':
case 'dash':
case 'tdash':
case 'doge':
case 'tdoge':
case 'zec':
case 'tzec':
supportedAddressTypes.should.eql(['p2sh']);
break;
default:
throw new Error(`unexpected coin ${coin.getChain()}`);
}
});

it('generates address matching the fixtures', async function () {
const addresses = getParameters().map((p) => {
const label = { chain: p.chain === undefined ? 'default' : p.chain };
try {
return [label, generateAddress(coin.name, p)];
} catch (e) {
return [label, { error: e.message }];
}
});

shouldEqualJSON(addresses, await getFixture(coin, 'addresses-by-chain', addresses));
it('throws InvalidAddressDerivationPropertyError when chain is non-finite', function () {
assert.throws(
() =>
assertFixedScriptWalletAddress('btc', {
chain: Infinity,
index: 0,
keychains,
format: 'base58',
address: 'anything',
}),
InvalidAddressDerivationPropertyError
);
});

it('validates and verifies generated addresses', function () {
getParameters().forEach((p) => {
if (p.chain && !coin.supportsAddressChain(p.chain)) {
assert.throws(() => generateAddress(coin.name, p));
return;
}

const address = generateAddress(coin.name, p);
coin.isValidAddress(address).should.eql(true);
if (address !== address.toUpperCase()) {
coin.isValidAddress(address.toUpperCase()).should.eql(false);
}
coin.verifyAddress({ address, keychains });
});
it('throws InvalidAddressDerivationPropertyError when index is non-finite', function () {
assert.throws(
() =>
assertFixedScriptWalletAddress('btc', {
chain: 0,
index: NaN,
keychains,
format: 'base58',
address: 'anything',
}),
InvalidAddressDerivationPropertyError
);
});

it('defaults to canonical address', function () {
getParameters().forEach((p) => {
if (!p.chain || coin.supportsAddressChain(p.chain)) {
const address = generateAddress(coin.name, p);
coin.canonicalAddress(address).should.eql(address);
}
});
it('throws when keychains is missing', function () {
assert.throws(
() =>
assertFixedScriptWalletAddress('btc', {
chain: 0,
index: 0,
keychains: undefined as unknown as { pub: string }[],
format: 'base58',
address: 'anything',
}),
/missing required param keychains/
);
});
});

it('respects format parameter', function () {
// Only test coins that actually support multiple address formats (BCH/BCHA)
// These are the only coins where the format parameter matters
const cashaddrPrefixes: Record<string, string> = {
bch: 'bitcoincash:',
tbch: 'bchtest:',
bcha: 'ecash:',
tbcha: 'ectest:',
};
describe('address matching', function () {
it('throws UnexpectedAddressError when address does not match derived address', function () {
assert.throws(
() =>
assertFixedScriptWalletAddress('btc', {
chain: 0,
index: 0,
keychains,
format: 'base58',
address: '3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
}),
UnexpectedAddressError
);
});

const expectedPrefix = cashaddrPrefixes[coin.getChain()];
if (!expectedPrefix) {
this.skip();
}
it('succeeds for btc p2sh (chain 0)', function () {
const address = generateAddress('btc', { keychains, chain: 0 });
assert.doesNotThrow(() =>
assertFixedScriptWalletAddress('btc', {
chain: 0,
index: 0,
keychains,
format: 'base58',
address,
})
);
});

const chain = chainCodes[0];
const params = { keychains, chain };
it('succeeds for btc p2wsh (chain 20)', function () {
const address = generateAddress('btc', { keychains, chain: 20 });
assert.doesNotThrow(() =>
assertFixedScriptWalletAddress('btc', {
chain: 20,
index: 0,
keychains,
format: 'base58',
address,
})
);
});

// Generate with cashaddr format
const addressCashaddr = generateAddress(coin.name, { ...params, format: 'cashaddr' });
coin.isValidAddress(addressCashaddr).should.eql(true);
addressCashaddr.should.startWith(expectedPrefix, `cashaddr should start with ${expectedPrefix}`);
it('succeeds for btc p2tr (chain 40)', function () {
const address = generateAddress('btc', { keychains, chain: 40 });
assert.doesNotThrow(() =>
assertFixedScriptWalletAddress('btc', {
chain: 40,
index: 0,
keychains,
format: 'base58',
address,
})
);
});

// Generate with base58 format explicitly
const addressBase58 = generateAddress(coin.name, { ...params, format: 'base58' });
coin.isValidAddress(addressBase58).should.eql(true);
addressBase58.should.not.match(/.*:.*/, 'base58 should not contain colon separator');
it('succeeds for bch cashaddr (chain 0)', function () {
const address = generateAddress('bch', { keychains, chain: 0, format: 'cashaddr' });
assert.doesNotThrow(() =>
assertFixedScriptWalletAddress('bch', {
chain: 0,
index: 0,
keychains,
format: 'cashaddr',
address,
})
);
});

// Verify formats produce different strings
addressCashaddr.should.not.equal(addressBase58, 'cashaddr and base58 should produce different address strings');
it('succeeds for a non-zero index', function () {
const address = generateAddress('btc', { keychains, chain: 0, index: 5 });
assert.doesNotThrow(() =>
assertFixedScriptWalletAddress('btc', {
chain: 0,
index: 5,
keychains,
format: 'base58',
address,
})
);
});

utxoCoins.forEach((otherCoin) => {
it(`has expected address compatability with ${otherCoin.getChain()}`, async function () {
getParameters().forEach((p) => {
if (p.chain && (!coin.supportsAddressChain(p.chain) || !otherCoin.supportsAddressChain(p.chain))) {
return;
}
const address = generateAddress(coin.name, p);
const otherAddress = generateAddress(otherCoin.name, p);
(address === otherAddress).should.eql(isCompatibleAddress(coin, otherCoin));
coin.isValidAddress(otherAddress).should.eql(isCompatibleAddress(coin, otherCoin));
});
});
it('throws UnexpectedAddressError when index does not match', function () {
const address = generateAddress('btc', { keychains, chain: 0, index: 0 });
assert.throws(
() =>
assertFixedScriptWalletAddress('btc', {
chain: 0,
index: 1,
keychains,
format: 'base58',
address,
}),
UnexpectedAddressError
);
});
});
}

utxoCoins.forEach((c) => run(c));
});

This file was deleted.

Loading