From eef9b384c05c085e72f1193159781ee601e8b709 Mon Sep 17 00:00:00 2001 From: alextse-bg Date: Wed, 29 Apr 2026 14:58:29 -0400 Subject: [PATCH 1/3] feat(sdk-core): added OFC BitGo signing on trading accounts object make wallet passphrase optional when signing trading account TXs allow the use of prv when signing trading account TXs Ticket: WCN-217-1 --- .../src/bitgo/trading/iTradingAccount.ts | 13 +- .../src/bitgo/trading/tradingAccount.ts | 71 ++++++- modules/sdk-core/src/bitgo/wallet/iWallet.ts | 1 + .../test/unit/bitgo/trading/tradingAccount.ts | 196 ++++++++++++++++++ .../bitgo/wallet/ofcWalletSignTransaction.ts | 68 ++++++ 5 files changed, 341 insertions(+), 8 deletions(-) create mode 100644 modules/sdk-core/test/unit/bitgo/trading/tradingAccount.ts create mode 100644 modules/sdk-core/test/unit/bitgo/wallet/ofcWalletSignTransaction.ts diff --git a/modules/sdk-core/src/bitgo/trading/iTradingAccount.ts b/modules/sdk-core/src/bitgo/trading/iTradingAccount.ts index 462c7e25ba..350606eecb 100644 --- a/modules/sdk-core/src/bitgo/trading/iTradingAccount.ts +++ b/modules/sdk-core/src/bitgo/trading/iTradingAccount.ts @@ -1,8 +1,19 @@ import { ITradingNetwork } from './network'; +/** + * Parameters for the signing a payload from the trading account. + * If both walletPassphrase and prv are not provided, the BitGo key will be used. + * + * @note If wallet has userKeySigningRequired set to true, then attempting to sign with BitGo key will throw. + * + * @param payload - The payload to sign + * @param walletPassphrase - The passphrase of the wallet that will be used to decrypt the user key and sign the payload. + * @param prv - The decrypted user key prv used to sign the payload + */ export interface SignPayloadParameters { payload: string | Record; - walletPassphrase: string; + walletPassphrase?: string; + prv?: string; } export interface ITradingAccount { diff --git a/modules/sdk-core/src/bitgo/trading/tradingAccount.ts b/modules/sdk-core/src/bitgo/trading/tradingAccount.ts index 40489647ae..aca75014c2 100644 --- a/modules/sdk-core/src/bitgo/trading/tradingAccount.ts +++ b/modules/sdk-core/src/bitgo/trading/tradingAccount.ts @@ -23,20 +23,77 @@ export class TradingAccount implements ITradingAccount { } /** - * Signs an arbitrary payload with the user key on this trading account + * Signs an arbitrary payload. Use the user key if passphrase/prv is provided, or the BitGo key if not. * @param params * @param params.payload arbitrary payload object (string | Record) * @param params.walletPassphrase passphrase on this trading account, used to unlock the account user key + * @param params.prv user private key, used to sign the payload locally * @returns hex-encoded signature of the payload */ async signPayload(params: SignPayloadParameters): Promise { - const key = (await this.wallet.baseCoin.keychains().get({ id: this.wallet.keyIds()[0] })) as any; - const prv = this.wallet.bitgo.decrypt({ - input: key.encryptedPrv, - password: params.walletPassphrase, - }); + // if no passphrase is provided, attempt to sign using the wallet's bitgo key remotely + if (!params.walletPassphrase && !params.prv) { + return this.signPayloadByBitGoKey(params); + } + // if a passphrase is provided, we must be trying to sign using the user private key - decrypt and sign locally + return this.signPayloadByUserKey(params); + } + + /** + * Signs the payload of a trading account via the trading account BitGo key + * @param params + * @private + */ + private async signPayloadByBitGoKey( + params: Omit + ): Promise { + const walletData = this.wallet.toJSON(); + if (walletData.userKeySigningRequired) { + throw new Error( + 'Wallet must use user key to sign ofc transaction, please provide the wallet passphrase or visit your wallet settings page to configure one.' + ); + } + if (walletData.keys.length < 2) { + throw new Error( + 'Wallet does not support BitGo signing. Please reach out to support@bitgo.com to resolve this issue.' + ); + } + + // we do not parse the payload here, we instead sends the payload as a stringified JSON to be signed, just like how we process it locally + const url = this.wallet.url('/tx/sign'); + const payload = typeof params.payload !== 'string' ? JSON.stringify(params.payload) : params.payload; + const { signature } = await this.wallet.bitgo.post(url).send({ payload }).result(); + + return signature; + } + + /** + * Signs the payload of a trading account locally by fetching the user's encrypted private key and decrypt using passphrase + * @param params + * @private + */ + private async signPayloadByUserKey(params: SignPayloadParameters): Promise { + if (!params.prv && !params.walletPassphrase) { + throw new Error( + 'Must provide either prv or walletPassphrase to sign payload using user key. Please provide the wallet passphrase or visit your wallet settings page to configure one.' + ); + } + + let prv: string; + if (params.prv) { + prv = params.prv; + } else { + const key = await this.wallet.baseCoin.keychains().get({ id: this.wallet.keyIds()[0] }); + if (!key.encryptedPrv) { + throw new Error('Expected encryptedPrv to be present on user keychain.'); + } + prv = this.wallet.bitgo.decrypt({ + input: key.encryptedPrv, + password: params.walletPassphrase, + }); + } const payload = typeof params.payload === 'string' ? params.payload : JSON.stringify(params.payload); - return ((await this.wallet.baseCoin.signMessage({ prv }, payload)) as any).toString('hex'); + return (await this.wallet.baseCoin.signMessage({ prv }, payload)).toString('hex'); } /** diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index 1f65b5a977..76242ed54f 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -909,6 +909,7 @@ export interface WalletData { evmKeyRingReferenceWalletId?: string; isParent?: boolean; enabledChildChains?: string[]; + userKeySigningRequired?: boolean; } export interface RecoverTokenOptions { diff --git a/modules/sdk-core/test/unit/bitgo/trading/tradingAccount.ts b/modules/sdk-core/test/unit/bitgo/trading/tradingAccount.ts new file mode 100644 index 0000000000..13c324de4f --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/trading/tradingAccount.ts @@ -0,0 +1,196 @@ +/** + * @prettier + */ +import sinon from 'sinon'; +import 'should'; +import { TradingAccount } from '../../../../src/bitgo/trading/tradingAccount'; + +describe('TradingAccount', function () { + let tradingAccount: TradingAccount; + let mockBitGo: any; + let mockWallet: any; + let mockBaseCoin: any; + let sendStub: sinon.SinonStub; + + const enterpriseId = 'test-enterprise-id'; + const walletPassphrase = 'test-passphrase'; + const encryptedPrv = 'encrypted-prv'; + const decryptedPrv = 'decrypted-prv'; + const signature = 'aabbccdd'; + const payload = { data: 'test-payload' }; + const payloadString = JSON.stringify(payload); + + beforeEach(function () { + sendStub = sinon.stub(); + sendStub.withArgs({ payload: payloadString }).returns({ result: sinon.stub().resolves({ signature }) }); + + mockBitGo = { + post: sinon.stub().returns({ send: sendStub }), + decrypt: sinon + .stub() + .callsFake(({ input, password }) => + input === encryptedPrv && password === walletPassphrase ? decryptedPrv : undefined + ), + }; + + mockBaseCoin = { + keychains: sinon.stub().returns({ + get: sinon.stub().resolves({ encryptedPrv }), + }), + signMessage: sinon.stub().callsFake(async (key: { prv: string }) => { + if (key.prv === decryptedPrv) { + return Buffer.from(signature, 'hex'); + } + throw new Error(`signMessage called with unexpected prv: ${key.prv}`); + }), + }; + + mockWallet = { + id: sinon.stub().returns('test-wallet-id'), + keyIds: sinon.stub().returns(['user-key-id', 'bitgo-key-id']), + url: sinon.stub().returns('https://example.com/wallet/test-wallet-id/tx/sign'), + toJSON: sinon.stub().returns({ + id: 'test-wallet-id', + keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'], + userKeySigningRequired: undefined, // default is undefined + }), + baseCoin: mockBaseCoin, + bitgo: mockBitGo, + }; + + tradingAccount = new TradingAccount(enterpriseId, mockWallet, mockBitGo); + }); + + afterEach(function () { + sinon.restore(); + }); + + describe('id', function () { + it('should return the wallet id', function () { + tradingAccount.id.should.equal('test-wallet-id'); + mockWallet.id.calledOnce.should.be.true(); + }); + }); + + describe('signPayload', function () { + describe('without walletPassphrase or prv (BitGo remote signing)', function () { + it('should sign using the BitGo key remotely when no passphrase is provided', async function () { + const result = await tradingAccount.signPayload({ payload }); + + mockWallet.toJSON.calledOnce.should.be.true(); + mockWallet.url.calledWith('/tx/sign').should.be.true(); + mockBitGo.post.calledOnce.should.be.true(); + sendStub.calledWith({ payload: JSON.stringify(payload) }).should.be.true(); + result.should.equal(signature); + }); + + it('should sign a string payload remotely when no passphrase is provided', async function () { + const result = await tradingAccount.signPayload({ payload: payloadString }); + sendStub.calledWith({ payload: payloadString }).should.be.true(); + result.should.equal(signature); + }); + + it('should throw if userKeySigningRequired is set and no passphrase and prv are provided', async function () { + mockWallet.toJSON.onCall(mockWallet.toJSON.callCount).returns({ + id: 'test-wallet-id', + keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'], + userKeySigningRequired: true, + }); + + await tradingAccount + .signPayload({ payload }) + .should.be.rejectedWith( + 'Wallet must use user key to sign ofc transaction, please provide the wallet passphrase or visit your wallet settings page to configure one.' + ); + }); + + it('should throw if wallet has fewer than 2 keys and no passphrase and prv are provided', async function () { + mockWallet.toJSON.onCall(mockWallet.toJSON.callCount).returns({ + id: 'test-wallet-id', + keys: ['user-key-id'], + userKeySigningRequired: undefined, + }); + + await tradingAccount + .signPayload({ payload }) + .should.be.rejectedWith( + 'Wallet does not support BitGo signing. Please reach out to support@bitgo.com to resolve this issue.' + ); + }); + }); + + describe('with walletPassphrase (local user key signing)', function () { + it('should decrypt the user key and sign the payload locally', async function () { + const result = await tradingAccount.signPayload({ payload, walletPassphrase }); + + mockBaseCoin.keychains().get.calledWith({ id: 'user-key-id' }).should.be.true(); + mockBitGo.decrypt.calledWith({ input: encryptedPrv, password: walletPassphrase }).should.be.true(); + mockBaseCoin.signMessage.calledOnce.should.be.true(); + result.should.equal(Buffer.from(signature, 'hex').toString('hex')); + }); + + it('should stringify a Record payload before signing locally', async function () { + await tradingAccount.signPayload({ payload, walletPassphrase }); + + const signMessageCall = mockBaseCoin.signMessage.getCall(0); + signMessageCall.args[1].should.equal(JSON.stringify(payload)); + }); + + it('should pass a string payload directly to signMessage', async function () { + await tradingAccount.signPayload({ payload: payloadString, walletPassphrase }); + + const signMessageCall = mockBaseCoin.signMessage.getCall(0); + signMessageCall.args[1].should.equal(payloadString); + }); + }); + + describe('with both walletPassphrase and prv', function () { + it('should use prv directly and not call decrypt when both are provided', async function () { + await tradingAccount.signPayload({ payload, walletPassphrase, prv: decryptedPrv }); + + mockBitGo.decrypt.called.should.be.false(); + mockBaseCoin.keychains.called.should.be.false(); + const signMessageCall = mockBaseCoin.signMessage.getCall(0); + signMessageCall.args[0].should.deepEqual({ prv: decryptedPrv }); + }); + }); + + describe('with prv (local user key signing without decryption)', function () { + it('should sign using the provided prv without calling decrypt', async function () { + const result = await tradingAccount.signPayload({ payload, prv: decryptedPrv }); + + mockBitGo.decrypt.called.should.be.false(); + mockBaseCoin.keychains.called.should.be.false(); + mockBaseCoin.signMessage.calledOnce.should.be.true(); + result.should.equal(Buffer.from(signature, 'hex').toString('hex')); + }); + + it('should not use BitGo remote signing when prv is provided', async function () { + await tradingAccount.signPayload({ payload, prv: decryptedPrv }); + + mockBitGo.post.called.should.be.false(); + }); + + it('should pass the prv directly to signMessage', async function () { + await tradingAccount.signPayload({ payload, prv: decryptedPrv }); + + const signMessageCall = mockBaseCoin.signMessage.getCall(0); + signMessageCall.args[0].should.deepEqual({ prv: decryptedPrv }); + }); + + it('should stringify a Record payload before signing with prv', async function () { + await tradingAccount.signPayload({ payload, prv: decryptedPrv }); + + const signMessageCall = mockBaseCoin.signMessage.getCall(0); + signMessageCall.args[1].should.equal(JSON.stringify(payload)); + }); + + it('should pass a string payload directly to signMessage when signing with prv', async function () { + await tradingAccount.signPayload({ payload: payloadString, prv: decryptedPrv }); + + const signMessageCall = mockBaseCoin.signMessage.getCall(0); + signMessageCall.args[1].should.equal(payloadString); + }); + }); + }); +}); diff --git a/modules/sdk-core/test/unit/bitgo/wallet/ofcWalletSignTransaction.ts b/modules/sdk-core/test/unit/bitgo/wallet/ofcWalletSignTransaction.ts new file mode 100644 index 0000000000..1b04a7dc21 --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/wallet/ofcWalletSignTransaction.ts @@ -0,0 +1,68 @@ +/** + * @prettier + */ +import sinon from 'sinon'; +import 'should'; +import { Wallet } from '../../../../src'; + +describe('Wallet - OFC signTransaction', function () { + let wallet: Wallet; + let mockBitGo: any; + let mockBaseCoin: any; + let mockWalletData: any; + + beforeEach(function () { + mockBitGo = { + url: sinon.stub().returns('https://test.bitgo.com'), + post: sinon.stub(), + get: sinon.stub(), + setRequestTracer: sinon.stub(), + }; + + mockBaseCoin = { + getFamily: sinon.stub().returns('ofc'), + url: sinon.stub().returns('https://test.bitgo.com/wallet'), + keychains: sinon.stub(), + supportsTss: sinon.stub().returns(false), + getMPCAlgorithm: sinon.stub(), + presignTransaction: sinon.stub().resolvesArg(0), + keyIdsForSigning: sinon.stub().returns([0]), + signTransaction: sinon.stub().resolves({ halfSigned: { payload: 'test', signature: 'aabbcc' } }), + }; + + mockWalletData = { + id: 'test-wallet-id', + coin: 'ofcusdt', + keys: ['user-key', 'backup-key', 'bitgo-key'], + multisigType: 'onchain', + enterprise: 'ent-id', + }; + + wallet = new Wallet(mockBitGo, mockBaseCoin, mockWalletData); + }); + + afterEach(function () { + sinon.restore(); + }); + + it('should pass prv and paylaod to baseCoin.signTransaction', async function () { + const txPrebuild = { txInfo: { payload: '{"amount":"100"}' } } as any; + const prv = 'test-prv'; + + await wallet.signTransaction({ txPrebuild, prv }); + + mockBaseCoin.signTransaction.calledOnce.should.be.true(); + mockBaseCoin.signTransaction.calledWith({ txPrebuild, prv }); + const callArgs = mockBaseCoin.signTransaction.getCall(0).args[0]; + callArgs.prv.should.equal(prv); + }); + + it('should return the result from baseCoin.signTransaction', async function () { + const txPrebuild = { txInfo: { payload: '{"amount":"100"}' } } as any; + const prv = 'test-prv'; + + const result = await wallet.signTransaction({ txPrebuild, prv }); + + result.should.deepEqual({ halfSigned: { payload: 'test', signature: 'aabbcc' } }); + }); +}); From 06d0ec57890391918bf69ea811c52e3a38be1427 Mon Sep 17 00:00:00 2001 From: alextse-bg Date: Mon, 27 Apr 2026 12:09:16 -0400 Subject: [PATCH 2/3] feat(sdk-core): make wallet passphrase optioanl for prepareAllocation TICKET: WCN-217 --- modules/sdk-core/src/bitgo/trading/network/network.ts | 7 ++----- modules/sdk-core/src/bitgo/trading/network/types.ts | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/modules/sdk-core/src/bitgo/trading/network/network.ts b/modules/sdk-core/src/bitgo/trading/network/network.ts index ed8698d721..90902cac32 100644 --- a/modules/sdk-core/src/bitgo/trading/network/network.ts +++ b/modules/sdk-core/src/bitgo/trading/network/network.ts @@ -109,7 +109,7 @@ export class TradingNetwork implements ITradingNetwork { /** * Prepare an allocation for submission - * @param {string} walletPassphrase ofc wallet passphrase + * @param {string} walletPassphrase ofc wallet passphrase - required only when signing via user key * @param {string} connectionId connection to whom to make the allocation or deallocation * @param {string=} clientExternalId one time generated uuid v4 * @param {string} currency currency for which the allocation should be made. e.g. btc / tbtc @@ -130,10 +130,7 @@ export class TradingNetwork implements ITradingNetwork { } const payload = JSON.stringify(body); - - const prv = await this.wallet.getPrv({ walletPassphrase }); - const signedBuffer: Buffer = await this.wallet.baseCoin.signMessage({ prv }, payload); - const signature = signedBuffer.toString('hex'); + const signature = await this.wallet.toTradingAccount().signPayload({ payload, walletPassphrase }); return { ...body, diff --git a/modules/sdk-core/src/bitgo/trading/network/types.ts b/modules/sdk-core/src/bitgo/trading/network/types.ts index e6da5283f4..0d120fa07c 100644 --- a/modules/sdk-core/src/bitgo/trading/network/types.ts +++ b/modules/sdk-core/src/bitgo/trading/network/types.ts @@ -125,7 +125,7 @@ export type GetNetworkAllocationByIdResponse = { }; export type PrepareNetworkAllocationParams = Omit & { - walletPassphrase: string; + walletPassphrase?: string; clientExternalId?: string; nonce?: string; }; From c9e064af652ba815ac5b30699677aadfb0fe8a2a Mon Sep 17 00:00:00 2001 From: mrdanish26 Date: Thu, 23 Apr 2026 15:06:38 -0700 Subject: [PATCH 3/3] feat(express): support optional passphrase in handleV2OFCSignPayload When no walletPassphrase is present in the request body or environment, pass undefined to tradingAccount.signPayload() instead of throwing. The SDK routes passphrase-less signing through KMS internally. Ticket: WCN-215-1 --- modules/express/src/clientRoutes.ts | 13 ++- .../test/unit/typedRoutes/ofcSignPayload.ts | 93 +++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index 0ae3d95021..dd39f1cece 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -405,6 +405,15 @@ function getWalletPwFromEnv(walletId: string): string { return walletPw; } +/** + * Returns the wallet passphrase from the environment, or undefined if not set. + * Unlike getWalletPwFromEnv, this does not throw when the env variable is absent. + * Use this when the passphrase is optional (e.g. KMS-backed wallets). + */ +function findWalletPwFromEnv(walletId: string): string | undefined { + return process.env[`WALLET_${walletId}_PASSPHRASE`]; +} + async function getEncryptedPrivKey(path: string, walletId: string): Promise { const privKeyFile = await fs.readFile(path, { encoding: 'utf8' }); const encryptedPrivKey = JSON.parse(privKeyFile); @@ -631,7 +640,9 @@ export async function handleV2OFCSignPayload( throw new ApiResponseError(`Could not find OFC wallet ${walletId}`, 404); } - const walletPassphrase = bodyWalletPassphrase || getWalletPwFromEnv(wallet.id()); + // Prefer the passphrase from the request body; fall back to the env var. + // If neither is present, pass undefined — signPayload() routes to KMS internally. + const walletPassphrase = bodyWalletPassphrase ?? findWalletPwFromEnv(wallet.id()); const tradingAccount = wallet.toTradingAccount(); const stringifiedPayload = typeof payload === 'string' ? payload : JSON.stringify(payload); const signature = await tradingAccount.signPayload({ diff --git a/modules/express/test/unit/typedRoutes/ofcSignPayload.ts b/modules/express/test/unit/typedRoutes/ofcSignPayload.ts index 59cdd54f4d..9b0685183d 100644 --- a/modules/express/test/unit/typedRoutes/ofcSignPayload.ts +++ b/modules/express/test/unit/typedRoutes/ofcSignPayload.ts @@ -223,10 +223,103 @@ describe('OfcSignPayload codec tests', function () { const decodedResponse = assertDecode(OfcSignPayloadResponse200, result.body); assert.strictEqual(decodedResponse.signature, mockSignPayloadResponse.signature); + // Verify env passphrase was forwarded to signPayload + const signCall = mockTradingAccount.signPayload.getCall(0); + assert.ok(signCall, 'tradingAccount.signPayload should have been called'); + assert.strictEqual(signCall.args[0].walletPassphrase, 'env_passphrase', 'env passphrase should be forwarded'); + // Cleanup environment variable delete process.env['WALLET_ofc-wallet-id-123_PASSPHRASE']; }); + it('should pass undefined walletPassphrase to signPayload when no passphrase in body or env (KMS path)', async function () { + const requestBody = { + walletId: 'ofc-wallet-id-no-passphrase', + payload: { amount: '1000000', currency: 'USD' }, + // no walletPassphrase + }; + + // Ensure no env var is set for this wallet + delete process.env['WALLET_ofc-wallet-id-no-passphrase_PASSPHRASE']; + + const mockTradingAccount = { + signPayload: sinon.stub().resolves(mockSignPayloadResponse.signature), + }; + + const mockWallet = { + id: () => requestBody.walletId, + toTradingAccount: sinon.stub().returns(mockTradingAccount), + }; + + const walletsGetStub = sinon.stub().resolves(mockWallet); + const mockWallets = { get: walletsGetStub }; + const mockCoin = { wallets: sinon.stub().returns(mockWallets) }; + sinon.stub(BitGo.prototype, 'coin').returns(mockCoin as any); + + const result = await agent + .post('/api/v2/ofc/signPayload') + .set('Authorization', 'Bearer test_access_token_12345') + .set('Content-Type', 'application/json') + .send(requestBody); + + assert.strictEqual(result.status, 200); + const decodedResponse = assertDecode(OfcSignPayloadResponse200, result.body); + assert.strictEqual(decodedResponse.signature, mockSignPayloadResponse.signature); + + // signPayload must be called with walletPassphrase=undefined so the SDK routes to KMS + const signCall = mockTradingAccount.signPayload.getCall(0); + assert.ok(signCall, 'tradingAccount.signPayload should have been called'); + assert.strictEqual( + signCall.args[0].walletPassphrase, + undefined, + 'walletPassphrase should be undefined to trigger KMS signing' + ); + }); + + it('should prefer body walletPassphrase over env passphrase', async function () { + const requestBody = { + walletId: 'ofc-wallet-id-123', + payload: { amount: '500' }, + walletPassphrase: 'body_passphrase', + }; + + // Set a different env passphrase — body should win + process.env['WALLET_ofc-wallet-id-123_PASSPHRASE'] = 'env_passphrase'; + + const mockTradingAccount = { + signPayload: sinon.stub().resolves(mockSignPayloadResponse.signature), + }; + + const mockWallet = { + id: () => requestBody.walletId, + toTradingAccount: sinon.stub().returns(mockTradingAccount), + }; + + const walletsGetStub = sinon.stub().resolves(mockWallet); + const mockWallets = { get: walletsGetStub }; + const mockCoin = { wallets: sinon.stub().returns(mockWallets) }; + sinon.stub(BitGo.prototype, 'coin').returns(mockCoin as any); + + const result = await agent + .post('/api/v2/ofc/signPayload') + .set('Authorization', 'Bearer test_access_token_12345') + .set('Content-Type', 'application/json') + .send(requestBody); + + assert.strictEqual(result.status, 200); + + // body passphrase should take precedence + const signCall = mockTradingAccount.signPayload.getCall(0); + assert.ok(signCall, 'tradingAccount.signPayload should have been called'); + assert.strictEqual( + signCall.args[0].walletPassphrase, + 'body_passphrase', + 'body passphrase should take precedence over env' + ); + + delete process.env['WALLET_ofc-wallet-id-123_PASSPHRASE']; + }); + it('should successfully sign complex nested JSON payload', async function () { const requestBody = { walletId: 'ofc-wallet-id-123',