Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
85 changes: 80 additions & 5 deletions modules/sdk-coin-icp/src/icp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Ecdsa,
ECDSAUtils,
Environments,
InvalidAddressError,
KeyPair,
MPCAlgorithm,
MultisigType,
Expand All @@ -17,8 +18,8 @@ import {
SignedTransaction,
SigningError,
SignTransactionOptions,
TssVerifyAddressOptions,
VerifyTransactionOptions,
verifyMPCWalletAddress,
} from '@bitgo/sdk-core';
import { coins, NetworkType, BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
import { Principal } from '@dfinity/principal';
Expand All @@ -41,6 +42,7 @@ import {
SigningPayload,
IcpTransactionExplanation,
TransactionHexParams,
TssVerifyIcpAddressOptions,
UnsignedSweepRecoveryTransaction,
} from './lib/iface';
import { TransactionBuilderFactory } from './lib/transactionBuilderFactory';
Expand Down Expand Up @@ -141,8 +143,81 @@ export class Icp extends BaseCoin {
return true;
}

async isWalletAddress(params: TssVerifyAddressOptions): Promise<boolean> {
return this.isValidAddress(params.address);
/**
* Verify that an address belongs to this wallet.
*
* @param {TssVerifyIcpAddressOptions} params - Verification parameters
* @returns {Promise<boolean>} True if address belongs to wallet
* @throws {InvalidAddressError} If address format is invalid
* @throws {Error} If invalid wallet version or missing parameters
*/
async isWalletAddress(params: TssVerifyIcpAddressOptions): Promise<boolean> {
const { address, rootAddress, walletVersion } = params;

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

if (walletVersion === 1) {
return this.verifyMemoBasedAddress(address, rootAddress);
Comment thread
danielzhao122 marked this conversation as resolved.
Outdated
}

return this.verifyKeyDerivedAddress(params, address, rootAddress);
}

/**
* Verifies a memo-based address for wallet version 1.
*
* @param {string} address - The full address to verify (must include memoId)
* @param {string | undefined} rootAddress - The wallet's root address
* @returns {boolean} True if the address is valid
* @throws {Error} If rootAddress is missing or memoId is missing
*/
private verifyMemoBasedAddress(address: string, rootAddress: string | undefined): boolean {
if (!rootAddress) {
throw new Error('rootAddress is required for wallet version 1');
}
const extractedRootAddress = utils.validateMemoAndReturnRootAddress(address);
if (extractedRootAddress === address) {
throw new Error('memoId is required for wallet version 1 addresses');
}

return extractedRootAddress?.toLowerCase() === rootAddress.toLowerCase();
}

/**
* Verifies a key-derived address using MPC wallet verification.
*
* @param {TssVerifyIcpAddressOptions} params - Verification parameters
* @param {string} address - The full address to verify
* @param {string | undefined} rootAddress - The wallet's root address
* @returns {Promise<boolean>} True if the address matches the derived address
* @throws {Error} If keychains are missing or address doesn't match
*/
private async verifyKeyDerivedAddress(
params: TssVerifyIcpAddressOptions,
address: string,
rootAddress: string | undefined
): Promise<boolean> {
const { index } = params;
const parsedIndex = typeof index === 'string' ? parseInt(index, 10) : index;

const isVerifyingRootAddress = rootAddress && address.toLowerCase() === rootAddress.toLowerCase();
if (isVerifyingRootAddress && parsedIndex !== 0) {
throw new Error(`Root address verification requires index 0, but got index ${index}`);
}

const result = await verifyMPCWalletAddress(
{ ...params, keyCurve: 'secp256k1' },
this.isValidAddress.bind(this),
(pubKey) => utils.getAddressFromPublicKey(pubKey)
);

if (!result) {
throw new InvalidAddressError(`invalid address: ${address}`);
}

return true;
}

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

private async getAddressFromPublicKey(hexEncodedPublicKey: string) {
private getAddressFromPublicKey(hexEncodedPublicKey: string): string {
return utils.getAddressFromPublicKey(hexEncodedPublicKey);
}

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

const senderAddress = await this.getAddressFromPublicKey(publicKey);
const senderAddress = this.getAddressFromPublicKey(publicKey);
const balance = await this.getAccountBalance(publicKey);
const feeData = await this.getFeeData();
const actualBalance = balance.minus(feeData);
Expand Down
6 changes: 6 additions & 0 deletions modules/sdk-coin-icp/src/lib/iface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
TransactionExplanation as BaseTransactionExplanation,
TransactionType as BitGoTransactionType,
TssVerifyAddressOptions,
} from '@bitgo/sdk-core';

export const MAX_INGRESS_TTL = 5 * 60 * 1000_000_000; // 5 minutes in nanoseconds
Expand Down Expand Up @@ -216,3 +217,8 @@ export interface TransactionHexParams {
transactionHex: string;
signableHex?: string;
}

export interface TssVerifyIcpAddressOptions extends TssVerifyAddressOptions {
rootAddress?: string;
walletVersion?: number;
}
24 changes: 18 additions & 6 deletions modules/sdk-coin-icp/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,14 @@ export class Utils implements BaseUtils {
return undefined;
}
const [rootAddress, memoId] = address.split('?memoId=');
if (memoId && this.validateMemo(BigInt(memoId))) {
return rootAddress;
if (memoId) {
try {
if (this.validateMemo(BigInt(memoId))) {
return rootAddress;
}
} catch {
return undefined;
}
}
return address;
}
Expand Down Expand Up @@ -210,8 +216,14 @@ export class Utils implements BaseUtils {
const publicKeyBuffer = Buffer.from(publicKeyHex, 'hex');
const ellipticKey = secp256k1.ProjectivePoint.fromHex(publicKeyBuffer.toString('hex'));
const uncompressedPublicKeyHex = ellipticKey.toHex(false);
const derEncodedKey = agent.wrapDER(Buffer.from(uncompressedPublicKeyHex, 'hex'), agent.SECP256K1_OID);
return derEncodedKey;
const uncompressedKeyBuffer = Buffer.from(uncompressedPublicKeyHex, 'hex');
return agent.wrapDER(
uncompressedKeyBuffer.buffer.slice(
uncompressedKeyBuffer.byteOffset,
uncompressedKeyBuffer.byteOffset + uncompressedKeyBuffer.byteLength
),
agent.SECP256K1_OID
);
}

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