Delegation & Staking
Manage delegation contracts, validator nodes, and staking operations on MultiversX.
Overview
The DelegationController provides an API for:
- Validator Operations - Create delegation contracts, manage validator nodes
- Delegator Operations - Stake, unstake, claim rewards
- Contract Management - Configure fees, caps, and metadata
DelegationController
import 'package:abidock_mvx/abidock_mvx.dart';
final controller = DelegationController(
chainId: const ChainId.mainnet(),
gasLimitEstimator: GasEstimator(networkProvider: provider),
);
Validator Operations
Create Delegation Contract
Create a new staking provider contract:
import 'dart:io';
// Account.fromPem takes the PEM *content*, not a file path.
final pemContent = await File('validator.pem').readAsString();
final account = await Account.fromPem(pemContent);
final accountInfo = await provider.getAccount(account.address);
final input = NewDelegationContractInput(
totalDelegationCap: BigInt.from(10000000) * BigInt.from(10).pow(18), // 10M EGLD cap
serviceFee: BigInt.from(1000), // 10% fee (10000 = 100%)
amount: Balance.fromEgld(1250), // 1250 EGLD initial stake
);
final tx = await controller.createTransactionForNewDelegationContract(
account,
accountInfo.nonce,
input,
);
final hash = await provider.sendTransaction(tx);
print('Delegation contract created: $hash');
- Minimum deposit of 1250 EGLD to create a delegation contract
- A validator node requires 2500 EGLD of base stake before it can be staked
- Service fee is in basis points (1000 = 10%, 10000 = 100%)
Add Validator Nodes
Adding a node proves ownership of the validator key: for each BLS public key you supply a BLS signature over the delegation contract address bytes.
Validator identities are BLS12-381 keypairs (32-byte secret, 96-byte public
key, 96-byte signature). No pure-Dart BLS12-381 implementation ships with this
package, so ValidatorSecretKey.sign throws UnimplementedError. Wire up a
native BLS plugin, an FFI binding, or a remote signing service and hand it to
ValidatorSigner.custom.
import 'dart:typed_data';
// Your BLS backend, exposed as Uint8List Function(Uint8List).
final signer = ValidatorSigner.custom(
(Uint8List bytes) => myBlsBackend.sign(bytes),
);
// The proof is a signature over the delegation contract address bytes.
final Uint8List proofPayload = Uint8List.fromList(delegationContract.bytes);
final Uint8List signedMessage = signer.sign(proofPayload);
// BLS public keys are 96-byte values, built from hex or raw bytes.
final validatorPublicKey = ValidatorPublicKey.fromHex('00e9...');
final input = AddNodesInput(
delegationContract: delegationContract,
publicKeys: [validatorPublicKey],
signedMessages: [signedMessage],
);
final tx = await controller.createTransactionForAddingNodes(
account,
accountInfo.nonce,
input,
);
publicKeys and signedMessages must be the same length -- the factory
throws ArgumentError otherwise, since each key is paired positionally with
its proof.
Reading validator keys from PEM
ValidatorSecretKey.fromPem parses PEM content, not a path, and the PEM
header carries the matching public key hex:
final pemText = await File('validator_key.pem').readAsString();
final secretKey = ValidatorSecretKey.fromPem(pemText);
// Multiple entries in one file:
final allKeys = parseValidatorKeys(pemText);
The secret key is still useful for storage and transport (toPem); only the
signing step needs the external BLS backend.
Stake Nodes
Activate validator nodes for consensus:
final input = ManageNodesInput(
delegationContract: delegationContract,
publicKeys: [validatorPublicKey1, validatorPublicKey2],
);
final tx = await controller.createTransactionForStakingNodes(
account,
accountInfo.nonce,
input,
);
Unstake Nodes
Remove nodes from active validation:
final input = ManageNodesInput(
delegationContract: delegationContract,
publicKeys: [validatorPublicKey],
);
final tx = await controller.createTransactionForUnstakingNodes(
account,
accountInfo.nonce,
input,
);
Unbond Nodes
Complete unbonding after unstaking period:
final input = ManageNodesInput(
delegationContract: delegationContract,
publicKeys: [validatorPublicKey],
);
final tx = await controller.createTransactionForUnbondingNodes(
account,
accountInfo.nonce,
input,
);
Remove Nodes
Remove validator keys from contract:
final input = ManageNodesInput(
delegationContract: delegationContract,
publicKeys: [validatorPublicKey],
);
final tx = await controller.createTransactionForRemovingNodes(
account,
accountInfo.nonce,
input,
);
Unjail Nodes
Unjail slashed validator nodes:
final input = UnjailingNodesInput(
delegationContract: delegationContract,
publicKeys: [jailedValidatorKey],
// The fine is 2.5 EGLD per jailed node -- multiply by the number of keys
// you are unjailing. Any other amount is rejected by the chain.
amount: Balance.fromEgld(2.5),
);
final tx = await controller.createTransactionForUnjailingNodes(
account,
accountInfo.nonce,
input,
);
Contract Configuration
Change Service Fee
final input = ChangeServiceFeeInput(
delegationContract: delegationContract,
serviceFee: BigInt.from(1500), // Change to 15%
);
final tx = await controller.createTransactionForChangingServiceFee(
account,
accountInfo.nonce,
input,
);
Modify Delegation Cap
final input = ModifyDelegationCapInput(
delegationContract: delegationContract,
delegationCap: BigInt.from(5000000) * BigInt.from(10).pow(18), // 5M EGLD
);
final tx = await controller.createTransactionForModifyingDelegationCap(
account,
accountInfo.nonce,
input,
);
Set Metadata
final input = SetMetadataInput(
delegationContract: delegationContract,
name: 'My Staking Provider',
website: 'https://mystaking.com',
identifier: 'mystaking',
);
final tx = await controller.createTransactionForSettingMetadata(
account,
accountInfo.nonce,
input,
);
Automatic Activation
Enable/disable automatic node activation:
final input = ManageDelegationContractInput(
delegationContract: delegationContract,
);
// Enable
final enableTx = await controller.createTransactionForSettingAutomaticActivation(
account,
accountInfo.nonce,
input,
);
// Disable
final disableTx = await controller.createTransactionForUnsettingAutomaticActivation(
account,
accountInfo.nonce,
input,
);
Cap Check on Redelegate
Control whether delegation cap applies to reward redelegation:
final input = ManageDelegationContractInput(
delegationContract: delegationContract,
);
// Enable cap check
final enableTx = await controller.createTransactionForSettingCapCheckOnRedelegateRewards(
account,
accountInfo.nonce,
input,
);
// Disable cap check
final disableTx = await controller.createTransactionForUnsettingCapCheckOnRedelegateRewards(
account,
accountInfo.nonce,
input,
);
Delegator Operations
Delegate EGLD
Stake EGLD with a staking provider:
final input = DelegateInput(
delegationContract: Address.fromBech32('erd1qqqqqqqqqqqqqpgq...'),
amount: Balance.fromEgld(100), // Delegate 100 EGLD
);
final tx = await controller.createTransactionForDelegating(
account,
accountInfo.nonce,
input,
);
final hash = await provider.sendTransaction(tx);
print('Delegated: $hash');
Undelegate EGLD
Initiate unstaking. Funds stay locked for the unbonding period -- 10 epochs,
roughly 10 days at the current epoch length -- before withdraw releases them:
final input = UndelegateInput(
delegationContract: delegationContract,
amount: Balance.fromEgld(50), // Undelegate 50 EGLD
);
final tx = await controller.createTransactionForUndelegating(
account,
accountInfo.nonce,
input,
);
Withdraw Funds
Claim unbonded EGLD after unbonding period:
final input = WithdrawInput(
delegationContract: delegationContract,
);
final tx = await controller.createTransactionForWithdrawing(
account,
accountInfo.nonce,
input,
);
Claim Rewards
Claim accumulated staking rewards:
final input = WithdrawInput(
delegationContract: delegationContract,
);
final tx = await controller.createTransactionForClaimingRewards(
account,
accountInfo.nonce,
input,
);
Redelegate Rewards
Automatically restake rewards:
final input = WithdrawInput(
delegationContract: delegationContract,
);
final tx = await controller.createTransactionForRedelegatingRewards(
account,
accountInfo.nonce,
input,
);
Parsing Outcomes
Use DelegationOutcomeParser to extract results:
final parser = DelegationOutcomeParser();
// Parse delegation contract creation
final tx = await provider.getTransaction(hash);
final results = parser.parseCreateNewDelegationContract(tx);
for (final result in results) {
print('Contract address: ${result.contractAddress}');
}
Complete Example
import 'package:abidock_mvx/abidock_mvx.dart';
void main() async {
final provider = GatewayNetworkProvider.mainnet();
final account = await Account.fromMnemonic('your mnemonic...');
final controller = DelegationController(
chainId: const ChainId.mainnet(),
gasLimitEstimator: GasEstimator(networkProvider: provider),
);
// Get account info
final accountInfo = await provider.getAccount(account.address);
// Choose a staking provider
final stakingProvider = Address.fromBech32('erd1qqqqqqqqqqqqqpgq...');
// Delegate 100 EGLD
final delegateTx = await controller.createTransactionForDelegating(
account,
accountInfo.nonce,
DelegateInput(
delegationContract: stakingProvider,
amount: Balance.fromEgld(100),
),
);
final hash = await provider.sendTransaction(delegateTx);
print('Delegated: $hash');
// Wait for rewards to accumulate...
// Claim rewards
final claimTx = await controller.createTransactionForClaimingRewards(
account,
accountInfo.nonce.increment(),
WithdrawInput(delegationContract: stakingProvider),
);
await provider.sendTransaction(claimTx);
print('Rewards claimed!');
}
Service Fee Reference
| Fee (basis points) | Percentage |
|---|---|
| 500 | 5% |
| 1000 | 10% |
| 1500 | 15% |
| 2000 | 20% |
| 10000 | 100% |
Important Notes
- Unbonding Period - Undelegated funds are locked for 10 epochs (about 10 days) before they can be withdrawn
- Minimum Stake - Check provider's minimum delegation requirement
- Rewards - Rewards accumulate continuously and can be claimed anytime
- Validator Keys - Keep validator secret keys secure and backed up
- Slashing - Jailed nodes require unjail fee and may lose rewards