|
| 1 | +/** |
| 2 | + * Delegate bandwidth resources to a Tron address using the BitGo API. |
| 3 | + * |
| 4 | + * The script queries net_usage (freeBandwidthUsed + stakedBandwidthUsed) before |
| 5 | + * and after the max-size lookup to detect any account state change that could |
| 6 | + * invalidate the queried delegatable amount. If net_usage has changed, the |
| 7 | + * delegation is aborted. |
| 8 | + * |
| 9 | + * Steps: |
| 10 | + * 1. Query net_usage |
| 11 | + * 2. Query getAccountResources for maxResourcesDelegatable.bandwidthSun |
| 12 | + * 3. Query net_usage again — abort if it changed since step 1 |
| 13 | + * 4. Delegate |
| 14 | + * 5. Query net_usage after delegation |
| 15 | + * |
| 16 | + * Copyright 2026, BitGo, Inc. All Rights Reserved. |
| 17 | + */ |
| 18 | +import { BitGo, WalletCoinSpecific } from 'bitgo'; |
| 19 | +import type { Wallet } from 'bitgo'; |
| 20 | + |
| 21 | +// TODO: change to 'production' for mainnet |
| 22 | +const env = 'staging'; |
| 23 | +const bitgo = new BitGo({ env }); |
| 24 | + |
| 25 | +// TODO: change to 'trx' for mainnet |
| 26 | +const coin = 'ttrx'; |
| 27 | + |
| 28 | +// TODO: set your wallet id |
| 29 | +const walletId = '69d500a2359312cee530061c42dff0cc'; |
| 30 | + |
| 31 | +// TODO: set your wallet passphrase |
| 32 | +const walletPassphrase = 'Ghghjkg!455544llll'; |
| 33 | + |
| 34 | +// TODO: set OTP code |
| 35 | +const otp = '000000'; |
| 36 | + |
| 37 | +// TODO: set your access token here |
| 38 | +// You can get this from User Settings > Developer Options > Add Access Token |
| 39 | +const accessToken = 'v2x6a81534b000a0e1dd48706b24a1e90add4ea2fd9fbe5a66080e637176534afbc'; |
| 40 | + |
| 41 | +// TODO: set the address to receive the delegated bandwidth |
| 42 | +const recipientAddress = 'TGmRquG8ddJ6PiiLyxeWmY7xU4KDGwPRUH'; |
| 43 | + |
| 44 | +// TODO: adjust the reserve amount (in SUN) to keep back from the delegatable bandwidth |
| 45 | +const bandwidthSunReserve = '600000000'; |
| 46 | + |
| 47 | +interface NetUsage { |
| 48 | + freeBandwidthUsed: number; |
| 49 | + stakedBandwidthUsed: number; |
| 50 | +} |
| 51 | + |
| 52 | +/** |
| 53 | + * Returns the bandwidth consumed by the given address in the current window, |
| 54 | + * split into free and staked portions. |
| 55 | + * Equivalent to net_usage on the Tron node. |
| 56 | + */ |
| 57 | +async function getNetUsage(wallet: Wallet, address: string): Promise<NetUsage> { |
| 58 | + const { resources } = await wallet.getAccountResources({ addresses: [address] }); |
| 59 | + const info = resources[0]; |
| 60 | + return { |
| 61 | + freeBandwidthUsed: info?.freeBandwidthUsed ?? 0, |
| 62 | + stakedBandwidthUsed: info?.stakedBandwidthUsed ?? 0, |
| 63 | + }; |
| 64 | +} |
| 65 | + |
| 66 | +function logNetUsage(label: string, usage: NetUsage): void { |
| 67 | + console.log(`${label} — freeBandwidthUsed: ${usage.freeBandwidthUsed}, stakedBandwidthUsed: ${usage.stakedBandwidthUsed}, total: ${usage.freeBandwidthUsed + usage.stakedBandwidthUsed}`); |
| 68 | +} |
| 69 | + |
| 70 | +async function main() { |
| 71 | + bitgo.authenticateWithAccessToken({ accessToken }); |
| 72 | + |
| 73 | + const wallet = await bitgo.coin(coin).wallets().getWallet({ id: walletId }); |
| 74 | + const coinSpecific = wallet.coinSpecific() as WalletCoinSpecific; |
| 75 | + const rootAddress = coinSpecific.rootAddress as string; |
| 76 | + |
| 77 | + console.log('Wallet ID:', wallet.id()); |
| 78 | + console.log('Root Address:', rootAddress); |
| 79 | + |
| 80 | + // Step 1: Query net_usage before the max-size lookup |
| 81 | + const netUsageBefore = await getNetUsage(wallet, rootAddress); |
| 82 | + logNetUsage('\n[Step 1] net_usage before getAccountResources', netUsageBefore); |
| 83 | + |
| 84 | + // Step 2: Fetch account resources to get maxResourcesDelegatable.bandwidthSun |
| 85 | + const { resources, failedAddresses } = await wallet.getAccountResources({ addresses: [rootAddress] }); |
| 86 | + |
| 87 | + if (failedAddresses.length > 0) { |
| 88 | + console.warn('Failed to fetch resources for:', failedAddresses); |
| 89 | + } |
| 90 | + |
| 91 | + console.log('\n[Step 2] Account Resources:'); |
| 92 | + console.log(JSON.stringify(resources, null, 2)); |
| 93 | + |
| 94 | + const rootResources = resources[0]; |
| 95 | + if (!rootResources) { |
| 96 | + throw new Error(`No resource info returned for root address ${rootAddress}`); |
| 97 | + } |
| 98 | + |
| 99 | + // Step 3: Query net_usage again and abort if the account state changed |
| 100 | + const netUsageAfter = await getNetUsage(wallet, rootAddress); |
| 101 | + logNetUsage('\n[Step 3] net_usage after getAccountResources', netUsageAfter); |
| 102 | + |
| 103 | + if ( |
| 104 | + netUsageAfter.freeBandwidthUsed !== netUsageBefore.freeBandwidthUsed || |
| 105 | + netUsageAfter.stakedBandwidthUsed !== netUsageBefore.stakedBandwidthUsed |
| 106 | + ) { |
| 107 | + throw new Error( |
| 108 | + `Account state changed between queries — ` + |
| 109 | + `freeBandwidthUsed: ${netUsageBefore.freeBandwidthUsed} → ${netUsageAfter.freeBandwidthUsed}, ` + |
| 110 | + `stakedBandwidthUsed: ${netUsageBefore.stakedBandwidthUsed} → ${netUsageAfter.stakedBandwidthUsed}. ` + |
| 111 | + `The queried maxResourcesDelegatable may no longer be valid. Aborting delegation.` |
| 112 | + ); |
| 113 | + } |
| 114 | + console.log('net_usage unchanged — account state is consistent, proceeding with delegation.'); |
| 115 | + |
| 116 | + // `maxResourcesDelegatable.bandwidthSun` is the max bandwidth (in SUN) the root |
| 117 | + // address can currently delegate to another address. |
| 118 | + const bandwidthSun = rootResources.maxResourcesDelegatable?.bandwidthSun; |
| 119 | + if (!bandwidthSun || bandwidthSun === '0') { |
| 120 | + console.log('Root address has no delegatable bandwidth. Nothing to delegate.'); |
| 121 | + return; |
| 122 | + } |
| 123 | + |
| 124 | + const adjustedBandwidthSun = (BigInt(bandwidthSun) - BigInt(bandwidthSunReserve)).toString(); |
| 125 | + if (BigInt(adjustedBandwidthSun) <= BigInt(0)) { |
| 126 | + console.log('Delegatable bandwidth after adjustment is zero or negative. Nothing to delegate.'); |
| 127 | + return; |
| 128 | + } |
| 129 | + |
| 130 | + console.log(`\nDelegatable bandwidth: ${bandwidthSun} SUN`); |
| 131 | + console.log(`Adjusted bandwidth (after ${bandwidthSunReserve} SUN reserve): ${adjustedBandwidthSun} SUN`); |
| 132 | + console.log(`Recipient: ${recipientAddress}`); |
| 133 | + |
| 134 | + // Unlock the session before sending |
| 135 | + const unlock = await bitgo.unlock({ otp, duration: 3600 }); |
| 136 | + if (!unlock) { |
| 137 | + throw new Error('Error unlocking session'); |
| 138 | + } |
| 139 | + |
| 140 | + // Step 4: Delegate bandwidth from the root address to the recipient. |
| 141 | + // stakingParams carries the delegation details; recipients must contain the same |
| 142 | + // address with amount "0" (the actual amount lives in stakingParams.amount). |
| 143 | + const result = await wallet.sendMany({ |
| 144 | + type: 'delegateResource', |
| 145 | + stakingParams: { |
| 146 | + receiver_address: recipientAddress, |
| 147 | + amount: adjustedBandwidthSun, |
| 148 | + resource: 'bandwidth', |
| 149 | + }, |
| 150 | + recipients: [ |
| 151 | + { |
| 152 | + address: recipientAddress, |
| 153 | + amount: '0', |
| 154 | + }, |
| 155 | + ], |
| 156 | + walletPassphrase, |
| 157 | + otp, |
| 158 | + }); |
| 159 | + |
| 160 | + console.log('\n[Step 4] Delegate resource transaction result:'); |
| 161 | + console.dir(result, { depth: 6 }); |
| 162 | + |
| 163 | + // Step 5: Query net_usage after delegation |
| 164 | + const netUsagePostDelegation = await getNetUsage(wallet, rootAddress); |
| 165 | + logNetUsage('\n[Step 5] net_usage after delegation', netUsagePostDelegation); |
| 166 | + console.log( |
| 167 | + `net_usage delta (delegation overhead) — ` + |
| 168 | + `freeBandwidthUsed: +${netUsagePostDelegation.freeBandwidthUsed - netUsageAfter.freeBandwidthUsed}, ` + |
| 169 | + `stakedBandwidthUsed: +${netUsagePostDelegation.stakedBandwidthUsed - netUsageAfter.stakedBandwidthUsed}` |
| 170 | + ); |
| 171 | +} |
| 172 | + |
| 173 | +main().catch((e) => console.error(e)); |
0 commit comments