Claim and re-delegate rewards
Once a delegation accrues rewards you have two choices, and they are the same
transaction shape with opposite destinations: claimRewards pays your pending
rewards out to your wallet as EGLD, while reDelegateRewards re-stakes them into
the same contract to compound. Both are addressed to the delegation contract,
carry no value and no arguments, and differ only in the function name on the wire.
This recipe builds both, both ways (controller and factory), and parses completed
ones.
The default npm start parses a real claim and a real re-delegate on devnet, so
you see the parse work without a funded wallet.
Prerequisites
- Node.js >= 20.19.0.
- For the default
parseandpayloaddemos: devnet network access only. - For an actual claim or re-delegate: a devnet PEM wallet that has an active delegation with pending rewards, plus gas.
Install
mkdir claim-and-redelegate-rewards
cd claim-and-redelegate-rewards
# Create the project files shown on this page.
npm install
npm run build
Complete project files omitted from the main walkthrough
{
"name": "cookbook-recipe-claim-and-redelegate-rewards",
"version": "1.0.0",
"private": true,
"description": "Cookbook recipe — claim delegation rewards to your wallet or re-delegate (compound) them, with the DelegationController and factory.",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit --strict"
},
"dependencies": {
"@multiversx/sdk-core": "15.4.1",
"axios": "1.18.1",
"bignumber.js": "9.3.1",
"protobufjs": "7.6.5"
},
"devDependencies": {
"@types/node": "20.19.43",
"typescript": "5.9.3"
},
"engines": {
"node": ">=20.19.0"
}
}
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"declaration": false,
"sourceMap": false,
"outDir": "dist",
"types": ["node"]
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
// src/index.ts - CLI entry point for the claim-and-redelegate-rewards recipe.
//
// Modes:
// npm start -> "parse" real historical devnet claim
// and re-delegate txs (no wallet needed)
// npm start -- payload -> build (do not send) both txs and print
// their decoded wire payloads, offline
// npm start -- send <pem> -> actually claim (needs a funded PEM);
// add --redelegate to compound instead;
// add --factory for the factory path
//
// With an unfunded wallet, `send` fails cleanly with "insufficient funds".
import { Account, Address, DevnetEntrypoint } from '@multiversx/sdk-core';
import {
claimRewardsViaController,
claimRewardsViaFactory,
redelegateRewardsViaController,
redelegateRewardsViaFactory,
describeRewardsPayload,
parseClaimedAmount,
parseRedelegatedAmount,
} from './rewards';
const EXAMPLE_DELEGATION_CONTRACT = 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllllscktaww';
/** Format a wei amount as an approximate EGLD string (rewards are often sub-EGLD). */
function toEgld(wei: bigint): string {
const whole = wei / 10n ** 18n;
const frac = (wei % 10n ** 18n).toString().padStart(18, '0').slice(0, 6);
return `${whole.toString()}.${frac}`;
}
// Real, completed devnet transactions for the default parse demo.
const EXAMPLE_CLAIM_TX = '058c94408349866e6df91b49637b39a49409eabb1ae58b07f12100ea178a2a72';
const EXAMPLE_REDELEGATE_TX = '5dd411346ef6029946abf24746fbebef0df770423bd56d54cb6a5a22246ed694';
async function runParse(): Promise<void> {
const entrypoint = new DevnetEntrypoint({ clientName: 'mvx-cookbook' });
console.log(`Parsing completed claim ${EXAMPLE_CLAIM_TX} ...`);
const claimed = await parseClaimedAmount(entrypoint, EXAMPLE_CLAIM_TX);
console.log(` parsed claimed amount: ${claimed} wei (see Pitfalls: often 0)`);
console.log(`\nParsing completed re-delegate ${EXAMPLE_REDELEGATE_TX} ...`);
const redelegated = await parseRedelegatedAmount(entrypoint, EXAMPLE_REDELEGATE_TX);
console.log(` parsed re-staked amount: ${redelegated} wei (${toEgld(redelegated)} EGLD)`);
}
async function runPayload(): Promise<void> {
const entrypoint = new DevnetEntrypoint({ clientName: 'mvx-cookbook' });
const factory = entrypoint.createDelegationTransactionsFactory();
const throwaway = await entrypoint.createAccount();
const input = { delegationContract: Address.newFromBech32(EXAMPLE_DELEGATION_CONTRACT) };
const claimTx = await factory.createTransactionForClaimingRewards(throwaway.address, input);
const redelegateTx = await factory.createTransactionForRedelegatingRewards(throwaway.address, input);
for (const [label, tx] of [
['claim', claimTx],
['re-delegate', redelegateTx],
] as const) {
const p = describeRewardsPayload(tx);
console.log(`${label}: function=${p.function} value=${p.valueWei} receiver=${p.receiver} gasLimit=${p.gasLimit}`);
}
}
async function runSend(pemPath: string, redelegate: boolean, useFactory: boolean): Promise<void> {
const entrypoint = new DevnetEntrypoint({ clientName: 'mvx-cookbook' });
const sender = await Account.newFromPem(pemPath);
sender.nonce = await entrypoint.recallAccountNonce(sender.address);
const op = redelegate ? 're-delegate' : 'claim';
console.log(`Delegator: ${sender.address.toBech32()} (nonce ${sender.nonce})`);
console.log(`Operation: ${op}, path ${useFactory ? 'factory' : 'controller'}`);
const build = redelegate
? useFactory
? redelegateRewardsViaFactory
: redelegateRewardsViaController
: useFactory
? claimRewardsViaFactory
: claimRewardsViaController;
const tx = await build(entrypoint, sender, EXAMPLE_DELEGATION_CONTRACT);
const txHash = await entrypoint.sendTransaction(tx);
console.log(` broadcast txHash: ${txHash}`);
}
async function main(): Promise<void> {
const [mode, ...rest] = process.argv.slice(2);
if (mode === 'send') {
const pemPath = rest[0];
if (!pemPath) {
console.error('Usage: npm start -- send <pemPath> [--redelegate] [--factory]');
process.exitCode = 1;
return;
}
try {
await runSend(pemPath, rest.includes('--redelegate'), rest.includes('--factory'));
} catch (err) {
console.error(`Rewards tx rejected: ${(err as Error).message}`);
process.exitCode = 1;
}
return;
}
if (mode === 'payload') {
await runPayload();
return;
}
await runParse();
}
main().catch((err: unknown) => {
console.error(err);
process.exitCode = 1;
});
Claiming and re-delegating
// src/rewards.ts - the subject of this recipe: what to do with accrued
// delegation rewards. Two operations, same shape, opposite destinations:
// - claimRewards -> withdraw pending rewards to your wallet as EGLD;
// - reDelegateRewards -> re-stake pending rewards into the same contract,
// compounding, without them ever hitting your wallet.
// Both are addressed to the delegation contract, carry NO value and NO
// arguments (the contract computes what you are owed), and differ only in the
// function name on the wire (`claimRewards` vs `reDelegateRewards`).
import { Account, Address, DelegationTransactionsOutcomeParser } from '@multiversx/sdk-core';
import type { DevnetEntrypoint, Transaction } from '@multiversx/sdk-core';
function contractInput(delegationContract: string): { delegationContract: Address } {
return { delegationContract: Address.newFromBech32(delegationContract) };
}
/** Claim rewards - controller path (build, nonce, sign in one call). */
export async function claimRewardsViaController(
entrypoint: DevnetEntrypoint,
sender: Account,
delegationContract: string,
): Promise<Transaction> {
const controller = entrypoint.createDelegationController();
return controller.createTransactionForClaimingRewards(
sender,
sender.getNonceThenIncrement(),
contractInput(delegationContract),
);
}
/** Claim rewards - factory path (build only; caller signs). */
export async function claimRewardsViaFactory(
entrypoint: DevnetEntrypoint,
sender: Account,
delegationContract: string,
): Promise<Transaction> {
const factory = entrypoint.createDelegationTransactionsFactory();
const transaction = await factory.createTransactionForClaimingRewards(
sender.address,
contractInput(delegationContract),
);
transaction.nonce = sender.getNonceThenIncrement();
transaction.signature = await sender.signTransaction(transaction);
return transaction;
}
/** Re-delegate (compound) rewards - controller path. */
export async function redelegateRewardsViaController(
entrypoint: DevnetEntrypoint,
sender: Account,
delegationContract: string,
): Promise<Transaction> {
const controller = entrypoint.createDelegationController();
return controller.createTransactionForRedelegatingRewards(
sender,
sender.getNonceThenIncrement(),
contractInput(delegationContract),
);
}
/** Re-delegate (compound) rewards - factory path. */
export async function redelegateRewardsViaFactory(
entrypoint: DevnetEntrypoint,
sender: Account,
delegationContract: string,
): Promise<Transaction> {
const factory = entrypoint.createDelegationTransactionsFactory();
const transaction = await factory.createTransactionForRedelegatingRewards(
sender.address,
contractInput(delegationContract),
);
transaction.nonce = sender.getNonceThenIncrement();
transaction.signature = await sender.signTransaction(transaction);
return transaction;
}
export interface RewardsPayload {
function: string;
receiver: string;
valueWei: bigint;
gasLimit: bigint;
}
/** Decode a claim / re-delegate transaction's wire fields (neither has args). */
export function describeRewardsPayload(transaction: Transaction): RewardsPayload {
const parts = Buffer.from(transaction.data).toString().split('@');
return {
function: parts[0] ?? '',
receiver: transaction.receiver.toBech32(),
valueWei: transaction.value,
gasLimit: transaction.gasLimit,
};
}
/**
* Parse a completed claim transaction. The parser reads the `claimRewards`
* log event's first topic. Note: the claimed EGLD is delivered as a separate
* smart-contract result, and that first topic is frequently empty - so this
* often returns 0n even when rewards were paid out. See the recipe Pitfalls.
*/
export async function parseClaimedAmount(entrypoint: DevnetEntrypoint, txHash: string): Promise<bigint> {
const transactionOnNetwork = await entrypoint.getTransaction(txHash);
const parser = new DelegationTransactionsOutcomeParser();
return parser.parseClaimRewards(transactionOnNetwork)[0]?.amount ?? 0n;
}
/**
* Parse a completed re-delegate transaction for the re-staked amount.
* Re-delegation emits a `delegate` event, so the parser reports the amount
* that was compounded back into the contract.
*/
export async function parseRedelegatedAmount(entrypoint: DevnetEntrypoint, txHash: string): Promise<bigint> {
const transactionOnNetwork = await entrypoint.getTransaction(txHash);
const parser = new DelegationTransactionsOutcomeParser();
return parser.parseRedelegateRewards(transactionOnNetwork)[0]?.amount ?? 0n;
}
Run it
# Parse a real completed claim and re-delegate - no wallet, no funds:
npm start
# Inspect both wire payloads offline:
npm start -- payload
# Actually claim (needs a funded devnet PEM with active delegation):
npm start -- send ./wallet.pem
# ...or compound instead of claiming:
npm start -- send ./wallet.pem --redelegate
Output of the default parse mode, and of payload:
Parsing completed claim 058c9440...178a2a72 ...
parsed claimed amount: 0 wei (see Pitfalls: often 0)
Parsing completed re-delegate 5dd41134...246ed694 ...
parsed re-staked amount: 397533231571055684 wei (0.397533 EGLD)
claim: function=claimRewards value=0 receiver=erd1qqq...scktaww gasLimit=11068000
re-delegate: function=reDelegateRewards value=0 receiver=erd1qqq...scktaww gasLimit=11075500
How it works
Controller vs factory. controller.createTransactionForClaimingRewards /
createTransactionForRedelegatingRewards build, set the nonce, and sign in one
call. The factory equivalents build only. Both take just { delegationContract },
the contract computes what you are owed, so you never pass an amount.
Same shape, opposite destination. claimRewards moves your rewards to your
wallet; reDelegateRewards stakes them back into the contract. Neither carries a
value, they act on rewards the contract already holds for you.
Parsing. parseRedelegateRewards internally parses the delegate event
(re-delegation is a delegation), so it reports the compounded amount.
parseClaimRewards reads the claimRewards event, see the pitfall below.
Pitfalls
The claimed EGLD is delivered as a separate smart-contract result, and the
claimRewards log event's first topic (which the parser reads) is frequently
empty, so parseClaimRewards returns 0n even when rewards were paid. To read the
actual amount received, inspect the value-bearing smart-contract result or the
account balance delta. parseRedelegateRewards does not have this issue because it
reads the delegate event.
If the provider enabled "check cap on re-delegate," compounding is refused once the contract hits its total delegation cap. The transaction is well-formed; the contract rejects it at execution. Read the contract config first.
claimRewards never fails just because you have nothing to claim; it completes and
moves 0 EGLD. Do not treat a successful claim as proof that rewards existed.
See also
- Delegate (stake) EGLD is the stake that earns these rewards.
- Undelegate and withdraw is the exit path, when you want the principal back too.
- Read a delegation contract's state reads your claimable rewards before claiming.