Skip to main content
IntermediateEst6 minsdk-core15.4.1Project build checked

Delegate (stake) EGLD

Delegating is the everyday delegation write: you send EGLD to a staking provider's contract and it stakes on your behalf. The wire payload is just delegate with no arguments, the amount you stake travels in the transaction's value. This recipe builds that transaction both ways (controller and factory) and parses a completed one for the staked amount.

The default npm start parses a real, already-completed devnet delegate, so you see the parse work without a funded wallet.

Prerequisites

  • Node.js >= 20.19.0.
  • For the default parse and payload demos: devnet network access only.
  • For an actual stake: a devnet PEM wallet with the EGLD you want to delegate plus gas.

Install

mkdir delegate-stake
cd delegate-stake
# Create the project files shown on this page.
npm install
npm run build
Complete project files omitted from the main walkthrough
package.json
{
"name": "cookbook-recipe-delegate-stake",
"version": "1.0.0",
"private": true,
"description": "Cookbook recipe — delegate (stake) EGLD to an existing delegation contract with delegate (controller and factory), then parse the staked amount.",
"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"
}
}
tsconfig.json
{
"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
// src/index.ts - CLI entry point for the delegate-stake recipe.
//
// Three modes:
// npm start -> "parse" a real historical devnet delegate
// (no wallet, no funds needed)
// npm start -- payload -> build (do not send) a delegate and print
// its decoded wire payload, offline
// npm start -- send <pem> -> actually delegate (needs a funded devnet
// PEM); add --factory for the factory path
//
// With an unfunded wallet, `send` fails cleanly with "insufficient funds",
// proving the payload and signature are well-formed.

import { Account, Address, DevnetEntrypoint } from '@multiversx/sdk-core';
import {
delegateViaController,
delegateViaFactory,
describeDelegatePayload,
parseDelegatedAmount,
} from './delegate';

// A real, live devnet staking-provider contract to delegate to.
const EXAMPLE_DELEGATION_CONTRACT = 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllllscktaww';

// A real, completed `delegate` transaction on devnet (10 EGLD staked).
const EXAMPLE_DELEGATE_TX = 'e706916a8349331e1d5b03d6741d3f967df09fb336cc5b80b50c65906fefda98';

// Stake 1 EGLD in the demos.
const STAKE_AMOUNT_WEI = 1n * 10n ** 18n;

async function runParse(txHash: string): Promise<void> {
const entrypoint = new DevnetEntrypoint({ clientName: 'mvx-cookbook' });
console.log(`Parsing completed delegate ${txHash} ...`);
const amount = await parseDelegatedAmount(entrypoint, txHash);
console.log(` staked amount: ${amount} wei (${amount / 10n ** 18n} EGLD)`);
}

async function runPayload(): Promise<void> {
const entrypoint = new DevnetEntrypoint({ clientName: 'mvx-cookbook' });
const factory = entrypoint.createDelegationTransactionsFactory();
const throwaway = await entrypoint.createAccount();
const tx = await factory.createTransactionForDelegating(throwaway.address, {
delegationContract: Address.newFromBech32(EXAMPLE_DELEGATION_CONTRACT),
amount: STAKE_AMOUNT_WEI,
});
const p = describeDelegatePayload(tx);
console.log(`function: ${p.function} (no arguments - amount travels in value)`);
console.log(`value: ${p.valueWei} wei (${p.valueWei / 10n ** 18n} EGLD)`);
console.log(`receiver: ${p.receiver} (the delegation contract)`);
console.log(`gasLimit: ${p.gasLimit}`);
}

async function runSend(pemPath: string, useFactory: boolean): Promise<void> {
const entrypoint = new DevnetEntrypoint({ clientName: 'mvx-cookbook' });
const sender = await Account.newFromPem(pemPath);
sender.nonce = await entrypoint.recallAccountNonce(sender.address);
console.log(`Delegator: ${sender.address.toBech32()} (nonce ${sender.nonce})`);
console.log(`Path: ${useFactory ? 'factory' : 'controller'}`);

const delegate = useFactory ? delegateViaFactory : delegateViaController;
const tx = await delegate(entrypoint, sender, EXAMPLE_DELEGATION_CONTRACT, STAKE_AMOUNT_WEI);
console.log(` value: ${tx.value} wei`);
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> [--factory]');
process.exitCode = 1;
return;
}
try {
await runSend(pemPath, rest.includes('--factory'));
} catch (err) {
console.error(`Delegate rejected: ${(err as Error).message}`);
process.exitCode = 1;
}
return;
}

if (mode === 'payload') {
await runPayload();
return;
}

const txHash = mode === 'parse' && rest[0] ? rest[0] : EXAMPLE_DELEGATE_TX;
await runParse(txHash);
}

main().catch((err: unknown) => {
console.error(err);
process.exitCode = 1;
});

Delegating

src/delegate.ts
// src/delegate.ts - the subject of this recipe: delegating (staking) EGLD to
// an existing delegation contract with `delegate`, two ways (controller and
// factory), then parsing the outcome for the staked amount.
//
// `delegate` is the simplest delegation write: you send EGLD to a staking
// provider's contract and it stakes it on your behalf. The wire payload is
// just the function name `delegate` with NO arguments - the amount you stake
// travels in the transaction's `value`, not in the data. The receiver is the
// delegation contract itself (not the delegation manager - that is only for
// creating a contract; see the create-delegation-contract recipe).

import { Account, Address, DelegationTransactionsOutcomeParser } from '@multiversx/sdk-core';
import type { DevnetEntrypoint, Transaction } from '@multiversx/sdk-core';

/**
* Delegate path 1 - the controller. `createTransactionForDelegating` builds,
* sets the nonce, and signs in one call.
*/
export async function delegateViaController(
entrypoint: DevnetEntrypoint,
sender: Account,
delegationContract: string,
amount: bigint,
): Promise<Transaction> {
const controller = entrypoint.createDelegationController();
return controller.createTransactionForDelegating(sender, sender.getNonceThenIncrement(), {
delegationContract: Address.newFromBech32(delegationContract),
amount,
});
}

/**
* Delegate path 2 - the factory. Builds the unsigned transaction only; the
* caller sets the nonce and signs.
*/
export async function delegateViaFactory(
entrypoint: DevnetEntrypoint,
sender: Account,
delegationContract: string,
amount: bigint,
): Promise<Transaction> {
const factory = entrypoint.createDelegationTransactionsFactory();
const transaction = await factory.createTransactionForDelegating(sender.address, {
delegationContract: Address.newFromBech32(delegationContract),
amount,
});
transaction.nonce = sender.getNonceThenIncrement();
transaction.signature = await sender.signTransaction(transaction);
return transaction;
}

export interface DelegatePayload {
function: string;
receiver: string;
valueWei: bigint;
gasLimit: bigint;
}

/** Decode a delegate transaction's wire fields. `delegate` carries no args. */
export function describeDelegatePayload(transaction: Transaction): DelegatePayload {
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 delegate transaction for the staked amount. The parser
* reads the `delegate` log event emitted by the contract.
*/
export async function parseDelegatedAmount(
entrypoint: DevnetEntrypoint,
txHash: string,
): Promise<bigint> {
const transactionOnNetwork = await entrypoint.getTransaction(txHash);
const parser = new DelegationTransactionsOutcomeParser();
const outcome = parser.parseDelegate(transactionOnNetwork);
return outcome[0]?.amount ?? 0n;
}

Run it

# Parse a real completed devnet delegate - no wallet, no funds:
npm start

# Inspect the delegate wire payload offline:
npm start -- payload

# Actually stake (needs a funded devnet PEM); add --factory for the factory path:
npm start -- send ./wallet.pem

Output of the default parse mode, and of payload:

Parsing completed delegate e706916a...06fefda98 ...
staked amount: 10000000000000000000 wei (10 EGLD)

function: delegate (no arguments - amount travels in value)
value: 1000000000000000000 wei (1 EGLD)
receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllllscktaww (the delegation contract)
gasLimit: 11062000

How it works

Controller vs factory. controller.createTransactionForDelegating(account, nonce, input) builds, sets the nonce, and signs in one call. factory.createTransactionForDelegating(address, input) only builds the unsigned transaction; you set nonce and signature. The input is { delegationContract, amount }, where delegationContract is an Address and amount is wei.

The amount is the value, not an argument. Unlike unDelegate, delegate puts nothing in the data beyond the function name. The staked amount is the transaction's value, sent to the delegation contract.

Parsing the outcome. DelegationTransactionsOutcomeParser.parseDelegate(transactionOnNetwork) reads the delegate log event and returns the staked amount. The default npm start runs it against a historical delegate, which is why it needs no funds.

Pitfalls

Pitfall 1: delegate goes to the contract, not the manager

The receiver is the delegation contract itself (erd1qqq...scktaww above), not the delegation manager. The manager address is only for createNewDelegationContract. Sending delegate to the manager fails.

Pitfall 2: each provider sets its own minimum

There is no single protocol-wide minimum to delegate; a staking provider can set a per-delegation minimum in its own contract. A too-small delegate will be rejected by the contract, not by the SDK. Read the provider's config first (see the query recipe).

Pitfall 3: the amount is in wei (10^18 per EGLD)

amount: 1_000_000_000_000_000_000n is 1 EGLD. Passing 1n delegates one wei. Always scale by 10^18, and keep amounts as bigint.

See also