Skip to content

Commit 909c667

Browse files
Merge pull request #7366 from BitGo/BTC-2652.remove-generateaddress
feat(abstract-utxo): refactor address generation and test structure
2 parents 60cdd6a + 93f72ea commit 909c667

28 files changed

+460
-1381
lines changed

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 7 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { bip32 } from '@bitgo/secp256k1';
77
import { bitgo, getMainnet, isMainnet, isTestnet } from '@bitgo/utxo-lib';
88
import {
99
AddressCoinSpecific,
10-
AddressTypeChainMismatchError,
1110
BaseCoin,
1211
BitGoBase,
1312
CreateAddressFormat,
@@ -25,24 +24,17 @@ import {
2524
MismatchedRecipient,
2625
MultisigType,
2726
multisigTypes,
28-
P2shP2wshUnsupportedError,
29-
P2trMusig2UnsupportedError,
30-
P2trUnsupportedError,
31-
P2wshUnsupportedError,
3227
ParseTransactionOptions as BaseParseTransactionOptions,
3328
PrecreateBitGoOptions,
3429
PresignTransactionOptions,
3530
RequestTracer,
36-
sanitizeLegacyPath,
3731
SignedTransaction,
3832
SignTransactionOptions as BaseSignTransactionOptions,
3933
SupplementGenerateWalletOptions,
4034
TransactionParams as BaseTransactionParams,
4135
TransactionPrebuild as BaseTransactionPrebuild,
4236
Triple,
4337
TxIntentMismatchRecipientError,
44-
UnexpectedAddressError,
45-
UnsupportedAddressTypeError,
4638
VerificationOptions,
4739
VerifyAddressOptions as BaseVerifyAddressOptions,
4840
VerifyTransactionOptions as BaseVerifyTransactionOptions,
@@ -81,6 +73,7 @@ import {
8173
} from './transaction/descriptor/verifyTransaction';
8274
import { assertDescriptorWalletAddress, getDescriptorMapFromWallet, isDescriptorWallet } from './descriptor';
8375
import { getChainFromNetwork, getFamilyFromNetwork, getFullNameFromNetwork } from './names';
76+
import { assertFixedScriptWalletAddress } from './address/fixedScript';
8477
import { CustomChangeOptions } from './transaction/fixedScript';
8578
import { toBip32Triple, UtxoKeychain, UtxoNamedKeychains } from './keychains';
8679
import { verifyKeySignature, verifyUserPublicKey } from './verifyKey';
@@ -116,7 +109,7 @@ type UtxoCustomSigningFunction<TNumber extends number | bigint> = {
116109
}): Promise<SignedTransaction>;
117110
};
118111

119-
const { getExternalChainCode, isChainCode, scriptTypeForChain, outputScripts } = bitgo;
112+
const { isChainCode, scriptTypeForChain, outputScripts } = bitgo;
120113

121114
type Unspent<TNumber extends number | bigint = number> = bitgo.Unspent<TNumber>;
122115

@@ -707,7 +700,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
707700
* @throws {UnexpectedAddressError}
708701
*/
709702
async isWalletAddress(params: VerifyAddressOptions<UtxoCoinSpecific>, wallet?: IWallet): Promise<boolean> {
710-
const { address, addressType, keychains, chain, index } = params;
703+
const { address, keychains, chain, index } = params;
711704

712705
if (!this.isValidAddress(address)) {
713706
throw new InvalidAddressError(`invalid address: ${address}`);
@@ -738,21 +731,15 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
738731
throw new Error('missing required param keychains');
739732
}
740733

741-
const expectedAddress = this.generateAddress({
742-
format: params.format,
743-
addressType: addressType as ScriptType2Of3,
734+
assertFixedScriptWalletAddress(this.network, {
735+
address,
744736
keychains,
745-
threshold: 2,
737+
format: params.format ?? 'base58',
738+
addressType: params.addressType,
746739
chain,
747740
index,
748741
});
749742

750-
if (expectedAddress.address !== address) {
751-
throw new UnexpectedAddressError(
752-
`address validation failure: expected ${expectedAddress.address} but got ${address}`
753-
);
754-
}
755-
756743
return true;
757744
}
758745

@@ -781,103 +768,6 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
781768
return [KeyIndices.USER, KeyIndices.BACKUP, KeyIndices.BITGO];
782769
}
783770

784-
/**
785-
* TODO(BG-11487): Remove addressType, segwit, and bech32 params in SDKv6
786-
* Generate an address for a wallet based on a set of configurations
787-
* @param params.addressType {string} Deprecated
788-
* @param params.keychains {[object]} Array of objects with xpubs
789-
* @param params.threshold {number} Minimum number of signatures
790-
* @param params.chain {number} Derivation chain (see https://github.com/BitGo/unspents/blob/master/src/codes.ts for
791-
* the corresponding address type of a given chain code)
792-
* @param params.index {number} Derivation index
793-
* @param params.segwit {boolean} Deprecated
794-
* @param params.bech32 {boolean} Deprecated
795-
* @returns {{chain: number, index: number, coin: number, coinSpecific: {outputScript, redeemScript}}}
796-
*/
797-
generateAddress(params: GenerateFixedScriptAddressOptions): AddressDetails {
798-
let derivationIndex = 0;
799-
if (_.isInteger(params.index) && (params.index as number) > 0) {
800-
derivationIndex = params.index as number;
801-
}
802-
803-
const { keychains, threshold, chain, segwit = false, bech32 = false } = params as GenerateFixedScriptAddressOptions;
804-
805-
let derivationChain = getExternalChainCode('p2sh');
806-
if (_.isNumber(chain) && _.isInteger(chain) && isChainCode(chain)) {
807-
derivationChain = chain;
808-
}
809-
810-
function convertFlagsToAddressType(): ScriptType2Of3 {
811-
if (isChainCode(chain)) {
812-
return utxolib.bitgo.scriptTypeForChain(chain);
813-
}
814-
if (_.isBoolean(segwit) && segwit) {
815-
return 'p2shP2wsh';
816-
} else if (_.isBoolean(bech32) && bech32) {
817-
return 'p2wsh';
818-
} else {
819-
return 'p2sh';
820-
}
821-
}
822-
823-
const addressType = params.addressType || convertFlagsToAddressType();
824-
825-
if (addressType !== utxolib.bitgo.scriptTypeForChain(derivationChain)) {
826-
throw new AddressTypeChainMismatchError(addressType, derivationChain);
827-
}
828-
829-
if (!this.supportsAddressType(addressType)) {
830-
switch (addressType) {
831-
case 'p2sh':
832-
throw new Error(`internal error: p2sh should always be supported`);
833-
case 'p2shP2wsh':
834-
throw new P2shP2wshUnsupportedError();
835-
case 'p2wsh':
836-
throw new P2wshUnsupportedError();
837-
case 'p2tr':
838-
throw new P2trUnsupportedError();
839-
case 'p2trMusig2':
840-
throw new P2trMusig2UnsupportedError();
841-
default:
842-
throw new UnsupportedAddressTypeError();
843-
}
844-
}
845-
846-
let signatureThreshold = 2;
847-
if (_.isInteger(threshold)) {
848-
signatureThreshold = threshold as number;
849-
if (signatureThreshold <= 0) {
850-
throw new Error('threshold has to be positive');
851-
}
852-
if (signatureThreshold > keychains.length) {
853-
throw new Error('threshold cannot exceed number of keys');
854-
}
855-
}
856-
857-
const path = '0/0/' + derivationChain + '/' + derivationIndex;
858-
const hdNodes = keychains.map(({ pub }) => bip32.fromBase58(pub));
859-
const derivedKeys = hdNodes.map((hdNode) => hdNode.derivePath(sanitizeLegacyPath(path)).publicKey);
860-
861-
const { outputScript, redeemScript, witnessScript, address } = this.createMultiSigAddress(
862-
addressType,
863-
signatureThreshold,
864-
derivedKeys
865-
);
866-
867-
return {
868-
address: this.canonicalAddress(address, params.format),
869-
chain: derivationChain,
870-
index: derivationIndex,
871-
coin: this.getChain(),
872-
coinSpecific: {
873-
outputScript: outputScript.toString('hex'),
874-
redeemScript: redeemScript && redeemScript.toString('hex'),
875-
witnessScript: witnessScript && witnessScript.toString('hex'),
876-
},
877-
addressType,
878-
};
879-
}
880-
881771
/**
882772
* @returns input psbt added with deterministic MuSig2 nonce for bitgo key for each MuSig2 inputs.
883773
* @param psbtHex all MuSig2 inputs should contain user MuSig2 nonce
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import _ from 'lodash';
2+
import {
3+
AddressTypeChainMismatchError,
4+
CreateAddressFormat,
5+
InvalidAddressDerivationPropertyError,
6+
UnexpectedAddressError,
7+
P2shP2wshUnsupportedError,
8+
P2trMusig2UnsupportedError,
9+
P2trUnsupportedError,
10+
P2wshUnsupportedError,
11+
UnsupportedAddressTypeError,
12+
sanitizeLegacyPath,
13+
} from '@bitgo/sdk-core';
14+
import * as utxolib from '@bitgo/utxo-lib';
15+
import { bitgo } from '@bitgo/utxo-lib';
16+
import { bip32 } from '@bitgo/secp256k1';
17+
18+
type ScriptType2Of3 = bitgo.outputScripts.ScriptType2Of3;
19+
20+
export interface FixedScriptAddressCoinSpecific {
21+
outputScript?: string;
22+
redeemScript?: string;
23+
witnessScript?: string;
24+
}
25+
26+
export interface GenerateAddressOptions {
27+
addressType?: ScriptType2Of3;
28+
chain?: number;
29+
index?: number;
30+
segwit?: boolean;
31+
bech32?: boolean;
32+
}
33+
34+
interface GenerateFixedScriptAddressOptions extends GenerateAddressOptions {
35+
format?: CreateAddressFormat;
36+
keychains: { pub: string }[];
37+
}
38+
39+
function canonicalAddress(network: utxolib.Network, address: string, format?: CreateAddressFormat): string {
40+
if (format === 'cashaddr') {
41+
const script = utxolib.addressFormat.toOutputScriptTryFormats(address, network);
42+
return utxolib.addressFormat.fromOutputScriptWithFormat(script, format, network);
43+
}
44+
// Default to canonical format (base58 for most coins)
45+
return utxolib.addressFormat.toCanonicalFormat(address, network);
46+
}
47+
48+
function supportsAddressType(network: utxolib.Network, addressType: ScriptType2Of3): boolean {
49+
return utxolib.bitgo.outputScripts.isSupportedScriptType(network, addressType);
50+
}
51+
52+
export function generateAddressWithChainAndIndex(
53+
network: utxolib.Network,
54+
keychains: { pub: string }[],
55+
chain: bitgo.ChainCode,
56+
index: number,
57+
format: CreateAddressFormat | undefined
58+
): string {
59+
const path = '0/0/' + chain + '/' + index;
60+
const hdNodes = keychains.map(({ pub }) => bip32.fromBase58(pub));
61+
const derivedKeys = hdNodes.map((hdNode) => hdNode.derivePath(sanitizeLegacyPath(path)).publicKey);
62+
const addressType = bitgo.scriptTypeForChain(chain);
63+
64+
const { scriptPubKey: outputScript } = utxolib.bitgo.outputScripts.createOutputScript2of3(derivedKeys, addressType);
65+
66+
const address = utxolib.address.fromOutputScript(outputScript, network);
67+
68+
return canonicalAddress(network, address, format);
69+
}
70+
71+
/**
72+
* Generate an address for a wallet based on a set of configurations
73+
* @param params.addressType {string} Deprecated
74+
* @param params.keychains {[object]} Array of objects with xpubs
75+
* @param params.threshold {number} Minimum number of signatures
76+
* @param params.chain {number} Derivation chain (see https://github.com/BitGo/unspents/blob/master/src/codes.ts for
77+
* the corresponding address type of a given chain code)
78+
* @param params.index {number} Derivation index
79+
* @param params.segwit {boolean} Deprecated
80+
* @param params.bech32 {boolean} Deprecated
81+
* @returns {string} The generated address
82+
*/
83+
export function generateAddress(network: utxolib.Network, params: GenerateFixedScriptAddressOptions): string {
84+
let derivationIndex = 0;
85+
if (_.isInteger(params.index) && (params.index as number) > 0) {
86+
derivationIndex = params.index as number;
87+
}
88+
89+
const { keychains, chain, segwit = false, bech32 = false } = params as GenerateFixedScriptAddressOptions;
90+
91+
let derivationChain = bitgo.getExternalChainCode('p2sh');
92+
if (_.isNumber(chain) && _.isInteger(chain) && bitgo.isChainCode(chain)) {
93+
derivationChain = chain;
94+
}
95+
96+
function convertFlagsToAddressType(): ScriptType2Of3 {
97+
if (bitgo.isChainCode(chain)) {
98+
return bitgo.scriptTypeForChain(chain);
99+
}
100+
if (_.isBoolean(segwit) && segwit) {
101+
return 'p2shP2wsh';
102+
} else if (_.isBoolean(bech32) && bech32) {
103+
return 'p2wsh';
104+
} else {
105+
return 'p2sh';
106+
}
107+
}
108+
109+
const addressType = params.addressType || convertFlagsToAddressType();
110+
111+
if (addressType !== utxolib.bitgo.scriptTypeForChain(derivationChain)) {
112+
throw new AddressTypeChainMismatchError(addressType, derivationChain);
113+
}
114+
115+
if (!supportsAddressType(network, addressType)) {
116+
switch (addressType) {
117+
case 'p2sh':
118+
throw new Error(`internal error: p2sh should always be supported`);
119+
case 'p2shP2wsh':
120+
throw new P2shP2wshUnsupportedError();
121+
case 'p2wsh':
122+
throw new P2wshUnsupportedError();
123+
case 'p2tr':
124+
throw new P2trUnsupportedError();
125+
case 'p2trMusig2':
126+
throw new P2trMusig2UnsupportedError();
127+
default:
128+
throw new UnsupportedAddressTypeError();
129+
}
130+
}
131+
132+
return generateAddressWithChainAndIndex(network, keychains, derivationChain, derivationIndex, params.format);
133+
}
134+
135+
type Keychain = {
136+
pub: string;
137+
};
138+
139+
export function assertFixedScriptWalletAddress(
140+
network: utxolib.Network,
141+
{
142+
chain,
143+
index,
144+
keychains,
145+
format,
146+
addressType,
147+
address,
148+
}: {
149+
chain: number | undefined;
150+
index: number;
151+
keychains: Keychain[];
152+
format: CreateAddressFormat;
153+
addressType: string | undefined;
154+
address: string;
155+
}
156+
): void {
157+
if ((_.isUndefined(chain) && _.isUndefined(index)) || !(_.isFinite(chain) && _.isFinite(index))) {
158+
throw new InvalidAddressDerivationPropertyError(
159+
`address validation failure: invalid chain (${chain}) or index (${index})`
160+
);
161+
}
162+
163+
if (!keychains) {
164+
throw new Error('missing required param keychains');
165+
}
166+
167+
const expectedAddress = generateAddress(network, {
168+
format,
169+
addressType: addressType as ScriptType2Of3,
170+
keychains,
171+
chain,
172+
index,
173+
});
174+
175+
if (expectedAddress !== address) {
176+
throw new UnexpectedAddressError(`address validation failure: expected ${expectedAddress} but got ${address}`);
177+
}
178+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export {
2+
generateAddress,
3+
generateAddressWithChainAndIndex,
4+
assertFixedScriptWalletAddress,
5+
FixedScriptAddressCoinSpecific,
6+
} from './fixedScript';

modules/abstract-utxo/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './abstractUtxoCoin';
2+
export * from './address';
23
export * from './config';
34
export * from './recovery';
45
export * from './replayProtection';

0 commit comments

Comments
 (0)