Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions examples/ts/create-go-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Create a Go Account wallet at BitGo.
* This makes use of the convenience function generateWallet with type: 'trading'
*
* IMPORTANT: You must backup the encrypted private key and encrypted wallet passphrase!
*
* Copyright 2025, BitGo, Inc. All Rights Reserved.
*/

import { BitGoAPI } from '@bitgo/sdk-api';
import { coins } from 'bitgo';
require('dotenv').config({ path: '../../.env' });

const bitgo = new BitGoAPI({
accessToken: process.env.TESTNET_ACCESS_TOKEN,
env: 'test', // Change this to env: 'production' when you are ready for production
});

// Go Accounts use the 'ofc' (Off-Chain) coin
const coin = 'ofc';
bitgo.register(coin, coins.Ofc.createInstance);

// TODO: set a label for your new Go Account here
const label = 'Example Go Account Wallet';

// TODO: set your passphrase for your new wallet here (encrypts the private key)
const passphrase = 'go_account_wallet_passphrase';

// TODO: set your passcode encryption code here (encrypts the passphrase itself)
const passcodeEncryptionCode = 'encryption_code_for_passphrase';

// TODO: set your enterprise ID for your new wallet here
const enterprise = 'your_enterprise_id';

async function main() {
const response = await bitgo.coin(coin).wallets().generateWallet({
label,
passphrase,
passcodeEncryptionCode,
enterprise,
type: 'trading', // Required for Go Accounts
});

// Type guard to ensure we got a Go Account response
if (!('userKeychain' in response)) {
throw new Error('Go account missing required user keychain');
}

const { wallet, userKeychain, encryptedWalletPassphrase } = response;

console.log(`Wallet ID: ${wallet.id()}`);

console.log('BACKUP THE FOLLOWING INFORMATION: ');
console.log('User Keychain:');
console.log(`Keychain ID: ${userKeychain.id}`);
console.log(`Public Key: ${userKeychain.pub}`);
console.log(`Encrypted Private Key: ${userKeychain.encryptedPrv}`);

console.log(`Encrypted Wallet Passphrase: ${encryptedWalletPassphrase}`);

// Create receive address for Go Account
const receiveAddress = await wallet.createAddress();
console.log('Go Account Receive Address:', receiveAddress.address);
}

main().catch((e) => console.error('Error creating Go Account:', e));

45 changes: 45 additions & 0 deletions modules/bitgo/test/v2/unit/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,51 @@ describe('V2 Wallets:', function () {
params.passphrase
);
});

it('should generate Go Account wallet', async () => {
const ofcWallets = bitgo.coin('ofc').wallets();

const params: GenerateWalletOptions = {
label: 'Go Account Wallet',
passphrase: 'go_account_password',
enterprise: 'enterprise-id',
passcodeEncryptionCode: 'originalPasscodeEncryptionCode',
type: 'trading',
};

const keychainId = 'user_keychain_id';

// Mock keychain creation and upload
nock(bgUrl)
.post('/api/v2/ofc/key', function (body) {
body.should.have.property('encryptedPrv');
body.should.have.property('originalPasscodeEncryptionCode');
body.keyType.should.equal('independent');
body.source.should.equal('user');
return true;
})
.reply(200, { id: keychainId, pub: 'userPub', encryptedPrv: 'encryptedPrivateKey' });

// Mock wallet creation
const walletNock = nock(bgUrl)
.post('/api/v2/ofc/wallet/add', function (body) {
body.type.should.equal('trading');
body.m.should.equal(1);
body.n.should.equal(1);
body.keys.should.have.length(1);
body.keys[0].should.equal(keychainId);
return true;
})
.reply(200, { id: 'wallet123', keys: [keychainId] });

const response = await ofcWallets.generateWallet(params);

walletNock.isDone().should.be.true();

assert.ok(response.encryptedWalletPassphrase);
assert.ok(response.wallet);
assert.ok('userKeychain' in response);
});
});

describe('Generate TSS wallet:', function () {
Expand Down
2 changes: 1 addition & 1 deletion modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export interface SupplementGenerateWalletOptions {
rootPrivateKey?: string;
disableKRSEmail?: boolean;
multisigType?: 'tss' | 'onchain' | 'blsdkg';
type: 'hot' | 'cold' | 'custodial' | 'advanced';
type: 'hot' | 'cold' | 'custodial' | 'advanced' | 'trading';
subType?: 'lightningCustody' | 'lightningSelfCustody' | 'onPrem';
coinSpecific?: { [coinName: string]: unknown };
evmKeyRingReferenceWalletId?: string;
Expand Down
2 changes: 2 additions & 0 deletions modules/sdk-core/src/bitgo/keychain/iKeychains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export interface Keychain {
coinSpecific?: { [coinName: string]: unknown };
// Alternative encryptedPrv using webauthn and the prf extension
webauthnDevices?: KeychainWebauthnDevice[];
// Ethereum address derived from xpub
ethAddress?: string;
}

export type OptionalKeychainEncryptedKey = Pick<Keychain, 'encryptedPrv' | 'webauthnDevices'>;
Expand Down
28 changes: 26 additions & 2 deletions modules/sdk-core/src/bitgo/wallet/iWallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as t from 'io-ts';

import { IRequestTracer } from '../../api';
import { KeychainsTriplet, LightningKeychainsTriplet } from '../baseCoin';
import { Keychain } from '../keychain';
import { IWallet, PaginationOptions, WalletShare } from './iWallet';
import { Wallet } from './wallet';

Expand All @@ -19,6 +20,14 @@ export interface LightningWalletWithKeychains extends LightningKeychainsTriplet
encryptedWalletPassphrase?: string;
}

export interface GoAccountWalletWithUserKeychain {
responseType: 'GoAccountWalletWithUserKeychain';
wallet: IWallet;
userKeychain: Keychain;
warning?: string;
encryptedWalletPassphrase?: string;
}

export interface GetWalletOptions {
allTokens?: boolean;
reqId?: IRequestTracer;
Expand Down Expand Up @@ -68,7 +77,7 @@ export interface GenerateWalletOptions {
isDistributedCustody?: boolean;
bitgoKeyId?: string;
commonKeychain?: string;
type?: 'hot' | 'cold' | 'custodial';
type?: 'hot' | 'cold' | 'custodial' | 'trading';
subType?: 'lightningCustody' | 'lightningSelfCustody';
evmKeyRingReferenceWalletId?: string;
}
Expand All @@ -86,6 +95,19 @@ export const GenerateLightningWalletOptionsCodec = t.strict(

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

export const GenerateGoAccountWalletOptionsCodec = t.strict(
{
label: t.string,
passphrase: t.string,
enterprise: t.string,
passcodeEncryptionCode: t.string,
type: t.literal('trading'),
},
'GenerateGoAccountWalletOptions'
);

export type GenerateGoAccountWalletOptions = t.TypeOf<typeof GenerateGoAccountWalletOptionsCodec>;

export interface GetWalletByAddressOptions {
address?: string;
reqId?: IRequestTracer;
Expand Down Expand Up @@ -214,7 +236,9 @@ export interface IWallets {
get(params?: GetWalletOptions): Promise<Wallet>;
list(params?: ListWalletOptions): Promise<{ wallets: IWallet[] }>;
add(params?: AddWalletOptions): Promise<any>;
generateWallet(params?: GenerateWalletOptions): Promise<WalletWithKeychains | LightningWalletWithKeychains>;
generateWallet(
params?: GenerateWalletOptions
): Promise<WalletWithKeychains | LightningWalletWithKeychains | GoAccountWalletWithUserKeychain>;
listShares(params?: Record<string, unknown>): Promise<any>;
getShare(params?: { walletShareId?: string }): Promise<any>;
updateShare(params?: UpdateShareOptions): Promise<any>;
Expand Down
77 changes: 75 additions & 2 deletions modules/sdk-core/src/bitgo/wallet/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ import {
BulkUpdateWalletShareOptionsRequest,
BulkUpdateWalletShareResponse,
GenerateBaseMpcWalletOptions,
GenerateGoAccountWalletOptions,
GenerateGoAccountWalletOptionsCodec,
GenerateLightningWalletOptions,
GenerateLightningWalletOptionsCodec,
GenerateMpcWalletOptions,
GenerateSMCMpcWalletOptions,
GenerateWalletOptions,
GetWalletByAddressOptions,
GetWalletOptions,
GoAccountWalletWithUserKeychain,
IWallets,
LightningWalletWithKeychains,
ListWalletOptions,
Expand All @@ -47,7 +50,7 @@ import { createEvmKeyRingWallet, validateEvmKeyRingWalletParams } from '../evm/e
* Check if a wallet is a WalletWithKeychains
*/
export function isWalletWithKeychains(
wallet: WalletWithKeychains | LightningWalletWithKeychains
wallet: WalletWithKeychains | LightningWalletWithKeychains | GoAccountWalletWithUserKeychain
): wallet is WalletWithKeychains {
return wallet.responseType === 'WalletWithKeychains';
}
Expand Down Expand Up @@ -213,6 +216,57 @@ export class Wallets implements IWallets {
};
}

/**
* Generate a Go Account wallet
* @param params GenerateGoAccountWalletOptions
* @returns Promise<GoAccountWalletWithUserKeychain>
*/
private async generateGoAccountWallet(
params: GenerateGoAccountWalletOptions
): Promise<GoAccountWalletWithUserKeychain> {
const reqId = new RequestTracer();
this.bitgo.setRequestTracer(reqId);

const { label, passphrase, enterprise, passcodeEncryptionCode } = params;

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

const keychainParams: AddKeychainOptions = {
pub: keychain.pub,
encryptedPrv: this.bitgo.encrypt({ password: passphrase, input: keychain.prv }),
originalPasscodeEncryptionCode: passcodeEncryptionCode,
keyType: 'independent',
source: 'user',
};

const userKeychain = await this.baseCoin.keychains().add(keychainParams);

const walletParams: SupplementGenerateWalletOptions = {
label,
m: 1,
n: 1,
type: 'trading',
enterprise,
keys: [userKeychain.id],
};

const newWallet = await this.bitgo.post(this.baseCoin.url('/wallet/add')).send(walletParams).result();
const wallet = new Wallet(this.bitgo, this.baseCoin, newWallet);

const result: GoAccountWalletWithUserKeychain = {
wallet,
userKeychain,
responseType: 'GoAccountWalletWithUserKeychain',
};

// Add warning if the user keychain has an encrypted private key
if (!_.isUndefined(userKeychain.encryptedPrv)) {
result.warning = 'Be sure to backup the user keychain -- it is not stored anywhere else!';
}

return result;
}

/**
* Generate a new wallet
* 1. Creates the user keychain locally on the client, and encrypts it with the provided passphrase
Expand Down Expand Up @@ -246,7 +300,7 @@ export class Wallets implements IWallets {
*/
async generateWallet(
params: GenerateWalletOptions = {}
): Promise<WalletWithKeychains | LightningWalletWithKeychains> {
): Promise<WalletWithKeychains | LightningWalletWithKeychains | GoAccountWalletWithUserKeychain> {
// Assign the default multiSig type value based on the coin
if (!params.multisigType) {
params.multisigType = this.baseCoin.getDefaultMultisigType();
Expand All @@ -270,6 +324,25 @@ export class Wallets implements IWallets {
return walletData;
}

// Go Account wallet generation
if (this.baseCoin.getFamily() === 'ofc' && params.type === 'trading') {
const options = decodeOrElse(
GenerateGoAccountWalletOptionsCodec.name,
GenerateGoAccountWalletOptionsCodec,
params,
(errors) => {
throw new Error(`error(s) parsing generate go account request params: ${errors}`);
}
);

const walletData = await this.generateGoAccountWallet(options);
walletData.encryptedWalletPassphrase = this.bitgo.encrypt({
input: options.passphrase,
password: options.passcodeEncryptionCode,
});
return walletData;
}

common.validateParams(params, ['label'], ['passphrase', 'userKey', 'backupXpub']);
if (typeof params.label !== 'string') {
throw new Error('missing required string parameter label');
Expand Down