From 5c4971473419d67c6c7c1d8f493589ed7b7549c1 Mon Sep 17 00:00:00 2001 From: Zahin Mohammad Date: Wed, 5 Aug 2026 14:28:46 -0400 Subject: [PATCH] fix(sdk-core,express): allow passwordless OFC wallet generation Ticket: WCN-1859 --- .../test/unit/typedRoutes/generateWallet.ts | 1 - modules/sdk-core/src/bitgo/wallet/iWallets.ts | 46 ++++++++++++++----- modules/sdk-core/src/bitgo/wallet/wallets.ts | 27 ++++++----- .../unit/bitgo/wallet/walletOptionsCodecs.ts | 38 +++++++++++++++ .../unit/bitgo/wallet/walletsGoAccount.ts | 45 +++++++++++++++++- 5 files changed, 131 insertions(+), 26 deletions(-) diff --git a/modules/express/test/unit/typedRoutes/generateWallet.ts b/modules/express/test/unit/typedRoutes/generateWallet.ts index 01cb4c1493..2041992606 100644 --- a/modules/express/test/unit/typedRoutes/generateWallet.ts +++ b/modules/express/test/unit/typedRoutes/generateWallet.ts @@ -467,7 +467,6 @@ describe('Generate Wallet Typed Routes Tests', function () { const res = await agent.post(`/api/v2/${coin}/wallet/generate`).send({ label, enterprise, - passphrase: 'test-passphrase', type: 'trading', userKeySigningRequired: false, }); diff --git a/modules/sdk-core/src/bitgo/wallet/iWallets.ts b/modules/sdk-core/src/bitgo/wallet/iWallets.ts index 211228ff42..f8472165a3 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallets.ts @@ -295,20 +295,42 @@ export const GenerateLightningWalletOptionsCodec = t.intersection( ); export type GenerateLightningWalletOptions = t.TypeOf; +const GenerateGoAccountWalletBaseOptionsCodec = t.intersection([ + t.strict({ + label: t.string, + enterprise: t.string, + type: t.literal('trading'), + }), + t.partial({ + // Codec intentionally accepts only 2: v1 is the implicit default and never sent on the wire. + encryptionVersion: t.literal(2), + }), +]); + +const GenerateGoAccountWalletPasswordOptionsCodec = t.intersection([ + t.strict({ + passphrase: t.string, + passcodeEncryptionCode: t.string, + }), + t.partial({ + userKeySigningRequired: t.boolean, + }), +]); + +const GenerateGoAccountWalletPasswordlessOptionsCodec = t.intersection([ + t.strict({ + userKeySigningRequired: t.literal(false), + }), + t.partial({ + passphrase: t.undefined, + passcodeEncryptionCode: t.undefined, + }), +]); + export const GenerateGoAccountWalletOptionsCodec = t.intersection( [ - t.strict({ - label: t.string, - passphrase: t.string, - enterprise: t.string, - passcodeEncryptionCode: t.string, - type: t.literal('trading'), - }), - t.partial({ - // Codec intentionally accepts only 2: v1 is the implicit default and never sent on the wire. - encryptionVersion: t.literal(2), - userKeySigningRequired: t.boolean, - }), + GenerateGoAccountWalletBaseOptionsCodec, + t.union([GenerateGoAccountWalletPasswordOptionsCodec, GenerateGoAccountWalletPasswordlessOptionsCodec]), ], 'GenerateGoAccountWalletOptions' ); diff --git a/modules/sdk-core/src/bitgo/wallet/wallets.ts b/modules/sdk-core/src/bitgo/wallet/wallets.ts index c06ef44819..55959f3c5a 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallets.ts @@ -240,16 +240,19 @@ export class Wallets implements IWallets { const keychainParams: AddKeychainOptions = { pub: keychain.pub, - encryptedPrv: await this.bitgo.encrypt({ - password: passphrase, - input: keychain.prv, - encryptionVersion, - }), - originalPasscodeEncryptionCode: passcodeEncryptionCode, keyType: 'independent', source: 'user', }; + if (passphrase !== undefined && passcodeEncryptionCode !== undefined) { + keychainParams.encryptedPrv = await this.bitgo.encrypt({ + password: passphrase, + input: keychain.prv, + encryptionVersion, + }); + keychainParams.originalPasscodeEncryptionCode = passcodeEncryptionCode; + } + const userKeychain = await this.baseCoin.keychains().add(keychainParams); const walletParams: SupplementGenerateWalletOptions = { @@ -351,11 +354,13 @@ export class Wallets implements IWallets { ); const walletData = await this.generateGoAccountWallet(options); - walletData.encryptedWalletPassphrase = await this.bitgo.encrypt({ - input: options.passphrase, - password: options.passcodeEncryptionCode, - encryptionVersion: options.encryptionVersion, - }); + if (options.passphrase !== undefined && options.passcodeEncryptionCode !== undefined) { + walletData.encryptedWalletPassphrase = await this.bitgo.encrypt({ + input: options.passphrase, + password: options.passcodeEncryptionCode, + encryptionVersion: options.encryptionVersion, + }); + } return walletData; } diff --git a/modules/sdk-core/test/unit/bitgo/wallet/walletOptionsCodecs.ts b/modules/sdk-core/test/unit/bitgo/wallet/walletOptionsCodecs.ts index af77a23b28..e1ed4f30d9 100644 --- a/modules/sdk-core/test/unit/bitgo/wallet/walletOptionsCodecs.ts +++ b/modules/sdk-core/test/unit/bitgo/wallet/walletOptionsCodecs.ts @@ -46,4 +46,42 @@ describe('wallet options codecs with encryptionVersion', () => { it('GenerateGoAccountWalletOptionsCodec works without encryptionVersion', () => { assert.ok(isRight(GenerateGoAccountWalletOptionsCodec.decode(goAccountBase))); }); + + it('GenerateGoAccountWalletOptionsCodec accepts password fields when user key signing is disabled', () => { + assert.ok( + isRight( + GenerateGoAccountWalletOptionsCodec.decode({ + ...goAccountBase, + userKeySigningRequired: false, + }) + ) + ); + }); + + it('GenerateGoAccountWalletOptionsCodec accepts omitted password fields when user key signing is disabled', () => { + assert.ok( + isRight( + GenerateGoAccountWalletOptionsCodec.decode({ + label: 'test', + enterprise: 'ent', + type: 'trading', + userKeySigningRequired: false, + }) + ) + ); + }); + + it('GenerateGoAccountWalletOptionsCodec rejects a single password field when user key signing is disabled', () => { + assert.ok( + isLeft( + GenerateGoAccountWalletOptionsCodec.decode({ + label: 'test', + passphrase: 'pass', + enterprise: 'ent', + type: 'trading', + userKeySigningRequired: false, + }) + ) + ); + }); }); diff --git a/modules/sdk-core/test/unit/bitgo/wallet/walletsGoAccount.ts b/modules/sdk-core/test/unit/bitgo/wallet/walletsGoAccount.ts index c30167b809..8071297638 100644 --- a/modules/sdk-core/test/unit/bitgo/wallet/walletsGoAccount.ts +++ b/modules/sdk-core/test/unit/bitgo/wallet/walletsGoAccount.ts @@ -17,7 +17,7 @@ describe('Wallets - GoAccount (OFC trading) wallet creation', function () { beforeEach(function () { mockKeychains = { create: sinon.stub().returns({ pub: userPub, prv: userPrv }), - add: sinon.stub().resolves({ id: 'user-key-id', pub: userPub, encryptedPrv: 'encrypted-prv' }), + add: sinon.stub().callsFake(async (params) => ({ id: 'user-key-id', ...params })), }; const mockWalletData = { id: 'wallet-id', keys: ['user-key-id'] }; @@ -56,7 +56,7 @@ describe('Wallets - GoAccount (OFC trading) wallet creation', function () { }); it('should forward userKeySigningRequired: false in coinSpecific when provided', async function () { - await wallets.generateWallet({ + const result = await wallets.generateWallet({ label: 'Test OFC Wallet', passphrase: 'test-passphrase', enterprise: 'enterprise-123', @@ -69,6 +69,11 @@ describe('Wallets - GoAccount (OFC trading) wallet creation', function () { const sentParams = postChain.send.firstCall.args[0]; sentParams.should.have.property('coinSpecific'); sentParams.coinSpecific.should.have.property('userKeySigningRequired', false); + + const keychainParams = mockKeychains.add.firstCall.args[0]; + keychainParams.should.have.property('encryptedPrv', `encrypted:test-passphrase:${userPrv}`); + keychainParams.should.have.property('originalPasscodeEncryptionCode', 'pce-code'); + result.should.have.property('encryptedWalletPassphrase', 'encrypted:pce-code:test-passphrase'); }); it('should forward userKeySigningRequired: true in coinSpecific when explicitly set', async function () { @@ -97,4 +102,40 @@ describe('Wallets - GoAccount (OFC trading) wallet creation', function () { const sentParams = postChain.send.firstCall.args[0]; sentParams.should.not.have.property('coinSpecific'); }); + + it('should generate a public-only user key when user key signing is disabled and password fields are omitted', async function () { + const result = await wallets.generateWallet({ + label: 'Passwordless OFC Wallet', + enterprise: 'enterprise-123', + type: 'trading', + userKeySigningRequired: false, + }); + + assert.ok(mockKeychains.add.calledOnce, 'user key should be uploaded once'); + const keychainParams = mockKeychains.add.firstCall.args[0]; + keychainParams.should.have.property('pub', userPub); + keychainParams.should.not.have.property('encryptedPrv'); + keychainParams.should.not.have.property('originalPasscodeEncryptionCode'); + + assert.ok(postChain.send.calledOnce, 'POST /wallet/add should be called once'); + postChain.send.firstCall.args[0].coinSpecific.should.have.property('userKeySigningRequired', false); + result.should.not.have.property('encryptedWalletPassphrase'); + result.should.not.have.property('warning'); + }); + + it('should reject passwordless generation when only one password field is supplied', async function () { + await assert.rejects( + wallets.generateWallet({ + label: 'Passwordless OFC Wallet', + enterprise: 'enterprise-123', + type: 'trading', + userKeySigningRequired: false, + passphrase: 'partial-password-input', + }), + /error\(s\) parsing generate go account request params/ + ); + + assert.strictEqual(mockKeychains.add.called, false); + assert.strictEqual(postChain.send.called, false); + }); });