From 335e26d2720d4ef217c7edb4d7b189f945936213 Mon Sep 17 00:00:00 2001 From: Bhavi Dhingra Date: Wed, 25 Mar 2026 00:12:05 +0530 Subject: [PATCH] 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 --- modules/express/src/clientRoutes.ts | 45 +++++++++ modules/express/src/typedRoutes/api/index.ts | 18 ++++ .../typedRoutes/api/v2/accountResources.ts | 95 +++++++++++++++++++ .../typedRoutes/api/v2/resourceDelegations.ts | 71 ++++++++++++++ 4 files changed, 229 insertions(+) create mode 100644 modules/express/src/typedRoutes/api/v2/accountResources.ts create mode 100644 modules/express/src/typedRoutes/api/v2/resourceDelegations.ts diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index 8e6ee42e63..d94277a7bc 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -1012,6 +1012,41 @@ async function handleV2SendMany(req: ExpressApiRouteRequest<'express.v2.wallet.s return result; } +/** + * handle get account resources + * @param req + */ +async function handleV2AccountResources(req: ExpressApiRouteRequest<'express.v2.wallet.accountresources', 'get'>) { + const bitgo = req.bitgo; + const coin = bitgo.coin(req.decoded.coin); + const wallet = await coin.wallets().get({ id: req.decoded.id }); + const addresses = Array.isArray(req.decoded.addresses) ? req.decoded.addresses : [req.decoded.addresses]; + return wallet.getAccountResources({ + addresses, + destinationAddress: req.decoded.destinationAddress, + }); +} + +/** + * handle get resource delegations + * @param req + */ +async function handleV2ResourceDelegations( + req: ExpressApiRouteRequest<'express.v2.wallet.resourcedelegations', 'get'> +) { + const bitgo = req.bitgo; + const coin = req.decoded.coin; + const walletId = req.decoded.id; + const query: Record = {}; + if (req.decoded.type) query.type = req.decoded.type; + if (req.decoded.resource) query.resource = req.decoded.resource; + if (req.decoded.limit) query.limit = req.decoded.limit; + return bitgo + .get(bitgo.url(`/${coin}/wallet/${walletId}/resourcedelegations`, 2)) + .query(query) + .result(); +} + /** * payload meant for prebuildAndSignTransaction() in sdk-core which * validates the payload and makes the appropriate request to WP to @@ -1770,6 +1805,16 @@ export function setupAPIRoutes(app: express.Application, config: Config): void { typedPromiseWrapper(handleV2ConsolidateAccount), ]); + // TRX resource delegation + router.get('express.v2.wallet.accountresources', [ + prepareBitGo(config), + typedPromiseWrapper(handleV2AccountResources), + ]); + router.get('express.v2.wallet.resourcedelegations', [ + prepareBitGo(config), + typedPromiseWrapper(handleV2ResourceDelegations), + ]); + // Miscellaneous router.post('express.canonicaladdress', [prepareBitGo(config), typedPromiseWrapper(handleCanonicalAddress)]); router.post('express.verifycoinaddress', [prepareBitGo(config), typedPromiseWrapper(handleV2VerifyAddress)]); diff --git a/modules/express/src/typedRoutes/api/index.ts b/modules/express/src/typedRoutes/api/index.ts index 74b16957d9..283da8227a 100644 --- a/modules/express/src/typedRoutes/api/index.ts +++ b/modules/express/src/typedRoutes/api/index.ts @@ -51,6 +51,8 @@ import { PostWalletEnableTokens } from './v2/walletEnableTokens'; import { PostWalletSweep } from './v2/walletSweep'; import { PostWalletAccelerateTx } from './v2/walletAccelerateTx'; import { PostIsWalletAddress } from './v2/isWalletAddress'; +import { GetAccountResources } from './v2/accountResources'; +import { GetResourceDelegations } from './v2/resourceDelegations'; // Too large types can cause the following error // @@ -322,6 +324,18 @@ export const ExpressV2WalletAccelerateTxApiSpec = apiSpec({ }, }); +export const ExpressV2WalletAccountResourcesApiSpec = apiSpec({ + 'express.v2.wallet.accountresources': { + get: GetAccountResources, + }, +}); + +export const ExpressV2WalletResourceDelegationsApiSpec = apiSpec({ + 'express.v2.wallet.resourcedelegations': { + get: GetResourceDelegations, + }, +}); + export type ExpressApi = typeof ExpressPingApiSpec & typeof ExpressPingExpressApiSpec & typeof ExpressLoginApiSpec & @@ -360,6 +374,8 @@ export type ExpressApi = typeof ExpressPingApiSpec & typeof ExpressV2CanonicalAddressApiSpec & typeof ExpressV2WalletSweepApiSpec & typeof ExpressV2WalletAccelerateTxApiSpec & + typeof ExpressV2WalletAccountResourcesApiSpec & + typeof ExpressV2WalletResourceDelegationsApiSpec & typeof ExpressWalletManagementApiSpec; export const ExpressApi: ExpressApi = { @@ -401,6 +417,8 @@ export const ExpressApi: ExpressApi = { ...ExpressV2CanonicalAddressApiSpec, ...ExpressV2WalletSweepApiSpec, ...ExpressV2WalletAccelerateTxApiSpec, + ...ExpressV2WalletAccountResourcesApiSpec, + ...ExpressV2WalletResourceDelegationsApiSpec, ...ExpressWalletManagementApiSpec, }; diff --git a/modules/express/src/typedRoutes/api/v2/accountResources.ts b/modules/express/src/typedRoutes/api/v2/accountResources.ts new file mode 100644 index 0000000000..6c1e6489a8 --- /dev/null +++ b/modules/express/src/typedRoutes/api/v2/accountResources.ts @@ -0,0 +1,95 @@ +import * as t from 'io-ts'; +import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http'; +import { BitgoExpressError } from '../../schemas/error'; + +/** + * Path parameters for account resources endpoint + */ +export const AccountResourcesParams = { + /** Coin identifier (e.g., 'trx', 'ttrx') */ + coin: t.string, + /** Wallet ID */ + id: t.string, +} as const; + +/** + * Query parameters for account resources endpoint + */ +export const AccountResourcesQuery = { + /** On-chain addresses to query resources for (comma-separated string or repeated query params) */ + addresses: t.union([t.string, t.array(t.string)]), + /** Optional destination address to calculate energy deficit for token transfers */ + destinationAddress: optional(t.string), +} as const; + +/** + * Account resource information for a single address + */ +export const AccountResourceInfo = t.intersection([ + t.type({ + address: t.string, + free_bandwidth_available: t.number, + free_bandwidth_used: t.number, + staked_bandwidth_available: t.number, + staked_bandwidth_used: t.number, + energy_available: t.number, + energy_used: t.number, + resourceDeficitForAssetTransfer: t.intersection([ + t.type({ + bandwidthDeficit: t.number, + bandwidthSunRequired: t.string, + }), + t.partial({ + energyDeficit: t.number, + energySunRequired: t.string, + }), + ]), + }), + t.partial({ + maxResourcesDelegatable: t.type({ + bandwidthWeight: t.string, + energyWeight: t.string, + }), + }), +]); + +/** + * Failed address information + */ +export const FailedAddressInfo = t.type({ + address: t.string, + error: t.string, +}); + +/** + * Response for account resources + */ +export const AccountResourcesResponse = { + /** Account resources for the queried addresses */ + 200: t.type({ + resources: t.array(AccountResourceInfo), + failedAddresses: t.array(FailedAddressInfo), + }), + /** Invalid request */ + 400: BitgoExpressError, +} as const; + +/** + * Get Account Resources + * + * Query BANDWIDTH and ENERGY resource information for TRX wallet addresses. + * Returns resource availability, usage, and optional deficit calculations + * for token transfers. + * + * @operationId express.v2.wallet.accountresources + * @tag express + */ +export const GetAccountResources = httpRoute({ + path: '/api/v2/{coin}/wallet/{id}/accountresources', + method: 'GET', + request: httpRequest({ + params: AccountResourcesParams, + query: AccountResourcesQuery, + }), + response: AccountResourcesResponse, +}); diff --git a/modules/express/src/typedRoutes/api/v2/resourceDelegations.ts b/modules/express/src/typedRoutes/api/v2/resourceDelegations.ts new file mode 100644 index 0000000000..6ed9cb34e9 --- /dev/null +++ b/modules/express/src/typedRoutes/api/v2/resourceDelegations.ts @@ -0,0 +1,71 @@ +import * as t from 'io-ts'; +import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http'; +import { BitgoExpressError } from '../../schemas/error'; + +/** + * Path parameters for resource delegations endpoint + */ +export const ResourceDelegationsParams = { + /** Coin identifier (e.g., 'trx', 'ttrx') */ + coin: t.string, + /** Wallet ID */ + id: t.string, +} as const; + +/** + * Query parameters for resource delegations endpoint + */ +export const ResourceDelegationsQuery = { + /** Filter by delegation type: 'owner' for outgoing, 'receiver' for incoming */ + type: optional(t.union([t.literal('owner'), t.literal('receiver')])), + /** Filter by resource type (case-insensitive: energy, ENERGY, bandwidth, BANDWIDTH) */ + resource: optional(t.string), + /** Maximum number of results to return */ + limit: optional(t.string), +} as const; + +/** + * A single delegation record + */ +export const DelegationRecord = t.type({ + from: t.string, + to: t.string, + amount: t.string, + resource: t.string, +}); + +/** + * Response for resource delegations + */ +export const ResourceDelegationsResponse = { + /** Resource delegations for the wallet */ + 200: t.type({ + address: t.string, + coin: t.string, + delegations: t.type({ + outgoing: t.array(DelegationRecord), + incoming: t.array(DelegationRecord), + }), + }), + /** Invalid request */ + 400: BitgoExpressError, +} as const; + +/** + * Get Resource Delegations + * + * Query active outgoing and incoming ENERGY/BANDWIDTH resource delegations + * for a TRX wallet. + * + * @operationId express.v2.wallet.resourcedelegations + * @tag express + */ +export const GetResourceDelegations = httpRoute({ + path: '/api/v2/{coin}/wallet/{id}/resourcedelegations', + method: 'GET', + request: httpRequest({ + params: ResourceDelegationsParams, + query: ResourceDelegationsQuery, + }), + response: ResourceDelegationsResponse, +});