Skip to content

Commit 9203816

Browse files
authored
Merge pull request #8326 from BitGo/CAAS-1012-add-go-account-withdrawal-script
chore: add go account withdrawal script
2 parents 432fd39 + b1aa74a commit 9203816

1 file changed

Lines changed: 194 additions & 0 deletions

File tree

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/**
2+
* Go Account withdrawal — complete 3-step flow
3+
*
4+
* Demonstrates the full end-to-end withdrawal from a Go Account (OFC wallet):
5+
*
6+
* Step 1: Build — wallet.prebuildTransaction() → prebuild
7+
* Step 2: Sign — tradingAccount.signPayload(prebuild.payload) → signature
8+
* Step 3: Submit — POST /tx/send { halfSigned: { payload, signature } } → txid
9+
*
10+
* All three steps run in sequence in this single script.
11+
* If you only need the signing step (e.g. the payload was built separately),
12+
* see sign-transaction.ts instead.
13+
*
14+
* The signing step decrypts the user key locally — your passphrase is NEVER
15+
* sent over the network.
16+
*
17+
* Required environment variables (in examples/.env):
18+
* TESTNET_ACCESS_TOKEN - your BitGo access token
19+
* OFC_WALLET_ID - the wallet ID of your Go Account
20+
* OFC_WALLET_PASSPHRASE - the passphrase used when the wallet was created
21+
*
22+
* Copyright 2025, BitGo, Inc. All Rights Reserved.
23+
*/
24+
25+
import { BitGoAPI } from '@bitgo/sdk-api';
26+
import { Wallet } from '@bitgo/sdk-core';
27+
import { coins } from 'bitgo';
28+
import { tokens as staticTokens, OfcTokenConfig } from '@bitgo/statics';
29+
require('dotenv').config({ path: '../../../.env' });
30+
31+
// Initialize BitGo SDK
32+
const bitgo = new BitGoAPI({
33+
accessToken: process.env.TESTNET_ACCESS_TOKEN,
34+
env: 'test', // Change to 'production' for mainnet
35+
});
36+
37+
// Go Accounts use the 'ofc' (Off-Chain) coin family.
38+
// When withdrawing a specific token, the coin context must be the token (e.g. 'ofctsol'),
39+
// not the base 'ofc' coin — the BitGo API rejects builds on the base coin directly.
40+
const baseCoin = 'ofc';
41+
bitgo.register(baseCoin, coins.Ofc.createInstance);
42+
43+
// ---------------------------------------------------------------------------
44+
// Configuration — update these values or set them as environment variables
45+
// ---------------------------------------------------------------------------
46+
47+
/** The wallet ID of your Go Account */
48+
const walletId = process.env.OFC_WALLET_ID || 'your_wallet_id';
49+
50+
/** Passphrase used to encrypt the wallet user key when the wallet was created */
51+
const walletPassphrase = process.env.OFC_WALLET_PASSPHRASE || 'your_wallet_passphrase';
52+
53+
/**
54+
* Withdrawal destination address.
55+
* For OFC wallets this is typically a BitGo address or counterparty address.
56+
*/
57+
const recipientAddress = process.env.RECIPIENT_ADDRESS || 'your_recipient_address';
58+
59+
/**
60+
* Amount to withdraw, in the base unit of the token being sent.
61+
* For USD-pegged stablecoins (e.g. USDC) this is in micro-units: 1 USDC = 1_000_000.
62+
* For BTC this is satoshis: 1 BTC = 100_000_000.
63+
*/
64+
const withdrawalAmount = process.env.WITHDRAWAL_AMOUNT || '1000000'; // e.g. 1.00 USDC (6 decimals)
65+
66+
/**
67+
* Token to withdraw. Leave undefined to use the wallet's default token.
68+
* Examples: 'ofctsol:usdc', 'ofcttrx:usdt', 'ofcbtc', 'ofceth'
69+
*
70+
* Note: For OFC wallets, the token name embedded in the recipient address usually
71+
* determines which token moves — check BitGo docs for your specific flow.
72+
*/
73+
const token: string | undefined = 'ofctsol';
74+
75+
// ---------------------------------------------------------------------------
76+
77+
async function main() {
78+
console.log('=== Go Account Withdrawal (Build → Sign → Submit) ===\n');
79+
80+
// When a specific token is given (e.g. 'ofctsol'), the API requires that coin
81+
// context for building — using the base 'ofc' coin will be rejected.
82+
// OfcToken.createTokenConstructor() overrides getChain() to return the token
83+
// type (e.g. 'ofctsol'), which is what the BitGo API URL expects.
84+
const effectiveCoin = token || baseCoin;
85+
if (effectiveCoin !== baseCoin) {
86+
const allOfcTokens: OfcTokenConfig[] = [
87+
...staticTokens.bitcoin.ofc.tokens,
88+
...staticTokens.testnet.ofc.tokens,
89+
];
90+
const tokenConfig = allOfcTokens.find((t) => t.type === effectiveCoin);
91+
if (!tokenConfig) {
92+
throw new Error(`Unknown OFC token: ${effectiveCoin}. Check @bitgo/statics for valid token names.`);
93+
}
94+
bitgo.register(effectiveCoin, coins.OfcToken.createTokenConstructor(tokenConfig));
95+
}
96+
97+
// -------------------------------------------------------------------------
98+
// Step 1: Fetch the wallet and build the withdrawal transaction
99+
//
100+
// The GET /wallet endpoint only accepts the base 'ofc' coin path.
101+
// After fetching, we re-wrap the wallet with the token coin so that
102+
// prebuildTransaction hits the correct path (e.g. /ofctsol/wallet/.../tx/build).
103+
// -------------------------------------------------------------------------
104+
console.log(`Fetching wallet ${walletId}...`);
105+
const rawWallet = await bitgo.coin(baseCoin).wallets().get({ id: walletId });
106+
const wallet =
107+
effectiveCoin === baseCoin
108+
? rawWallet
109+
: new Wallet(bitgo, bitgo.coin(effectiveCoin), (rawWallet as Wallet)._wallet);
110+
console.log(`✓ Wallet: ${wallet.label()} (${wallet.id()}) [coin: ${effectiveCoin}]\n`);
111+
112+
console.log('Building withdrawal transaction...');
113+
const buildParams: {
114+
recipients: { address: string; amount: string; tokenName?: string }[];
115+
} = {
116+
recipients: [
117+
{
118+
address: recipientAddress,
119+
amount: withdrawalAmount,
120+
...(token ? { tokenName: token } : {}),
121+
},
122+
],
123+
};
124+
125+
const prebuild = await wallet.prebuildTransaction(buildParams);
126+
console.log('✓ Transaction built successfully');
127+
console.log('\nPrebuild result:');
128+
console.log(JSON.stringify(prebuild, null, 2));
129+
130+
// -------------------------------------------------------------------------
131+
// Step 2: Sign the transaction payload (the core of this script)
132+
//
133+
// The trading account's signPayload method:
134+
// 1. Fetches the encrypted user key from BitGo
135+
// 2. Decrypts it locally using your walletPassphrase
136+
// 3. Signs the payload using Bitcoin message signing (secp256k1)
137+
// 4. Returns a hex-encoded 65-byte recoverable signature
138+
//
139+
// Your passphrase is NEVER sent over the network.
140+
// -------------------------------------------------------------------------
141+
console.log('\nSigning transaction payload...');
142+
const tradingAccount = wallet.toTradingAccount();
143+
144+
// The payload to sign is the inner payload string from the prebuild result,
145+
// not the whole prebuild object. OfcToken.signTransaction signs txPrebuild.payload.
146+
const payload = prebuild.payload as string;
147+
148+
const signature = await tradingAccount.signPayload({
149+
payload,
150+
walletPassphrase,
151+
});
152+
153+
console.log('✓ Payload signed successfully');
154+
console.log(`\nSignature (hex): ${signature}`);
155+
console.log(`Payload: ${payload}`);
156+
157+
// -------------------------------------------------------------------------
158+
// Step 3: Submit the half-signed transaction to BitGo
159+
//
160+
// The 'halfSigned' object carries the payload + signature. BitGo will
161+
// validate the signature against the registered public key and, if valid,
162+
// co-sign and broadcast the transaction.
163+
// -------------------------------------------------------------------------
164+
console.log('\nSubmitting signed transaction to BitGo...');
165+
// wallet.submitTransaction() runs the body through an io-ts codec (TxSendBody)
166+
// that strips unknown fields — including `payload` — from halfSigned before
167+
// sending. For OFC the server needs both fields, so call the endpoint directly.
168+
const sendUrl = (wallet as Wallet).baseCoin.url('/wallet/' + wallet.id() + '/tx/send');
169+
const sendResult = await (bitgo as any).post(sendUrl).send({ halfSigned: { payload, signature } }).result();
170+
171+
console.log('✓ Transaction submitted successfully!');
172+
console.log('\nTransaction result:');
173+
console.log(JSON.stringify(sendResult, null, 2));
174+
175+
// Summary
176+
console.log('\n' + '='.repeat(60));
177+
console.log('WITHDRAWAL SUMMARY');
178+
console.log('='.repeat(60));
179+
console.log(` Wallet ID : ${wallet.id()}`);
180+
console.log(` Recipient : ${recipientAddress}`);
181+
console.log(` Amount : ${withdrawalAmount}`);
182+
if (token) {
183+
console.log(` Token : ${token}`);
184+
}
185+
if (sendResult?.txid) {
186+
console.log(` Transaction : ${sendResult.txid}`);
187+
}
188+
console.log('='.repeat(60));
189+
}
190+
191+
main().catch((e) => {
192+
console.error('\n❌ Error during Go Account withdrawal:', e);
193+
process.exit(1);
194+
});

0 commit comments

Comments
 (0)