Skip to content

Commit 54eecc4

Browse files
authored
Merge pull request #7292 from BitGo/WP-6378-isWalletAddress-coins
feat: implement isWalletAddress for mpc eddsa coins
2 parents 08adced + a64baf3 commit 54eecc4

File tree

11 files changed

+257
-64
lines changed

11 files changed

+257
-64
lines changed

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@ import {
77
Environments,
88
ExplanationResult,
99
KeyPair,
10-
MethodNotImplementedError,
1110
MPCAlgorithm,
1211
ParsedTransaction,
1312
ParseTransactionOptions,
1413
SignedTransaction,
1514
SignTransactionOptions as BaseSignTransactionOptions,
1615
UnsignedTransaction,
17-
VerifyAddressOptions,
1816
VerifyTransactionOptions,
17+
TssVerifyAddressOptions,
1918
EDDSAMethods,
2019
EDDSAMethodTypes,
2120
MPCTx,
@@ -29,6 +28,7 @@ import {
2928
MultisigType,
3029
multisigTypes,
3130
AuditDecryptedKeyParams,
31+
verifyEddsaTssWalletAddress,
3232
} from '@bitgo/sdk-core';
3333
import { BaseCoin as StaticsBaseCoin, coins, PolkadotSpecNameType } from '@bitgo/statics';
3434
import { Interface, KeyPair as DotKeyPair, Transaction, TransactionBuilderFactory, Utils } from './lib';
@@ -642,8 +642,12 @@ export class Dot extends BaseCoin {
642642
return {};
643643
}
644644

645-
async isWalletAddress(params: VerifyAddressOptions): Promise<boolean> {
646-
throw new MethodNotImplementedError();
645+
async isWalletAddress(params: TssVerifyAddressOptions): Promise<boolean> {
646+
return verifyEddsaTssWalletAddress(
647+
params,
648+
(address) => this.isValidAddress(address),
649+
(publicKey) => this.getAddressFromPublicKey(publicKey)
650+
);
647651
}
648652

649653
async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {

modules/sdk-coin-dot/test/unit/dot.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BitGoAPI } from '@bitgo/sdk-api';
22
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
33
import { randomBytes } from 'crypto';
44
import should = require('should');
5+
import assert = require('assert');
56
import { Dot, Tdot, KeyPair } from '../../src';
67
import * as testData from '../fixtures';
78
import { chainName, txVersion, genesisHash, specVersion } from '../resources';
@@ -670,4 +671,71 @@ describe('DOT:', function () {
670671
);
671672
});
672673
});
674+
675+
describe('isWalletAddress', () => {
676+
it('should verify valid wallet address with correct keychain and index', async function () {
677+
const address = '5DxD9nT16GQLrU6aB5pSS5VtxoZbVju3NHUCcawxZyZCTf74';
678+
const commonKeychain =
679+
'6d2d5150f6e435dfd9b4f225f2cc29d95ec3b61b34e8bec98693b1a7ffe44cd764f99ee5058838d785c73360ad4f24d78e0255ab2c368c09060b29a9b27f040e';
680+
const index = '3';
681+
const keychains = [{ id: '1', type: 'tss' as const, commonKeychain }];
682+
683+
const result = await basecoin.isWalletAddress({ keychains, address, index });
684+
result.should.equal(true);
685+
});
686+
687+
it('should return false for address with incorrect keychain', async function () {
688+
const address = '5DxD9nT16GQLrU6aB5pSS5VtxoZbVju3NHUCcawxZyZCTf74';
689+
const wrongKeychain =
690+
'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
691+
const index = '3';
692+
const keychains = [{ id: '1', type: 'tss' as const, commonKeychain: wrongKeychain }];
693+
694+
const result = await basecoin.isWalletAddress({ keychains, address, index });
695+
result.should.equal(false);
696+
});
697+
698+
it('should return false for address with incorrect index', async function () {
699+
const address = '5DxD9nT16GQLrU6aB5pSS5VtxoZbVju3NHUCcawxZyZCTf74';
700+
const commonKeychain =
701+
'6d2d5150f6e435dfd9b4f225f2cc29d95ec3b61b34e8bec98693b1a7ffe44cd764f99ee5058838d785c73360ad4f24d78e0255ab2c368c09060b29a9b27f040e';
702+
const wrongIndex = '999';
703+
const keychains = [{ id: '1', type: 'tss' as const, commonKeychain }];
704+
705+
const result = await basecoin.isWalletAddress({ keychains, address, index: wrongIndex });
706+
result.should.equal(false);
707+
});
708+
709+
it('should throw error for invalid address', async function () {
710+
const invalidAddress = 'invalidaddress';
711+
const commonKeychain =
712+
'6d2d5150f6e435dfd9b4f225f2cc29d95ec3b61b34e8bec98693b1a7ffe44cd764f99ee5058838d785c73360ad4f24d78e0255ab2c368c09060b29a9b27f040e';
713+
const index = '3';
714+
const keychains = [{ id: '1', type: 'tss' as const, commonKeychain }];
715+
716+
await assert.rejects(async () => await basecoin.isWalletAddress({ keychains, address: invalidAddress, index }), {
717+
message: `invalid address: ${invalidAddress}`,
718+
});
719+
});
720+
});
721+
722+
describe('getAddressFromPublicKey', () => {
723+
it('should convert public key to SS58 address for testnet', function () {
724+
const publicKey = '53845d7b6a6e4a666fa2a0f500b88849b02926da5590993731d2b428b7643690';
725+
const expectedAddress = '5DxD9nT16GQLrU6aB5pSS5VtxoZbVju3NHUCcawxZyZCTf74';
726+
727+
const address = basecoin.getAddressFromPublicKey(publicKey);
728+
address.should.equal(expectedAddress);
729+
});
730+
731+
it('should convert public key to SS58 address for mainnet', function () {
732+
const publicKey = '53845d7b6a6e4a666fa2a0f500b88849b02926da5590993731d2b428b7643690';
733+
// Mainnet uses different SS58 prefix (0) vs testnet (42)
734+
const address = prodCoin.getAddressFromPublicKey(publicKey);
735+
address.should.be.type('string');
736+
address.length.should.be.greaterThan(0);
737+
// Should be different from testnet address
738+
address.should.not.equal('5DxD9nT16GQLrU6aB5pSS5VtxoZbVju3NHUCcawxZyZCTf74');
739+
});
740+
});
673741
});

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

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ import {
1111
MultisigType,
1212
multisigTypes,
1313
MPCAlgorithm,
14-
InvalidAddressError,
15-
EDDSAMethods,
1614
TssVerifyAddressOptions,
1715
MPCType,
16+
verifyEddsaTssWalletAddress,
1817
} from '@bitgo/sdk-core';
1918
import { BaseCoin as StaticsBaseCoin, CoinFamily } from '@bitgo/statics';
2019
import utils from './lib/utils';
@@ -92,29 +91,11 @@ export class Iota extends BaseCoin {
9291
* @param params
9392
*/
9493
async isWalletAddress(params: TssVerifyAddressOptions): Promise<boolean> {
95-
const { keychains, address, index } = params;
96-
97-
if (!this.isValidAddress(address)) {
98-
throw new InvalidAddressError(`invalid address: ${address}`);
99-
}
100-
101-
if (!keychains) {
102-
throw new Error('missing required param keychains');
103-
}
104-
105-
for (const keychain of keychains) {
106-
const MPC = await EDDSAMethods.getInitializedMpcInstance();
107-
const commonKeychain = keychain.commonKeychain as string;
108-
109-
const derivationPath = 'm/' + index;
110-
const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64);
111-
const expectedAddress = utils.getAddressFromPublicKey(derivedPublicKey);
112-
113-
if (address !== expectedAddress) {
114-
return false;
115-
}
116-
}
117-
return true;
94+
return verifyEddsaTssWalletAddress(
95+
params,
96+
(address) => this.isValidAddress(address),
97+
(publicKey) => utils.getAddressFromPublicKey(publicKey)
98+
);
11899
}
119100

120101
/**

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import {
2323
ITokenEnablement,
2424
KeyPair,
2525
Memo,
26-
MethodNotImplementedError,
2726
MPCAlgorithm,
2827
MPCConsolidationRecoveryOptions,
2928
MPCRecoveryOptions,
@@ -50,8 +49,9 @@ import {
5049
TransactionExplanation,
5150
TransactionParams,
5251
TransactionRecipient,
53-
VerifyAddressOptions,
5452
VerifyTransactionOptions,
53+
TssVerifyAddressOptions,
54+
verifyEddsaTssWalletAddress,
5555
} from '@bitgo/sdk-core';
5656
import { auditEddsaPrivateKey, getDerivationPath } from '@bitgo/sdk-lib-mpc';
5757
import { BaseNetwork, CoinFamily, coins, SolCoin, BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
@@ -560,8 +560,22 @@ export class Sol extends BaseCoin {
560560
return true;
561561
}
562562

563-
async isWalletAddress(params: VerifyAddressOptions): Promise<boolean> {
564-
throw new MethodNotImplementedError();
563+
async isWalletAddress(params: TssVerifyAddressOptions): Promise<boolean> {
564+
return verifyEddsaTssWalletAddress(
565+
params,
566+
(address) => this.isValidAddress(address),
567+
(publicKey) => this.getAddressFromPublicKey(publicKey)
568+
);
569+
}
570+
571+
/**
572+
* Converts a Solana public key to an address
573+
* @param publicKey Hex-encoded public key (64 hex characters = 32 bytes)
574+
* @returns Base58-encoded Solana address
575+
*/
576+
getAddressFromPublicKey(publicKey: string): string {
577+
const publicKeyBuffer = Buffer.from(publicKey, 'hex');
578+
return base58.encode(publicKeyBuffer);
565579
}
566580

567581
/**

modules/sdk-coin-sol/test/unit/sol.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3353,4 +3353,61 @@ describe('SOL:', function () {
33533353
);
33543354
});
33553355
});
3356+
3357+
describe('isWalletAddress', () => {
3358+
it('should verify valid wallet address with correct keychain and index', async function () {
3359+
const address = '7YAesfwPk41VChUgr65bm8FEep7ymWqLSW5rpYB5zZPY';
3360+
const commonKeychain =
3361+
'8ea32ecacfc83effbd2e2790ee44fa7c59b4d86c29a12f09fb613d8195f93f4e21875cad3b98adada40c040c54c3569467df41a020881a6184096378701862bd';
3362+
const index = '1';
3363+
const keychains = [{ id: '1', type: 'tss' as const, commonKeychain }];
3364+
3365+
const result = await basecoin.isWalletAddress({ keychains, address, index });
3366+
result.should.equal(true);
3367+
});
3368+
3369+
it('should return false for address with incorrect keychain', async function () {
3370+
const address = '7YAesfwPk41VChUgr65bm8FEep7ymWqLSW5rpYB5zZPY';
3371+
const wrongKeychain =
3372+
'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
3373+
const index = '1';
3374+
const keychains = [{ id: '1', type: 'tss' as const, commonKeychain: wrongKeychain }];
3375+
3376+
const result = await basecoin.isWalletAddress({ keychains, address, index });
3377+
result.should.equal(false);
3378+
});
3379+
3380+
it('should return false for address with incorrect index', async function () {
3381+
const address = '7YAesfwPk41VChUgr65bm8FEep7ymWqLSW5rpYB5zZPY';
3382+
const commonKeychain =
3383+
'8ea32ecacfc83effbd2e2790ee44fa7c59b4d86c29a12f09fb613d8195f93f4e21875cad3b98adada40c040c54c3569467df41a020881a6184096378701862bd';
3384+
const wrongIndex = '999';
3385+
const keychains = [{ id: '1', type: 'tss' as const, commonKeychain }];
3386+
3387+
const result = await basecoin.isWalletAddress({ keychains, address, index: wrongIndex });
3388+
result.should.equal(false);
3389+
});
3390+
3391+
it('should throw error for invalid address', async function () {
3392+
const invalidAddress = 'invalidaddress';
3393+
const commonKeychain =
3394+
'8ea32ecacfc83effbd2e2790ee44fa7c59b4d86c29a12f09fb613d8195f93f4e21875cad3b98adada40c040c54c3569467df41a020881a6184096378701862bd';
3395+
const index = '1';
3396+
const keychains = [{ id: '1', type: 'tss' as const, commonKeychain }];
3397+
3398+
await assert.rejects(async () => await basecoin.isWalletAddress({ keychains, address: invalidAddress, index }), {
3399+
message: `invalid address: ${invalidAddress}`,
3400+
});
3401+
});
3402+
});
3403+
3404+
describe('getAddressFromPublicKey', () => {
3405+
it('should convert public key to base58 address', function () {
3406+
const publicKey = '61220a9394802b1d1df37b35f7a3197970f48081092cee011fc98f7b71b2bd43';
3407+
const expectedAddress = '7YAesfwPk41VChUgr65bm8FEep7ymWqLSW5rpYB5zZPY';
3408+
3409+
const address = basecoin.getAddressFromPublicKey(publicKey);
3410+
address.should.equal(expectedAddress);
3411+
});
3412+
});
33563413
});

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
EDDSAMethods,
88
EDDSAMethodTypes,
99
Environments,
10-
InvalidAddressError,
1110
KeyPair,
1211
MPCAlgorithm,
1312
MPCRecoveryOptions,
@@ -30,6 +29,7 @@ import {
3029
MultisigType,
3130
multisigTypes,
3231
AuditDecryptedKeyParams,
32+
verifyEddsaTssWalletAddress,
3333
} from '@bitgo/sdk-core';
3434
import { BaseCoin as StaticsBaseCoin, BaseNetwork, coins, SuiCoin } from '@bitgo/statics';
3535
import BigNumber from 'bignumber.js';
@@ -188,12 +188,11 @@ export class Sui extends BaseCoin {
188188
}
189189

190190
async isWalletAddress(params: TssVerifyAddressOptions): Promise<boolean> {
191-
const { address: newAddress } = params;
192-
193-
if (!this.isValidAddress(newAddress)) {
194-
throw new InvalidAddressError(`invalid address: ${newAddress}`);
195-
}
196-
return true;
191+
return verifyEddsaTssWalletAddress(
192+
params,
193+
(address) => this.isValidAddress(address),
194+
(publicKey) => this.getAddressFromPublicKey(publicKey)
195+
);
197196
}
198197

199198
async parseTransaction(params: SuiParseTransactionOptions): Promise<SuiParsedTransaction> {

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

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
MPCTxs,
3131
MPCSweepRecoveryOptions,
3232
AuditDecryptedKeyParams,
33+
extractCommonKeychain,
3334
} from '@bitgo/sdk-core';
3435
import { auditEddsaPrivateKey, getDerivationPath } from '@bitgo/sdk-lib-mpc';
3536
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
@@ -159,29 +160,21 @@ export class Ton extends BaseCoin {
159160
throw new InvalidAddressError(`invalid address: ${newAddress}`);
160161
}
161162

162-
if (!keychains) {
163-
throw new Error('missing required param keychains');
164-
}
165-
166-
for (const keychain of keychains) {
167-
const [address, memoId] = newAddress.split('?memoId=');
168-
const MPC = await EDDSAMethods.getInitializedMpcInstance();
169-
const commonKeychain = keychain.commonKeychain as string;
163+
const [address, memoId] = newAddress.split('?memoId=');
170164

171-
const derivationPath = 'm/' + index;
172-
const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64);
173-
const expectedAddress = await Utils.default.getAddressFromPublicKey(derivedPublicKey);
165+
// TON supports memoId for address tagging - verify it matches the index
166+
if (memoId) {
167+
return memoId === `${index}`;
168+
}
174169

175-
if (memoId) {
176-
return memoId === `${index}`;
177-
}
170+
const commonKeychain = extractCommonKeychain(keychains);
178171

179-
if (address !== expectedAddress) {
180-
return false;
181-
}
182-
}
172+
const MPC = await EDDSAMethods.getInitializedMpcInstance();
173+
const derivationPath = 'm/' + index;
174+
const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64);
175+
const expectedAddress = await Utils.default.getAddressFromPublicKey(derivedPublicKey);
183176

184-
return true;
177+
return address === expectedAddress;
185178
}
186179

187180
async parseTransaction(params: TonParseTransactionOptions): Promise<ParsedTransaction> {

modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
VerifyTransactionOptions,
4848
AuditKeyParams,
4949
AuditDecryptedKeyParams,
50+
TssVerifyAddressOptions,
5051
} from './iBaseCoin';
5152
import { IInscriptionBuilder } from '../inscriptionBuilder';
5253
import {
@@ -346,7 +347,7 @@ export abstract class BaseCoin implements IBaseCoin {
346347
* @param params
347348
* @return true iff address is a wallet address. Must return false if address is outside wallet.
348349
*/
349-
abstract isWalletAddress(params: VerifyAddressOptions): Promise<boolean>;
350+
abstract isWalletAddress(params: VerifyAddressOptions | TssVerifyAddressOptions): Promise<boolean>;
350351

351352
/**
352353
* convert address into desired address format.

0 commit comments

Comments
 (0)