diff --git a/examples/ts/create-go-account.ts b/examples/ts/create-go-account.ts new file mode 100644 index 0000000000..072c0e11d5 --- /dev/null +++ b/examples/ts/create-go-account.ts @@ -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)); + diff --git a/modules/bitgo/test/v2/unit/wallets.ts b/modules/bitgo/test/v2/unit/wallets.ts index b08e11bb1f..a55e4b200d 100644 --- a/modules/bitgo/test/v2/unit/wallets.ts +++ b/modules/bitgo/test/v2/unit/wallets.ts @@ -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 () { diff --git a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts index 86ed77f77a..e22677c966 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts @@ -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; diff --git a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts index 41ae44cbf9..77d59ac299 100644 --- a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts +++ b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts @@ -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; diff --git a/modules/sdk-core/src/bitgo/wallet/iWallets.ts b/modules/sdk-core/src/bitgo/wallet/iWallets.ts index a1a3ea9295..79db270d84 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallets.ts @@ -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'; @@ -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; @@ -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; } @@ -86,6 +95,19 @@ export const GenerateLightningWalletOptionsCodec = t.strict( export type GenerateLightningWalletOptions = t.TypeOf; +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; + export interface GetWalletByAddressOptions { address?: string; reqId?: IRequestTracer; @@ -214,7 +236,9 @@ export interface IWallets { get(params?: GetWalletOptions): Promise; list(params?: ListWalletOptions): Promise<{ wallets: IWallet[] }>; add(params?: AddWalletOptions): Promise; - generateWallet(params?: GenerateWalletOptions): Promise; + generateWallet( + params?: GenerateWalletOptions + ): Promise; listShares(params?: Record): Promise; getShare(params?: { walletShareId?: string }): Promise; updateShare(params?: UpdateShareOptions): Promise; diff --git a/modules/sdk-core/src/bitgo/wallet/wallets.ts b/modules/sdk-core/src/bitgo/wallet/wallets.ts index 36da5fe171..77faeeac69 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallets.ts @@ -24,6 +24,8 @@ import { BulkUpdateWalletShareOptionsRequest, BulkUpdateWalletShareResponse, GenerateBaseMpcWalletOptions, + GenerateGoAccountWalletOptions, + GenerateGoAccountWalletOptionsCodec, GenerateLightningWalletOptions, GenerateLightningWalletOptionsCodec, GenerateMpcWalletOptions, @@ -31,6 +33,7 @@ import { GenerateWalletOptions, GetWalletByAddressOptions, GetWalletOptions, + GoAccountWalletWithUserKeychain, IWallets, LightningWalletWithKeychains, ListWalletOptions, @@ -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'; } @@ -213,6 +216,57 @@ export class Wallets implements IWallets { }; } + /** + * Generate a Go Account wallet + * @param params GenerateGoAccountWalletOptions + * @returns Promise + */ + private async generateGoAccountWallet( + params: GenerateGoAccountWalletOptions + ): Promise { + 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 @@ -246,7 +300,7 @@ export class Wallets implements IWallets { */ async generateWallet( params: GenerateWalletOptions = {} - ): Promise { + ): Promise { // Assign the default multiSig type value based on the coin if (!params.multisigType) { params.multisigType = this.baseCoin.getDefaultMultisigType(); @@ -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');