Skip to content

Commit 016611d

Browse files
committed
feat: added isWalletAddress implementation for canton
Ticket: COIN-6323
1 parent a9a5880 commit 016611d

File tree

4 files changed

+150
-6
lines changed

4 files changed

+150
-6
lines changed

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

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ import {
1111
SignedTransaction,
1212
SignTransactionOptions,
1313
TransactionType,
14-
TssVerifyAddressOptions,
1514
VerifyTransactionOptions,
1615
TransactionExplanation as BaseTransactionExplanation,
1716
BaseTransaction,
1817
PopulatedIntent,
1918
PrebuildTransactionWithIntentOptions,
19+
TssVerifyAddressOptions,
20+
InvalidAddressError,
21+
extractCommonKeychain,
22+
EDDSAMethods,
2023
} from '@bitgo/sdk-core';
2124
import { auditEddsaPrivateKey } from '@bitgo/sdk-lib-mpc';
2225
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
@@ -115,8 +118,24 @@ export class Canton extends BaseCoin {
115118
}
116119

117120
/** @inheritDoc */
118-
isWalletAddress(params: TssVerifyAddressOptions): Promise<boolean> {
119-
throw new Error('Method not implemented.');
121+
async isWalletAddress(params: TssVerifyAddressOptions): Promise<boolean> {
122+
// TODO: refactor this and use the `verifyEddsaMemoBasedWalletAddress` once published from sdk-core
123+
const { keychains, address: newAddress, index } = params;
124+
const [addressPart, memoId] = newAddress.split('?memoId=');
125+
if (!this.isValidAddress(addressPart)) {
126+
throw new InvalidAddressError(`invalid address: ${newAddress}`);
127+
}
128+
if (memoId && memoId !== index) {
129+
throw new InvalidAddressError(`invalid memoId index: ${memoId}`);
130+
}
131+
const commonKeychain = extractCommonKeychain(keychains);
132+
const MPC = await EDDSAMethods.getInitializedMpcInstance();
133+
const derivationPath = 'm/0';
134+
const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64);
135+
const publicKeyBase64 = Buffer.from(derivedPublicKey, 'hex').toString('base64');
136+
const rootAddressFingerprint = utils.getAddressFromPublicKey(publicKeyBase64);
137+
const rootAddress = `${rootAddressFingerprint.slice(0, 5)}::${rootAddressFingerprint}`;
138+
return addressPart === rootAddress;
120139
}
121140

122141
/** @inheritDoc */
@@ -163,6 +182,11 @@ export class Canton extends BaseCoin {
163182
return utils.isValidAddress(address);
164183
}
165184

185+
getAddressFromPublicKey(publicKeyHex: string): string {
186+
const publicKeyBase64 = Buffer.from(publicKeyHex, 'hex').toString('base64');
187+
return utils.getAddressFromPublicKey(publicKeyBase64);
188+
}
189+
166190
/** @inheritDoc */
167191
signTransaction(params: SignTransactionOptions): Promise<SignedTransaction> {
168192
throw new Error('Method not implemented.');

modules/sdk-coin-canton/test/integration/canton.integration.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
import assert from 'assert';
22

3+
import { BitGoAPI } from '@bitgo/sdk-api';
34
import { TransactionType } from '@bitgo/sdk-core';
5+
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
46

57
import { getCantonBuilderFactory } from '../helper';
68
import {
9+
CANTON_RECEIVE_ADDRESS,
710
GenerateTopologyResponse,
811
TransferAcceptRawTransaction,
912
TransferRejectRawTransaction,
1013
WalletInitRawTransaction,
1114
} from '../resources';
15+
import { Tcanton } from '../../src';
1216

1317
describe('Canton integration tests', function () {
18+
let bitgo: TestBitGoAPI;
19+
let basecoin: Tcanton;
20+
before(() => {
21+
bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' });
22+
bitgo.safeRegister('tcanton', Tcanton.createInstance);
23+
basecoin = bitgo.coin('tcanton') as Tcanton;
24+
});
25+
1426
describe('Explain raw transaction', function () {
1527
const factory = getCantonBuilderFactory('tcanton');
1628
it('should explain raw wallet init transaction', function () {
@@ -41,4 +53,71 @@ describe('Canton integration tests', function () {
4153
assert.equal(explainTxData.inputAmount, '5.0000000000');
4254
});
4355
});
56+
57+
describe('isWalletAddress', function () {
58+
let keychains;
59+
let commonKeychain: string;
60+
before(function () {
61+
commonKeychain =
62+
'19bdfe2a4b498a05511381235a8892d54267807c4a3f654e310b938b8b424ff4adedbe92f4c146de641c67508a961324c8504cdf8e0c0acbb68d6104ccccd781';
63+
keychains = [
64+
{
65+
id: '6424c353eaf78d000766e95949868468',
66+
source: 'user',
67+
type: 'tss',
68+
commonKeychain:
69+
'19bdfe2a4b498a05511381235a8892d54267807c4a3f654e310b938b8b424ff4adedbe92f4c146de641c67508a961324c8504cdf8e0c0acbb68d6104ccccd781',
70+
encryptedPrv:
71+
'{"iv":"cZd5i7L4RxtwrALW2rK7UA==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"5zgoH1Bd3Fw=","ct":"9vVlnXFRtrM9FVEo+d2chbGHlM9lFZemueBuAs3BIkPo33Fo7jzwwNK/kIWkEyg+NmEBd5IaqAS157nvvvwzzsmMWlQdUz9qbmXNv3pg987cXFR08exS+4uhwP1YNOjJTRvRNcO9ZqHb46d4fmyJ/yC9/susCge7r/EsbaN5C3afv1dzybuq912FwaQElZLYYp5BICudFOMZ9k0UDMfKM/PMDkH7WexoGHr9GKq/bgCH2B39TZZyHKU6Uy47lXep2s6h0DrMwHOrnmiL3DZjOj88Ynvphlzxuo4eOlD2UHia2+nvIaISYs29Pr0DAvREutchvcBpExj1kWWPv7hQYrv8F0NAdatsbWl3w+xKyfiMKo1USlrwyJviypGtQtXOJyw0XPN0rv2+L5lW8BbjpzHfYYN13fJTedlGTFhhkzVtbbPAKE02kx7zCJcjYaiexdSTsrDLScYNT9/Jhdt27KpsooehwVohLfSKz4vbFfRu2MPZw3/+c/hfiJNgtz6esWbnxGrcE8U2IwPYCaK+Ghk4DcqWNIni59RI5B5kAsQOToII40qPN510uTgxBSPO7q7MHgkxdd4CqBq+ojr9j0P7oao8E5Y+CBDJrojDoCh1oCCDW9vo2dXlVcD8SIbw7U/9AfvEbA4xyE/5md1M7CIwLnWs2Ynv0YtaKoqhdS9x6FmHlMDhN/DKHinrwmowtrTT82fOkpO5g9saSmgU7Qy3gLt8t+VwdEyeFeQUKRSyci8qgqXQaZIg4+aXgaSOnlCFMtmB8ekYxEhTY5uzRfrNgS4s1QeqFBpNtUF+Ydi297pbVXnJoXAN+SVWd80GCx+yI2dpVC89k3rOWK9WeyqlnzuLJWp2RIOB9cdW8GFv/fN+QAJpYeVxOE4+nZDsKnsj8nKcg9t4Dlx1G6gLM1/Vq9YxNLbuzuRC0asUYvdMnoMvszmpm++TxndYisgNYscpZSoz7wvcazJNEPfhPVjEkd6tUUuN4GM35H0DmKCUQNT+a6B6hmHlTZvjxiyGAg5bY59hdjvJ+22QduazlEEC6LI3HrA7uK0TpplWzS1tCIFvTMUhj65DEZmNJ2+ZY9bQ4vsMf+DRR3OOG4t+DMlNfjOd3zNv3QoY95BjfWpryFwPzDq7bCP67JDsoj7j2TY5FRSrRkD77H0Ewlux2cWfjRTwcMHcdQxxuV0OP0aNjGDjybFN"}',
72+
},
73+
{
74+
id: '6424c353eaf78d000766e96137d4404b',
75+
source: 'backup',
76+
type: 'tss',
77+
commonKeychain:
78+
'19bdfe2a4b498a05511381235a8892d54267807c4a3f654e310b938b8b424ff4adedbe92f4c146de641c67508a961324c8504cdf8e0c0acbb68d6104ccccd781',
79+
encryptedPrv:
80+
'{"iv":"vi0dPef/Rx7kG/pRySQi6Q==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"9efhQsiEvVs=","ct":"Gw6atvf6gxKzsjtl3xseipO3rAxp1mAz7Yu1ihFsi5/lf2vMZegApgZx+pyILFS9KKLHbNF3U6WgSYdrr2t4vzdLsXkH1WIxfHS+cd2C5N59yADZDnPJBT6pv/IRvaYelP0Ck3nIYQ2hSMm8op+VOWC/SzHeh7slYDqwEHTGan0Wigfvk1yRd7CCJTaEAomnc/4eFi2NY3X3gt/3opy9IAgknnwUFohn96EWpEQ0F6pbzH/Z8VF6gF+DUcrrByAxExUPnHQZiFk3YHU/vVV4FxBU/mVAE8xBsBn5ul5e5SUMPfc7TBuJWv4BByTNg9xDShF/91Yx2nbfUm5d9QmM8lpKgzzQvcK8POAPk87gRCuKnsGh5vNS0UppkHc+ocfzRQlGA6jze7QyyQO0rMj5Ly8kWjwk2vISvKYHYS1NR7VU549UIXo7NXjatunKSc3+IreoRUHIshiaLg6hl+pxCCuc0qQ43V0mdIfCjTN8gkGWLNk8R7tAGPz9jyapQPcPEGHgEz0ATIi6yMNWCsibS2eLiE1uVEJONoM4lk6FPl3Q2CHbW2MeEbqjY8hbaw18mNb2xSBH/Fwpiial+Tvi2imqgnCO4ZpO9bllKftZPcQy0stN+eGBlb5ufyflKkDSiChHYroGjEpmiFicdde48cJszF52uKNnf1q67fA9/S2FAHQab3EXojxH2Gbk+kkV2h/TYKFFZSWC3vi4e8mO+vjMUcR0AdsgPFyEIz0SCGuba3CnTLNdEuZwsauAeHkx2vUTnRgJPVgNeeuXmsVG76Sy2ggJHuals0Hj8U2Xda0qO1RuFfoCWfss9wn6HGRwPPkhSB/8oNguAqmRVGKkd8Zwt3IvrTd9fk0/rFFDJKGz7WyNHkYgUmNiGcItD12v0jx7FZ52EJzl3Av1RyJUQK18+8EYPh3SGiU9dt7VX0aF0uo6JouKhOeldUvMP+AugQz8fUclwTQsbboVg27Yxo0DyATVwThW5a56R6Qf5ZiQJluFuzs5y98rq0S5q046lE6o3vVmJpEdwjeSCJoET5CL4nTgkXyWvhm4eB8u/e66l3o0qbaSx8q9YYmT9EpRcl5TP4ThLBKETYdzVvg4exjQfektMatk5EyUpEIhZPXh5vXpJZesdfO9LJ8zTaHBsBjDPU7cdNgQMbebpataRi8A0el2/IJXl+E+olgAz5zC4i2O1Q=="}',
81+
},
82+
{
83+
id: '6424c353eaf78d000766e9510b125fba',
84+
source: 'bitgo',
85+
type: 'tss',
86+
commonKeychain:
87+
'19bdfe2a4b498a05511381235a8892d54267807c4a3f654e310b938b8b424ff4adedbe92f4c146de641c67508a961324c8504cdf8e0c0acbb68d6104ccccd781',
88+
verifiedVssProof: true,
89+
isBitGo: true,
90+
},
91+
];
92+
});
93+
it('should return true when receive address is valid', async function () {
94+
const address = CANTON_RECEIVE_ADDRESS.VALID_ADDRESS;
95+
const index = '1';
96+
const params = { commonKeychain, address: address, index, keychains };
97+
const isValid = await basecoin.isWalletAddress(params);
98+
isValid.should.equal(true);
99+
});
100+
101+
it('should throw error when receive address is invalid', async function () {
102+
const address = CANTON_RECEIVE_ADDRESS.INVALID_ADDRESS;
103+
const index = '1';
104+
const params = { commonKeychain, address: address, index, keychains };
105+
try {
106+
await basecoin.isWalletAddress(params);
107+
} catch (e) {
108+
assert.equal(e.message, 'invalid address: ' + address);
109+
}
110+
});
111+
112+
it('should throw error when receive address memoId is incorrect index', async function () {
113+
const address = CANTON_RECEIVE_ADDRESS.VALID_ADDRESS;
114+
const index = '2';
115+
const params = { commonKeychain, address: address, index, keychains };
116+
try {
117+
await basecoin.isWalletAddress(params);
118+
} catch (e) {
119+
assert.equal(e.message, 'invalid memoId index: 1');
120+
}
121+
});
122+
});
44123
});

modules/sdk-coin-canton/test/resources.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const WalletInitRequestData = {
4141
};
4242

4343
export const OneStepEnablement = {
44-
partyId: 'ravi-test-party-1::12205b4e3537a95126d90604592344d8ad3c3ddccda4f79901954280ee19c576714d',
44+
partyId: 'ravi-test-party-1::1220a43d89dc7d8f85316116aac093667f769fce55411aef6846ccb933b2e1a3b598',
4545
commandId: '3935a06d-3b03-41be-99a5-95b2ecaabf7d',
4646
};
4747

@@ -63,9 +63,9 @@ export const InvalidOneStepPreApprovalPrepareResponse = {
6363
};
6464

6565
export const CANTON_ADDRESSES = {
66-
VALID_ADDRESS: '12205::12205b4e3537a95126d90604592344d8ad3c3ddccda4f79901954280ee19c576714d',
66+
VALID_ADDRESS: '1220a::1220a43d89dc7d8f85316116aac093667f769fce55411aef6846ccb933b2e1a3b598',
6767
// party hint is not 5 characters
68-
INVALID_PARTY_HINT: '123456::12205b4e3537a95126d90604592344d8ad3c3ddccda4f79901954280ee19c576714d',
68+
INVALID_PARTY_HINT: '123456::1220a43d89dc7d8f85316116aac093667f769fce55411aef6846ccb933b2e1a3b598',
6969
// fingerprint is not a valid hex value
7070
INVALID_FINGERPRINT: '12205::12205b4e3537a95126d9060459234gd8ad3c3ddccda4f79901954280ee19c576714d',
7171
MISSING_PARTY_HINT: '::12205b4e3537a95126d9060459234gd8ad3c3ddccda4f79901954280ee19c576714d',
@@ -78,6 +78,11 @@ export const CANTON_BLOCK_HEIGHT = {
7878
NEGATIVE_BLOCK_HASH: '-100',
7979
};
8080

81+
export const CANTON_RECEIVE_ADDRESS = {
82+
VALID_ADDRESS: `${CANTON_ADDRESSES.VALID_ADDRESS}?memoId=1`,
83+
INVALID_ADDRESS: `${CANTON_ADDRESSES.INVALID_FINGERPRINT}?memoId=1`,
84+
};
85+
8186
export const TransferAcceptance = {
8287
partyId: 'ravi-test-party-1::12205b4e3537a95126d90604592344d8ad3c3ddccda4f79901954280ee19c576714d',
8388
commandId: '3935a06d-3b03-41be-99a5-95b2ecaabf7d',

modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,39 @@ export async function verifyEddsaTssWalletAddress(
5858

5959
return address === expectedAddress;
6060
}
61+
62+
/**
63+
* Verifies if an address belongs to a wallet using EdDSA TSS memo based address generation.
64+
*
65+
* @param params - Verification options including keychains, address, and derivation index
66+
* @param isValidAddress - Coin-specific function to validate address format
67+
* @param getAddressFromPublicKey - Coin-specific function to convert public key to address
68+
* @returns true if the address matches the derived address, false otherwise
69+
* @throws {InvalidAddressError} if the address is invalid or memo is invalid
70+
* @throws {Error} if required parameters are missing or invalid
71+
*/
72+
export async function verifyEddsaMemoBasedWalletAddress(
73+
params: TssVerifyAddressOptions,
74+
isValidAddress: (address: string) => boolean,
75+
getAddressFromPublicKey: (publicKey: string) => string
76+
): Promise<boolean> {
77+
const { keychains, address, index } = params;
78+
const [addressPart, memoId] = address.split('?memoId=');
79+
80+
if (memoId && memoId !== index) {
81+
throw new InvalidAddressError(`invalid memoId: ${memoId}`);
82+
}
83+
84+
if (!isValidAddress(addressPart)) {
85+
throw new InvalidAddressError(`invalid address: ${addressPart}`);
86+
}
87+
88+
const commonKeychain = extractCommonKeychain(keychains);
89+
90+
const MPC = await EDDSAMethods.getInitializedMpcInstance();
91+
const derivationPath = 'm/0';
92+
const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64);
93+
const expectedAddress = getAddressFromPublicKey(derivedPublicKey);
94+
95+
return addressPart === expectedAddress;
96+
}

0 commit comments

Comments
 (0)