diff --git a/modules/sdk-coin-icp/src/icp.ts b/modules/sdk-coin-icp/src/icp.ts index 0440aff66c3..0e3930b8a39 100644 --- a/modules/sdk-coin-icp/src/icp.ts +++ b/modules/sdk-coin-icp/src/icp.ts @@ -259,6 +259,10 @@ export class Icp extends BaseCoin { return 'ecdsa'; } + allowsAccountConsolidations(): boolean { + return true; // platform enforces V2-only (CECHO-2443) + } + /** @inheritDoc **/ getHashFunction(): Hash { return createHash('sha256'); diff --git a/modules/sdk-coin-icp/test/unit/icp.ts b/modules/sdk-coin-icp/test/unit/icp.ts index b44873664b6..ab36420ac7c 100644 --- a/modules/sdk-coin-icp/test/unit/icp.ts +++ b/modules/sdk-coin-icp/test/unit/icp.ts @@ -1,5 +1,6 @@ import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; import { BitGoAPI } from '@bitgo/sdk-api'; +import { Ecdsa } from '@bitgo/sdk-core'; import utils from '../../src/lib/utils'; import { getBuilderFactory } from './getBuilderFactory'; @@ -430,8 +431,9 @@ describe('Internet computer', function () { }); }); - describe('Wallet VersionKey 2+', () => { + describe('V2 non-memoId wallets', () => { let keychains; + let derivedAddressAtIndex1: string; before(function () { keychains = [ @@ -439,6 +441,12 @@ describe('Internet computer', function () { { commonKeychain: addressVerificationData.commonKeychain }, { commonKeychain: addressVerificationData.commonKeychain }, ]; + // Mirror addressVerification.ts:88-97: secp256k1 path m/{index}, first 33 bytes. + derivedAddressAtIndex1 = utils.getAddressFromPublicKey( + Buffer.from(new Ecdsa().deriveUnhardened(addressVerificationData.commonKeychain, 'm/1'), 'hex') + .subarray(0, 33) + .toString('hex') + ); }); it('should verify a valid TSS root address (index 0)', async function () { @@ -454,6 +462,50 @@ describe('Internet computer', function () { result.should.equal(true); }); + it('should verify a derived address at index 1', async function () { + const params = { + address: derivedAddressAtIndex1, + rootAddress: addressVerificationData.rootAddress, + keychains: keychains, + index: 1, + walletVersion: 2, + }; + + const result = await basecoin.isWalletAddress(params); + result.should.equal(true); + }); + + it('should reject when derived index-1 address is verified at index 0', async function () { + const params = { + address: derivedAddressAtIndex1, + rootAddress: addressVerificationData.rootAddress, + keychains: keychains, + index: 0, + walletVersion: 2, + }; + + await basecoin + .isWalletAddress(params) + .should.be.rejectedWith( + `address validation failure: address ${derivedAddressAtIndex1} is not a wallet address` + ); + }); + + it('should reject memo-suffixed addresses for V2 wallets', async function () { + const addressWithMemo = `${addressVerificationData.rootAddress}?memoId=5`; + const params = { + address: addressWithMemo, + rootAddress: addressVerificationData.rootAddress, + keychains: keychains, + index: 0, + walletVersion: 2, + }; + + await basecoin + .isWalletAddress(params) + .should.be.rejectedWith(`address validation failure: address ${addressWithMemo} is not a wallet address`); + }); + it('should throw error for invalid TSS address', async function () { const invalidAddress = testData.Accounts.account2.address; diff --git a/modules/sdk-coin-icp/test/unit/transaction.ts b/modules/sdk-coin-icp/test/unit/transaction.ts index b92217c99d1..b73c34d514f 100644 --- a/modules/sdk-coin-icp/test/unit/transaction.ts +++ b/modules/sdk-coin-icp/test/unit/transaction.ts @@ -60,6 +60,22 @@ describe('ICP Transaction', () => { payloadsData.payloads.length.should.equal(1); }); + it('should preserve memo suffix on receiver address when building a transfer', async () => { + const txn = new Transaction(config); + txn.icpTransactionData = testData.IcpTransactionData; + const factory = getBuilderFactory('ticp'); + const txBuilder = factory.getTransferBuilder(); + txBuilder.initBuilder(txn); + txBuilder.sender(testData.Accounts.account1.address, testData.Accounts.account1.publicKey); + txBuilder.receiverId(testData.Accounts.account6.address); + txBuilder.amount('10'); + txBuilder.memo(Number(testData.MetaDataWithMemo.memo)); + await txBuilder.build(); + + const icpTransaction = txBuilder.transaction.icpTransaction; + should.equal(icpTransaction.operations[1].account.address, testData.Accounts.account6.address); + }); + it('build a json transaction from raw hex', async () => { await tx.fromRawTransaction(serializedTxHex); const json = tx.toJson(); diff --git a/modules/sdk-coin-icp/test/unit/transactionBuilder/transactionBuilder.ts b/modules/sdk-coin-icp/test/unit/transactionBuilder/transactionBuilder.ts index 22d4ea4c768..999dd5f0169 100644 --- a/modules/sdk-coin-icp/test/unit/transactionBuilder/transactionBuilder.ts +++ b/modules/sdk-coin-icp/test/unit/transactionBuilder/transactionBuilder.ts @@ -207,6 +207,7 @@ describe('ICP Transaction Builder with default memo as 0', async () => { should.equal(icpTransaction.metadata.memo, DEFAULT_MEMO); should.equal(icpTransaction.operations[0].account.address, testData.Accounts.account1.address); should.equal(icpTransaction.operations[1].account.address, testData.Accounts.account2.address); + should.equal(icpTransaction.operations[2].account.address, testData.Accounts.account1.address); should.equal(icpTransaction.operations[0].amount.value, '-10'); should.equal(icpTransaction.operations[1].amount.value, '10'); should.equal(icpTransaction.operations[2].amount.value, '-10000');