Skip to content

Commit d557e31

Browse files
authored
Merge pull request #7268 from BitGo/CAAS-491
feat(sdk-core): generate Go account wallet
2 parents bb1674a + 7d5e752 commit d557e31

File tree

6 files changed

+216
-5
lines changed

6 files changed

+216
-5
lines changed

examples/ts/create-go-account.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Create a Go Account wallet at BitGo.
3+
* This makes use of the convenience function generateWallet with type: 'trading'
4+
*
5+
* IMPORTANT: You must backup the encrypted private key and encrypted wallet passphrase!
6+
*
7+
* Copyright 2025, BitGo, Inc. All Rights Reserved.
8+
*/
9+
10+
import { BitGoAPI } from '@bitgo/sdk-api';
11+
import { coins } from 'bitgo';
12+
require('dotenv').config({ path: '../../.env' });
13+
14+
const bitgo = new BitGoAPI({
15+
accessToken: process.env.TESTNET_ACCESS_TOKEN,
16+
env: 'test', // Change this to env: 'production' when you are ready for production
17+
});
18+
19+
// Go Accounts use the 'ofc' (Off-Chain) coin
20+
const coin = 'ofc';
21+
bitgo.register(coin, coins.Ofc.createInstance);
22+
23+
// TODO: set a label for your new Go Account here
24+
const label = 'Example Go Account Wallet';
25+
26+
// TODO: set your passphrase for your new wallet here (encrypts the private key)
27+
const passphrase = 'go_account_wallet_passphrase';
28+
29+
// TODO: set your passcode encryption code here (encrypts the passphrase itself)
30+
const passcodeEncryptionCode = 'encryption_code_for_passphrase';
31+
32+
// TODO: set your enterprise ID for your new wallet here
33+
const enterprise = 'your_enterprise_id';
34+
35+
async function main() {
36+
const response = await bitgo.coin(coin).wallets().generateWallet({
37+
label,
38+
passphrase,
39+
passcodeEncryptionCode,
40+
enterprise,
41+
type: 'trading', // Required for Go Accounts
42+
});
43+
44+
// Type guard to ensure we got a Go Account response
45+
if (!('userKeychain' in response)) {
46+
throw new Error('Go account missing required user keychain');
47+
}
48+
49+
const { wallet, userKeychain, encryptedWalletPassphrase } = response;
50+
51+
console.log(`Wallet ID: ${wallet.id()}`);
52+
53+
console.log('BACKUP THE FOLLOWING INFORMATION: ');
54+
console.log('User Keychain:');
55+
console.log(`Keychain ID: ${userKeychain.id}`);
56+
console.log(`Public Key: ${userKeychain.pub}`);
57+
console.log(`Encrypted Private Key: ${userKeychain.encryptedPrv}`);
58+
59+
console.log(`Encrypted Wallet Passphrase: ${encryptedWalletPassphrase}`);
60+
61+
// Create receive address for Go Account
62+
const receiveAddress = await wallet.createAddress();
63+
console.log('Go Account Receive Address:', receiveAddress.address);
64+
}
65+
66+
main().catch((e) => console.error('Error creating Go Account:', e));
67+

modules/bitgo/test/v2/unit/wallets.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,51 @@ describe('V2 Wallets:', function () {
515515
params.passphrase
516516
);
517517
});
518+
519+
it('should generate Go Account wallet', async () => {
520+
const ofcWallets = bitgo.coin('ofc').wallets();
521+
522+
const params: GenerateWalletOptions = {
523+
label: 'Go Account Wallet',
524+
passphrase: 'go_account_password',
525+
enterprise: 'enterprise-id',
526+
passcodeEncryptionCode: 'originalPasscodeEncryptionCode',
527+
type: 'trading',
528+
};
529+
530+
const keychainId = 'user_keychain_id';
531+
532+
// Mock keychain creation and upload
533+
nock(bgUrl)
534+
.post('/api/v2/ofc/key', function (body) {
535+
body.should.have.property('encryptedPrv');
536+
body.should.have.property('originalPasscodeEncryptionCode');
537+
body.keyType.should.equal('independent');
538+
body.source.should.equal('user');
539+
return true;
540+
})
541+
.reply(200, { id: keychainId, pub: 'userPub', encryptedPrv: 'encryptedPrivateKey' });
542+
543+
// Mock wallet creation
544+
const walletNock = nock(bgUrl)
545+
.post('/api/v2/ofc/wallet/add', function (body) {
546+
body.type.should.equal('trading');
547+
body.m.should.equal(1);
548+
body.n.should.equal(1);
549+
body.keys.should.have.length(1);
550+
body.keys[0].should.equal(keychainId);
551+
return true;
552+
})
553+
.reply(200, { id: 'wallet123', keys: [keychainId] });
554+
555+
const response = await ofcWallets.generateWallet(params);
556+
557+
walletNock.isDone().should.be.true();
558+
559+
assert.ok(response.encryptedWalletPassphrase);
560+
assert.ok(response.wallet);
561+
assert.ok('userKeychain' in response);
562+
});
518563
});
519564

520565
describe('Generate TSS wallet:', function () {

modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ export interface SupplementGenerateWalletOptions {
219219
rootPrivateKey?: string;
220220
disableKRSEmail?: boolean;
221221
multisigType?: 'tss' | 'onchain' | 'blsdkg';
222-
type: 'hot' | 'cold' | 'custodial' | 'advanced';
222+
type: 'hot' | 'cold' | 'custodial' | 'advanced' | 'trading';
223223
subType?: 'lightningCustody' | 'lightningSelfCustody' | 'onPrem';
224224
coinSpecific?: { [coinName: string]: unknown };
225225
evmKeyRingReferenceWalletId?: string;

modules/sdk-core/src/bitgo/keychain/iKeychains.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export interface Keychain {
4646
coinSpecific?: { [coinName: string]: unknown };
4747
// Alternative encryptedPrv using webauthn and the prf extension
4848
webauthnDevices?: KeychainWebauthnDevice[];
49+
// Ethereum address derived from xpub
50+
ethAddress?: string;
4951
}
5052

5153
export type OptionalKeychainEncryptedKey = Pick<Keychain, 'encryptedPrv' | 'webauthnDevices'>;

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

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as t from 'io-ts';
22

33
import { IRequestTracer } from '../../api';
44
import { KeychainsTriplet, LightningKeychainsTriplet } from '../baseCoin';
5+
import { Keychain } from '../keychain';
56
import { IWallet, PaginationOptions, WalletShare } from './iWallet';
67
import { Wallet } from './wallet';
78

@@ -19,6 +20,14 @@ export interface LightningWalletWithKeychains extends LightningKeychainsTriplet
1920
encryptedWalletPassphrase?: string;
2021
}
2122

23+
export interface GoAccountWalletWithUserKeychain {
24+
responseType: 'GoAccountWalletWithUserKeychain';
25+
wallet: IWallet;
26+
userKeychain: Keychain;
27+
warning?: string;
28+
encryptedWalletPassphrase?: string;
29+
}
30+
2231
export interface GetWalletOptions {
2332
allTokens?: boolean;
2433
reqId?: IRequestTracer;
@@ -68,7 +77,7 @@ export interface GenerateWalletOptions {
6877
isDistributedCustody?: boolean;
6978
bitgoKeyId?: string;
7079
commonKeychain?: string;
71-
type?: 'hot' | 'cold' | 'custodial';
80+
type?: 'hot' | 'cold' | 'custodial' | 'trading';
7281
subType?: 'lightningCustody' | 'lightningSelfCustody';
7382
evmKeyRingReferenceWalletId?: string;
7483
}
@@ -86,6 +95,19 @@ export const GenerateLightningWalletOptionsCodec = t.strict(
8695

8796
export type GenerateLightningWalletOptions = t.TypeOf<typeof GenerateLightningWalletOptionsCodec>;
8897

98+
export const GenerateGoAccountWalletOptionsCodec = t.strict(
99+
{
100+
label: t.string,
101+
passphrase: t.string,
102+
enterprise: t.string,
103+
passcodeEncryptionCode: t.string,
104+
type: t.literal('trading'),
105+
},
106+
'GenerateGoAccountWalletOptions'
107+
);
108+
109+
export type GenerateGoAccountWalletOptions = t.TypeOf<typeof GenerateGoAccountWalletOptionsCodec>;
110+
89111
export interface GetWalletByAddressOptions {
90112
address?: string;
91113
reqId?: IRequestTracer;
@@ -214,7 +236,9 @@ export interface IWallets {
214236
get(params?: GetWalletOptions): Promise<Wallet>;
215237
list(params?: ListWalletOptions): Promise<{ wallets: IWallet[] }>;
216238
add(params?: AddWalletOptions): Promise<any>;
217-
generateWallet(params?: GenerateWalletOptions): Promise<WalletWithKeychains | LightningWalletWithKeychains>;
239+
generateWallet(
240+
params?: GenerateWalletOptions
241+
): Promise<WalletWithKeychains | LightningWalletWithKeychains | GoAccountWalletWithUserKeychain>;
218242
listShares(params?: Record<string, unknown>): Promise<any>;
219243
getShare(params?: { walletShareId?: string }): Promise<any>;
220244
updateShare(params?: UpdateShareOptions): Promise<any>;

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

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@ import {
2424
BulkUpdateWalletShareOptionsRequest,
2525
BulkUpdateWalletShareResponse,
2626
GenerateBaseMpcWalletOptions,
27+
GenerateGoAccountWalletOptions,
28+
GenerateGoAccountWalletOptionsCodec,
2729
GenerateLightningWalletOptions,
2830
GenerateLightningWalletOptionsCodec,
2931
GenerateMpcWalletOptions,
3032
GenerateSMCMpcWalletOptions,
3133
GenerateWalletOptions,
3234
GetWalletByAddressOptions,
3335
GetWalletOptions,
36+
GoAccountWalletWithUserKeychain,
3437
IWallets,
3538
LightningWalletWithKeychains,
3639
ListWalletOptions,
@@ -47,7 +50,7 @@ import { createEvmKeyRingWallet, validateEvmKeyRingWalletParams } from '../evm/e
4750
* Check if a wallet is a WalletWithKeychains
4851
*/
4952
export function isWalletWithKeychains(
50-
wallet: WalletWithKeychains | LightningWalletWithKeychains
53+
wallet: WalletWithKeychains | LightningWalletWithKeychains | GoAccountWalletWithUserKeychain
5154
): wallet is WalletWithKeychains {
5255
return wallet.responseType === 'WalletWithKeychains';
5356
}
@@ -213,6 +216,57 @@ export class Wallets implements IWallets {
213216
};
214217
}
215218

219+
/**
220+
* Generate a Go Account wallet
221+
* @param params GenerateGoAccountWalletOptions
222+
* @returns Promise<GoAccountWalletWithUserKeychain>
223+
*/
224+
private async generateGoAccountWallet(
225+
params: GenerateGoAccountWalletOptions
226+
): Promise<GoAccountWalletWithUserKeychain> {
227+
const reqId = new RequestTracer();
228+
this.bitgo.setRequestTracer(reqId);
229+
230+
const { label, passphrase, enterprise, passcodeEncryptionCode } = params;
231+
232+
const keychain = this.baseCoin.keychains().create();
233+
234+
const keychainParams: AddKeychainOptions = {
235+
pub: keychain.pub,
236+
encryptedPrv: this.bitgo.encrypt({ password: passphrase, input: keychain.prv }),
237+
originalPasscodeEncryptionCode: passcodeEncryptionCode,
238+
keyType: 'independent',
239+
source: 'user',
240+
};
241+
242+
const userKeychain = await this.baseCoin.keychains().add(keychainParams);
243+
244+
const walletParams: SupplementGenerateWalletOptions = {
245+
label,
246+
m: 1,
247+
n: 1,
248+
type: 'trading',
249+
enterprise,
250+
keys: [userKeychain.id],
251+
};
252+
253+
const newWallet = await this.bitgo.post(this.baseCoin.url('/wallet/add')).send(walletParams).result();
254+
const wallet = new Wallet(this.bitgo, this.baseCoin, newWallet);
255+
256+
const result: GoAccountWalletWithUserKeychain = {
257+
wallet,
258+
userKeychain,
259+
responseType: 'GoAccountWalletWithUserKeychain',
260+
};
261+
262+
// Add warning if the user keychain has an encrypted private key
263+
if (!_.isUndefined(userKeychain.encryptedPrv)) {
264+
result.warning = 'Be sure to backup the user keychain -- it is not stored anywhere else!';
265+
}
266+
267+
return result;
268+
}
269+
216270
/**
217271
* Generate a new wallet
218272
* 1. Creates the user keychain locally on the client, and encrypts it with the provided passphrase
@@ -246,7 +300,7 @@ export class Wallets implements IWallets {
246300
*/
247301
async generateWallet(
248302
params: GenerateWalletOptions = {}
249-
): Promise<WalletWithKeychains | LightningWalletWithKeychains> {
303+
): Promise<WalletWithKeychains | LightningWalletWithKeychains | GoAccountWalletWithUserKeychain> {
250304
// Assign the default multiSig type value based on the coin
251305
if (!params.multisigType) {
252306
params.multisigType = this.baseCoin.getDefaultMultisigType();
@@ -270,6 +324,25 @@ export class Wallets implements IWallets {
270324
return walletData;
271325
}
272326

327+
// Go Account wallet generation
328+
if (this.baseCoin.getFamily() === 'ofc' && params.type === 'trading') {
329+
const options = decodeOrElse(
330+
GenerateGoAccountWalletOptionsCodec.name,
331+
GenerateGoAccountWalletOptionsCodec,
332+
params,
333+
(errors) => {
334+
throw new Error(`error(s) parsing generate go account request params: ${errors}`);
335+
}
336+
);
337+
338+
const walletData = await this.generateGoAccountWallet(options);
339+
walletData.encryptedWalletPassphrase = this.bitgo.encrypt({
340+
input: options.passphrase,
341+
password: options.passcodeEncryptionCode,
342+
});
343+
return walletData;
344+
}
345+
273346
common.validateParams(params, ['label'], ['passphrase', 'userKey', 'backupXpub']);
274347
if (typeof params.label !== 'string') {
275348
throw new Error('missing required string parameter label');

0 commit comments

Comments
 (0)