Skip to content
Draft
43 changes: 29 additions & 14 deletions modules/abstract-eth/src/abstractEthLikeNewCoins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ import {
VerifyAddressOptions as BaseVerifyAddressOptions,
VerifyTransactionOptions,
Wallet,
verifyMPCWalletAddress,
TssVerifyAddressOptions,
isTssVerifyAddressOptions,
} from '@bitgo/sdk-core';
import { getDerivationPath } from '@bitgo/sdk-lib-mpc';
import { bip32 } from '@bitgo/secp256k1';
Expand Down Expand Up @@ -402,8 +405,11 @@ export interface VerifyEthAddressOptions extends BaseVerifyAddressOptions {
baseAddress: string;
coinSpecific: EthAddressCoinSpecifics;
forwarderVersion: number;
walletVersion?: number;
}

export type TssVerifyEthAddressOptions = TssVerifyAddressOptions & VerifyEthAddressOptions;

const debug = debugLib('bitgo:v2:ethlike');

export const optionalDeps = {
Expand Down Expand Up @@ -2731,32 +2737,41 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
* @throws {UnexpectedAddressError}
* @returns {boolean} True iff address is a wallet address
*/
async isWalletAddress(params: VerifyEthAddressOptions): Promise<boolean> {
async isWalletAddress(params: VerifyEthAddressOptions | TssVerifyEthAddressOptions): Promise<boolean> {
const ethUtil = optionalDeps.ethUtil;

let expectedAddress;
let actualAddress;

const { address, coinSpecific, baseAddress, impliedForwarderVersion = coinSpecific?.forwarderVersion } = params;
const { address, impliedForwarderVersion } = params;

if (address && !this.isValidAddress(address)) {
throw new InvalidAddressError(`invalid address: ${address}`);
}

// base address is required to calculate the salt which is used in calculateForwarderV1Address method
if (_.isUndefined(baseAddress) || !this.isValidAddress(baseAddress)) {
throw new InvalidAddressError('invalid base address');
if (impliedForwarderVersion === 0) {
return true;
}
// Verify MPC wallet address for wallet version 3 and 6 (MPC receive address)
if (isTssVerifyAddressOptions(params) && params.walletVersion !== 5) {
return verifyMPCWalletAddress({ ...params, keyCurve: 'secp256k1' }, this.isValidAddress, (pubKey) => {
const derivedPublicKey = Buffer.from(pubKey, 'hex').subarray(0, 33).toString('hex');
return new KeyPairLib({ pub: derivedPublicKey }).getAddress();
});
} else {
// Verify forwarder receive address
const { coinSpecific, baseAddress } = params;

if (!_.isObject(coinSpecific)) {
throw new InvalidAddressVerificationObjectPropertyError(
'address validation failure: coinSpecific field must be an object'
);
}
// base address is required to calculate the salt which is used in calculateForwarderV1Address method
if (_.isUndefined(baseAddress) || !this.isValidAddress(baseAddress)) {
throw new InvalidAddressError('invalid base address');
}

if (!_.isObject(coinSpecific)) {
throw new InvalidAddressVerificationObjectPropertyError(
'address validation failure: coinSpecific field must be an object'
);
}

if (impliedForwarderVersion === 0 || impliedForwarderVersion === 3 || impliedForwarderVersion === 5) {
return true;
} else {
const ethNetwork = this.getNetwork();
const forwarderFactoryAddress = ethNetwork?.forwarderFactoryAddress as string;
const forwarderImplementationAddress = ethNetwork?.forwarderImplementationAddress as string;
Expand Down
20 changes: 11 additions & 9 deletions modules/abstract-utxo/src/abstractUtxoCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -706,17 +706,23 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
throw new InvalidAddressError(`invalid address: ${address}`);
}

if (!keychains || !keychains.every((kc) => !!kc.pub)) {
throw new Error('missing required param keychains or public key');
}

if (wallet && isDescriptorWallet(wallet)) {
if (!keychains) {
throw new Error('missing required param keychains');
}
if (!isTriple(keychains)) {
throw new Error('keychains must be a triple');
}

assertDescriptorWalletAddress(
this.network,
params,
getDescriptorMapFromWallet(wallet, toBip32Triple(keychains), getPolicyForEnv(this.bitgo.env))
getDescriptorMapFromWallet(
wallet,
toBip32Triple(keychains as Triple<{ pub: string }>),
getPolicyForEnv(this.bitgo.env)
)
);
return true;
}
Expand All @@ -727,13 +733,9 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
);
}

if (!keychains) {
throw new Error('missing required param keychains');
}

assertFixedScriptWalletAddress(this.network, {
address,
keychains,
keychains: keychains as { pub: string }[],
format: params.format ?? 'base58',
addressType: params.addressType,
chain,
Expand Down
7 changes: 4 additions & 3 deletions modules/sdk-coin-algo/src/algo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,17 +555,18 @@ export class Algo extends BaseCoin {
async isWalletAddress(params: VerifyAlgoAddressOptions): Promise<boolean> {
const {
address,
keychains,
keychains: unvalidatedKeychains,
coinSpecific: { bitgoPubKey },
} = params;

if (!this.isValidAddress(address)) {
throw new InvalidAddressError(`invalid address: ${address}`);
}

if (!keychains) {
throw new Error('missing required param keychains');
if (!unvalidatedKeychains || !unvalidatedKeychains.every((kc) => !!kc.pub)) {
throw new Error('missing required param keychains or public key');
}
const keychains = unvalidatedKeychains as { pub: string }[];

const effectiveKeychain = bitgoPubKey ? keychains.slice(0, -1).concat([{ pub: bitgoPubKey }]) : keychains;
const pubKeys = effectiveKeychain.map((key) => this.stellarAddressToAlgoAddress(key.pub));
Expand Down
7 changes: 4 additions & 3 deletions modules/sdk-coin-avaxp/src/avaxp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,15 @@ export class AvaxP extends BaseCoin {
* @param params.keychains public keys to generate the wallet
*/
async isWalletAddress(params: VerifyAddressOptions): Promise<boolean> {
const { address, keychains } = params;
const { address, keychains: unvalidatedKeychains } = params;

if (!this.isValidAddress(address)) {
throw new InvalidAddressError(`invalid address: ${address}`);
}
if (!keychains || keychains.length !== 3) {
throw new Error('Invalid keychains');
if (!unvalidatedKeychains || unvalidatedKeychains.length !== 3 || !unvalidatedKeychains.every((kc) => !!kc.pub)) {
throw new Error('missing required param keychains or public key');
}
const keychains = unvalidatedKeychains as { pub: string }[];

// multisig addresses are separated by ~
const splitAddresses = address.split('~');
Expand Down
7 changes: 4 additions & 3 deletions modules/sdk-coin-flrp/src/flrp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,14 +199,15 @@ export class Flrp extends BaseCoin {
* @param params.keychains public keys to generate the wallet
*/
async isWalletAddress(params: VerifyAddressOptions): Promise<boolean> {
const { address, keychains } = params;
const { address, keychains: unvalidatedKeychains } = params;

if (!this.isValidAddress(address)) {
throw new InvalidAddressError(`invalid address: ${address}`);
}
if (!keychains || keychains.length !== 3) {
throw new Error('Invalid keychains');
if (!unvalidatedKeychains || unvalidatedKeychains.length !== 3 || !unvalidatedKeychains.every((kc) => !!kc.pub)) {
throw new Error('missing required param keychains or public key');
}
const keychains = unvalidatedKeychains as { pub: string }[];

// multisig addresses are separated by ~
const splitAddresses = address.split('~');
Expand Down
7 changes: 4 additions & 3 deletions modules/sdk-coin-stx/src/stx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,11 @@ export class Stx extends BaseCoin {
* @param {String} params.baseAddress - the base address from the wallet
*/
async isWalletAddress(params: VerifyAddressOptions): Promise<boolean> {
const { address, keychains } = params;
if (!keychains || keychains.length !== 3) {
throw new Error('Invalid keychains');
const { address, keychains: unvalidatedKeychains } = params;
if (!unvalidatedKeychains || unvalidatedKeychains.length !== 3 || !unvalidatedKeychains.every((kc) => !!kc.pub)) {
throw new Error('missing required param keychains or public key');
}
const keychains = unvalidatedKeychains as { pub: string }[];
const pubs = keychains.map((keychain) => StxLib.Utils.xpubToSTXPubkey(keychain.pub));
const addressVersion = StxLib.Utils.getAddressVersion(address);
const baseAddress = StxLib.Utils.getSTXAddressFromPubKeys(pubs, addressVersion).address;
Expand Down
27 changes: 22 additions & 5 deletions modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,18 @@ export interface VerifyAddressOptions {
format?: CreateAddressFormat;
address: string;
addressType?: string;
keychains?: {
pub: string;
}[];
/**
* Keychains containing the pub or commonKeychain for HD derivation.
*/
keychains?: Pick<Keychain, 'pub' | 'commonKeychain'>[];
error?: string;
coinSpecific?: AddressCoinSpecific;
impliedForwarderVersion?: number;
/**
* Derivation index for the address.
* Used to derive child addresses from the root keychain via HD derivation path: m/{index}
*/
index?: number | string;
}

/**
Expand All @@ -168,12 +174,23 @@ export interface TssVerifyAddressOptions {
* For MPC wallets, the commonKeychain (combined public key from MPC key generation)
* should be identical across all keychains (user, backup, bitgo).
*/
keychains: Keychain[];
keychains: Pick<Keychain, 'commonKeychain'>[];
/**
* Derivation index for the address.
* Used to derive child addresses from the root keychain via HD derivation path: m/{index}
*/
index: string;
index: number | string;
}

export function isTssVerifyAddressOptions<T extends VerifyAddressOptions | TssVerifyAddressOptions>(
params: T
): params is T & TssVerifyAddressOptions {
return !!(
'keychains' in params &&
'index' in params &&
'address' in params &&
params.keychains?.some((kc) => 'commonKeychain' in kc && !!kc.commonKeychain)
);
}

export interface TransactionParams {
Expand Down
31 changes: 24 additions & 7 deletions modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Ecdsa } from 'modules/sdk-core/src/account-lib/mpc';
import { TssVerifyAddressOptions } from '../../baseCoin/iBaseCoin';
import { InvalidAddressError } from '../../errors';
import { EDDSAMethods } from '../../tss';
Expand Down Expand Up @@ -43,18 +44,34 @@ export async function verifyEddsaTssWalletAddress(
isValidAddress: (address: string) => boolean,
getAddressFromPublicKey: (publicKey: string) => string
): Promise<boolean> {
const { keychains, address, index } = params;
return verifyMPCWalletAddress({ ...params, keyCurve: 'ed25519' }, isValidAddress, getAddressFromPublicKey);
}

/**
* Verifies if an address belongs to a wallet using ECDSA TSS MPC derivation.
* This is a common implementation for ECDSA-based MPC coins (ETH, BTC, etc.)
*
* @param params - Verification options including keychains, address, and derivation index
* @param isValidAddress - Coin-specific function to validate address format
* @param getAddressFromPublicKey - Coin-specific function to convert public key to address
* @returns true if the address matches the derived address, false otherwise
* @throws {InvalidAddressError} if the address is invalid
* @throws {Error} if required parameters are missing or invalid
*/
export async function verifyMPCWalletAddress(
params: TssVerifyAddressOptions & {
keyCurve: 'secp256k1' | 'ed25519';
},
isValidAddress: (address: string) => boolean,
getAddressFromPublicKey: (publicKey: string) => string
): Promise<boolean> {
const { keychains, address, index } = params;
if (!isValidAddress(address)) {
throw new InvalidAddressError(`invalid address: ${address}`);
}

const mpc = params.keyCurve === 'secp256k1' ? new Ecdsa() : await EDDSAMethods.getInitializedMpcInstance();
const commonKeychain = extractCommonKeychain(keychains);

const MPC = await EDDSAMethods.getInitializedMpcInstance();
const derivationPath = 'm/' + index;
const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64);
const derivedPublicKey = mpc.deriveUnhardened(commonKeychain, 'm/' + index);
const expectedAddress = getAddressFromPublicKey(derivedPublicKey);

return address === expectedAddress;
}
Loading
Loading