From 3a6d1b6dd5a604c872c4525228f037c219770d94 Mon Sep 17 00:00:00 2001 From: Himanshu Singroha Date: Tue, 3 Mar 2026 11:33:50 +0530 Subject: [PATCH] feat: add support of custodial/cold wallet Ticket: CGARD-395 TICKET: CGARD-395 --- modules/sdk-core/src/bitgo/evm/evmUtils.ts | 5 +- modules/sdk-core/src/bitgo/wallet/wallets.ts | 1 + .../unit/bitgo/wallet/walletsEvmKeyring.ts | 158 ++++++++++++++++++ 3 files changed, 163 insertions(+), 1 deletion(-) diff --git a/modules/sdk-core/src/bitgo/evm/evmUtils.ts b/modules/sdk-core/src/bitgo/evm/evmUtils.ts index cb549a88a0..78df133eae 100644 --- a/modules/sdk-core/src/bitgo/evm/evmUtils.ts +++ b/modules/sdk-core/src/bitgo/evm/evmUtils.ts @@ -13,6 +13,8 @@ export interface CreateEvmKeyRingWalletParams { evmKeyRingReferenceWalletId: string; bitgo: BitGoBase; baseCoin: IBaseCoin; + /** Enterprise ID - required for cold/custodial wallets */ + enterprise?: string; } /** @@ -39,11 +41,12 @@ export function validateEvmKeyRingWalletParams(params: any, baseCoin: IBaseCoin) * @returns Promise - The created wallet with its keychains */ export async function createEvmKeyRingWallet(params: CreateEvmKeyRingWalletParams): Promise { - const { label, evmKeyRingReferenceWalletId, bitgo, baseCoin } = params; + const { label, evmKeyRingReferenceWalletId, bitgo, baseCoin, enterprise } = params; // For EVM keyring wallets, this bypasses the normal key generation process since keys are shared via keyring const addWalletParams = { label, evmKeyRingReferenceWalletId, + ...(enterprise && { enterprise }), }; const newWallet = await bitgo.post(baseCoin.url('/wallet/add')).send(addWalletParams).result(); diff --git a/modules/sdk-core/src/bitgo/wallet/wallets.ts b/modules/sdk-core/src/bitgo/wallet/wallets.ts index 0593d44295..ddb06bebf3 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallets.ts @@ -359,6 +359,7 @@ export class Wallets implements IWallets { evmKeyRingReferenceWalletId: evmKeyRingReferenceWalletId!, bitgo: this.bitgo, baseCoin: this.baseCoin, + enterprise, }); } diff --git a/modules/sdk-core/test/unit/bitgo/wallet/walletsEvmKeyring.ts b/modules/sdk-core/test/unit/bitgo/wallet/walletsEvmKeyring.ts index 8e929bc779..0453123cb8 100644 --- a/modules/sdk-core/test/unit/bitgo/wallet/walletsEvmKeyring.ts +++ b/modules/sdk-core/test/unit/bitgo/wallet/walletsEvmKeyring.ts @@ -201,4 +201,162 @@ describe('Wallets', function () { } }); }); + + describe('EVM Keyring - Cold/Custodial wallet support', function () { + beforeEach(function () { + mockBaseCoin.isEVM.returns(true); + mockBaseCoin.supportsTss.returns(true); + mockBaseCoin.getDefaultMultisigType.returns('tss'); + }); + + it('should create cold EVM keyring wallet with enterprise (type inherited from reference)', async function () { + const mockWalletResponse = { + id: '597f1f77bcf86cd799439011', + keys: ['user-key', 'backup-key', 'bitgo-key'], + }; + + const sendStub = sinon.stub().returns({ + result: sinon.stub().resolves(mockWalletResponse), + }); + + mockBitGo.post.returns({ + send: sendStub, + } as any); + + mockBaseCoin.keychains.returns({ + get: sinon.stub().resolves({ id: 'keychain-id', pub: 'public-key' }), + } as any); + + const result = await wallets.generateWallet({ + label: 'Cold EVM Keyring Wallet', + evmKeyRingReferenceWalletId: '507f1f77bcf86cd799439011', + enterprise: 'test-enterprise-id', + }); + + result.should.have.property('wallet'); + + const sentParams = sendStub.firstCall.args[0]; + sentParams.should.not.have.property('type'); + sentParams.should.have.property('enterprise', 'test-enterprise-id'); + sentParams.should.have.property('evmKeyRingReferenceWalletId', '507f1f77bcf86cd799439011'); + }); + + it('should create custodial EVM keyring wallet with enterprise (type inherited from reference)', async function () { + const mockWalletResponse = { + id: '597f1f77bcf86cd799439012', + keys: ['user-key', 'backup-key', 'bitgo-key'], + }; + + const sendStub = sinon.stub().returns({ + result: sinon.stub().resolves(mockWalletResponse), + }); + + mockBitGo.post.returns({ + send: sendStub, + } as any); + + mockBaseCoin.keychains.returns({ + get: sinon.stub().resolves({ id: 'keychain-id', pub: 'public-key' }), + } as any); + + const result = await wallets.generateWallet({ + label: 'Custodial EVM Keyring Wallet', + evmKeyRingReferenceWalletId: '507f1f77bcf86cd799439011', + enterprise: 'test-enterprise-id', + }); + + result.should.have.property('wallet'); + + const sentParams = sendStub.firstCall.args[0]; + sentParams.should.not.have.property('type'); + sentParams.should.have.property('enterprise', 'test-enterprise-id'); + sentParams.should.have.property('evmKeyRingReferenceWalletId', '507f1f77bcf86cd799439011'); + }); + + it('should create EVM keyring wallet without type (inherits from reference wallet)', async function () { + const mockWalletResponse = { + id: '597f1f77bcf86cd799439013', + keys: ['user-key', 'backup-key', 'bitgo-key'], + }; + + const sendStub = sinon.stub().returns({ + result: sinon.stub().resolves(mockWalletResponse), + }); + + mockBitGo.post.returns({ + send: sendStub, + } as any); + + mockBaseCoin.keychains.returns({ + get: sinon.stub().resolves({ id: 'keychain-id', pub: 'public-key' }), + } as any); + + const result = await wallets.generateWallet({ + label: 'Hot EVM Keyring Wallet', + evmKeyRingReferenceWalletId: '507f1f77bcf86cd799439011', + }); + + result.should.have.property('wallet'); + + const sentParams = sendStub.firstCall.args[0]; + sentParams.should.not.have.property('type'); + sentParams.should.have.property('evmKeyRingReferenceWalletId', '507f1f77bcf86cd799439011'); + }); + + it('should pass enterprise parameter for cold EVM keyring wallet via add method', async function () { + const mockWalletResponse = { + id: 'new-cold-wallet-id', + keys: ['user-key', 'backup-key', 'bitgo-key'], + }; + + const sendStub = sinon.stub().returns({ + result: sinon.stub().resolves(mockWalletResponse), + }); + + mockBitGo.post.returns({ + send: sendStub, + } as any); + + const result = await wallets.add({ + label: 'Cold EVM Keyring Child', + evmKeyRingReferenceWalletId: 'cold-parent-wallet-id', + enterprise: 'enterprise-123', + }); + + result.should.have.property('wallet'); + + const sentParams = sendStub.firstCall.args[0]; + sentParams.should.not.have.property('type'); + sentParams.should.have.property('enterprise', 'enterprise-123'); + sentParams.should.have.property('evmKeyRingReferenceWalletId', 'cold-parent-wallet-id'); + }); + + it('should pass enterprise parameter for custodial EVM keyring wallet via add method', async function () { + const mockWalletResponse = { + id: 'new-custodial-wallet-id', + keys: ['user-key', 'backup-key', 'bitgo-key'], + }; + + const sendStub = sinon.stub().returns({ + result: sinon.stub().resolves(mockWalletResponse), + }); + + mockBitGo.post.returns({ + send: sendStub, + } as any); + + const result = await wallets.add({ + label: 'Custodial EVM Keyring Child', + evmKeyRingReferenceWalletId: 'custodial-parent-wallet-id', + enterprise: 'enterprise-456', + }); + + result.should.have.property('wallet'); + + const sentParams = sendStub.firstCall.args[0]; + sentParams.should.not.have.property('type'); + sentParams.should.have.property('enterprise', 'enterprise-456'); + sentParams.should.have.property('evmKeyRingReferenceWalletId', 'custodial-parent-wallet-id'); + }); + }); });