From d1b529a9c9456d120352fc4d851a8b57b7b337ab Mon Sep 17 00:00:00 2001 From: Zahin Mohammad Date: Thu, 18 Jun 2026 14:53:18 -0400 Subject: [PATCH] feat(sdk-core): make GoStakeOptions.walletPassphrase optional OFC wallets that sign remotely via the BitGo key (userKeySigningRequired === false) never read the wallet passphrase, but the required type forced callers to pass one anyway. Relaxing the field to optional lets those callers omit it; the runtime already passes it through and ignores a falsy value on the remote-signing path. Add a unit test covering stake() without a passphrase for an OFC wallet that signs remotely, asserting the /tx/sign endpoint is hit and the preview and finalize calls succeed. Ticket: WCN-1018 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../v2/unit/staking/goStakingWalletCommon.ts | 64 +++++++++++++++++++ .../src/bitgo/staking/goStakingInterfaces.ts | 6 +- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/modules/bitgo/test/v2/unit/staking/goStakingWalletCommon.ts b/modules/bitgo/test/v2/unit/staking/goStakingWalletCommon.ts index 62fccbe22f..24fa219720 100644 --- a/modules/bitgo/test/v2/unit/staking/goStakingWalletCommon.ts +++ b/modules/bitgo/test/v2/unit/staking/goStakingWalletCommon.ts @@ -124,6 +124,70 @@ describe('Go Staking Wallet Common', function () { msScope1.isDone().should.be.True(); msScope2.isDone().should.be.True(); }); + + it('should stake without a wallet passphrase for an OFC wallet that signs remotely (userKeySigningRequired === false)', async function () { + // OFC wallet signs remotely via the BitGo key instead of decrypting the user key locally. + const remoteWallet = new Wallet(bitgo, baseCoin, { + id: 'remoteWalletId', + coin: ofcCoin, + enterprise: enterprise.id, + keys: ['5b3424f91bf349930e340175', '5b3424f91bf349930e340174', '5b3424f91bf349930e340173'], + coinSpecific: { userKeySigningRequired: false }, + }); + const remoteStakingWallet = remoteWallet.toGoStakingWallet(); + + // prebuildAndSignTransaction still fetches the user keychain (index 0), but with no passphrase + // the decrypt check is skipped and the key is never used to sign. + nock(microservicesUri).get(`/api/v2/${ofcCoin}/key/${remoteStakingWallet.wallet.keyIds()[0]}`).reply(200, { + id: remoteStakingWallet.wallet.keyIds()[0], + pub: 'xpub661MyMwAqRbcFq65dvGMeEVb81KKDRRkWkawSVesWcyevGc5gr8V27LjNfkktaMuKtM362jhgKy2eu35RdArcmmEAoULzAvgKkJpWQPvLXM', + source: 'user', + coinSpecific: {}, + }); + + const preview = fixtures.previewGoStakingRequest(coin); + const msScope1 = nock(microservicesUri) + .post(`/api/go-staking/v1/${ofcCoin}/accounts/${remoteStakingWallet.accountId}/requests/preview`, { + amount: '1', + clientId: 'clientId', + type: 'STAKE', + }) + .reply(201, preview); + + // remote signing: the BitGo key signs the payload and returns the signature + const signature = + '1f4f7e789ef485363b33dca7273717a4a4413330f2badbcaf302758bc404ff8e930943db81aa2c77007738afa23166934c2a3136bae281bea037095be7183c0602'; + const signScope = nock(microservicesUri) + .post(`/api/v2/ofc/wallet/${remoteWallet.id()}/tx/sign`, { payload: preview.payload }) + .reply(200, { signature }); + + const expected = fixtures.finalizeGoStakingRequest(coin, 'STAKE'); + const msScope2 = nock(microservicesUri) + .post(`/api/go-staking/v1/${ofcCoin}/accounts/${remoteStakingWallet.accountId}/requests/finalize`, { + amount: '1', + clientId: 'clientId', + frontTransferSendRequest: { + halfSigned: { + payload: preview.payload, + signature, + }, + }, + type: 'STAKE', + }) + .reply(201, expected); + + const stakingRequest = await remoteStakingWallet.stake({ + amount: '1', + clientId: 'clientId', + }); + + should.exist(stakingRequest); + + stakingRequest.should.deepEqual(expected); + msScope1.isDone().should.be.True(); + signScope.isDone().should.be.True(); + msScope2.isDone().should.be.True(); + }); }); describe('unstake', function () { diff --git a/modules/sdk-core/src/bitgo/staking/goStakingInterfaces.ts b/modules/sdk-core/src/bitgo/staking/goStakingInterfaces.ts index 7949b5e435..d9ac108167 100644 --- a/modules/sdk-core/src/bitgo/staking/goStakingInterfaces.ts +++ b/modules/sdk-core/src/bitgo/staking/goStakingInterfaces.ts @@ -32,7 +32,11 @@ interface GoStakingRequestProperties { export interface GoStakeOptions { amount: string; clientId?: string; - walletPassphrase: string; + /** + * Passphrase used to decrypt the user key and sign locally. Optional: omitted for OFC wallets that + * sign remotely via the BitGo key (userKeySigningRequired === false). + */ + walletPassphrase?: string; } export interface BaseGoStakeOptions {