Skip to content

Commit e4d1cab

Browse files
feat(sdk-core,express): plumb userKeySigningRequired through generateWallet
Add optional `userKeySigningRequired?: boolean` to `GenerateWalletOptions` and `GenerateGoAccountWalletOptionsCodec` in `@bitgo/sdk-core`, and forward it into the `coinSpecific` payload sent to `POST /wallet/add` when creating OFC trading wallets via `generateGoAccountWallet`. Add the same field to the Express `GenerateWalletBody` io-ts codec so that the Express route `POST /api/v2/{coin}/wallet/generate` accepts and forwards the field to the SDK. Because Express routes pass `req.decoded` directly to `coin.wallets().generateWallet()`, no additional handler changes are required. Why: CAAS clients create OFC wallets through Express or the SDK, not by hitting the raw wallet-platform endpoint directly. Without this change, the `userKeySigningRequired` flag accepted by the WP `v2.wallet.add` endpoint (WCN-1441) was unreachable via the public SDK/Express surface, so CAAS clients had no way to opt out of user-key signing at wallet creation time. Tests: new `walletsGoAccount.ts` unit tests verify that `userKeySigningRequired: false` is forwarded in `coinSpecific.ofc`, that `true` is also forwarded when explicitly set, and that `coinSpecific` is omitted when the field is absent. Express typed-route tests verify the codec accepts `false` and rejects non-boolean values. Ticket: WCN-1442
1 parent ef25827 commit e4d1cab

5 files changed

Lines changed: 161 additions & 1 deletion

File tree

modules/express/src/typedRoutes/api/v2/generateWallet.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export const GenerateWalletBody = {
5555
passphrase: t.string,
5656
})
5757
),
58+
/** OFC only. When false, BitGo signs OFC payloads on behalf of the user without requiring a user-key signature. Defaults to true (user must sign). */
59+
userKeySigningRequired: optional(t.boolean),
5860
} as const;
5961

6062
export const GenerateWalletResponse200 = t.union([

modules/express/test/unit/typedRoutes/generateWallet.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,42 @@ describe('Generate Wallet Typed Routes Tests', function () {
440440
evmKeyRingReferenceWalletId
441441
);
442442
});
443+
444+
it('should forward userKeySigningRequired to generateWallet for OFC wallet', async function () {
445+
const coin = 'tofc';
446+
const label = 'OFC Test Wallet';
447+
const enterprise = 'enterprise123';
448+
449+
const mockWallet = {
450+
id: 'ofcWallet123',
451+
coin,
452+
label,
453+
toJSON: sinon.stub().returns({ id: 'ofcWallet123', coin, label }),
454+
};
455+
456+
const walletResponse = {
457+
wallet: mockWallet,
458+
userKeychain: { id: 'userKey123' },
459+
};
460+
461+
const generateWalletStub = sinon.stub().resolves(walletResponse);
462+
const walletsStub = { generateWallet: generateWalletStub } as any;
463+
const coinStub = { wallets: sinon.stub().returns(walletsStub) } as any;
464+
465+
sinon.stub(BitGo.prototype, 'coin').returns(coinStub);
466+
467+
const res = await agent.post(`/api/v2/${coin}/wallet/generate`).send({
468+
label,
469+
enterprise,
470+
passphrase: 'test-passphrase',
471+
type: 'trading',
472+
userKeySigningRequired: false,
473+
});
474+
475+
res.status.should.equal(200);
476+
generateWalletStub.should.have.been.calledOnce();
477+
generateWalletStub.firstCall.args[0].should.have.property('userKeySigningRequired', false);
478+
});
443479
});
444480

445481
describe('Codec Validation', function () {
@@ -575,6 +611,21 @@ describe('Generate Wallet Typed Routes Tests', function () {
575611
res.body.should.have.property('error');
576612
res.body.error.should.match(/disableTransactionNotifications/);
577613
});
614+
615+
it('should return 400 when userKeySigningRequired is not boolean', async function () {
616+
const coin = 'tofc';
617+
618+
const res = await agent.post(`/api/v2/${coin}/wallet/generate`).send({
619+
label: 'Test OFC Wallet',
620+
enterprise: 'enterprise123',
621+
type: 'trading',
622+
userKeySigningRequired: 'false',
623+
});
624+
625+
res.status.should.equal(400);
626+
res.body.should.have.property('error');
627+
res.body.error.should.match(/userKeySigningRequired/);
628+
});
578629
});
579630

580631
describe('Handler Errors', function () {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ export interface GenerateWalletOptions {
262262
encryptionVersion?: EncryptionVersion;
263263
/** Delegates user/backup key creation to an external signer (onchain multisig only). */
264264
createKeychainCallback?: CreateKeychainCallback;
265+
/** OFC only. When false, BitGo signs OFC payloads on behalf of the user without requiring a user-key signature. Defaults to true (user must sign). */
266+
userKeySigningRequired?: boolean;
265267
}
266268

267269
export interface GenerateWalletWithExternalSignerOptions
@@ -305,6 +307,7 @@ export const GenerateGoAccountWalletOptionsCodec = t.intersection(
305307
t.partial({
306308
// Codec intentionally accepts only 2: v1 is the implicit default and never sent on the wire.
307309
encryptionVersion: t.literal(2),
310+
userKeySigningRequired: t.boolean,
308311
}),
309312
],
310313
'GenerateGoAccountWalletOptions'

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ export class Wallets implements IWallets {
234234
const reqId = new RequestTracer();
235235
this.bitgo.setRequestTracer(reqId);
236236

237-
const { label, passphrase, enterprise, passcodeEncryptionCode, encryptionVersion } = params;
237+
const { label, passphrase, enterprise, passcodeEncryptionCode, encryptionVersion, userKeySigningRequired } = params;
238238

239239
const keychain = this.baseCoin.keychains().create();
240240

@@ -259,6 +259,9 @@ export class Wallets implements IWallets {
259259
type: 'trading',
260260
enterprise,
261261
keys: [userKeychain.id],
262+
...(userKeySigningRequired !== undefined && {
263+
coinSpecific: { [this.baseCoin.getChain()]: { userKeySigningRequired } },
264+
}),
262265
};
263266

264267
const newWallet = await this.bitgo.post(this.baseCoin.url('/wallet/add')).send(walletParams).result();
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import * as assert from 'assert';
2+
import * as sinon from 'sinon';
3+
import 'should';
4+
import { Wallets } from '../../../../src/bitgo/wallet/wallets';
5+
6+
describe('Wallets - GoAccount (OFC trading) wallet creation', function () {
7+
let wallets: Wallets;
8+
let mockBitGo: any;
9+
let mockBaseCoin: any;
10+
let mockKeychains: any;
11+
let postChain: any;
12+
13+
const userPrv = 'xprvSomeUserPrivateKey';
14+
const userPub =
15+
'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8';
16+
17+
beforeEach(function () {
18+
mockKeychains = {
19+
create: sinon.stub().returns({ pub: userPub, prv: userPrv }),
20+
add: sinon.stub().resolves({ id: 'user-key-id', pub: userPub, encryptedPrv: 'encrypted-prv' }),
21+
};
22+
23+
const mockWalletData = { id: 'wallet-id', keys: ['user-key-id'] };
24+
25+
postChain = {
26+
send: sinon.stub(),
27+
};
28+
postChain.send.returns({
29+
result: sinon.stub().resolves(mockWalletData),
30+
});
31+
32+
mockBitGo = {
33+
post: sinon.stub().returns(postChain),
34+
encrypt: sinon
35+
.stub()
36+
.callsFake(
37+
async ({ password, input }: { password: string; input: string }) => `encrypted:${password}:${input}`
38+
),
39+
setRequestTracer: sinon.stub(),
40+
};
41+
42+
mockBaseCoin = {
43+
getFamily: sinon.stub().returns('ofc'),
44+
getChain: sinon.stub().returns('ofc'),
45+
getDefaultMultisigType: sinon.stub().returns('onchain'),
46+
supportsTss: sinon.stub().returns(false),
47+
keychains: sinon.stub().returns(mockKeychains),
48+
url: sinon.stub().returns('/test/url'),
49+
};
50+
51+
wallets = new Wallets(mockBitGo, mockBaseCoin);
52+
});
53+
54+
afterEach(function () {
55+
sinon.restore();
56+
});
57+
58+
it('should forward userKeySigningRequired: false in coinSpecific when provided', async function () {
59+
await wallets.generateWallet({
60+
label: 'Test OFC Wallet',
61+
passphrase: 'test-passphrase',
62+
enterprise: 'enterprise-123',
63+
passcodeEncryptionCode: 'pce-code',
64+
type: 'trading',
65+
userKeySigningRequired: false,
66+
});
67+
68+
assert.ok(postChain.send.calledOnce, 'POST /wallet/add should be called once');
69+
const sentParams = postChain.send.firstCall.args[0];
70+
sentParams.should.have.property('coinSpecific');
71+
sentParams.coinSpecific.should.have.property('ofc');
72+
sentParams.coinSpecific.ofc.should.have.property('userKeySigningRequired', false);
73+
});
74+
75+
it('should forward userKeySigningRequired: true in coinSpecific when explicitly set', async function () {
76+
await wallets.generateWallet({
77+
label: 'Test OFC Wallet',
78+
passphrase: 'test-passphrase',
79+
enterprise: 'enterprise-123',
80+
passcodeEncryptionCode: 'pce-code',
81+
type: 'trading',
82+
userKeySigningRequired: true,
83+
});
84+
85+
const sentParams = postChain.send.firstCall.args[0];
86+
sentParams.coinSpecific.ofc.should.have.property('userKeySigningRequired', true);
87+
});
88+
89+
it('should omit coinSpecific when userKeySigningRequired is not provided', async function () {
90+
await wallets.generateWallet({
91+
label: 'Test OFC Wallet',
92+
passphrase: 'test-passphrase',
93+
enterprise: 'enterprise-123',
94+
passcodeEncryptionCode: 'pce-code',
95+
type: 'trading',
96+
});
97+
98+
const sentParams = postChain.send.firstCall.args[0];
99+
sentParams.should.not.have.property('coinSpecific');
100+
});
101+
});

0 commit comments

Comments
 (0)