Skip to content

Commit 496a288

Browse files
bhavidhingraclaude
andcommitted
feat(sdk-core): add bulk TRX resource delegation SDK methods
Adds buildAccountDelegations, sendAccountDelegation, sendAccountDelegations to the Wallet class and IWallet interface, mirroring the consolidation API. Adds Express typed route schema and handler for POST /api/v2/:coin/wallet/:id/delegateResources with TSS/custodial/hot wallet branching and partial-success (202) response handling. CHALO-287 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent eb28af8 commit 496a288

6 files changed

Lines changed: 453 additions & 0 deletions

File tree

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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));

modules/express/src/clientRoutes.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,53 @@ export async function handleV2ResourceDelegations(
10521052
});
10531053
}
10541054

1055+
/**
1056+
* Handle bulk resource delegation (e.g. TRX ENERGY/BANDWIDTH delegation).
1057+
* Builds, signs, and sends one on-chain delegation transaction per entry in req.body.delegations.
1058+
* @param req
1059+
*/
1060+
export async function handleV2DelegateResources(
1061+
req: ExpressApiRouteRequest<'express.v2.wallet.delegateresources', 'post'>
1062+
) {
1063+
const bitgo = req.bitgo;
1064+
const coin = bitgo.coin(req.decoded.coin);
1065+
1066+
if (!Array.isArray(req.decoded.delegations) || req.decoded.delegations.length === 0) {
1067+
throw new Error('delegations must be a non-empty array');
1068+
}
1069+
1070+
const wallet = await coin.wallets().get({ id: req.decoded.id });
1071+
1072+
let result: any;
1073+
try {
1074+
if (coin.supportsTss()) {
1075+
result = await wallet.sendAccountDelegations(createTSSSendParams(req, wallet));
1076+
} else {
1077+
result = await wallet.sendAccountDelegations(createSendParams(req));
1078+
}
1079+
} catch (err) {
1080+
err.status = 400;
1081+
throw err;
1082+
}
1083+
1084+
// Handle partial success / failure
1085+
if (result.failure.length > 0) {
1086+
let msg = '';
1087+
let status = 202;
1088+
1089+
if (result.success.length > 0) {
1090+
msg = `Transactions failed: ${result.failure.length} and succeeded: ${result.success.length}`;
1091+
} else {
1092+
status = 400;
1093+
msg = `All transactions failed`;
1094+
}
1095+
1096+
throw apiResponse(status, result, msg);
1097+
}
1098+
1099+
return result;
1100+
}
1101+
10551102
/**
10561103
* payload meant for prebuildAndSignTransaction() in sdk-core which
10571104
* validates the payload and makes the appropriate request to WP to
@@ -1830,6 +1877,10 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
18301877
prepareBitGo(config),
18311878
typedPromiseWrapper(handleV2ResourceDelegations),
18321879
]);
1880+
router.post('express.v2.wallet.delegateresources', [
1881+
prepareBitGo(config),
1882+
typedPromiseWrapper(handleV2DelegateResources),
1883+
]);
18331884

18341885
// Miscellaneous
18351886
router.post('express.canonicaladdress', [prepareBitGo(config), typedPromiseWrapper(handleCanonicalAddress)]);

modules/express/src/typedRoutes/api/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import { PostWalletAccelerateTx } from './v2/walletAccelerateTx';
5454
import { PostIsWalletAddress } from './v2/isWalletAddress';
5555
import { GetAccountResources } from './v2/accountResources';
5656
import { GetResourceDelegations } from './v2/resourceDelegations';
57+
import { PostDelegateResources } from './v2/delegateResources';
5758

5859
// Too large types can cause the following error
5960
//
@@ -176,6 +177,12 @@ export const ExpressV2WalletConsolidateAccountApiSpec = apiSpec({
176177
},
177178
});
178179

180+
export const ExpressV2WalletDelegateResourcesApiSpec = apiSpec({
181+
'express.v2.wallet.delegateresources': {
182+
post: PostDelegateResources,
183+
},
184+
});
185+
179186
export const ExpressWalletFanoutUnspentsApiSpec = apiSpec({
180187
'express.v1.wallet.fanoutunspents': {
181188
put: PutFanoutUnspents,
@@ -381,6 +388,7 @@ export type ExpressApi = typeof ExpressPingApiSpec &
381388
typeof ExpressV2WalletAccelerateTxApiSpec &
382389
typeof ExpressV2WalletAccountResourcesApiSpec &
383390
typeof ExpressV2WalletResourceDelegationsApiSpec &
391+
typeof ExpressV2WalletDelegateResourcesApiSpec &
384392
typeof ExpressWalletManagementApiSpec;
385393

386394
export const ExpressApi: ExpressApi = {
@@ -424,6 +432,7 @@ export const ExpressApi: ExpressApi = {
424432
...ExpressV2WalletAccelerateTxApiSpec,
425433
...ExpressV2WalletAccountResourcesApiSpec,
426434
...ExpressV2WalletResourceDelegationsApiSpec,
435+
...ExpressV2WalletDelegateResourcesApiSpec,
427436
...ExpressWalletManagementApiSpec,
428437
};
429438

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
3+
import { BitgoExpressError } from '../../schemas/error';
4+
5+
/**
6+
* Path parameters for the delegate resources endpoint
7+
*/
8+
export const DelegateResourcesParams = {
9+
/** Coin identifier (e.g., 'trx', 'ttrx') */
10+
coin: t.string,
11+
/** Wallet ID */
12+
id: t.string,
13+
} as const;
14+
15+
/**
16+
* A single resource delegation entry
17+
*/
18+
export const DelegationEntryCodec = t.type({
19+
/** On-chain address that will receive the delegated resources */
20+
receiverAddress: t.string,
21+
/** Amount of TRX (in SUN) to stake for the delegation */
22+
amount: t.string,
23+
/** Resource type to delegate (e.g. 'ENERGY', 'BANDWIDTH') */
24+
resource: t.string,
25+
});
26+
27+
/**
28+
* Request body for delegating resources to multiple receiver addresses.
29+
* Each delegation entry triggers a separate on-chain staking transaction
30+
* from the wallet's root address to the receiver address.
31+
*
32+
* Signing behaviour by wallet type:
33+
* - Hot (non-TSS) → signed locally with walletPassphrase and submitted
34+
* - Custodial non-TSS → sent for BitGo approval via initiateTransaction
35+
* - TSS (any) → build response contains txRequestId; signed by TSS service
36+
*/
37+
export const DelegateResourcesRequestBody = {
38+
/** Delegation entries — one on-chain transaction is built per entry */
39+
delegations: t.array(DelegationEntryCodec),
40+
41+
/** Wallet passphrase to decrypt the user key (hot wallets) */
42+
walletPassphrase: optional(t.string),
43+
/** Extended private key (alternative to walletPassphrase) */
44+
xprv: optional(t.string),
45+
/** One-time password for 2FA */
46+
otp: optional(t.string),
47+
48+
/** API version for TSS transaction request response ('lite' or 'full') */
49+
apiVersion: optional(t.union([t.literal('lite'), t.literal('full')])),
50+
} as const;
51+
52+
/**
53+
* Response for the delegate resources operation.
54+
* Returns arrays of successful and failed delegation transactions.
55+
*/
56+
export const DelegateResourcesResponse = t.type({
57+
/** Successfully sent delegation transactions */
58+
success: t.array(t.unknown),
59+
/** Errors from failed delegation transactions */
60+
failure: t.array(t.unknown),
61+
});
62+
63+
/**
64+
* Response for partial success or failure cases (202/400).
65+
* Includes both the transaction results and error metadata.
66+
*/
67+
export const DelegateResourcesErrorResponse = t.intersection([DelegateResourcesResponse, BitgoExpressError]);
68+
69+
/**
70+
* Bulk Resource Delegation
71+
*
72+
* Delegates resources (ENERGY or BANDWIDTH) from a wallet's root address to one or more
73+
* receiver addresses. Each delegation entry produces a separate on-chain staking transaction.
74+
* This is the resource-delegation analogue of the consolidateAccount endpoint.
75+
*
76+
* Supported coins: TRON (trx, ttrx) and any future coins that support resource delegation.
77+
*
78+
* The API may return partial success (status 202) if some delegations succeed but others fail.
79+
*
80+
* @operationId express.v2.wallet.delegateresources
81+
* @tag express
82+
*/
83+
export const PostDelegateResources = httpRoute({
84+
path: '/api/v2/{coin}/wallet/{id}/delegateResources',
85+
method: 'POST',
86+
request: httpRequest({
87+
params: DelegateResourcesParams,
88+
body: DelegateResourcesRequestBody,
89+
}),
90+
response: {
91+
/** All delegations succeeded */
92+
200: DelegateResourcesResponse,
93+
/** Partial success — some delegations succeeded, others failed */
94+
202: DelegateResourcesErrorResponse,
95+
/** All delegations failed */
96+
400: DelegateResourcesErrorResponse,
97+
},
98+
});

0 commit comments

Comments
 (0)