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 -