diff --git a/examples/ts/list-wallet-addresses-by-balance.ts b/examples/ts/list-wallet-addresses-by-balance.ts index b068825614..0ec87ee38e 100644 --- a/examples/ts/list-wallet-addresses-by-balance.ts +++ b/examples/ts/list-wallet-addresses-by-balance.ts @@ -22,7 +22,7 @@ async function main() { const addresses = await wallet.addressesByBalance({ token: 'tapt:usdt', }); - console.log(JSON.stringify(addresses.addresses)); + console.log(JSON.stringify(addresses.addresses, null, 2)); } main().catch((e) => console.error(e)); diff --git a/examples/ts/trx/delegate-resource.ts b/examples/ts/trx/delegate-resource.ts new file mode 100644 index 0000000000..24da5465f5 --- /dev/null +++ b/examples/ts/trx/delegate-resource.ts @@ -0,0 +1,74 @@ +/** + * Build and sign a TRX DelegateResource transaction via the BitGo platform. + * + * DelegateResource allows a TRX holder who has frozen TRX (via FreezeBalanceV2) + * to delegate the resulting BANDWIDTH or ENERGY resources to another address, + * without transferring TRX itself. + * + * Prerequisites: + * - Valid BitGo access token + * - TRX wallet with frozen balance (via FreezeBalanceV2) + * - Wallet passphrase for signing + * + * Copyright 2026, BitGo, Inc. All Rights Reserved. + */ +import { BitGoAPI } from '@bitgo/sdk-api'; +import { Ttrx } from '@bitgo/sdk-coin-trx'; +require('dotenv').config({ path: '../../../.env' }); + +// TODO: change to 'production' for mainnet +const bitgo = new BitGoAPI({ + accessToken: process.env.TESTNET_ACCESS_TOKEN, + env: 'test', +}); + +const coin = 'ttrx'; +bitgo.register(coin, Ttrx.createInstance); + +const walletId = ''; +const walletPassphrase = ''; +const otp = '000000'; + +// TODO: set the receiver address (the address that will use the delegated resources) +const receiverAddress = ''; + +// TODO: set the amount of frozen TRX to delegate, in SUN (1 TRX = 1,000,000 SUN) +const amountSun = '1000000'; + +// TODO: set the resource type to delegate: 'energy' or 'bandwidth' +const resource = 'bandwidth'; + +async function main() { + const wallet = await bitgo.coin(coin).wallets().getWallet({ id: walletId }); + console.log('Wallet ID:', wallet.id()); + + // Unlock the session for signing + const unlock = await bitgo.unlock({ otp, duration: 3600 }); + if (!unlock) { + throw new Error('error unlocking session'); + } + + // Build, sign, and send the transaction in one step + // The SDK handles prebuild, user half-sign, platform co-signing, and broadcasting + const result = await wallet.sendMany({ + type: 'delegateResource', + stakingParams: { + receiver_address: receiverAddress, + amount: amountSun, + resource, + }, + recipients: [ + { + address: receiverAddress, + amount: '0', + }, + ], + walletPassphrase, + }); + + console.log('Transaction sent successfully!'); + console.log('TX ID:', result.txid); + console.log('Result:', JSON.stringify(result, null, 2)); +} + +main().catch((e) => console.error(e)); diff --git a/examples/ts/trx/get-account-resources.ts b/examples/ts/trx/get-account-resources.ts index 01fa1973bf..3f061146b3 100644 --- a/examples/ts/trx/get-account-resources.ts +++ b/examples/ts/trx/get-account-resources.ts @@ -6,31 +6,29 @@ * * Copyright 2026, BitGo, Inc. All Rights Reserved. */ -import { BitGo } from 'bitgo'; +import { BitGoAPI } from "@bitgo/sdk-api"; +import { Ttrx } from "@bitgo/sdk-coin-trx"; +require('dotenv').config({ path: '../../../.env' }); // TODO: change to 'production' for mainnet -const env = 'test'; -const bitgo = new BitGo({ env }); +const bitgo = new BitGoAPI({ + accessToken: process.env.TESTNET_ACCESS_TOKEN, + env: 'test', +}); // TODO: change to 'trx' for mainnet or 'ttrx:' for testnet token const coin = 'ttrx'; +bitgo.register(coin, Ttrx.createInstance); -// TODO: set your wallet id const walletId = ''; -// TODO: set your access token here -// You can get this from User Settings > Developer Options > Add Access Token -const accessToken = ''; - // TODO: set the addresses to query -// Note: To get energy deficit for a token transfer, make sure the token exists in the address. +// Note: To get energy deficit for a token transfer/consolidation, make sure the token exists in the address. const addresses = ['']; async function main() { - bitgo.authenticateWithAccessToken({ accessToken }); const wallet = await bitgo.coin(coin).wallets().getWallet({ id: walletId }); - console.log('Wallet ID:', wallet.id()); const resources = await wallet.getAccountResources({ addresses }); diff --git a/examples/ts/trx/get-resource-delegations.ts b/examples/ts/trx/get-resource-delegations.ts new file mode 100644 index 0000000000..0c22f84e24 --- /dev/null +++ b/examples/ts/trx/get-resource-delegations.ts @@ -0,0 +1,49 @@ +/** + * Get resource delegations for a TRX wallet at BitGo. + * + * This tool will help you see how to use the BitGo SDK to query + * outgoing and incoming ENERGY/BANDWIDTH delegations for a wallet. + * + * Prerequisites: + * - Valid BitGo access token + * - TRX wallet ID + * + * Copyright 2026, BitGo, Inc. All Rights Reserved. + */ +import { BitGoAPI } from '@bitgo/sdk-api'; +import { Ttrx } from '@bitgo/sdk-coin-trx'; +require('dotenv').config({ path: '../../../.env' }); + +// TODO: change to 'production' for mainnet +const bitgo = new BitGoAPI({ + accessToken: process.env.TESTNET_ACCESS_TOKEN, + env: 'test', +}); + +// TODO: change to 'trx' for mainnet +const coin = 'ttrx'; +bitgo.register(coin, Ttrx.createInstance); + +const walletId = ''; + +// TODO: (optional) filter by delegation type +const type: 'outgoing' | 'incoming' | undefined = undefined; + +// TODO: (optional) filter by resource type (e.g. 'ENERGY', 'energy', 'BANDWIDTH', 'bandwidth') +const resource: string | undefined = undefined; + +// TODO: (optional) maximum number of results to return +const limit: number | undefined = undefined; + +async function main() { + const wallet = await bitgo.coin(coin).wallets().get({ id: walletId }); + const result = await wallet.getResourceDelegations({ type, resource, limit }); + + console.log('Wallet Address:', result.address); + console.log('Coin:', result.coin); + console.log('Outgoing Delegations:', result.delegations.outgoing.length); + console.log('Incoming Delegations:', result.delegations.incoming.length); + console.log('Resource Delegations:', JSON.stringify(result, null, 2)); +} + +main().catch((e) => console.error(e)); diff --git a/examples/ts/trx/undelegate-resource.ts b/examples/ts/trx/undelegate-resource.ts new file mode 100644 index 0000000000..e4911581d3 --- /dev/null +++ b/examples/ts/trx/undelegate-resource.ts @@ -0,0 +1,75 @@ +/** + * Build and sign a TRX UndelegateResource transaction via the BitGo platform. + * + * DelegateResource allows a TRX holder who has frozen TRX (via FreezeBalanceV2) + * to reclaim the BANDWIDTH or ENERGY resource from another address. + * + * Prerequisites: + * - Valid BitGo access token + * - TRX wallet with frozen balance (via FreezeBalanceV2) + * - Wallet passphrase for signing + * + * Copyright 2026, BitGo, Inc. All Rights Reserved. + */ +import { WalletCoinSpecific } from 'bitgo'; +import {BitGoAPI} from "@bitgo/sdk-api"; +import {Ttrx} from "@bitgo/sdk-coin-trx"; +require('dotenv').config({ path: '../../../.env' }); + +// TODO: change to 'production' for mainnet +const bitgo = new BitGoAPI({ + accessToken: process.env.TESTNET_ACCESS_TOKEN, + env: 'test', +}); + +const coin = 'ttrx'; +bitgo.register(coin, Ttrx.createInstance); + +const walletId = ''; +const walletPassphrase = ''; +const otp = '000000'; + +// TODO: set the receiver address (the address from which resource will be reclaimed) +const receiverAddress = ''; + +// TODO: set the amount of frozen TRX to undelegate, in SUN (1 TRX = 1,000,000 SUN) +const amountSun = '1000000'; + +// TODO: set the resource type to undelegate: 'energy' or 'bandwidth' +const resource = 'bandwidth'; + +async function main() { + + const wallet = await bitgo.coin(coin).wallets().getWallet({ id: walletId }); + console.log('Wallet ID:', wallet.id()); + + // Unlock the session for signing + const unlock = await bitgo.unlock({ otp, duration: 3600 }); + if (!unlock) { + throw new Error('error unlocking session'); + } + + // Build, sign, and send the transaction in one step + // The SDK handles prebuild, user half-sign, platform co-signing, and broadcasting + const result = await wallet.sendMany({ + type: 'undelegateResource', + stakingParams: { + receiver_address: receiverAddress, + amount: amountSun, + resource, + }, + recipients: [ + { + address: receiverAddress, + amount: '0', + }, + ], + walletPassphrase, + }); + + console.log('Transaction sent successfully!'); + console.log('TX ID:', result.txid); + console.log('Result:', JSON.stringify(result, null, 2)); +} + +main().catch((e) => console.error(e)); diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index ff0c454456..260b545e58 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -1036,17 +1036,14 @@ export 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 !== undefined) query.limit = String(req.decoded.limit); - if (req.decoded.nextBatchPrevId) query.nextBatchPrevId = req.decoded.nextBatchPrevId; - return bitgo - .get(bitgo.url(`/${coin}/wallet/${walletId}/resourcedelegations`, 2)) - .query(query) - .result(); + const coin = bitgo.coin(req.decoded.coin); + const wallet = await coin.wallets().get({ id: req.decoded.id }); + return wallet.getResourceDelegations({ + type: req.decoded.type, + resource: req.decoded.resource, + limit: req.decoded.limit, + nextBatchPrevId: req.decoded.nextBatchPrevId, + }); } /** diff --git a/modules/express/test/unit/clientRoutes/trxResourceDelegation.ts b/modules/express/test/unit/clientRoutes/trxResourceDelegation.ts index 6790bbaf33..6fb5d8ed8d 100644 --- a/modules/express/test/unit/clientRoutes/trxResourceDelegation.ts +++ b/modules/express/test/unit/clientRoutes/trxResourceDelegation.ts @@ -99,19 +99,18 @@ describe('TRX Resource Delegation handlers', () => { }, }; - function createBitgoStub(result: unknown) { - const resultStub = sandbox.stub().resolves(result); - const queryStub = sandbox.stub().returns({ result: resultStub }); - const getStub = sandbox.stub().returns({ query: queryStub }); - const urlStub = sandbox - .stub() - .returns('https://test.bitgo.com/api/v2/ttrx/wallet/walletId123/resourcedelegations'); - const bitgoStub = sinon.createStubInstance(BitGo as any, { get: getStub, url: urlStub }); - return { bitgoStub, getStub, queryStub, resultStub }; + function createMocks(result: unknown) { + const getResourceDelegationsStub = sandbox.stub().resolves(result); + const walletStub = { getResourceDelegations: getResourceDelegationsStub }; + const coinStub = { + wallets: () => ({ get: () => Promise.resolve(walletStub) }), + }; + const bitgoStub = sinon.createStubInstance(BitGo as any, { coin: coinStub }); + return { bitgoStub, getResourceDelegationsStub }; } it('should forward type and resource query params', async () => { - const { bitgoStub, queryStub } = createBitgoStub(mockDelegations); + const { bitgoStub, getResourceDelegationsStub } = createMocks(mockDelegations); const mockRequest = { bitgo: bitgoStub, @@ -124,11 +123,16 @@ describe('TRX Resource Delegation handlers', () => { }; await handleV2ResourceDelegations(mockRequest as express.Request & typeof mockRequest); - queryStub.should.be.calledOnceWith({ type: 'outgoing', resource: 'ENERGY' }); + getResourceDelegationsStub.should.be.calledOnceWith({ + type: 'outgoing', + resource: 'ENERGY', + limit: undefined, + nextBatchPrevId: undefined, + }); }); - it('should convert limit to string when forwarding', async () => { - const { bitgoStub, queryStub } = createBitgoStub(mockDelegations); + it('should forward limit param', async () => { + const { bitgoStub, getResourceDelegationsStub } = createMocks(mockDelegations); const mockRequest = { bitgo: bitgoStub, @@ -140,11 +144,16 @@ describe('TRX Resource Delegation handlers', () => { }; await handleV2ResourceDelegations(mockRequest as express.Request & typeof mockRequest); - queryStub.should.be.calledOnceWith({ limit: '10' }); + getResourceDelegationsStub.should.be.calledOnceWith({ + type: undefined, + resource: undefined, + limit: 10, + nextBatchPrevId: undefined, + }); }); it('should forward nextBatchPrevId for pagination', async () => { - const { bitgoStub, queryStub } = createBitgoStub(mockDelegations); + const { bitgoStub, getResourceDelegationsStub } = createMocks(mockDelegations); const mockRequest = { bitgo: bitgoStub, @@ -157,11 +166,16 @@ describe('TRX Resource Delegation handlers', () => { }; await handleV2ResourceDelegations(mockRequest as express.Request & typeof mockRequest); - queryStub.should.be.calledOnceWith({ type: 'incoming', nextBatchPrevId: 'cursor-abc123' }); + getResourceDelegationsStub.should.be.calledOnceWith({ + type: 'incoming', + resource: undefined, + limit: undefined, + nextBatchPrevId: 'cursor-abc123', + }); }); it('should return codec-valid delegations result', async () => { - const { bitgoStub } = createBitgoStub(mockDelegations); + const { bitgoStub } = createMocks(mockDelegations); const mockRequest = { bitgo: bitgoStub, diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index 1be3f6578d..0eca56f6d9 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -579,6 +579,33 @@ export interface GetAccountResourcesResponse { failedAddresses: FailedAddressInfo[]; } +export interface GetResourceDelegationsOptions { + type?: 'outgoing' | 'incoming'; + resource?: string; + limit?: number; + nextBatchPrevId?: string; +} + +export interface DelegationRecord { + id: string; + coin: string; + ownerAddress: string; + receiverAddress: string; + resource: 'ENERGY' | 'BANDWIDTH'; + balance: string; + updatedAt: string; +} + +export interface GetResourceDelegationsResponse { + address: string; + coin: string; + delegations: { + outgoing: DelegationRecord[]; + incoming: DelegationRecord[]; + nextBatchPrevId?: string; + }; +} + export type CreateAddressFormat = 'base58' | 'cashaddr'; export interface CreateAddressOptions { @@ -1029,6 +1056,7 @@ export interface IWallet { createAddress(params?: CreateAddressOptions): Promise; updateAddress(params?: UpdateAddressOptions): Promise; getAccountResources(params: GetAccountResourcesOptions): Promise; + getResourceDelegations(params?: GetResourceDelegationsOptions): Promise; listWebhooks(params?: PaginationOptions): Promise; simulateWebhook(params?: SimulateWebhookOptions): Promise; addWebhook(params?: ModifyWebhookOptions): Promise; diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 821c15da1d..69a95271c2 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -127,6 +127,8 @@ import { WalletInitResult, GetAccountResourcesOptions, GetAccountResourcesResponse, + GetResourceDelegationsOptions, + GetResourceDelegationsResponse, } from './iWallet'; const debug = require('debug')('bitgo:v2:wallet'); @@ -1491,6 +1493,21 @@ export class Wallet implements IWallet { return this.bitgo.post(this.url('/getAccountResources')).send(body).result(); } + /** + * Get resource delegations for this wallet + * @param params - optional filters: type, resource, limit, nextBatchPrevId + * @returns {Promise} - response from WP API + */ + async getResourceDelegations(params?: GetResourceDelegationsOptions): Promise { + const queryParams: Record = {}; + if (params?.type !== undefined) queryParams.type = params.type; + if (params?.resource !== undefined) queryParams.resource = params.resource; + if (params?.limit !== undefined) queryParams.limit = params.limit; + if (params?.nextBatchPrevId !== undefined) queryParams.nextBatchPrevId = params.nextBatchPrevId; + + return this.bitgo.get(this.url('/resourcedelegations')).query(queryParams).result(); + } + async updateWalletBuildDefaults(params: UpdateBuildDefaultOptions): Promise { common.validateParams(params, [], ['minFeeRate', 'changeAddressType', 'txFormat']); return this.updateWallet({ diff --git a/modules/sdk-core/test/unit/bitgo/wallet/getResourceDelegations.ts b/modules/sdk-core/test/unit/bitgo/wallet/getResourceDelegations.ts new file mode 100644 index 0000000000..34c2b0cea0 --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/wallet/getResourceDelegations.ts @@ -0,0 +1,108 @@ +import * as sinon from 'sinon'; +import 'should'; +import { Wallet } from '../../../../src'; + +describe('Wallet - getResourceDelegations', function () { + let wallet: Wallet; + let mockBitGo: any; + let mockBaseCoin: any; + let mockWalletData: any; + + beforeEach(function () { + mockBitGo = { + get: sinon.stub(), + }; + + mockBaseCoin = { + url: sinon.stub().returns('/test/coin'), + supportsTss: sinon.stub().returns(false), + }; + + mockWalletData = { + id: 'test-wallet-id', + keys: ['user-key', 'backup-key', 'bitgo-key'], + }; + + wallet = new Wallet(mockBitGo, mockBaseCoin, mockWalletData); + }); + + afterEach(function () { + sinon.restore(); + }); + + const mockResponse = { + address: 'TAddr123', + coin: 'ttrx', + delegations: { + outgoing: [], + incoming: [], + }, + }; + + function stubGet() { + const resultStub = sinon.stub().resolves(mockResponse); + const queryStub = sinon.stub().returns({ result: resultStub }); + mockBitGo.get.returns({ query: queryStub }); + return { queryStub, resultStub }; + } + + it('should call WP API with no query params when called with no options', async function () { + const { queryStub } = stubGet(); + + const result = await wallet.getResourceDelegations(); + + result.should.deepEqual(mockResponse); + sinon.assert.calledOnce(mockBitGo.get); + sinon.assert.calledWith(queryStub, {}); + }); + + it('should include type in query params when provided', async function () { + const { queryStub } = stubGet(); + + await wallet.getResourceDelegations({ type: 'outgoing' }); + + sinon.assert.calledWith(queryStub, { type: 'outgoing' }); + }); + + it('should include resource in query params when provided', async function () { + const { queryStub } = stubGet(); + + await wallet.getResourceDelegations({ resource: 'ENERGY' }); + + sinon.assert.calledWith(queryStub, { resource: 'ENERGY' }); + }); + + it('should include limit in query params when provided', async function () { + const { queryStub } = stubGet(); + + await wallet.getResourceDelegations({ limit: 10 }); + + sinon.assert.calledWith(queryStub, { limit: 10 }); + }); + + it('should include nextBatchPrevId in query params when provided', async function () { + const { queryStub } = stubGet(); + + await wallet.getResourceDelegations({ nextBatchPrevId: 'cursor-abc123' }); + + sinon.assert.calledWith(queryStub, { nextBatchPrevId: 'cursor-abc123' }); + }); + + it('should include all provided params in query', async function () { + const { queryStub } = stubGet(); + + await wallet.getResourceDelegations({ + type: 'incoming', + resource: 'BANDWIDTH', + limit: 5, + nextBatchPrevId: 'cursor-xyz', + }); + + sinon.assert.calledWith(queryStub, { + type: 'incoming', + resource: 'BANDWIDTH', + limit: 5, + nextBatchPrevId: 'cursor-xyz', + }); + }); +});