Skip to content

Commit bc6de97

Browse files
authored
Merge pull request #6872 from BitGo/COIN-5193-EVMkeyring
feat(sdk-core): EVM keyring wallet and address creation changes
2 parents 7529091 + 645eb31 commit bc6de97

File tree

8 files changed

+556
-4
lines changed

8 files changed

+556
-4
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ export interface SupplementGenerateWalletOptions {
208208
type: 'hot' | 'cold' | 'custodial';
209209
subType?: 'lightningCustody' | 'lightningSelfCustody' | 'onPrem';
210210
coinSpecific?: { [coinName: string]: unknown };
211+
referenceWalletId?: string;
211212
}
212213

213214
export interface FeeEstimateOptions {

modules/sdk-core/src/bitgo/wallet/iWallet.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,8 @@ export interface CreateAddressOptions {
500500
derivedAddress?: string;
501501
index?: number;
502502
onToken?: string;
503+
referenceCoin?: string;
504+
referenceAddress?: string;
503505
}
504506

505507
export interface UpdateAddressOptions {

modules/sdk-core/src/bitgo/wallet/iWallets.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export interface GenerateWalletOptions {
7070
commonKeychain?: string;
7171
type?: 'hot' | 'cold' | 'custodial';
7272
subType?: 'lightningCustody' | 'lightningSelfCustody';
73+
referenceWalletId?: string;
7374
}
7475

7576
export const GenerateLightningWalletOptionsCodec = t.strict(
@@ -170,6 +171,7 @@ export interface AddWalletOptions {
170171
initializationTxs?: any;
171172
disableTransactionNotifications?: boolean;
172173
gasPrice?: number;
174+
referenceWalletId?: string;
173175
}
174176

175177
type KeySignatures = {

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,6 +1251,8 @@ export class Wallet implements IWallet {
12511251
baseAddress,
12521252
allowSkipVerifyAddress = true,
12531253
onToken,
1254+
referenceCoin,
1255+
referenceAddress,
12541256
} = params;
12551257

12561258
if (!_.isUndefined(chain)) {
@@ -1325,6 +1327,30 @@ export class Wallet implements IWallet {
13251327
}
13261328
}
13271329

1330+
// Validate EVM keyring params, referenceAddress is required and referenceCoin is optional for EVM keyring
1331+
if (!_.isUndefined(referenceAddress)) {
1332+
if (!_.isString(referenceAddress)) {
1333+
throw new Error('referenceAddress has to be a string');
1334+
}
1335+
if (!this.baseCoin.isEVM()) {
1336+
throw new Error('referenceAddress is only supported for EVM chains');
1337+
}
1338+
if (!this.baseCoin.isValidAddress(referenceAddress)) {
1339+
throw new Error('referenceAddress must be a valid address');
1340+
}
1341+
addressParams.referenceAddress = referenceAddress;
1342+
1343+
if (!_.isUndefined(referenceCoin)) {
1344+
if (!_.isString(referenceCoin)) {
1345+
throw new Error('referenceCoin has to be a string');
1346+
}
1347+
addressParams.referenceCoin = referenceCoin;
1348+
}
1349+
} else if (!_.isUndefined(referenceCoin)) {
1350+
// referenceCoin cannot be used without referenceAddress
1351+
throw new Error('referenceAddress is required when using referenceCoin for EVM keyring');
1352+
}
1353+
13281354
// get keychains for address verification
13291355
const keychains = await Promise.all(this._wallet.keys.map((k) => this.baseCoin.keychains().get({ id: k, reqId })));
13301356
const rootAddress = _.get(this._wallet, 'receiveAddress.address');

modules/sdk-core/src/bitgo/wallet/wallets.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,21 @@ export class Wallets implements IWallets {
101101
throw new Error('missing required string parameter label');
102102
}
103103

104-
// no need to pass keys for (single) custodial wallets
105-
if (params.type !== 'custodial') {
104+
// Validate referenceWalletId parameter
105+
if (params.referenceWalletId) {
106+
if (!_.isString(params.referenceWalletId)) {
107+
throw new Error('invalid referenceWalletId argument, expecting string');
108+
}
109+
if (!this.baseCoin.isEVM()) {
110+
throw new Error('referenceWalletId is only supported for EVM chains');
111+
}
112+
}
113+
114+
// For wallets with referenceWalletId, skip multisig validation as configuration is inherited
115+
if (params.referenceWalletId) {
116+
// Skip all multisig validation - configuration will be inherited from reference wallet
117+
} else if (params.type !== 'custodial') {
118+
// Standard validation for non-custodial wallets without referenceWalletId
106119
if (Array.isArray(params.keys) === false || !_.isNumber(params.m) || !_.isNumber(params.n)) {
107120
throw new Error('invalid argument');
108121
}
@@ -272,9 +285,10 @@ export class Wallets implements IWallets {
272285
throw new Error('missing required string parameter label');
273286
}
274287

275-
const { type = 'hot', label, passphrase, enterprise, isDistributedCustody } = params;
288+
const { type = 'hot', label, passphrase, enterprise, isDistributedCustody, referenceWalletId } = params;
276289
const isTss = params.multisigType === 'tss' && this.baseCoin.supportsTss();
277290
const canEncrypt = !!passphrase && typeof passphrase === 'string';
291+
const isEVMWithReference = this.baseCoin.isEVM() && referenceWalletId;
278292

279293
const walletParams: SupplementGenerateWalletOptions = {
280294
label: label,
@@ -284,6 +298,11 @@ export class Wallets implements IWallets {
284298
type: !!params.userKey && params.multisigType !== 'onchain' ? 'cold' : type,
285299
};
286300

301+
// Add referenceWalletId to walletParams if provided for EVM chains
302+
if (isEVMWithReference) {
303+
walletParams.referenceWalletId = referenceWalletId;
304+
}
305+
287306
if (!_.isUndefined(params.passcodeEncryptionCode)) {
288307
if (!_.isString(params.passcodeEncryptionCode)) {
289308
throw new Error('passcodeEncryptionCode must be a string');
@@ -297,15 +316,59 @@ export class Wallets implements IWallets {
297316
walletParams.enterprise = enterprise;
298317
}
299318

319+
// Validate referenceWalletId for EVM keyring
320+
if (!_.isUndefined(referenceWalletId)) {
321+
if (!_.isString(referenceWalletId)) {
322+
throw new Error('invalid referenceWalletId argument, expecting string');
323+
}
324+
if (!this.baseCoin.isEVM()) {
325+
throw new Error('referenceWalletId is only supported for EVM chains');
326+
}
327+
}
328+
300329
// EVM TSS wallets must use wallet version 3, 5 and 6
330+
// Skip this validation for EVM keyring wallets as they inherit version from reference wallet
301331
if (
302332
isTss &&
303333
this.baseCoin.isEVM() &&
334+
!referenceWalletId &&
304335
!(params.walletVersion === 3 || params.walletVersion === 5 || params.walletVersion === 6)
305336
) {
306337
throw new Error('EVM TSS wallets are only supported for wallet version 3, 5 and 6');
307338
}
308339

340+
// Handle EVM keyring wallet creation with referenceWalletId
341+
if (isEVMWithReference) {
342+
// For EVM keyring wallets, multisigType will be inferred from the reference wallet
343+
// No need to explicitly validate TSS requirement here as it will be handled by bgms
344+
345+
// For EVM keyring wallets, we use the add method directly with referenceWalletId
346+
// This bypasses the normal key generation process since keys are shared via keyring
347+
const addWalletParams = {
348+
label,
349+
referenceWalletId,
350+
};
351+
352+
const newWallet = await this.bitgo.post(this.baseCoin.url('/wallet/add')).send(addWalletParams).result();
353+
354+
// For EVM keyring wallets, we need to get the keychains from the reference wallet
355+
const userKeychain = this.baseCoin.keychains().get({ id: newWallet.keys[KeyIndices.USER] });
356+
const backupKeychain = this.baseCoin.keychains().get({ id: newWallet.keys[KeyIndices.BACKUP] });
357+
const bitgoKeychain = this.baseCoin.keychains().get({ id: newWallet.keys[KeyIndices.BITGO] });
358+
359+
const [userKey, backupKey, bitgoKey] = await Promise.all([userKeychain, backupKeychain, bitgoKeychain]);
360+
361+
const result: WalletWithKeychains = {
362+
wallet: new Wallet(this.bitgo, this.baseCoin, newWallet),
363+
userKeychain: userKey,
364+
backupKeychain: backupKey,
365+
bitgoKeychain: bitgoKey,
366+
responseType: 'WalletWithKeychains',
367+
};
368+
369+
return result;
370+
}
371+
309372
if (isTss) {
310373
if (!this.baseCoin.supportsTss()) {
311374
throw new Error(`coin ${this.baseCoin.getFamily()} does not support TSS at this time`);

modules/sdk-core/test/unit/account-lib/mpc/tss/ecdsa/ecdsa.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import { loadWebAssembly } from '@bitgo/sdk-opensslbytes';
2121

2222
const openSSLBytes = loadWebAssembly().buffer;
2323

24-
describe('ecdsa tss', function () {
24+
describe('ecdsa tss', function (this: Mocha.Context) {
25+
this.timeout(60000);
2526
const ecdsa = new Ecdsa();
2627

2728
let signCombine1: SignCombineRT, signCombine2: SignCombineRT;

0 commit comments

Comments
 (0)