Skip to content

Commit ad52c5c

Browse files
Merge pull request #7686 from BitGo/WP-7080-address-verification-icp
feat: address verification for icp
2 parents 422b5fd + 4ab78c1 commit ad52c5c

File tree

5 files changed

+306
-30
lines changed

5 files changed

+306
-30
lines changed

modules/sdk-coin-icp/src/icp.ts

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Ecdsa,
99
ECDSAUtils,
1010
Environments,
11+
InvalidAddressError,
1112
KeyPair,
1213
MPCAlgorithm,
1314
MultisigType,
@@ -17,8 +18,9 @@ import {
1718
SignedTransaction,
1819
SigningError,
1920
SignTransactionOptions,
20-
TssVerifyAddressOptions,
2121
VerifyTransactionOptions,
22+
verifyMPCWalletAddress,
23+
UnexpectedAddressError,
2224
} from '@bitgo/sdk-core';
2325
import { coins, NetworkType, BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
2426
import { Principal } from '@dfinity/principal';
@@ -41,6 +43,7 @@ import {
4143
SigningPayload,
4244
IcpTransactionExplanation,
4345
TransactionHexParams,
46+
TssVerifyIcpAddressOptions,
4447
UnsignedSweepRecoveryTransaction,
4548
} from './lib/iface';
4649
import { TransactionBuilderFactory } from './lib/transactionBuilderFactory';
@@ -141,8 +144,52 @@ export class Icp extends BaseCoin {
141144
return true;
142145
}
143146

144-
async isWalletAddress(params: TssVerifyAddressOptions): Promise<boolean> {
145-
return this.isValidAddress(params.address);
147+
/**
148+
* Verify that an address belongs to this wallet.
149+
*
150+
* @param {TssVerifyIcpAddressOptions} params - Verification parameters
151+
* @returns {Promise<boolean>} True if address belongs to wallet
152+
* @throws {InvalidAddressError} If address format is invalid or doesn't match derived address
153+
* @throws {Error} If invalid wallet version or missing parameters
154+
*/
155+
async isWalletAddress(params: TssVerifyIcpAddressOptions): Promise<boolean> {
156+
const { address, rootAddress, walletVersion } = params;
157+
158+
if (!this.isValidAddress(address)) {
159+
throw new InvalidAddressError(`invalid address: ${address}`);
160+
}
161+
162+
let addressToVerify = address;
163+
if (walletVersion === 1) {
164+
if (!rootAddress) {
165+
throw new Error('rootAddress is required for wallet version 1');
166+
}
167+
const extractedRootAddress = utils.validateMemoAndReturnRootAddress(address);
168+
if (!extractedRootAddress || extractedRootAddress === address) {
169+
throw new Error('memoId is required for wallet version 1 addresses');
170+
}
171+
if (extractedRootAddress.toLowerCase() !== rootAddress.toLowerCase()) {
172+
throw new UnexpectedAddressError(
173+
`address validation failure: expected ${rootAddress} but got ${extractedRootAddress}`
174+
);
175+
}
176+
addressToVerify = rootAddress;
177+
}
178+
179+
const indexToVerify = walletVersion === 1 ? 0 : params.index;
180+
const result = await verifyMPCWalletAddress(
181+
{ ...params, address: addressToVerify, index: indexToVerify, keyCurve: 'secp256k1' },
182+
this.isValidAddress.bind(this),
183+
(pubKey) => utils.getAddressFromPublicKey(pubKey)
184+
);
185+
186+
if (!result) {
187+
throw new UnexpectedAddressError(
188+
`address validation failure: address ${addressToVerify} is not a wallet address`
189+
);
190+
}
191+
192+
return true;
146193
}
147194

148195
async parseTransaction(params: ParseTransactionOptions): Promise<ParsedTransaction> {
@@ -210,7 +257,7 @@ export class Icp extends BaseCoin {
210257
return createHash('sha256');
211258
}
212259

213-
private async getAddressFromPublicKey(hexEncodedPublicKey: string) {
260+
private getAddressFromPublicKey(hexEncodedPublicKey: string): string {
214261
return utils.getAddressFromPublicKey(hexEncodedPublicKey);
215262
}
216263

@@ -388,7 +435,7 @@ export class Icp extends BaseCoin {
388435
throw new Error('failed to derive public key');
389436
}
390437

391-
const senderAddress = await this.getAddressFromPublicKey(publicKey);
438+
const senderAddress = this.getAddressFromPublicKey(publicKey);
392439
const balance = await this.getAccountBalance(publicKey);
393440
const feeData = await this.getFeeData();
394441
const actualBalance = balance.minus(feeData);

modules/sdk-coin-icp/src/lib/iface.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
TransactionExplanation as BaseTransactionExplanation,
33
TransactionType as BitGoTransactionType,
4+
TssVerifyAddressOptions,
45
} from '@bitgo/sdk-core';
56

67
export const MAX_INGRESS_TTL = 5 * 60 * 1000_000_000; // 5 minutes in nanoseconds
@@ -216,3 +217,8 @@ export interface TransactionHexParams {
216217
transactionHex: string;
217218
signableHex?: string;
218219
}
220+
221+
export interface TssVerifyIcpAddressOptions extends TssVerifyAddressOptions {
222+
rootAddress?: string;
223+
walletVersion?: number;
224+
}

modules/sdk-coin-icp/src/lib/utils.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,14 @@ export class Utils implements BaseUtils {
7373
return undefined;
7474
}
7575
const [rootAddress, memoId] = address.split('?memoId=');
76-
if (memoId && this.validateMemo(BigInt(memoId))) {
77-
return rootAddress;
76+
if (memoId) {
77+
try {
78+
if (this.validateMemo(BigInt(memoId))) {
79+
return rootAddress;
80+
}
81+
} catch {
82+
return undefined;
83+
}
7884
}
7985
return address;
8086
}
@@ -210,8 +216,14 @@ export class Utils implements BaseUtils {
210216
const publicKeyBuffer = Buffer.from(publicKeyHex, 'hex');
211217
const ellipticKey = secp256k1.ProjectivePoint.fromHex(publicKeyBuffer.toString('hex'));
212218
const uncompressedPublicKeyHex = ellipticKey.toHex(false);
213-
const derEncodedKey = agent.wrapDER(Buffer.from(uncompressedPublicKeyHex, 'hex'), agent.SECP256K1_OID);
214-
return derEncodedKey;
219+
const uncompressedKeyBuffer = Buffer.from(uncompressedPublicKeyHex, 'hex');
220+
return agent.wrapDER(
221+
uncompressedKeyBuffer.buffer.slice(
222+
uncompressedKeyBuffer.byteOffset,
223+
uncompressedKeyBuffer.byteOffset + uncompressedKeyBuffer.byteLength
224+
),
225+
agent.SECP256K1_OID
226+
);
215227
}
216228

217229
/**
@@ -273,10 +285,10 @@ export class Utils implements BaseUtils {
273285
* Retrieves the address associated with a given hex-encoded public key.
274286
*
275287
* @param {string} hexEncodedPublicKey - The public key in hex-encoded format.
276-
* @returns {Promise<string>} A promise that resolves to the address derived from the provided public key.
288+
* @returns {string} The address derived from the provided public key.
277289
* @throws {Error} Throws an error if the provided public key is not in a valid hex-encoded format.
278290
*/
279-
async getAddressFromPublicKey(hexEncodedPublicKey: string): Promise<string> {
291+
getAddressFromPublicKey(hexEncodedPublicKey: string): string {
280292
if (!this.isValidPublicKey(hexEncodedPublicKey)) {
281293
throw new Error('Invalid hex-encoded public key format.');
282294
}

0 commit comments

Comments
 (0)