From b22a03a5ef18ae7fb85af8032c306aa152c86507 Mon Sep 17 00:00:00 2001 From: Bhavi Dhingra Date: Mon, 20 Apr 2026 14:35:23 +0530 Subject: [PATCH] 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 --- modules/express/src/clientRoutes.ts | 92 ++++++++ modules/express/src/typedRoutes/api/index.ts | 18 ++ .../typedRoutes/api/v2/delegateResources.ts | 105 +++++++++ .../typedRoutes/api/v2/undelegateResources.ts | 75 ++++++ modules/sdk-coin-trx/src/trx.ts | 5 + .../sdk-core/src/bitgo/baseCoin/baseCoin.ts | 4 + .../sdk-core/src/bitgo/baseCoin/iBaseCoin.ts | 1 + modules/sdk-core/src/bitgo/wallet/iWallet.ts | 33 +++ modules/sdk-core/src/bitgo/wallet/wallet.ts | 223 ++++++++++++++++++ 9 files changed, 556 insertions(+) create mode 100644 modules/express/src/typedRoutes/api/v2/delegateResources.ts create mode 100644 modules/express/src/typedRoutes/api/v2/undelegateResources.ts diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index b1f151031f..9ea90c17cf 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -1050,6 +1050,90 @@ export async function handleV2ResourceDelegations( }); } +/** + * Shared handler for bulk resource management (delegation / undelegation). + * Builds, signs, and sends one on-chain transaction per entry. + */ +async function handleV2ResourceManagement( + type: 'delegateResource' | 'undelegateResource', + req: + | ExpressApiRouteRequest<'express.v2.wallet.delegateresources', 'post'> + | ExpressApiRouteRequest<'express.v2.wallet.undelegateresources', 'post'> +) { + const bitgo = req.bitgo; + const coin = bitgo.coin(req.decoded.coin); + + if (type === 'delegateResource') { + const decoded = (req as ExpressApiRouteRequest<'express.v2.wallet.delegateresources', 'post'>).decoded; + if (!Array.isArray(decoded.delegations) || decoded.delegations.length === 0) { + throw new Error('delegations must be a non-empty array'); + } + } else { + const decoded = (req as ExpressApiRouteRequest<'express.v2.wallet.undelegateresources', 'post'>).decoded; + if (!Array.isArray(decoded.undelegations) || decoded.undelegations.length === 0) { + throw new Error('undelegations must be a non-empty array'); + } + } + + if (!coin.supportsResourceDelegation()) { + throw new Error(`${coin.getFamily()} does not support resource delegation`); + } + + const wallet = await coin.wallets().get({ id: req.decoded.id }); + + let result: any; + try { + const params = coin.supportsTss() ? createTSSSendParams(req, wallet) : createSendParams(req); + result = + type === 'delegateResource' + ? await wallet.sendResourceDelegations(params) + : await wallet.sendResourceUndelegations(params); + } catch (err) { + // Surface unexpected errors as 400 rather than 500 + (err as any).status = 400; + throw err; + } + + // Handle partial success / failure + if (result.failure.length > 0) { + let msg = ''; + let status = 202; + + if (result.success.length > 0) { + msg = `Transactions failed: ${result.failure.length} and succeeded: ${result.success.length}`; + } else { + status = 400; + msg = `All transactions failed`; + } + + throw apiResponse(status, result, msg); + } + + return result; +} + +/** + * Handle bulk resource delegation (e.g. TRX ENERGY/BANDWIDTH delegation). + * Builds, signs, and sends one on-chain delegation transaction per entry in req.body.delegations. + * @param req + */ +export async function handleV2DelegateResources( + req: ExpressApiRouteRequest<'express.v2.wallet.delegateresources', 'post'> +) { + return handleV2ResourceManagement('delegateResource', req); +} + +/** + * Handle bulk resource undelegation (e.g. TRX ENERGY/BANDWIDTH undelegation). + * Builds, signs, and sends one on-chain undelegation transaction per entry in req.body.undelegations. + * @param req + */ +export async function handleV2UndelegateResources( + req: ExpressApiRouteRequest<'express.v2.wallet.undelegateresources', 'post'> +) { + return handleV2ResourceManagement('undelegateResource', req); +} + /** * payload meant for prebuildAndSignTransaction() in sdk-core which * validates the payload and makes the appropriate request to WP to @@ -1830,6 +1914,14 @@ export function setupAPIRoutes(app: express.Application, config: Config): void { prepareBitGo(config), typedPromiseWrapper(handleV2ResourceDelegations), ]); + router.post('express.v2.wallet.delegateresources', [ + prepareBitGo(config), + typedPromiseWrapper(handleV2DelegateResources), + ]); + router.post('express.v2.wallet.undelegateresources', [ + prepareBitGo(config), + typedPromiseWrapper(handleV2UndelegateResources), + ]); // Miscellaneous router.post('express.canonicaladdress', [prepareBitGo(config), typedPromiseWrapper(handleCanonicalAddress)]); diff --git a/modules/express/src/typedRoutes/api/index.ts b/modules/express/src/typedRoutes/api/index.ts index 25129de955..ec10867458 100644 --- a/modules/express/src/typedRoutes/api/index.ts +++ b/modules/express/src/typedRoutes/api/index.ts @@ -56,6 +56,8 @@ import { PostWalletAccelerateTx } from './v2/walletAccelerateTx'; import { PostIsWalletAddress } from './v2/isWalletAddress'; import { GetAccountResources } from './v2/accountResources'; import { GetResourceDelegations } from './v2/resourceDelegations'; +import { PostDelegateResources } from './v2/delegateResources'; +import { PostUndelegateResources } from './v2/undelegateResources'; // Too large types can cause the following error // @@ -184,6 +186,18 @@ export const ExpressV2WalletConsolidateAccountApiSpec = apiSpec({ }, }); +export const ExpressV2WalletDelegateResourcesApiSpec = apiSpec({ + 'express.v2.wallet.delegateresources': { + post: PostDelegateResources, + }, +}); + +export const ExpressV2WalletUndelegateResourcesApiSpec = apiSpec({ + 'express.v2.wallet.undelegateresources': { + post: PostUndelegateResources, + }, +}); + export const ExpressWalletFanoutUnspentsApiSpec = apiSpec({ 'express.v1.wallet.fanoutunspents': { put: PutFanoutUnspents, @@ -389,6 +403,8 @@ export type ExpressApi = typeof ExpressPingApiSpec & typeof ExpressV2WalletAccelerateTxApiSpec & typeof ExpressV2WalletAccountResourcesApiSpec & typeof ExpressV2WalletResourceDelegationsApiSpec & + typeof ExpressV2WalletDelegateResourcesApiSpec & + typeof ExpressV2WalletUndelegateResourcesApiSpec & typeof ExpressWalletManagementApiSpec; export const ExpressApi: ExpressApi = { @@ -432,6 +448,8 @@ export const ExpressApi: ExpressApi = { ...ExpressV2WalletAccelerateTxApiSpec, ...ExpressV2WalletAccountResourcesApiSpec, ...ExpressV2WalletResourceDelegationsApiSpec, + ...ExpressV2WalletDelegateResourcesApiSpec, + ...ExpressV2WalletUndelegateResourcesApiSpec, ...ExpressWalletManagementApiSpec, }; diff --git a/modules/express/src/typedRoutes/api/v2/delegateResources.ts b/modules/express/src/typedRoutes/api/v2/delegateResources.ts new file mode 100644 index 0000000000..996978e247 --- /dev/null +++ b/modules/express/src/typedRoutes/api/v2/delegateResources.ts @@ -0,0 +1,105 @@ +import * as t from 'io-ts'; +import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http'; +import { BitgoExpressError } from '../../schemas/error'; + +/** + * Path parameters for the delegate resources endpoint + */ +export const DelegateResourcesParams = { + /** Coin identifier (e.g., 'trx', 'ttrx') */ + coin: t.string, + /** Wallet ID */ + id: t.string, +} as const; + +/** + * A single resource delegation entry + */ +export const DelegationEntryCodec = t.type({ + /** On-chain address that will receive the delegated resources */ + receiverAddress: t.string, + /** Amount of TRX (in SUN) to stake for the delegation */ + amount: t.string, + /** Resource type to delegate (e.g. 'ENERGY', 'BANDWIDTH') */ + resource: t.string, +}); + +/** + * Request body for delegating resources to multiple receiver addresses. + * Each delegation entry triggers a separate on-chain staking transaction + * from the wallet's root address to the receiver address. + * + * Signing behaviour by wallet type: + * - Hot (non-TSS) → signed locally with walletPassphrase and submitted + * - Custodial non-TSS → sent for BitGo approval via initiateTransaction + * - TSS (any) → build response contains txRequestId; signed by TSS service + */ +export const DelegateResourcesRequestBody = { + /** Delegation entries — one on-chain transaction is built per entry */ + delegations: t.array(DelegationEntryCodec), + + /** Wallet passphrase to decrypt the user key (hot wallets) */ + walletPassphrase: optional(t.string), + /** Extended private key (alternative to walletPassphrase) */ + xprv: optional(t.string), + /** One-time password for 2FA */ + otp: optional(t.string), + + /** API version for TSS transaction request response ('lite' or 'full') */ + apiVersion: optional(t.union([t.literal('lite'), t.literal('full')])), +} as const; + +export const DelegationFailureEntry = t.type({ + /** Human-readable error message */ + message: t.string, + /** Receiver address that failed, if available */ + receiverAddress: t.union([t.string, t.undefined]), +}); + +/** + * Response for the delegate resources operation. + * Returns arrays of successful and failed delegation transactions. + */ +export const DelegateResourcesResponse = t.type({ + /** Successfully sent delegation transactions */ + success: t.array(t.unknown), + /** Errors from failed delegation transactions */ + failure: t.array(DelegationFailureEntry), +}); + +/** + * Response for partial success or failure cases (202/400). + * Includes both the transaction results and error metadata. + */ +export const DelegateResourcesErrorResponse = t.intersection([DelegateResourcesResponse, BitgoExpressError]); + +/** + * Bulk Resource Delegation + * + * Delegates resources (ENERGY or BANDWIDTH) from a wallet's root address to one or more + * receiver addresses. Each delegation entry produces a separate on-chain staking transaction. + * This is the resource-delegation analogue of the consolidateAccount endpoint. + * + * Supported coins: TRON (trx, ttrx) and any future coins that support resource delegation. + * + * The API may return partial success (status 202) if some delegations succeed but others fail. + * + * @operationId express.v2.wallet.delegateresources + * @tag express + */ +export const PostDelegateResources = httpRoute({ + path: '/api/v2/{coin}/wallet/{id}/delegateResources', + method: 'POST', + request: httpRequest({ + params: DelegateResourcesParams, + body: DelegateResourcesRequestBody, + }), + response: { + /** All delegations succeeded */ + 200: DelegateResourcesResponse, + /** Partial success — some delegations succeeded, others failed */ + 202: DelegateResourcesErrorResponse, + /** All delegations failed */ + 400: DelegateResourcesErrorResponse, + }, +}); diff --git a/modules/express/src/typedRoutes/api/v2/undelegateResources.ts b/modules/express/src/typedRoutes/api/v2/undelegateResources.ts new file mode 100644 index 0000000000..c557b4c606 --- /dev/null +++ b/modules/express/src/typedRoutes/api/v2/undelegateResources.ts @@ -0,0 +1,75 @@ +import * as t from 'io-ts'; +import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http'; +import { BitgoExpressError } from '../../schemas/error'; +import { DelegateResourcesParams, DelegationEntryCodec, DelegationFailureEntry } from './delegateResources'; + +/** + * Request body for undelegating resources from multiple receiver addresses. + * Each undelegation entry triggers a separate on-chain transaction that reclaims + * previously delegated resources back to the wallet's root address. + * + * Signing behaviour by wallet type: + * - Hot (non-TSS) → signed locally with walletPassphrase and submitted + * - Custodial non-TSS → sent for BitGo approval via initiateTransaction + * - TSS (any) → build response contains txRequestId; signed by TSS service + */ +export const UndelegateResourcesRequestBody = { + /** Undelegation entries — one on-chain transaction is built per entry */ + undelegations: t.array(DelegationEntryCodec), + + /** Wallet passphrase to decrypt the user key (hot wallets) */ + walletPassphrase: optional(t.string), + /** Extended private key (alternative to walletPassphrase) */ + xprv: optional(t.string), + /** One-time password for 2FA */ + otp: optional(t.string), + + /** API version for TSS transaction request response ('lite' or 'full') */ + apiVersion: optional(t.union([t.literal('lite'), t.literal('full')])), +} as const; + +/** + * Response for the undelegate resources operation. + * Returns arrays of successful and failed undelegation transactions. + */ +export const UndelegateResourcesResponse = t.type({ + /** Successfully sent undelegation transactions */ + success: t.array(t.unknown), + /** Errors from failed undelegation transactions */ + failure: t.array(DelegationFailureEntry), +}); + +/** + * Response for partial success or failure cases (202/400). + */ +export const UndelegateResourcesErrorResponse = t.intersection([UndelegateResourcesResponse, BitgoExpressError]); + +/** + * Bulk Resource Undelegation + * + * Reclaims delegated resources (ENERGY or BANDWIDTH) back to a wallet's root address + * from one or more receiver addresses. Each entry produces a separate on-chain transaction. + * + * Supported coins: TRON (trx, ttrx) and any future coins that support resource delegation. + * + * The API may return partial success (status 202) if some undelegations succeed but others fail. + * + * @operationId express.v2.wallet.undelegateresources + * @tag express + */ +export const PostUndelegateResources = httpRoute({ + path: '/api/v2/{coin}/wallet/{id}/undelegateResources', + method: 'POST', + request: httpRequest({ + params: DelegateResourcesParams, + body: UndelegateResourcesRequestBody, + }), + response: { + /** All undelegations succeeded */ + 200: UndelegateResourcesResponse, + /** Partial success — some undelegations succeeded, others failed */ + 202: UndelegateResourcesErrorResponse, + /** All undelegations failed */ + 400: UndelegateResourcesErrorResponse, + }, +}); diff --git a/modules/sdk-coin-trx/src/trx.ts b/modules/sdk-coin-trx/src/trx.ts index 08c9b5f371..3c19182542 100644 --- a/modules/sdk-coin-trx/src/trx.ts +++ b/modules/sdk-coin-trx/src/trx.ts @@ -225,6 +225,11 @@ export class Trx extends BaseCoin { return true; } + /** @inheritDoc */ + supportsResourceDelegation(): boolean { + return true; + } + /** * Checks if this is a valid base58 * @param address diff --git a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts index 737024125c..6b393129ee 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts @@ -173,6 +173,10 @@ export abstract class BaseCoin implements IBaseCoin { return false; } + supportsResourceDelegation(): boolean { + return false; + } + /** * Gets config for how token enablements work for this coin * @returns diff --git a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts index 9f127d619f..e4cebd5548 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts @@ -575,6 +575,7 @@ export interface IBaseCoin { sweepWithSendMany(): boolean; transactionDataAllowed(): boolean; allowsAccountConsolidations(): boolean; + supportsResourceDelegation(): boolean; getTokenEnablementConfig(): TokenEnablementConfig; supportsTss(): boolean; supportsMultisig(): boolean; diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index 1f65b5a977..e513c95f20 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -63,6 +63,26 @@ export interface BuildConsolidationTransactionOptions extends PrebuildTransactio consolidateAddresses?: string[]; } +export interface ResourceDelegationEntry { + receiverAddress: string; + amount: string; + /** Resource type to delegate (e.g. 'ENERGY', 'BANDWIDTH'). */ + resource: string; +} + +export interface BuildResourceDelegationTransactionOptions + extends PrebuildTransactionOptions, + WalletSignTransactionOptions { + delegations: ResourceDelegationEntry[]; +} + +export interface BuildResourceUndelegationTransactionOptions + extends PrebuildTransactionOptions, + WalletSignTransactionOptions { + // receiverAddress denotes the account to undelegate FROM + undelegations: ResourceDelegationEntry[]; +} + export interface BuildTokenEnablementOptions extends PrebuildTransactionOptions { enableTokens: TokenEnablement[]; } @@ -251,6 +271,7 @@ export interface PrebuildTransactionResult extends TransactionPrebuild { pendingApprovalId?: string; reqId?: IRequestTracer; payload?: string; + stakingParams?: unknown; } export interface CustomSigningFunction { @@ -1096,6 +1117,18 @@ export interface IWallet { buildAccountConsolidations(params?: BuildConsolidationTransactionOptions): Promise; sendAccountConsolidation(params?: PrebuildAndSignTransactionOptions): Promise; sendAccountConsolidations(params?: BuildConsolidationTransactionOptions): Promise; + buildResourceDelegations(params: BuildResourceDelegationTransactionOptions): Promise; + sendResourceDelegation(params: PrebuildAndSignTransactionOptions): Promise; + sendResourceDelegations(params: BuildResourceDelegationTransactionOptions): Promise<{ + success: any[]; + failure: { message: string; receiverAddress?: string }[]; + }>; + buildResourceUndelegations(params: BuildResourceUndelegationTransactionOptions): Promise; + sendResourceUndelegation(params: PrebuildAndSignTransactionOptions): Promise; + sendResourceUndelegations(params: BuildResourceUndelegationTransactionOptions): Promise<{ + success: any[]; + failure: { message: string; receiverAddress?: string }[]; + }>; buildTokenEnablements(params?: BuildTokenEnablementOptions): Promise; sendTokenEnablement(params?: PrebuildAndSignTransactionOptions): Promise; sendTokenEnablements(params?: BuildTokenEnablementOptions): Promise; diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 97a124e777..60ae90dd5f 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -58,6 +58,8 @@ import { AddressesByBalanceOptions, AddressesOptions, BuildConsolidationTransactionOptions, + BuildResourceDelegationTransactionOptions, + BuildResourceUndelegationTransactionOptions, BuildTokenEnablementOptions, BulkCreateShareOption, BulkWalletShareKeychain, @@ -3410,6 +3412,227 @@ export class Wallet implements IWallet { } } + /** + * Shared build logic for resource delegation and undelegation. + * POSTs to the given endpoint and post-processes each prebuild. + */ + private async buildResourceManagements( + type: 'delegateResource' | 'undelegateResource', + params: BuildResourceDelegationTransactionOptions | BuildResourceUndelegationTransactionOptions + ): Promise { + if (!this.baseCoin.supportsResourceDelegation()) { + throw new Error(`${this.baseCoin.getFullName()} does not support resource delegation.`); + } + + if (type === 'delegateResource') { + const delegationParams = params as BuildResourceDelegationTransactionOptions; + if (!delegationParams.delegations || delegationParams.delegations.length === 0) { + throw new Error('delegations must be a non-empty array.'); + } + } else { + const undelegationParams = params as BuildResourceUndelegationTransactionOptions; + if (!undelegationParams.undelegations || undelegationParams.undelegations.length === 0) { + throw new Error('undelegations must be a non-empty array.'); + } + } + + const endpoint = type === 'delegateResource' ? '/delegateResources/build' : '/undelegateResources/build'; + const whitelistedParams = + type === 'delegateResource' + ? _.pick(params as BuildResourceDelegationTransactionOptions, ['delegations', 'apiVersion']) + : _.pick(params as BuildResourceUndelegationTransactionOptions, ['undelegations', 'apiVersion']); + debug('prebuilding resource management transactions (%s): %O', endpoint, whitelistedParams); + + if (params.reqId) { + this.bitgo.setRequestTracer(params.reqId); + } + + const buildResponse = (await this.bitgo + .post(this.baseCoin.url('/wallet/' + this.id() + endpoint)) + .send(whitelistedParams) + .result()) as { transactions: any[]; errors: any[] }; + + if (!Array.isArray(buildResponse.transactions)) { + throw new Error(`Unexpected response from ${endpoint}: missing transactions array`); + } + + if (buildResponse.errors && buildResponse.errors.length > 0) { + debug('build errors from %s: %O', endpoint, buildResponse.errors); + } + + const results: PrebuildTransactionResult[] = []; + for (const rawTx of buildResponse.transactions) { + let prebuild: PrebuildTransactionResult = (await this.baseCoin.postProcessPrebuild( + Object.assign(rawTx, { wallet: this, buildParams: params }) + )) as PrebuildTransactionResult; + + delete prebuild.wallet; + delete prebuild.buildParams; + + prebuild = _.extend({}, prebuild, { walletId: this.id() }); + debug('final resource management transaction prebuild: %O', prebuild); + results.push(prebuild); + } + return results; + } + + /** + * Shared signing logic for a single resource management transaction (delegation or undelegation). + * Signing flow by wallet type (mirrors sendAccountConsolidation): + * - TSS (hot or custodial) → sendManyTxRequests (requires txRequestId on prebuildTx) + * - Custodial non-TSS → initiateTransaction (BitGo auto-signs with its key) + * - Hot non-TSS → prebuildAndSignTransaction + submitTransaction + */ + private async sendResourceManagement( + type: 'delegateResource' | 'undelegateResource', + params: PrebuildAndSignTransactionOptions + ): Promise { + // Custodial non-TSS: BitGo holds the key, send for BitGo approval and signing. + // No local prebuild is required — the /tx/initiate API builds and signs server-side + // via internalInitiateTransaction → internalBuildTransaction in wallet-platform. + if (this._wallet.type === 'custodial' && this._wallet.multisigType !== 'tss') { + params.type = type; + return this.initiateTransaction(params as TxSendBody, params.reqId); + } + + if (typeof params.prebuildTx === 'string' || params.prebuildTx === undefined) { + throw new Error('Invalid prebuild for resource management transaction.'); + } + + // TSS path (hot or custodial): signing service holds the key shares + if (this._wallet.multisigType === 'tss') { + if (!params.prebuildTx.txRequestId) { + throw new Error('Resource management request missing txRequestId for TSS wallet.'); + } + return await this.sendManyTxRequests(params); + } + + // Hot non-TSS: user holds the key, sign locally and submit + const signedPrebuild = (await this.prebuildAndSignTransaction(params)) as any; + delete signedPrebuild.wallet; + // Relay stakingParams from the prebuild so send.ts can populate it on the transfer document + if (typeof params.prebuildTx === 'object' && params.prebuildTx.stakingParams) { + signedPrebuild.stakingParams = params.prebuildTx.stakingParams; + } + return await this.submitTransaction(signedPrebuild, params.reqId); + } + + /** + * Shared build → sign → send loop for resource delegation and undelegation. + * Validates the wallet passphrase upfront, then sends each transaction individually. + * Returns { success, failure } so partial success is handled gracefully. + */ + private async sendResourceManagements( + type: 'delegateResource' | 'undelegateResource', + params: BuildResourceDelegationTransactionOptions | BuildResourceUndelegationTransactionOptions + ): Promise<{ success: any[]; failure: { message: string; receiverAddress?: string }[] }> { + if (!this.baseCoin.supportsResourceDelegation()) { + throw new Error(`${this.baseCoin.getFullName()} does not support resource delegation.`); + } + + const apiVersion = + params.apiVersion ?? + (this.tssUtils && this.tssUtils.supportedTxRequestVersions().includes('full') ? 'full' : undefined); + + // Validate passphrase upfront to fail fast before building N transactions + await this.getKeychainsAndValidatePassphrase({ + reqId: params.reqId, + walletPassphrase: params.walletPassphrase, + customSigningFunction: params.customSigningFunction, + }); + + const unsignedBuilds = await this.buildResourceManagements(type, { ...params, apiVersion }); + const successfulTxs: any[] = []; + const failedTxs: { message: string; receiverAddress?: string }[] = []; + + for (const unsignedBuild of unsignedBuilds) { + const unsignedBuildWithOptions: PrebuildAndSignTransactionOptions = { + ...params, + apiVersion, + prebuildTx: unsignedBuild, + }; + try { + const sendTx = await this.sendResourceManagement(type, unsignedBuildWithOptions); + successfulTxs.push(sendTx); + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + failedTxs.push({ message }); + } + } + + return { success: successfulTxs, failure: failedTxs }; + } + + /** + * Builds a set of resource delegation transactions for the given delegation entries. + * Each entry delegates resources (e.g. ENERGY, BANDWIDTH) from this wallet's root address + * to a receiver address. Modelled after buildAccountConsolidations. + * + * @param params.delegations - Array of { receiverAddress, amount, resource } entries + * @returns Unsigned prebuild transaction results, one per delegation entry + */ + async buildResourceDelegations( + params: BuildResourceDelegationTransactionOptions + ): Promise { + return this.buildResourceManagements('delegateResource', params); + } + + /** + * Signs and sends a single resource delegation transaction. + * @param params.prebuildTx - A single prebuild result from buildResourceDelegations + */ + async sendResourceDelegation(params: PrebuildAndSignTransactionOptions = {}): Promise { + if (!this.baseCoin.supportsResourceDelegation()) { + throw new Error(`${this.baseCoin.getFullName()} does not support resource delegation.`); + } + return this.sendResourceManagement('delegateResource', params); + } + + /** + * Builds, signs, and sends all resource delegation transactions in a single call. + * @param params.delegations - Array of { receiverAddress, amount, resource } entries + */ + async sendResourceDelegations( + params: BuildResourceDelegationTransactionOptions + ): Promise<{ success: any[]; failure: { message: string; receiverAddress?: string }[] }> { + return this.sendResourceManagements('delegateResource', params); + } + + /** + * Builds a set of resource undelegation transactions for the given entries. + * Each entry reclaims delegated resources from this wallet's root address back from + * a receiver address. Modelled after buildResourceDelegations. + * + * @param params.undelegations - Array of { receiverAddress, amount, resource } entries + * @returns Unsigned prebuild transaction results, one per undelegation entry + */ + async buildResourceUndelegations( + params: BuildResourceUndelegationTransactionOptions + ): Promise { + return this.buildResourceManagements('undelegateResource', params); + } + + /** + * Signs and sends a single resource undelegation transaction. + * @param params.prebuildTx - A single prebuild result from buildResourceUndelegations + */ + async sendResourceUndelegation(params: PrebuildAndSignTransactionOptions = {}): Promise { + if (!this.baseCoin.supportsResourceDelegation()) { + throw new Error(`${this.baseCoin.getFullName()} does not support resource delegation.`); + } + return this.sendResourceManagement('undelegateResource', params); + } + + /** + * Builds, signs, and sends all resource undelegation transactions in a single call. + * @param params.undelegations - Array of { receiverAddress, amount, resource } entries + */ + async sendResourceUndelegations( + params: BuildResourceUndelegationTransactionOptions + ): Promise<{ success: any[]; failure: { message: string; receiverAddress?: string }[] }> { + return this.sendResourceManagements('undelegateResource', params); + } + /** * Builds a set of transactions that enables the specified tokens * @param params -