Skip to content

Commit 2b8c8a7

Browse files
committed
chore: add go account approve script
CAAS-1013 TICKET: CAAS-1013
1 parent 432fd39 commit 2b8c8a7

1 file changed

Lines changed: 170 additions & 0 deletions

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/**
2+
* Go Account withdrawal approval
3+
*
4+
* When a Go Account withdrawal is subject to an approval policy, the transaction
5+
* lands in a pending state and must be approved by a second wallet administrator
6+
* before BitGo co-signs and broadcasts it.
7+
*
8+
* Important: you CANNOT approve your own transaction — a different admin must
9+
* approve it. This script is intended to be run by the approving admin.
10+
*
11+
* This script supports two workflows:
12+
*
13+
* A) Approve a known pending approval by ID
14+
* Set PENDING_APPROVAL_ID and run the script.
15+
*
16+
* B) List all pending approvals for a wallet and approve the first one
17+
* Set OFC_WALLET_ID and leave PENDING_APPROVAL_ID empty — the script
18+
* will list pending approvals and approve the first pending one.
19+
*
20+
* Required environment variables (in examples/.env):
21+
* TESTNET_ACCESS_TOKEN - access token of the APPROVING admin
22+
* OFC_WALLET_ID - the wallet ID of your Go Account
23+
* OFC_WALLET_PASSPHRASE - passphrase of the approving admin's key
24+
* PENDING_APPROVAL_ID - (optional) specific approval ID to approve
25+
* APPROVAL_OTP - (optional) OTP / 2FA code if required
26+
*
27+
* Copyright 2025, BitGo, Inc. All Rights Reserved.
28+
*/
29+
30+
import { BitGoAPI } from '@bitgo/sdk-api';
31+
import { coins } from 'bitgo';
32+
require('dotenv').config({ path: '../../../.env' });
33+
34+
// Initialize BitGo SDK with the APPROVING admin's access token
35+
const bitgo = new BitGoAPI({
36+
accessToken: process.env.TESTNET_ACCESS_TOKEN,
37+
env: 'test', // Change to 'production' for mainnet
38+
});
39+
40+
const baseCoin = 'ofc';
41+
bitgo.register(baseCoin, coins.Ofc.createInstance);
42+
43+
// ---------------------------------------------------------------------------
44+
// Configuration
45+
// ---------------------------------------------------------------------------
46+
47+
/** Wallet ID whose pending approvals you want to manage */
48+
const walletId = process.env.OFC_WALLET_ID || 'your_wallet_id';
49+
50+
/** Passphrase of the approving admin (used to re-sign if required) */
51+
const walletPassphrase = process.env.OFC_WALLET_PASSPHRASE || 'your_wallet_passphrase';
52+
53+
/**
54+
* Specific pending approval ID to approve.
55+
* If empty the script lists all pending approvals for the wallet and approves
56+
* the first one it finds.
57+
*/
58+
const pendingApprovalId = process.env.PENDING_APPROVAL_ID || '';
59+
60+
/** OTP / 2FA code — required if your enterprise enforces it */
61+
const otp = process.env.APPROVAL_OTP || '';
62+
63+
// ---------------------------------------------------------------------------
64+
65+
async function main() {
66+
console.log('=== Go Account Withdrawal Approval ===\n');
67+
68+
const coin = bitgo.coin(baseCoin);
69+
70+
// -------------------------------------------------------------------------
71+
// Resolve the pending approval
72+
// -------------------------------------------------------------------------
73+
let approval: any;
74+
75+
if (pendingApprovalId) {
76+
console.log(`Fetching pending approval ${pendingApprovalId}...`);
77+
approval = await coin.pendingApprovals().get({ id: pendingApprovalId });
78+
console.log(`✓ Found pending approval: ${approval.id()}`);
79+
} else {
80+
console.log(`Listing pending approvals for wallet ${walletId}...`);
81+
const { pendingApprovals } = await coin.pendingApprovals().list({ walletId });
82+
83+
if (!pendingApprovals || pendingApprovals.length === 0) {
84+
console.log('No pending approvals found for this wallet.');
85+
return;
86+
}
87+
88+
console.log(`✓ Found ${pendingApprovals.length} pending approval(s):\n`);
89+
for (const pa of pendingApprovals) {
90+
console.log(` ID : ${pa.id()}`);
91+
console.log(` State : ${pa.state()}`);
92+
console.log(` Type : ${pa.type()}`);
93+
console.log('');
94+
}
95+
96+
// Pick the first approval that is still pending
97+
approval = pendingApprovals.find((pa) => pa.state() === 'pending');
98+
if (!approval) {
99+
console.log('No approvals in "pending" state found.');
100+
return;
101+
}
102+
console.log(`Approving first pending approval: ${approval.id()}\n`);
103+
}
104+
105+
// -------------------------------------------------------------------------
106+
// Print approval details before approving
107+
// -------------------------------------------------------------------------
108+
console.log('Approval details:');
109+
console.log(` ID : ${approval.id()}`);
110+
console.log(` State : ${approval.state()}`);
111+
console.log(` Type : ${approval.type()}`);
112+
console.log(` Approvals req. : ${approval.approvalsRequired()}`);
113+
114+
const state = approval.state();
115+
if (state === 'pendingLivenessVerification') {
116+
console.log(
117+
'\n⚠️ This approval is in "pendingLivenessVerification" state.\n' +
118+
' BitGo requires a liveness check (e.g. biometric or 2FA via the BitGo web app)\n' +
119+
' before this transaction can be approved programmatically.\n' +
120+
' Complete the liveness check in the BitGo portal and re-run this script.\n'
121+
);
122+
return;
123+
}
124+
if (state !== 'pending') {
125+
console.log(`\n⚠️ Approval is in state "${state}" — only "pending" approvals can be approved here.\n`);
126+
return;
127+
}
128+
const info = approval.info();
129+
if (info?.transactionRequest?.recipients) {
130+
console.log(' Recipients:');
131+
for (const r of info.transactionRequest.recipients) {
132+
console.log(` ${r.address} ${r.amount}${r.tokenName ? ' (' + r.tokenName + ')' : ''}`);
133+
}
134+
}
135+
console.log('');
136+
137+
// -------------------------------------------------------------------------
138+
// Approve
139+
//
140+
// The SDK's pendingApproval.approve() attempts to rebuild the transaction
141+
// before approving, which fails for OFC wallets (off-chain transactions
142+
// don't need to be rebuilt). For OFC we call the API directly:
143+
// PUT /api/v2/ofc/pendingapprovals/{id} { state: 'approved', otp }
144+
// -------------------------------------------------------------------------
145+
console.log('Approving...');
146+
const approveUrl = coin.url('/pendingapprovals/' + approval.id());
147+
const body: { state: string; otp?: string } = { state: 'approved' };
148+
if (otp) body.otp = otp;
149+
const result = await (bitgo as any).put(approveUrl).send(body).result();
150+
151+
console.log('✓ Approval submitted successfully!');
152+
console.log('\nApproval result:');
153+
console.log(JSON.stringify(result, null, 2));
154+
155+
// Summary
156+
console.log('\n' + '='.repeat(60));
157+
console.log('APPROVAL SUMMARY');
158+
console.log('='.repeat(60));
159+
console.log(` Approval ID : ${approval.id()}`);
160+
console.log(` New state : ${result.state || 'see result above'}`);
161+
if (result.txid) {
162+
console.log(` Transaction : ${result.txid}`);
163+
}
164+
console.log('='.repeat(60));
165+
}
166+
167+
main().catch((e) => {
168+
console.error('\n❌ Error during approval:', e);
169+
process.exit(1);
170+
});

0 commit comments

Comments
 (0)