Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
57 changes: 52 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,9 @@ import {
SignedTransaction,
SigningError,
SignTransactionOptions,
TssVerifyAddressOptions,
VerifyTransactionOptions,
verifyMPCWalletAddress,
UnexpectedAddressError,
} from '@bitgo/sdk-core';
import { coins, NetworkType, BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
import { Principal } from '@dfinity/principal';
Expand All @@ -41,6 +43,7 @@ import {
SigningPayload,
IcpTransactionExplanation,
TransactionHexParams,
TssVerifyIcpAddressOptions,
UnsignedSweepRecoveryTransaction,
} from './lib/iface';
import { TransactionBuilderFactory } from './lib/transactionBuilderFactory';
Expand Down Expand Up @@ -141,8 +144,52 @@ 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 or doesn't match derived address
* @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}`);
}

let addressToVerify = address;
if (walletVersion === 1) {
if (!rootAddress) {
throw new Error('rootAddress is required for wallet version 1');
}
const extractedRootAddress = utils.validateMemoAndReturnRootAddress(address);
if (!extractedRootAddress || extractedRootAddress === address) {
throw new Error('memoId is required for wallet version 1 addresses');
}
if (extractedRootAddress.toLowerCase() !== rootAddress.toLowerCase()) {
throw new UnexpectedAddressError(
`address validation failure: expected ${rootAddress} but got ${extractedRootAddress}`
);
}
addressToVerify = rootAddress;
}

const indexToVerify = walletVersion === 1 ? 0 : params.index;
const result = await verifyMPCWalletAddress(
{ ...params, address: addressToVerify, index: indexToVerify, keyCurve: 'secp256k1' },
this.isValidAddress.bind(this),
(pubKey) => utils.getAddressFromPublicKey(pubKey)
);

if (!result) {
throw new UnexpectedAddressError(
`address validation failure: address ${addressToVerify} is not a wallet address`
);
}

return true;
}

async parseTransaction(params: ParseTransactionOptions): Promise<ParsedTransaction> {
Expand Down Expand Up @@ -210,7 +257,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 +435,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