Skip to content

Commit f31f558

Browse files
committed
feat: address verification for icp
TICKET: WP-7080
1 parent ebed7be commit f31f558

File tree

5 files changed

+315
-30
lines changed

5 files changed

+315
-30
lines changed

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

Lines changed: 80 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,8 @@ import {
1718
SignedTransaction,
1819
SigningError,
1920
SignTransactionOptions,
20-
TssVerifyAddressOptions,
2121
VerifyTransactionOptions,
22+
verifyMPCWalletAddress,
2223
} from '@bitgo/sdk-core';
2324
import { coins, NetworkType, BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
2425
import { Principal } from '@dfinity/principal';
@@ -41,6 +42,7 @@ import {
4142
SigningPayload,
4243
IcpTransactionExplanation,
4344
TransactionHexParams,
45+
TssVerifyIcpAddressOptions,
4446
UnsignedSweepRecoveryTransaction,
4547
} from './lib/iface';
4648
import { TransactionBuilderFactory } from './lib/transactionBuilderFactory';
@@ -141,8 +143,81 @@ export class Icp extends BaseCoin {
141143
return true;
142144
}
143145

144-
async isWalletAddress(params: TssVerifyAddressOptions): Promise<boolean> {
145-
return this.isValidAddress(params.address);
146+
/**
147+
* Verify that an address belongs to this wallet.
148+
*
149+
* @param {TssVerifyIcpAddressOptions} params - Verification parameters
150+
* @returns {Promise<boolean>} True if address belongs to wallet
151+
* @throws {InvalidAddressError} If address format is invalid
152+
* @throws {Error} If invalid wallet version or missing parameters
153+
*/
154+
async isWalletAddress(params: TssVerifyIcpAddressOptions): Promise<boolean> {
155+
const { address, rootAddress, walletVersion } = params;
156+
157+
if (!this.isValidAddress(address)) {
158+
throw new InvalidAddressError(`invalid address: ${address}`);
159+
}
160+
161+
if (walletVersion === 1) {
162+
return this.verifyMemoBasedAddress(address, rootAddress);
163+
}
164+
165+
return this.verifyKeyDerivedAddress(params, address, rootAddress);
166+
}
167+
168+
/**
169+
* Verifies a memo-based address for wallet version 1.
170+
*
171+
* @param {string} address - The full address to verify (must include memoId)
172+
* @param {string | undefined} rootAddress - The wallet's root address
173+
* @returns {boolean} True if the address is valid
174+
* @throws {Error} If rootAddress is missing or memoId is missing
175+
*/
176+
private verifyMemoBasedAddress(address: string, rootAddress: string | undefined): boolean {
177+
if (!rootAddress) {
178+
throw new Error('rootAddress is required for wallet version 1');
179+
}
180+
const extractedRootAddress = utils.validateMemoAndReturnRootAddress(address);
181+
if (extractedRootAddress === address) {
182+
throw new Error('memoId is required for wallet version 1 addresses');
183+
}
184+
185+
return extractedRootAddress?.toLowerCase() === rootAddress.toLowerCase();
186+
}
187+
188+
/**
189+
* Verifies a key-derived address using MPC wallet verification.
190+
*
191+
* @param {TssVerifyIcpAddressOptions} params - Verification parameters
192+
* @param {string} address - The full address to verify
193+
* @param {string | undefined} rootAddress - The wallet's root address
194+
* @returns {Promise<boolean>} True if the address matches the derived address
195+
* @throws {Error} If keychains are missing or address doesn't match
196+
*/
197+
private async verifyKeyDerivedAddress(
198+
params: TssVerifyIcpAddressOptions,
199+
address: string,
200+
rootAddress: string | undefined
201+
): Promise<boolean> {
202+
const { index } = params;
203+
const parsedIndex = typeof index === 'string' ? parseInt(index, 10) : index;
204+
205+
const isVerifyingRootAddress = rootAddress && address.toLowerCase() === rootAddress.toLowerCase();
206+
if (isVerifyingRootAddress && parsedIndex !== 0) {
207+
throw new Error(`Root address verification requires index 0, but got index ${index}`);
208+
}
209+
210+
const result = await verifyMPCWalletAddress(
211+
{ ...params, keyCurve: 'secp256k1' },
212+
this.isValidAddress.bind(this),
213+
(pubKey) => utils.getAddressFromPublicKey(pubKey)
214+
);
215+
216+
if (!result) {
217+
throw new InvalidAddressError(`invalid address: ${address}`);
218+
}
219+
220+
return true;
146221
}
147222

148223
async parseTransaction(params: ParseTransactionOptions): Promise<ParsedTransaction> {
@@ -210,7 +285,7 @@ export class Icp extends BaseCoin {
210285
return createHash('sha256');
211286
}
212287

213-
private async getAddressFromPublicKey(hexEncodedPublicKey: string) {
288+
private getAddressFromPublicKey(hexEncodedPublicKey: string): string {
214289
return utils.getAddressFromPublicKey(hexEncodedPublicKey);
215290
}
216291

@@ -388,7 +463,7 @@ export class Icp extends BaseCoin {
388463
throw new Error('failed to derive public key');
389464
}
390465

391-
const senderAddress = await this.getAddressFromPublicKey(publicKey);
466+
const senderAddress = this.getAddressFromPublicKey(publicKey);
392467
const balance = await this.getAccountBalance(publicKey);
393468
const feeData = await this.getFeeData();
394469
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)