Skip to content

Commit ebfd681

Browse files
committed
feat(express): add typed routes for TRX delegation APIs
Add dedicated Express routes for accountresources and activedelegations endpoints with io-ts validation. TICKET: CHALO-346
1 parent 23d953b commit ebfd681

6 files changed

Lines changed: 260 additions & 21 deletions

File tree

modules/express/src/clientRoutes.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,40 @@ async function handleV2SendMany(req: ExpressApiRouteRequest<'express.v2.wallet.s
10121012
return result;
10131013
}
10141014

1015+
/**
1016+
* handle get account resources
1017+
* @param req
1018+
*/
1019+
async function handleV2AccountResources(req: ExpressApiRouteRequest<'express.v2.wallet.getaccountresources', 'post'>) {
1020+
const bitgo = req.bitgo;
1021+
const coin = bitgo.coin(req.decoded.coin);
1022+
const wallet = await coin.wallets().get({ id: req.decoded.id });
1023+
return wallet.getAccountResources({
1024+
addresses: req.decoded.addresses,
1025+
destinationAddress: req.decoded.destinationAddress,
1026+
});
1027+
}
1028+
1029+
/**
1030+
* handle get resource delegations
1031+
* @param req
1032+
*/
1033+
async function handleV2ResourceDelegations(
1034+
req: ExpressApiRouteRequest<'express.v2.wallet.resourcedelegations', 'get'>
1035+
) {
1036+
const bitgo = req.bitgo;
1037+
const coin = req.decoded.coin;
1038+
const walletId = req.decoded.id;
1039+
const query: Record<string, string> = {};
1040+
if (req.decoded.type) query.type = req.decoded.type;
1041+
if (req.decoded.resource) query.resource = req.decoded.resource;
1042+
if (req.decoded.limit) query.limit = req.decoded.limit;
1043+
return bitgo
1044+
.get(bitgo.url(`/${coin}/wallet/${walletId}/resourcedelegations`, 2))
1045+
.query(query)
1046+
.result();
1047+
}
1048+
10151049
/**
10161050
* payload meant for prebuildAndSignTransaction() in sdk-core which
10171051
* validates the payload and makes the appropriate request to WP to
@@ -1770,6 +1804,16 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
17701804
typedPromiseWrapper(handleV2ConsolidateAccount),
17711805
]);
17721806

1807+
// TRX resource delegation
1808+
router.post('express.v2.wallet.getaccountresources', [
1809+
prepareBitGo(config),
1810+
typedPromiseWrapper(handleV2AccountResources),
1811+
]);
1812+
router.get('express.v2.wallet.resourcedelegations', [
1813+
prepareBitGo(config),
1814+
typedPromiseWrapper(handleV2ResourceDelegations),
1815+
]);
1816+
17731817
// Miscellaneous
17741818
router.post('express.canonicaladdress', [prepareBitGo(config), typedPromiseWrapper(handleCanonicalAddress)]);
17751819
router.post('express.verifycoinaddress', [prepareBitGo(config), typedPromiseWrapper(handleV2VerifyAddress)]);

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ import { PostWalletEnableTokens } from './v2/walletEnableTokens';
5151
import { PostWalletSweep } from './v2/walletSweep';
5252
import { PostWalletAccelerateTx } from './v2/walletAccelerateTx';
5353
import { PostIsWalletAddress } from './v2/isWalletAddress';
54+
import { GetAccountResources } from './v2/accountResources';
55+
import { GetResourceDelegations } from './v2/resourceDelegations';
5456

5557
// Too large types can cause the following error
5658
//
@@ -322,6 +324,18 @@ export const ExpressV2WalletAccelerateTxApiSpec = apiSpec({
322324
},
323325
});
324326

327+
export const ExpressV2WalletAccountResourcesApiSpec = apiSpec({
328+
'express.v2.wallet.getaccountresources': {
329+
post: GetAccountResources,
330+
},
331+
});
332+
333+
export const ExpressV2WalletResourceDelegationsApiSpec = apiSpec({
334+
'express.v2.wallet.resourcedelegations': {
335+
get: GetResourceDelegations,
336+
},
337+
});
338+
325339
export type ExpressApi = typeof ExpressPingApiSpec &
326340
typeof ExpressPingExpressApiSpec &
327341
typeof ExpressLoginApiSpec &
@@ -360,6 +374,8 @@ export type ExpressApi = typeof ExpressPingApiSpec &
360374
typeof ExpressV2CanonicalAddressApiSpec &
361375
typeof ExpressV2WalletSweepApiSpec &
362376
typeof ExpressV2WalletAccelerateTxApiSpec &
377+
typeof ExpressV2WalletAccountResourcesApiSpec &
378+
typeof ExpressV2WalletResourceDelegationsApiSpec &
363379
typeof ExpressWalletManagementApiSpec;
364380

365381
export const ExpressApi: ExpressApi = {
@@ -401,6 +417,8 @@ export const ExpressApi: ExpressApi = {
401417
...ExpressV2CanonicalAddressApiSpec,
402418
...ExpressV2WalletSweepApiSpec,
403419
...ExpressV2WalletAccelerateTxApiSpec,
420+
...ExpressV2WalletAccountResourcesApiSpec,
421+
...ExpressV2WalletResourceDelegationsApiSpec,
404422
...ExpressWalletManagementApiSpec,
405423
};
406424

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 account resources endpoint
7+
*/
8+
export const AccountResourcesParams = {
9+
/** Coin identifier (e.g., 'trx', 'ttrx') */
10+
coin: t.string,
11+
/** Wallet ID */
12+
id: t.string,
13+
} as const;
14+
15+
/**
16+
* Body parameters for account resources endpoint
17+
*/
18+
export const AccountResourcesBody = {
19+
/** On-chain addresses to query resources for */
20+
addresses: t.array(t.string),
21+
/** Optional destination address to calculate energy deficit for token transfers */
22+
destinationAddress: optional(t.string),
23+
} as const;
24+
25+
/**
26+
* Account resource information for a single address
27+
*/
28+
export const AccountResourceInfo = t.intersection([
29+
t.type({
30+
address: t.string,
31+
free_bandwidth_available: t.number,
32+
free_bandwidth_used: t.number,
33+
staked_bandwidth_available: t.number,
34+
staked_bandwidth_used: t.number,
35+
energy_available: t.number,
36+
energy_used: t.number,
37+
resourceDeficitForAssetTransfer: t.intersection([
38+
t.type({
39+
bandwidthDeficit: t.number,
40+
bandwidthSunRequired: t.string,
41+
}),
42+
t.partial({
43+
energyDeficit: t.number,
44+
energySunRequired: t.string,
45+
}),
46+
]),
47+
}),
48+
t.partial({
49+
maxResourcesDelegatable: t.type({
50+
bandwidthSun: t.string,
51+
energySun: t.string,
52+
}),
53+
}),
54+
]);
55+
56+
/**
57+
* Failed address information
58+
*/
59+
export const FailedAddressInfo = t.type({
60+
address: t.string,
61+
error: t.string,
62+
});
63+
64+
/**
65+
* Response for account resources
66+
*/
67+
export const AccountResourcesResponse = {
68+
/** Account resources for the queried addresses */
69+
200: t.type({
70+
resources: t.array(AccountResourceInfo),
71+
failedAddresses: t.array(FailedAddressInfo),
72+
}),
73+
/** Invalid request */
74+
400: BitgoExpressError,
75+
} as const;
76+
77+
/**
78+
* Get Account Resources
79+
*
80+
* Query BANDWIDTH and ENERGY resource information for TRX wallet addresses.
81+
* Returns resource availability, usage, and optional deficit calculations
82+
* for token transfers.
83+
*
84+
* @operationId express.v2.wallet.getaccountresources
85+
* @tag express
86+
*/
87+
export const GetAccountResources = httpRoute({
88+
path: '/api/v2/{coin}/wallet/{id}/getaccountresources',
89+
method: 'POST',
90+
request: httpRequest({
91+
params: AccountResourcesParams,
92+
body: AccountResourcesBody,
93+
}),
94+
response: AccountResourcesResponse,
95+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import * as t from 'io-ts';
2+
import { NumberFromString } from 'io-ts-types';
3+
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
4+
import { BitgoExpressError } from '../../schemas/error';
5+
6+
/**
7+
* Path parameters for resource delegations endpoint
8+
*/
9+
export const ResourceDelegationsParams = {
10+
/** Coin identifier (e.g., 'trx', 'ttrx') */
11+
coin: t.string,
12+
/** Wallet ID */
13+
id: t.string,
14+
} as const;
15+
16+
/**
17+
* Query parameters for resource delegations endpoint
18+
*/
19+
export const ResourceDelegationsQuery = {
20+
/** Filter by delegation direction: 'outgoing' for delegations from this address, 'incoming' for delegations to this address; omit to fetch both */
21+
type: optional(t.union([t.literal('outgoing'), t.literal('incoming')])),
22+
/** Filter by resource type (case-insensitive: ENERGY, energy, BANDWIDTH, bandwidth) */
23+
resource: optional(t.string),
24+
/** Maximum number of results to return */
25+
limit: optional(NumberFromString),
26+
/** Pagination cursor from previous response */
27+
nextBatchPrevId: optional(t.string),
28+
} as const;
29+
30+
/**
31+
* A single delegation record
32+
*/
33+
export const DelegationRecord = t.type({
34+
id: t.string,
35+
coin: t.string,
36+
ownerAddress: t.string,
37+
receiverAddress: t.string,
38+
resource: t.union([t.literal('ENERGY'), t.literal('BANDWIDTH')]),
39+
balance: t.string,
40+
updatedAt: t.string,
41+
});
42+
43+
/**
44+
* Response for resource delegations
45+
*/
46+
export const ResourceDelegationsResponse = {
47+
/** Resource delegations for the wallet */
48+
200: t.type({
49+
address: t.string,
50+
coin: t.string,
51+
delegations: t.intersection([
52+
t.type({
53+
outgoing: t.array(DelegationRecord),
54+
incoming: t.array(DelegationRecord),
55+
}),
56+
t.partial({
57+
nextBatchPrevId: t.string,
58+
}),
59+
]),
60+
}),
61+
/** Invalid request */
62+
400: BitgoExpressError,
63+
} as const;
64+
65+
/**
66+
* Get Resource Delegations
67+
*
68+
* Query active outgoing and incoming ENERGY/BANDWIDTH resource delegations
69+
* for a TRX wallet.
70+
*
71+
* @operationId express.v2.wallet.resourcedelegations
72+
* @tag express
73+
*/
74+
export const GetResourceDelegations = httpRoute({
75+
path: '/api/v2/{coin}/wallet/{id}/resourcedelegations',
76+
method: 'GET',
77+
request: httpRequest({
78+
params: ResourceDelegationsParams,
79+
query: ResourceDelegationsQuery,
80+
}),
81+
response: ResourceDelegationsResponse,
82+
});

modules/sdk-core/src/bitgo/wallet/wallet.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1483,12 +1483,12 @@ export class Wallet implements IWallet {
14831483
throw new Error('addresses array cannot be empty');
14841484
}
14851485

1486-
const query: GetAccountResourcesOptions = { addresses };
1486+
const body: GetAccountResourcesOptions = { addresses };
14871487
if (destinationAddress) {
1488-
query.destinationAddress = destinationAddress;
1488+
body.destinationAddress = destinationAddress;
14891489
}
14901490

1491-
return this.bitgo.get(this.url('/getAccountResources')).query(query).result();
1491+
return this.bitgo.post(this.url('/getAccountResources')).send(body).result();
14921492
}
14931493

14941494
async updateWalletBuildDefaults(params: UpdateBuildDefaultOptions): Promise<unknown> {

modules/sdk-core/test/unit/bitgo/wallet/getAccountResources.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('Wallet - getAccountResources', function () {
1111

1212
beforeEach(function () {
1313
mockBitGo = {
14-
get: sinon.stub(),
14+
post: sinon.stub(),
1515
};
1616

1717
mockBaseCoin = {
@@ -40,8 +40,8 @@ describe('Wallet - getAccountResources', function () {
4040
],
4141
};
4242

43-
mockBitGo.get.returns({
44-
query: sinon.stub().returns({
43+
mockBitGo.post.returns({
44+
send: sinon.stub().returns({
4545
result: sinon.stub().resolves(mockResponse),
4646
}),
4747
});
@@ -50,18 +50,18 @@ describe('Wallet - getAccountResources', function () {
5050
const result = await wallet.getAccountResources({ addresses });
5151

5252
result.should.deepEqual(mockResponse);
53-
sinon.assert.calledOnce(mockBitGo.get);
54-
const queryStub = mockBitGo.get.returnValues[0].query;
55-
sinon.assert.calledWith(queryStub, { addresses });
53+
sinon.assert.calledOnce(mockBitGo.post);
54+
const sendStub = mockBitGo.post.returnValues[0].send;
55+
sinon.assert.calledWith(sendStub, { addresses });
5656
});
5757

5858
it('should call WP API with addresses and destinationAddress parameters', async function () {
5959
const mockResponse = {
6060
resources: [{ address: 'address1', balance: 100 }],
6161
};
6262

63-
mockBitGo.get.returns({
64-
query: sinon.stub().returns({
63+
mockBitGo.post.returns({
64+
send: sinon.stub().returns({
6565
result: sinon.stub().resolves(mockResponse),
6666
}),
6767
});
@@ -71,9 +71,9 @@ describe('Wallet - getAccountResources', function () {
7171
const result = await wallet.getAccountResources({ addresses, destinationAddress });
7272

7373
result.should.deepEqual(mockResponse);
74-
sinon.assert.calledOnce(mockBitGo.get);
75-
const queryStub = mockBitGo.get.returnValues[0].query;
76-
sinon.assert.calledWith(queryStub, { addresses, destinationAddress });
74+
sinon.assert.calledOnce(mockBitGo.post);
75+
const sendStub = mockBitGo.post.returnValues[0].send;
76+
sinon.assert.calledWith(sendStub, { addresses, destinationAddress });
7777
});
7878

7979
it('should throw error if addresses is not an array', async function () {
@@ -94,22 +94,22 @@ describe('Wallet - getAccountResources', function () {
9494
}
9595
});
9696

97-
it('should not include destinationAddress in query if not provided', async function () {
97+
it('should not include destinationAddress in body if not provided', async function () {
9898
const mockResponse = { resources: [] };
9999

100-
mockBitGo.get.returns({
101-
query: sinon.stub().returns({
100+
mockBitGo.post.returns({
101+
send: sinon.stub().returns({
102102
result: sinon.stub().resolves(mockResponse),
103103
}),
104104
});
105105

106106
const addresses = ['address1'];
107107
await wallet.getAccountResources({ addresses });
108108

109-
const queryStub = mockBitGo.get.returnValues[0].query;
110-
const queryArg = queryStub.firstCall.args[0];
111-
queryArg.should.deepEqual({ addresses });
112-
queryArg.should.not.have.property('destinationAddress');
109+
const sendStub = mockBitGo.post.returnValues[0].send;
110+
const bodyArg = sendStub.firstCall.args[0];
111+
bodyArg.should.deepEqual({ addresses });
112+
bodyArg.should.not.have.property('destinationAddress');
113113
});
114114
});
115115
});

0 commit comments

Comments
 (0)