|
| 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