diff --git a/.github/workflows/cookbook-ts-ci.yml b/.github/workflows/cookbook-ts-ci.yml new file mode 100644 index 000000000..58346c257 --- /dev/null +++ b/.github/workflows/cookbook-ts-ci.yml @@ -0,0 +1,19 @@ +name: Cookbook TS verification + +on: + push: + branches: + - master + pull_request: + +jobs: + cookbook_ts_test: + name: Cookbook TypeScript recipe test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Extract cookbook recipes and type-check + run: ./testing/cookbook-ts-ci.sh diff --git a/docs/developers/relayed-transactions.md b/docs/developers/relayed-transactions.md index f2d2f6ac0..3c37f143e 100644 --- a/docs/developers/relayed-transactions.md +++ b/docs/developers/relayed-transactions.md @@ -119,4 +119,4 @@ Here's an example of a relayed v3 transaction. Its intent is to call the `add` m The SDKs have built-in support for relayed transactions. Please follow: - [mxpy support](/sdk-and-tools/mxpy/mxpy-cli/#relayed-transactions-v3) - [sdk-py support](/sdk-and-tools/sdk-py/#relayed-transactions) - - [sdk-js v15 support](/sdk-and-tools/sdk-js/sdk-js-cookbook#relayed-transactions) + - [sdk-js relayed transactions](/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction) diff --git a/docs/developers/sc-calls-format.md b/docs/developers/sc-calls-format.md index ff7c2cd96..94c30f14e 100644 --- a/docs/developers/sc-calls-format.md +++ b/docs/developers/sc-calls-format.md @@ -78,7 +78,7 @@ There are multiple ways of formatting the data field: - manually convert each argument, and then join the function name, alongside the argument via the `@` character. - use a pre-defined arguments serializer, such as [the one found in sdk-js](https://github.com/multiversx/mx-sdk-js-core/blob/main/src/smartcontracts/argSerializer.ts). -- use sdk-js's [contract calls](/sdk-and-tools/sdk-js/sdk-js-cookbook/#smart-contracts). +- use sdk-js's [contract calls](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint). - use sdk-cpp's [contract calls](https://github.com/multiversx/mx-sdk-cpp/blob/main/src/smartcontracts/contract_call.cpp). - and so on diff --git a/docs/developers/signing-transactions/signing-programmatically.md b/docs/developers/signing-transactions/signing-programmatically.md index af601d8e9..407b30fdd 100644 --- a/docs/developers/signing-transactions/signing-programmatically.md +++ b/docs/developers/signing-transactions/signing-programmatically.md @@ -6,5 +6,5 @@ description: "Sign transactions programmatically using the SDKs: build, sign and In order to sign a transaction (or a message) using one of the SDKs, follow: -- [Signing objects using **sdk-js**](/sdk-and-tools/sdk-js/sdk-js-cookbook#signing-objects) +- [Signing objects using **sdk-js**](/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-transaction) - [Signing objects using **sdk-py**](/sdk-and-tools/sdk-py#signing-objects) diff --git a/docs/sdk-and-tools/overview.md b/docs/sdk-and-tools/overview.md index 5bd3de092..5064e8278 100644 --- a/docs/sdk-and-tools/overview.md +++ b/docs/sdk-and-tools/overview.md @@ -31,7 +31,7 @@ Note that Rust is also the recommended programming language for writing Smart Co | Name | Description | | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | [sdk-js](/sdk-and-tools/sdk-js) | High level overview about sdk-js. | -| [sdk-js cookbook](/sdk-and-tools/sdk-js/sdk-js-cookbook) | Learn how to handle common tasks by using sdk-js. | +| [sdk-js cookbook](/sdk-and-tools/sdk-js/cookbook) | Learn how to handle common tasks by using sdk-js. | | [Extending sdk-js](/sdk-and-tools/sdk-js/extending-sdk-js) | How to extend and tailor certain modules of sdk-js. | | [Writing and testing sdk-js interactions](/sdk-and-tools/sdk-js/writing-and-testing-sdk-js-interactions) | Write sdk-js interactions for Visual Studio Code | | [sdk-js signing providers](/sdk-and-tools/sdk-js/sdk-js-signing-providers) | Integrate sdk-js signing providers. | diff --git a/docs/sdk-and-tools/sdk-js/cookbook/accounts/account-from-keys.mdx b/docs/sdk-and-tools/sdk-js/cookbook/accounts/account-from-keys.mdx new file mode 100644 index 000000000..5163aa5d8 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/accounts/account-from-keys.mdx @@ -0,0 +1,157 @@ +--- +title: Create an Account from a KeyPair, secret key, or mnemonic +description: Build an sdk-core Account from a KeyPair, a raw secret key, or a mnemonic, fully offline, and learn which named constructors are sync versus async. +difficulty: beginner +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - typescript + - wallet +--- + +The `Account` is the object you sign with. This recipe builds one three ways: +from a `KeyPair`, from a raw secret key, and from a mnemonic, all fully offline, +with no network call. It also flags a real inconsistency in the named +constructors: one of them is asynchronous while the rest are not. + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates its own throwaway keys. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/account-from-keys +npm install +npm run build +npm start +``` + +## The code + +```ts title="src/index.ts" +// src/index.ts — build an Account four ways: from a KeyPair, from a raw +// secret key, from a mnemonic, and (for contrast) note the async PEM path. +// +// Fully offline. No devnet, no network call — every constructor here works +// on local key material. Fresh keys are generated on each run, so exact +// addresses differ; the equivalences printed do not. +// +// Grounded in the installed @multiversx/sdk-core v15.4.1 Account class +// (accounts/account.d.ts). + +import { Account, KeyPair, Mnemonic } from '@multiversx/sdk-core'; + +async function main(): Promise { + // === A. From a freshly generated KeyPair. === + // KeyPair.generate() makes a new Ed25519 keypair; newFromKeypair wraps it + // in an Account. Synchronous. + const keyPair = KeyPair.generate(); + const fromKeyPair = Account.newFromKeypair(keyPair); + console.log(`A. From KeyPair: ${fromKeyPair.address.toBech32()}`); + + // === B. From a raw secret key — the plain constructor. === + // We reuse the KeyPair's own secret key, so B is the same account as A and + // the addresses must match. Synchronous. + const fromSecretKey = new Account(keyPair.secretKey); + console.log(`B. From secretKey: ${fromSecretKey.address.toBech32()}`); + console.log( + ` A and B share one key, so addresses match: ${fromKeyPair.address.toBech32() === fromSecretKey.address.toBech32()}`, + ); + + // === C. From a mnemonic at addressIndex 0. === + // newFromMnemonic derives the key first, then builds the Account. + // Synchronous, despite deriving a key. + const mnemonic = Mnemonic.generate(); + const fromMnemonic = Account.newFromMnemonic(mnemonic.toString(), 0); + console.log(`C. From mnemonic: ${fromMnemonic.address.toBech32()}`); + + // === D. Every Account exposes the same surface. === + // address, publicKey, secretKey, and a LOCAL nonce (a bigint you manage + // yourself — see the Manage nonces recipe). An Account can sign, too. + console.log(`\nfromMnemonic.nonce starts at ${fromMnemonic.nonce} (a local bigint, not fetched from the network).`); + const data = new Uint8Array(Buffer.from('proof this account can sign')); + const signature = await fromMnemonic.sign(data); + const verified = await fromMnemonic.verify(data, signature); + console.log(`fromMnemonic can sign and verify its own data: ${verified}`); + + // === E. The one async constructor. === + // newFromPem is the odd one out: it returns Promise and must be + // awaited. newFromKeypair / new Account(...) / newFromMnemonic / + // newFromKeystore are all synchronous. See the "Save and load a PEM" recipe. + console.log('\nNote: Account.newFromPem(...) is async (returns a Promise); the four constructors used above are synchronous.'); + + console.log('\nExpected: A and B identical, two "true"-style confirmations.'); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +Keys are generated fresh each run, so addresses differ. A and B reuse one key, so +their addresses always match: + +```text +A. From KeyPair: erd13cj3sr6vxqknjaegpc3txvjtz3ermc3dvlznhj6lpfzm4wtu69vs2695hk +B. From secretKey: erd13cj3sr6vxqknjaegpc3txvjtz3ermc3dvlznhj6lpfzm4wtu69vs2695hk + A and B share one key, so addresses match: true +C. From mnemonic: erd1uast8l23rlhkee32xx59tmuc54gh8xp5eveayw7ecql9vqxtnqzsmg2yj6 + +fromMnemonic.nonce starts at 0 (a local bigint, not fetched from the network). +fromMnemonic can sign and verify its own data: true +``` + +## How it works + +Grounded in the installed `@multiversx/sdk-core` v15.4.1 `Account` class +(`accounts/account.d.ts`): + +1. **From a KeyPair.** `KeyPair.generate()` makes a new Ed25519 keypair; + `Account.newFromKeypair(keyPair)` wraps it. +2. **From a raw secret key.** `new Account(secretKey)`, the plain constructor. + Reusing the KeyPair's own key makes A and B the same account. +3. **From a mnemonic.** `Account.newFromMnemonic(phrase, addressIndex)` derives + the key first, then builds the Account. +4. **The shared surface.** Every `Account` exposes `address`, `publicKey`, + `secretKey`, and a *local* `nonce` (a `bigint` you manage yourself), and can + `sign` / `verify`. + +## Pitfalls + +:::warning[Pitfall 1: the named constructors are inconsistent, one is async] +`Account.newFromPem(...)` returns `Promise` and must be `await`ed. But +`Account.newFromKeypair(...)`, `new Account(...)`, `Account.newFromMnemonic(...)`, +and `Account.newFromKeystore(...)` are all **synchronous**. Forgetting to await +`newFromPem` gives you a `Promise`, not an account. Check the specific +constructor. +::: + +:::note[Pitfall 2: account.nonce starts at 0 and is not the on-chain nonce] +It is a local counter you must seed from the network before sending +transactions, see [Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces). +Constructing an account does not query the chain. +::: + +:::tip[Pitfall 3: same key means same address, always] +The address is a function of the public key alone. If two constructions share +key material (as A and B do here), they are the same account, there is no +per-object identity beyond the key. +::: + +## See also + +- [Generate a mnemonic + derive keys](/sdk-and-tools/sdk-js/cookbook/accounts/generate-mnemonic-derive-keys) + is where constructor C's key material comes from. +- [Save and load a keystore](/sdk-and-tools/sdk-js/cookbook/accounts/keystore-save-load) + persists an account as encrypted JSON. +- [Save and load a PEM (dev only)](/sdk-and-tools/sdk-js/cookbook/accounts/pem-save-load) + is the async `newFromPem` path in full. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/accounts/address-utilities.mdx b/docs/sdk-and-tools/sdk-js/cookbook/accounts/address-utilities.mdx new file mode 100644 index 000000000..21909b167 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/accounts/address-utilities.mdx @@ -0,0 +1,196 @@ +--- +title: Address utilities +description: The sdk-core Address toolkit, bech32/hex conversion, raw public key, shard computation, isSmartContract, and the HRP. Fully offline, no network. +difficulty: beginner +est_minutes: 7 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - typescript + - address +--- + +The sdk-core `Address` toolkit in one place: bech32 to hex conversion, building +an address from a raw public key, computing an address's shard, checking whether +an address is a smart contract, and the HRP (human-readable part). Fully offline: +an `Address` is a pure value type, with no network call. + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates one throwaway address and uses one fixed, + well-known contract address. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/address-utilities +npm install +npm run build +npm start +``` + +## The code + +```ts title="src/index.ts" +// src/index.ts — the sdk-core Address toolkit: bech32 <-> hex conversion, +// building an address from a raw public key, computing an address's shard, +// checking whether an address is a smart contract, and the HRP concept +// (LibraryConfig.DefaultAddressHrp). +// +// Fully offline. No devnet, no network — an Address is a pure value type. +// One address is generated fresh per run (so its exact value and shard vary); +// the ESDT system contract address is a fixed, well-known value. +// +// Grounded in the installed @multiversx/sdk-core v15.4.1: Address / +// AddressComputer (core/address.d.ts) and LibraryConfig (core/config.d.ts). + +import { Address, AddressComputer, LibraryConfig, Mnemonic } from '@multiversx/sdk-core'; + +function main(): void { + // A throwaway user address to work with. + const userAddress = Mnemonic.generate().deriveKey(0).generatePublicKey().toAddress(); + const bech32 = userAddress.toBech32(); + console.log(`User address (bech32): ${bech32}`); + + // === 1. bech32 <-> hex. === + // toHex() gives the 64-char (32-byte) public key; newFromHex parses it + // back. The round-trip must return the original bech32. + const hex = userAddress.toHex(); + const roundTripped = Address.newFromHex(hex); + console.log(`\n1. hex (${hex.length} chars): ${hex}`); + console.log(` bech32 -> hex -> bech32 round-trips: ${roundTripped.toBech32() === bech32}`); + + // === 2. From a raw public-key buffer. === + // The generic constructor accepts an Address, a bech32/hex string, or the + // raw 32 bytes. getPublicKey() returns those bytes. + const rawPublicKey = userAddress.getPublicKey(); + const fromBytes = new Address(rawPublicKey); + console.log(`2. new Address(rawPublicKeyBytes) matches: ${fromBytes.toBech32() === bech32}`); + + // === 3. Shard of an address. === + // AddressComputer defaults to 3 shards (without metachain); getShardOfAddress + // returns 0, 1, or 2 for a user address. + const computer = new AddressComputer(); + const shard = computer.getShardOfAddress(userAddress); + console.log(`3. Shard of this address: ${shard} (0, 1, or 2)`); + + // === 4. Is it a smart contract? === + // Contract addresses have a fixed all-zero prefix. The ESDT system contract + // is a known contract; a user address is not. + const esdtSystemContract = Address.newFromBech32( + 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u', + ); + console.log(`4. ESDT system contract isSmartContract(): ${esdtSystemContract.isSmartContract()}`); + console.log(` User address isSmartContract(): ${userAddress.isSmartContract()}`); + + // === 5. The HRP (human-readable part). === + // Every bech32 address starts with an HRP — "erd" on MultiversX. It is a + // global default on LibraryConfig, and each Address also carries its own. + console.log(`\n5. LibraryConfig.DefaultAddressHrp: "${LibraryConfig.DefaultAddressHrp}"`); + console.log(` This address's own HRP: "${userAddress.getHrp()}"`); + + // A per-call override builds an address under a different HRP WITHOUT + // touching global state — the safe way to handle a non-"erd" prefix. + const testHrpAddress = Address.newFromHex(hex, 'test'); + console.log(` Same key under HRP "test": ${testHrpAddress.toBech32()}`); + + // The two bech32 parse paths differ in strictness. The named constructor + // Address.newFromBech32 accepts ANY hrp (allowCustomHrp: true): + const lenient = Address.newFromBech32(testHrpAddress.toBech32()); + console.log(` Address.newFromBech32 accepts a "test" address (parsed hrp="${lenient.getHrp()}").`); + + // ...but the generic constructor new Address(bech32) validates the hrp + // against LibraryConfig.DefaultAddressHrp and rejects a mismatch. + let strictRejected = false; + try { + const parsed = new Address(testHrpAddress.toBech32()); + console.log(` new Address unexpectedly accepted hrp="${parsed.getHrp()}".`); + } catch { + strictRejected = true; + } + console.log(` new Address(...) rejects the "test" address while default is "erd": ${strictRejected}`); + + console.log('\nExpected: round-trip true, from-bytes true, a shard 0-2, isSmartContract true then false, HRP "erd", newFromBech32 lenient, and new Address(...) strict-rejects.'); +} + +main(); +``` + +## Run it + +One address is generated fresh per run, so its value and shard vary; the ESDT +system contract address is fixed: + +```text +User address (bech32): erd1ggdn9z7aunpuqrk7z3ywpngh5vcsaz0y6hslqvj92tudsahwghkqvj67m2 + +1. hex (64 chars): 421b328bdde4c3c00ede1448e0cd17a3310e89e4d5e1f0324552f8d876ee45ec + bech32 -> hex -> bech32 round-trips: true +2. new Address(rawPublicKeyBytes) matches: true +3. Shard of this address: 0 (0, 1, or 2) +4. ESDT system contract isSmartContract(): true + User address isSmartContract(): false + +5. LibraryConfig.DefaultAddressHrp: "erd" + This address's own HRP: "erd" + Same key under HRP "test": test1ggdn9z7aunpuqrk7z3ywpngh5vcsaz0y6hslqvj92tudsahwghkquaenm7 + Address.newFromBech32 accepts a "test" address (parsed hrp="test"). + new Address(...) rejects the "test" address while default is "erd": true +``` + +## How it works + +Grounded in the installed `@multiversx/sdk-core` v15.4.1 `Address` / +`AddressComputer` (`core/address.d.ts`) and `LibraryConfig` (`core/config.d.ts`): + +1. **bech32 to hex.** `toHex()` gives the 64-char (32-byte) public key; + `Address.newFromHex(hex)` parses it back. +2. **From raw bytes.** The generic constructor `new Address(bytes)` accepts the + 32-byte public key that `getPublicKey()` returns. +3. **Shard.** `new AddressComputer().getShardOfAddress(address)` returns 0, 1, or + 2 (the computer defaults to 3 shards without the metachain). +4. **Smart-contract check.** `isSmartContract()` is `true` for the ESDT system + contract and `false` for a user address, contract addresses have a fixed + all-zero prefix. +5. **HRP.** `LibraryConfig.DefaultAddressHrp` is the global default (`"erd"`); + each `Address` also carries its own via `getHrp()`. A per-call override like + `Address.newFromHex(hex, "test")` builds an address under a different HRP + without touching global state. + +## Pitfalls + +:::warning[Pitfall 1: new Address(bech32) and Address.newFromBech32(bech32) differ on HRP strictness] +The generic constructor validates the HRP against +`LibraryConfig.DefaultAddressHrp` (`allowCustomHrp: false`) and **throws** on a +mismatch, a `"test1..."` string is rejected while the default is `"erd"`. The +named `newFromBech32` uses `allowCustomHrp: true` and **accepts any HRP**. Use +`newFromBech32` to parse a non-`erd` address; use the strict constructor to +reject foreign ones. A real, verified divergence in v15.4.1. +::: + +:::note[Pitfall 2: an Address's HRP is fixed at construction] +Mutating `LibraryConfig.DefaultAddressHrp` afterward changes only *newly built* +addresses; existing objects keep the HRP they were made with. The config's own +doc comment warns never to alter it inside a library, prefer the per-call `hrp` +argument over changing the global. +::: + +:::tip[Pitfall 3: the hex form is the public key, not a transaction hash] +`toHex()` returns the 32-byte account public key (64 hex chars). Do not confuse +it with a 32-byte transaction hash, which is also 64 hex chars but a different +thing entirely. +::: + +## See also + +- [Create an Account from a KeyPair, secret key, or mnemonic](/sdk-and-tools/sdk-js/cookbook/accounts/account-from-keys) + is where the public key behind an address comes from. +- [Sign a message + verify a signature](/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-message) + is another pure-value, offline sdk-core recipe. +- [Compute a contract address before deploy](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/compute-contract-address) + is `AddressComputer.computeContractAddress`, the deploy-side counterpart to + `getShardOfAddress`. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/accounts/apply-guardian-to-transaction.mdx b/docs/sdk-and-tools/sdk-js/cookbook/accounts/apply-guardian-to-transaction.mdx new file mode 100644 index 000000000..5ffaef6c6 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/accounts/apply-guardian-to-transaction.mdx @@ -0,0 +1,235 @@ +--- +title: Apply a guardian to a transaction and co-sign it +description: Turn any transaction into a guarded one with TransactionComputer.applyGuardian and co-sign it, asserting version, options, and guardian fields on devnet. +difficulty: advanced +est_minutes: 9 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - guardian + - transaction + - gas + - devnet + - typescript +--- + +Once an account is guarded, *every* transaction it sends needs a guardian +co-signature. This recipe takes a plain EGLD transfer and makes it a guarded +transaction with `TransactionComputer.applyGuardian`, then co-signs it with both +the sender and the guardian, verifying the version, options, and guardian fields +end up correct. + +Unlike a relayer (which must share the sender's shard, see +[Build a relayed v3 transaction](/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction)), +a guardian has no shard constraint; it is purely a co-signer. + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates two fresh, unfunded accounts and only needs + devnet **read** access before proving the shape via a clean rejection. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/apply-guardian-to-transaction +npm install +npm run build +npm start +``` + +## The code + +First, a fresh sender and guardian (two independent accounts, no shard search +needed): + +```ts title="src/keys.ts" +// src/keys.ts — generate a sender and a guardian as two fresh, independent, +// intentionally-unfunded accounts. +// +// Unlike a relayer (which must share the sender's shard — see the +// relayed-v3-transaction recipe), a guardian has NO shard constraint: it is +// purely a co-signer. So there is no same-shard search here — two independent +// mnemonics are enough, reflecting that the guardian is a separate party (a +// guardian service, a second device) from the account owner. + +import { Account, Mnemonic } from '@multiversx/sdk-core'; + +/** Generate a fresh sender and a fresh guardian (both unfunded). */ +export async function generateSenderAndGuardian(): Promise<{ + sender: Account; + guardian: Account; +}> { + const sender = Account.newFromMnemonic(Mnemonic.generate().toString()); + const guardian = Account.newFromMnemonic(Mnemonic.generate().toString()); + return { sender, guardian }; +} +``` + +Then apply the guardian, assert the guarded fields, and co-sign: + +```ts title="src/guarded.ts" +// src/guarded.ts — the subject of this recipe: taking any transaction and +// making it a GUARDED transaction, then co-signing it. +// +// A guarded transaction carries a second signature from the account's +// guardian. `TransactionComputer.applyGuardian(tx, guardianAddress)` does +// three things (confirmed by reading the installed v15.4.1 source): +// 1. raises `version` to at least 2 (the minimum that supports options), +// 2. sets the TX_GUARDED bit in `options`, +// 3. sets `tx.guardian` to the guardian's address. +// It does NOT touch gas — the +50,000 for guarded transactions is the +// caller's job on a hand-built transaction (the controllers add it for you). +// +// Signing ORDER matters: applyGuardian must run BEFORE either party signs, so +// both signatures cover the version/options/guardian fields. + +import { Transaction, TransactionComputer } from '@multiversx/sdk-core'; +import type { Account, Address } from '@multiversx/sdk-core'; + +// sdk-core's internal constant for the extra guarded-transaction gas (guarded +// transactions require an extra 50,000 gas). It is not re-exported from the +// package barrel, so it is restated here. +const EXTRA_GAS_LIMIT_FOR_GUARDED_TRANSACTIONS = 50_000n; + +export interface GuardedBuildResult { + transaction: Transaction; + assertions: { + versionIsAtLeast2: boolean; + guardedBitSet: boolean; + guardianMatches: boolean; + hasSenderSignature: boolean; + hasGuardianSignature: boolean; + }; +} + +/** + * Build a plain EGLD transfer, apply a guardian to it, assert the guarded + * fields, then co-sign it with both the sender and the guardian. + */ +export async function buildGuardedTransfer( + sender: Account, + guardian: Account, + receiver: Address, + chainID: string, + amount: bigint, +): Promise { + const transaction = new Transaction({ + sender: sender.address, + receiver, + gasLimit: 50_000n, // minimum for a plain EGLD transfer, before the guarded extra + chainID, + value: amount, + nonce: sender.getNonceThenIncrement(), + }); + + const computer = new TransactionComputer(); + + // Apply the guardian BEFORE signing, and add the guarded gas ourselves. + computer.applyGuardian(transaction, guardian.address); + transaction.gasLimit += EXTRA_GAS_LIMIT_FOR_GUARDED_TRANSACTIONS; + + // Co-sign: the sender signs `signature`, the guardian signs + // `guardianSignature` — both over the same (now guarded) bytes. + transaction.signature = await sender.signTransaction(transaction); + transaction.guardianSignature = await guardian.signTransaction(transaction); + + return { + transaction, + assertions: { + versionIsAtLeast2: transaction.version >= 2, + guardedBitSet: computer.hasOptionsSetForGuardedTransaction(transaction), + guardianMatches: transaction.guardian.toBech32() === guardian.address.toBech32(), + hasSenderSignature: transaction.signature.length > 0, + hasGuardianSignature: transaction.guardianSignature.length > 0, + }, + }; +} +``` + +## Run it + +A real captured run (addresses differ every run): + +```text +Guarded transaction fields: + version: 2 + options: 2 + guardian: erd16pwwy3l8du89tljqht00tg8rt3tuhqysv85x5kakpda3e0znfmkqpdas4v + gasLimit: 100000 (50000 base + 50000 guarded extra) + signature length: 64 bytes + guardianSig length: 64 bytes + +Assertions: + version >= 2: true + guarded bit set: true + guardian field matches: true + sender signature set: true + guardian signature set: true + +Broadcasting the guarded transfer... +Rejected (expected for an unfunded wallet): ... insufficient funds for address erd12kzk5e... +``` + +## How it works + +`TransactionComputer.applyGuardian(tx, guardianAddress)` does exactly three things +(confirmed by reading the installed v15.4.1 source): + +1. raises `version` to at least `2` (the minimum that supports the options field), +2. sets the `TX_GUARDED` bit in `options` (so `options` becomes `2`), +3. sets `tx.guardian` to the guardian's address. + +It does **not** touch gas, the `50,000` guarded premium is the caller's job on a +hand-built transaction (the controllers add it for you). This recipe adds it +explicitly, so the transfer's `50,000` minimum becomes `100,000`. + +**Order matters.** `applyGuardian` runs *before* either party signs, so both +signatures cover the version/options/guardian fields. The sender signs +`signature`; the guardian signs `guardianSignature`. Both are 64-byte Ed25519 +signatures over the same serialized bytes. The unfunded broadcast is rejected +with a clean `insufficient funds` keyed to the sender, confirming the guarded +shape is well-formed. + +For built-in function and guardian detail, see +[docs.multiversx.com/developers/built-in-functions](https://docs.multiversx.com/developers/built-in-functions/). + +## Pitfalls + +:::danger[Pitfall 1: apply the guardian BEFORE signing] +`applyGuardian` mutates `version`, `options`, and `guardian`. Sign first and both +signatures cover the wrong bytes, so the network rejects them. Apply, then sign. +::: + +:::warning[Pitfall 2: a guarded transaction costs an extra 50,000 gas] +`applyGuardian` does not add it. On a hand-built transaction you must add `50,000` +yourself (the controllers do it automatically). Under-budget and the transaction +is rejected for gas. +::: + +:::note[Pitfall 3: the guarded constants are not exported from the barrel] +`TRANSACTION_OPTIONS_TX_GUARDED` and `EXTRA_GAS_LIMIT_FOR_GUARDED_TRANSACTIONS` +are internal to sdk-core. Read the guarded flag through +`TransactionComputer.hasOptionsSetForGuardedTransaction`, and restate the +`50,000` gas constant locally (as this recipe does). +::: + +:::note[Pitfall 4: the account must actually be guarded on-chain] +This recipe proves the transaction *shape* with unfunded accounts. A real guarded +transfer also requires the sender's account to be guarded +([Guard an account](/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account)) +with a guardian whose key produces the `guardianSignature`. +::: + +## See also + +- [Set a guardian on an account](/sdk-and-tools/sdk-js/cookbook/accounts/set-guardian) + nominates the guardian whose signature co-signs here. +- [Guard and unguard an account](/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account) + activates guardianship so guarded transactions are actually required. +- [Build a relayed v3 transaction](/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction) + is the other multi-signature shape, where a relayer pays gas instead of a + guardian co-signing. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/accounts/fetch-account-state.mdx b/docs/sdk-and-tools/sdk-js/cookbook/accounts/fetch-account-state.mdx new file mode 100644 index 000000000..224041264 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/accounts/fetch-account-state.mdx @@ -0,0 +1,210 @@ +--- +title: Fetch an account's on-chain state +description: Read any address's nonce, balance, username, and guarded flag, plus its key-value storage, through a network provider. Verified live against mainnet. +difficulty: beginner +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - network-provider + - api + - typescript +--- + +Read any account's public on-chain state through a provider: its nonce, balance, +username (herotag), guarded flag, and its key-value storage. This is the +sdk-core, read-**any**-address counterpart to sdk-dapp's `useGetAccount()`, which +only reads the connected wallet. + +Three reads, all on `INetworkProvider`: + +- `getAccount(address)` returns `nonce`, `balance`, `userName`, `isGuarded`, plus + the contract fields (`contractOwnerAddress`, `isContractUpgradable`, ...). +- `getAccountStorage(address)` returns all key-value entries. +- `getAccountStorageEntry(address, key)` returns one entry. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/fetch-account-state +npm install +npm run build +npm start +``` + +## Reading the account + +```ts title="src/accountState.ts" +// src/accountState.ts — the subject of this recipe: reading an ARBITRARY +// account's on-chain state through a provider. This is different from the +// sdk-dapp `useGetAccount()` hook, which reads the CONNECTED wallet — here you +// pass any address you like and read it directly. +// +// Three reads, all on INetworkProvider: +// - getAccount(address) → nonce, balance, username, guarded flag +// - getAccountStorage(address) → ALL key-value storage entries +// - getAccountStorageEntry(address, key) → one entry + +import type { + INetworkProvider, + AccountOnNetwork, + AccountStorage, + AccountStorageEntry, + Address, +} from '@multiversx/sdk-core'; + +/** Core account state: nonce, balance, username, guarded flag, contract fields. */ +export async function fetchAccount( + provider: INetworkProvider, + address: Address, +): Promise { + return provider.getAccount(address); +} + +/** Every key-value pair stored on the account. Values come back HEX-encoded. */ +export async function fetchAccountStorage( + provider: INetworkProvider, + address: Address, +): Promise { + return provider.getAccountStorage(address); +} + +/** + * A single storage entry by key. Unlike `getAccountStorage`, the single-entry + * endpoint returns the value already DECODED to a UTF-8 string. + */ +export async function fetchAccountStorageEntry( + provider: INetworkProvider, + address: Address, + key: string, +): Promise { + return provider.getAccountStorageEntry(address, key); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — read a live mainnet account's state and storage. No wallet, +// no gas — you are reading someone else's public on-chain state. +// +// Usage: +// npm run build && npm start [bech32Address] [storageKey] + +import { ApiNetworkProvider, Address } from '@multiversx/sdk-core'; +import { fetchAccount, fetchAccountStorage, fetchAccountStorageEntry } from './accountState'; + +const MAINNET_API = 'https://api.multiversx.com'; +const DEFAULT_ADDRESS = 'erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th'; +const DEFAULT_KEY = 'btc'; + +async function main(): Promise { + const addressArg = process.argv[2] ?? DEFAULT_ADDRESS; + const key = process.argv[3] ?? DEFAULT_KEY; + const provider = new ApiNetworkProvider(MAINNET_API, { clientName: 'mvx-cookbook' }); + const address = Address.newFromBech32(addressArg); + + const account = await fetchAccount(provider, address); + console.log(`Account ${account.address.toBech32()}`); + console.log(` nonce: ${account.nonce}`); + console.log(` balance: ${account.balance}`); + console.log(` username: ${account.userName ? account.userName : '(none)'}`); + console.log(` isGuarded: ${account.isGuarded}`); + + const storage = await fetchAccountStorage(provider, address); + console.log(`\nStorage — ${storage.entries.length} entries. Values are HEX from getAccountStorage:`); + for (const entry of storage.entries.slice(0, 3)) { + console.log(` ${entry.key} = ${entry.value}`); + } + + const single = await fetchAccountStorageEntry(provider, address, key); + console.log(`\ngetAccountStorageEntry('${key}') — value is DECODED here:`); + console.log(` ${single.key} = ${single.value}`); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start # a known mainnet account with storage +npm start # any account / key +``` + +Expected output (balance and nonce are live, they will differ): + +```text +Account erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th + nonce: 89 + balance: 1000010000000 + username: (none) + isGuarded: false + +Storage — 16 entries. Values are HEX from getAccountStorage: + btc = 626331716639747971736b3373333830713832673033686367346c637a677a7032373568617138326171 + eth = 307838433739313643643332633037623164633730353465324365423233663964443538396642334336 + ELRONDesdtBSK-baa025 = 12070001d1a94a4000 + +getAccountStorageEntry('btc') — value is DECODED here: + btc = bc1qf9tyqsk3s380q82g03hcg4lczgzp275haq82aq +``` + +## How it works + +**`getAccount` reads an arbitrary address.** You pass the address; nothing is +signed, no wallet is constructed. Contrast with `useGetAccount()` in sdk-dapp, +which is bound to whoever is logged in, this reads any account on the network. + +**`balance` and `nonce` are `bigint`.** Balance is in the smallest denomination +(10^18 = 1 EGLD). The `nonce` here is the same value +[Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) fetches +before sending, one read, two uses. + +**Bulk storage is hex; a single entry is decoded.** `getAccountStorage` returns +each value as a hex string; `getAccountStorageEntry` returns the same value +decoded to UTF-8. Verified live in the output above: `btc` is `62633171...` in +bulk and `bc1q...` as a single entry. + +## Pitfalls + +:::warning[Pitfall 1: userName is typed string but can be undefined] +`AccountOnNetwork.userName` is declared `string`, but an account with no herotag +returns `undefined` at runtime (confirmed above, the default account prints +`(none)`). Guard it (`account.userName ? ... : ...`) rather than trusting the +type. +::: + +:::note[Pitfall 2: getAccountStorage values are hex, not text] +Do not print `getAccountStorage` values as-is expecting readable strings, they +are hex. Either decode them yourself (`Buffer.from(value, 'hex').toString()`) or +use `getAccountStorageEntry`, which decodes for you. The two endpoints returning +the same key in different encodings is a real inconsistency, not a bug in your +code. +::: + +:::note[Pitfall 3: contract fields are optional] +For a plain wallet, `contractOwnerAddress`, `isContractUpgradable`, +`contractCodeHash`, and friends are absent. They are populated only when the +address is a smart contract. They are typed optional for exactly this reason. +::: + +## See also + +- [Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) + is what to do with the `nonce` this recipe reads before you send transactions. +- [Configure a network provider](/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider) + is the provider all three reads run on. +- [Fetch an account's token balances](/sdk-and-tools/sdk-js/cookbook/tokens/fetch-account-token-balances) + is the ESDT and NFT side of the same account. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/accounts/generate-mnemonic-derive-keys.mdx b/docs/sdk-and-tools/sdk-js/cookbook/accounts/generate-mnemonic-derive-keys.mdx new file mode 100644 index 000000000..688c17cd1 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/accounts/generate-mnemonic-derive-keys.mdx @@ -0,0 +1,160 @@ +--- +title: Generate a mnemonic + derive keys +description: Generate a BIP39 mnemonic with sdk-core and derive secret keys at several address indices, fully offline, with a determinism check and secrecy notes. +difficulty: beginner +est_minutes: 5 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - typescript + - wallet +--- + +Generate a BIP39 mnemonic and derive secret keys from it at different address +indices, fully offline. There is no network call anywhere in this recipe: +mnemonic generation and key derivation are pure local cryptography. No devnet +wallet, no funds, no gas. + +One mnemonic is the root of many accounts: `deriveKey(0)`, `deriveKey(1)`, and so +on each produce a distinct key and address. Because derivation is deterministic, +the same phrase always regenerates the same keys, which is exactly what makes a +mnemonic a portable backup. + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates its own throwaway mnemonic and never + touches the network. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/generate-mnemonic-derive-keys +npm install +npm run build +npm start +``` + +## The code + +```ts title="src/index.ts" +// src/index.ts — generate a mnemonic and derive secret keys from it at +// several address indices, then show the derivation is deterministic. +// +// Fully offline. No devnet, no network call of any kind — mnemonic +// generation and key derivation are pure local cryptography (BIP39 +// entropy -> words, then a deterministic derivation to Ed25519 keys). A +// fresh mnemonic is generated on every run, so exact words/addresses differ +// run to run; the shape of the output does not. +// +// Modeled on the mx-sdk-js-core cookbook source (cookbook/basics.ts, +// "Generating a mnemonic" / "Deriving secret keys from a mnemonic"). + +import { Mnemonic } from '@multiversx/sdk-core'; + +function main(): void { + // === 1. Generate a fresh 24-word mnemonic. === + // This is the ROOT SECRET. Anyone holding these words controls every + // account derived from them. Real code must never log a mnemonic; this + // recipe prints only the count and the first/last word because the phrase + // is a throwaway that is never funded. + const mnemonic = Mnemonic.generate(); + const words = mnemonic.getWords(); + console.log(`Generated a ${words.length}-word mnemonic.`); + console.log( + `First word: "${words[0] ?? ''}", last word: "${words[words.length - 1] ?? ''}".`, + ); + + // === 2. One mnemonic derives many accounts, selected by addressIndex. === + // deriveKey(i) walks the MultiversX derivation path at account index i and + // returns a UserSecretKey; its public key gives the on-chain Address. + console.log('\nAddresses derived from this one mnemonic:'); + for (const addressIndex of [0, 1, 2]) { + const secretKey = mnemonic.deriveKey(addressIndex); + const address = secretKey.generatePublicKey().toAddress(); + console.log(` addressIndex ${addressIndex} -> ${address.toBech32()}`); + } + + // === 3. Derivation is deterministic. === + // Re-importing the same phrase with Mnemonic.fromString and deriving at the + // same index yields the same key/address every time — this is what makes a + // mnemonic a portable backup. + const restored = Mnemonic.fromString(mnemonic.toString()); + const original0 = mnemonic.deriveKey(0).generatePublicKey().toAddress().toBech32(); + const restored0 = restored.deriveKey(0).generatePublicKey().toAddress().toBech32(); + console.log(`\nRe-derived from the same phrase (addressIndex 0) matches: ${original0 === restored0}`); + + // === 4. deriveKey() defaults to addressIndex 0. === + const defaultAddress = mnemonic.deriveKey().generatePublicKey().toAddress().toBech32(); + console.log(`deriveKey() with no argument == deriveKey(0): ${defaultAddress === original0}`); + + console.log('\nExpected: three distinct addresses, then two "true" lines.'); +} + +main(); +``` + +## Run it + +A fresh mnemonic is generated every run, so the exact words and addresses differ; +the shape does not. Three distinct addresses, then two `true` lines: + +```text +Generated a 24-word mnemonic. +First word: "lesson", last word: "grief". + +Addresses derived from this one mnemonic: + addressIndex 0 -> erd1f03w2cg7lxx8k8e9dafw3rxe092qhzlceq3xv6rqsj4kn5rsq6fq086964 + addressIndex 1 -> erd1r64f3gmpq23zf634x80evgfcpa5n8vt9jtmpqgtd5wuwwkez94usxeke2q + addressIndex 2 -> erd1ulvfmlh43ejl42jngekfu2pn2gugrewrxxgwgzmrf4h3flywqgeqkkl78g + +Re-derived from the same phrase (addressIndex 0) matches: true +deriveKey() with no argument == deriveKey(0): true +``` + +## How it works + +Grounded in the installed `@multiversx/sdk-core` v15.4.1 `Mnemonic` class: + +1. **Generate.** `Mnemonic.generate()` returns a fresh 24-word phrase; + `getWords()` lists the words. +2. **Derive at indices.** `mnemonic.deriveKey(addressIndex)` returns a + `UserSecretKey`; its `generatePublicKey().toAddress()` gives the on-chain + `Address`. One mnemonic backs many accounts, selected by `addressIndex`. +3. **Determinism.** `Mnemonic.fromString(phrase)` re-imports the phrase and + derives the identical key/address. +4. **Default index.** `deriveKey()` with no argument is `deriveKey(0)`. + +## Pitfalls + +:::danger[Pitfall 1: a mnemonic is the root secret, never log it in real code] +Anyone holding these 24 words controls every account derived from them. This +recipe prints only the word count and first/last word because the phrase is a +throwaway that is never funded. In production, treat the phrase like a private +key: never log it, never send it over the wire. +::: + +:::note[Pitfall 2: addressIndex selects the account and is not a passphrase] +`deriveKey(0)`, `deriveKey(1)`, `deriveKey(2)` are three different accounts from +the *same* mnemonic. The optional second argument, `deriveKey(addressIndex, +password)`, is a BIP39 passphrase (a "25th word"), a separate concept. Leave it +unset unless you deliberately use one. +::: + +:::tip[Pitfall 3: this gives you a key, not a funded account] +Deriving a key is free and offline; the account does not exist on-chain until it +receives EGLD. Fetching its nonce or balance needs a network provider, see +[Fetch an account's on-chain state](/sdk-and-tools/sdk-js/cookbook/accounts/fetch-account-state). +::: + +## See also + +- [Create an Account from a KeyPair, secret key, or mnemonic](/sdk-and-tools/sdk-js/cookbook/accounts/account-from-keys) + turns a derived key into an `Account`. +- [Save and load a keystore](/sdk-and-tools/sdk-js/cookbook/accounts/keystore-save-load) + encrypts a mnemonic (or a single key) at rest. +- [Save and load a PEM (dev only)](/sdk-and-tools/sdk-js/cookbook/accounts/pem-save-load) + is the unencrypted dev-wallet format. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account.mdx b/docs/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account.mdx new file mode 100644 index 000000000..739af946a --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account.mdx @@ -0,0 +1,217 @@ +--- +title: Guard and unguard an account +description: Activate guardianship with GuardAccount and remove it with UnGuardAccount via sdk-core, including the guardian co-sign requirement, verified on devnet. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - guardian + - transaction + - gas + - devnet + - typescript +--- + +Once a guardian is nominated (see +[Set a guardian](/sdk-and-tools/sdk-js/cookbook/accounts/set-guardian)), two +transactions toggle protection: + +- **`GuardAccount`** activates guardianship. From here on, every transaction from + the account must carry the guardian's co-signature. +- **`UnGuardAccount`** removes guardianship. Because the account is guarded when + you run this, the unguard transaction itself must be co-signed by the current + guardian. + +This recipe builds both with sdk-core's `AccountController` (and factory), decodes +the payloads, and shows the guarded flag being set on the unguard. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites. **No devnet EGLD required**, see "How it works". + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/guard-unguard-account +npm install +npm run build +npm start -- ./wallet.pem --guardian erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th +``` + +## The code + +```ts title="src/guard.ts" +// src/guard.ts — the subject of this recipe: turning guardianship on and off +// once a guardian has been nominated (see set-guardian for the nomination). +// +// - GuardAccount — activate guardianship. From here on, every transaction +// from this account must carry the guardian's co-signature. +// - UnGuardAccount — remove guardianship. Because the account is guarded +// when you run this, the UnGuardAccount transaction ITSELF must be +// co-signed by the current guardian — otherwise a stolen primary key +// could simply unguard the account and bypass the guardian entirely. +// +// Both are builtin functions on the caller's own account, so the receiver is +// the sender. Note the wire spelling: `GuardAccount` and `UnGuardAccount` +// (capital G in "Guard"). + +import { Address, TransactionComputer } from '@multiversx/sdk-core'; +import type { Account, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +/** Build a GuardAccount transaction (activate guardianship). */ +export async function guardAccount( + entrypoint: DevnetEntrypoint, + sender: Account, + useFactory: boolean, +): Promise { + if (useFactory) { + const factory = entrypoint.createAccountTransactionsFactory(); + const transaction = await factory.createTransactionForGuardingAccount(sender.address); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; + } + + const controller = entrypoint.createAccountController(); + return controller.createTransactionForGuardingAccount(sender, sender.getNonceThenIncrement(), {}); +} + +/** + * Build an UnGuardAccount transaction (remove guardianship). + * + * Pass the current `guardian` to have the SDK mark the transaction as guarded + * (version stays 2, options gets the TX_GUARDED bit, and `guardian` is set), + * ready for the guardian's co-signature in `guardianSignature`. Without it, + * the network rejects the unguard on a guarded account. + */ +export async function unguardAccount( + entrypoint: DevnetEntrypoint, + sender: Account, + guardian: Address | undefined, + useFactory: boolean, +): Promise { + if (useFactory) { + const factory = entrypoint.createAccountTransactionsFactory(); + const transaction = await factory.createTransactionForUnguardingAccount( + sender.address, + guardian ? { guardian } : {}, + ); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; + } + + const controller = entrypoint.createAccountController(); + return controller.createTransactionForUnguardingAccount( + sender, + sender.getNonceThenIncrement(), + guardian ? { guardian } : {}, + ); +} + +/** Decode a guard/unguard transaction's shape, including the guarded bit. */ +export function describeGuardPayload(transaction: Transaction): { + function: string; + receiver: string; + options: number; + isGuardedBitSet: boolean; + guardian: string; + gasLimit: string; +} { + const guardianBech = transaction.guardian.isEmpty() ? '(none)' : transaction.guardian.toBech32(); + // The TX_GUARDED options constant is not re-exported from the sdk-core + // barrel, so read the flag through TransactionComputer instead of + // hardcoding the bit. + const computer = new TransactionComputer(); + return { + function: Buffer.from(transaction.data).toString().split('@')[0] ?? '', + receiver: transaction.receiver.toBech32(), + options: transaction.options, + isGuardedBitSet: computer.hasOptionsSetForGuardedTransaction(transaction), + guardian: guardianBech, + gasLimit: transaction.gasLimit.toString(), + }; +} +``` + +## Run it + +```bash +npm start -- [--guardian ] [--factory] +``` + +A real captured run (unfunded wallet, guardian supplied): + +```text +GUARD ACCOUNT (activate): + function: GuardAccount + receiver: erd18l2cww3g3g6uc35wy60efd87ss85xdqj9gh9qn6ga6halz3jscaqd4dl3k + options: 0 (guarded bit set: false) + guardian: (none) + gasLimit: 318000 + +UNGUARD ACCOUNT (co-signed by current guardian): + function: UnGuardAccount + options: 2 (guarded bit set: true) + guardian: erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th + gasLimit: 371000 + +Broadcasting the GuardAccount transaction... +Rejected (expected for an unfunded wallet): ... insufficient funds for address erd18l2c... +``` + +## How it works + +Both are builtin functions on the caller's own account, so the receiver is the +**sender**. `GuardAccount` carries no arguments; its gas was `318000`, matching +`50,000 + 1,500 × 12 (data bytes) + 250,000`. + +The interesting one is `UnGuardAccount`. Passing the current guardian marks the +transaction as guarded: `options` becomes `2` (the `TX_GUARDED` bit), `guardian` +is populated, and the SDK adds the `50,000` guarded-transaction gas premium, +`371000` total versus the `321000` an unguarded build would cost. This is the +network's protection: a stolen primary key alone cannot lift guardianship, +because the unguard must itself be co-signed by the guardian being removed. Add +that co-signature with the guardian's key (see +[Apply a guardian to a transaction](/sdk-and-tools/sdk-js/cookbook/accounts/apply-guardian-to-transaction)) +before broadcasting a real unguard. + +For built-in function detail, see +[docs.multiversx.com/developers/built-in-functions](https://docs.multiversx.com/developers/built-in-functions/). + +## Pitfalls + +:::danger[Pitfall 1: unguarding a guarded account needs the guardian's co-signature] +Send `UnGuardAccount` from the primary key alone and the network rejects it. Pass +the current guardian so the transaction is marked guarded, then attach the +`guardianSignature`. +::: + +:::warning[Pitfall 2: the guarded unguard costs an extra 50,000 gas] +Because it is itself a guarded transaction. The controller adds it automatically +when a guardian is present (`371000` vs `321000` here); budget for it on a +hand-built transaction. +::: + +:::note[Pitfall 3: the TX_GUARDED options constant is not exported from the barrel] +`TRANSACTION_OPTIONS_TX_GUARDED` is internal to sdk-core. Read the flag through +`TransactionComputer.hasOptionsSetForGuardedTransaction(tx)` instead of +hardcoding `2`, as this recipe does. +::: + +## See also + +- [Set a guardian on an account](/sdk-and-tools/sdk-js/cookbook/accounts/set-guardian) + nominates the guardian you activate here. +- [Apply a guardian to a transaction](/sdk-and-tools/sdk-js/cookbook/accounts/apply-guardian-to-transaction) + attaches the guardian co-signature the unguard (and every guarded transaction) + needs. +- [Manage nonces (fetch-then-increment)](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) + is the nonce-fetch pattern this recipe uses. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/accounts/hash-signing-transaction.mdx b/docs/sdk-and-tools/sdk-js/cookbook/accounts/hash-signing-transaction.mdx new file mode 100644 index 000000000..fe2c9f890 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/accounts/hash-signing-transaction.mdx @@ -0,0 +1,205 @@ +--- +title: Hash-signing a transaction +description: Opt a transaction into hash signing with sdk-core, assert the version/options bits, and sign the hash correctly. Includes a real v15.4.1 signing trap. +difficulty: advanced +est_minutes: 10 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - typescript + - transaction +--- + +Opt a transaction into **hash signing**, assert the version and options bits are +set, and sign it correctly by signing the transaction *hash*. Along the way it +exposes a real v15.4.1 trap: `Account.signTransaction` does **not** +honor the hash-signing option, so its output fails verification. Fully offline, +nothing is broadcast. + +Hash signing sets the least-significant bit of the transaction `options` field. +When set, the signature must be over the **keccak-256 hash** of the serialized +transaction, not over the serialized bytes themselves. It lets a signer (for +example a hardware wallet) sign a short fixed-size digest instead of an +arbitrarily long payload. + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates its own throwaway keys and never touches + the network. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/hash-signing-transaction +npm install +npm run build +npm start +``` + +## The code + +```ts title="src/index.ts" +// src/index.ts — opt a transaction into HASH SIGNING, assert the version and +// options bits are set, and sign it correctly by signing the transaction +// HASH. Also demonstrates a real v15.4.1 trap: Account.signTransaction does +// NOT honor the hash-signing option, so its output fails verification. +// +// Fully offline. No devnet, no broadcast — this builds and signs locally and +// asserts on the bytes. Nothing is sent. Fresh keys each run. +// +// Background: hash signing sets the least-significant bit of the transaction +// `options` field. When set, the signature must be over the KECCAK-256 hash +// of the serialized transaction, not over the serialized bytes themselves. +// It lets a signer (e.g. a hardware wallet) sign a short fixed-size digest +// instead of an arbitrarily long payload. +// +// Grounded in the installed @multiversx/sdk-core v15.4.1: TransactionComputer +// (core/transactionComputer.d.ts) and Account (accounts/account.d.ts). + +import { Account, Mnemonic, Transaction, TransactionComputer } from '@multiversx/sdk-core'; +import { strict as assert } from 'node:assert'; + +async function main(): Promise { + const account = Account.newFromMnemonic(Mnemonic.generate().toString(), 0); + const other = Account.newFromMnemonic(Mnemonic.generate().toString(), 0); + const transactionComputer = new TransactionComputer(); + + const transaction = new Transaction({ + nonce: 7n, + value: 0n, + sender: account.address, + receiver: other.address, + gasLimit: 50000n, + chainID: 'D', + }); + + // === 1. Before: a default transaction is version 2, options 0. === + console.log(`1. Before: version=${transaction.version}, options=${transaction.options}`); + console.log(` hasOptionsSetForHashSigning: ${transactionComputer.hasOptionsSetForHashSigning(transaction)}`); + + // === 2. Opt into hash signing and assert the bits. === + // applyOptionsForHashSigning sets the options LSB (0b0001) and ensures + // version >= 2. This is the core of the recipe: proving the flags land. + transactionComputer.applyOptionsForHashSigning(transaction); + console.log(`\n2. After applyOptionsForHashSigning: version=${transaction.version}, options=${transaction.options}`); + + assert.equal(transaction.version, 2, 'version must be >= 2 for options to be honored'); + assert.equal(transaction.options & 0b0001, 0b0001, 'the hash-signing bit (0b0001) must be set'); + assert.equal(transactionComputer.hasOptionsSetForHashSigning(transaction), true); + // The hash-signing bit and the guarded bit (0b0010) are independent. + assert.equal(transactionComputer.hasOptionsSetForGuardedTransaction(transaction), false); + console.log(' Asserted: version === 2, options bit 0b0001 set, hasOptionsSetForHashSigning === true.'); + + // === 3. Sign CORRECTLY: sign the hash, verify against it. === + // computeHashForSigning returns the keccak-256 digest (32 bytes). + // computeBytesForVerifying already returns that same hash when the option + // is set, so signing the hash and verifying line up. + const hashForSigning = transactionComputer.computeHashForSigning(transaction); + console.log(`\n3. computeHashForSigning length: ${hashForSigning.length} bytes (keccak-256).`); + transaction.signature = account.secretKey.sign(hashForSigning); + const correct = await account.publicKey.verify( + transactionComputer.computeBytesForVerifying(transaction), + transaction.signature, + ); + console.log(` Sign the hash -> verify: ${correct} (expected true)`); + + // === 4. THE TRAP: Account.signTransaction does NOT honor the option. === + // Account.signTransaction serializes with computeBytesForSigning, which in + // v15.4.1 does NOT branch on the hash-signing option — it returns the full + // JSON bytes, not the hash. verifyTransactionSignature DOES branch (it + // verifies against the hash). So the two disagree and verification fails. + const trapSignature = await account.signTransaction(transaction); + const trapVerify = await account.verifyTransactionSignature(transaction, trapSignature); + console.log(`\n4. TRAP: account.signTransaction -> account.verifyTransactionSignature: ${trapVerify} (expected false!)`); + + const signingBytes = transactionComputer.computeBytesForSigning(transaction); + const verifyingBytes = transactionComputer.computeBytesForVerifying(transaction); + console.log( + ` Why: computeBytesForSigning is ${signingBytes.length} bytes (full JSON) but computeBytesForVerifying is ${verifyingBytes.length} bytes (the hash) — they differ.`, + ); + assert.equal(trapVerify, false, 'demonstrates the documented trap'); + assert.equal(correct, true, 'the manual hash-signing path is the correct one'); + + console.log('\nExpected: step 3 true (sign the hash), step 4 false (the trap). Use the step-3 path in real code.'); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +Keys are fresh each run; the true/false pattern and the byte counts are stable: + +```text +1. Before: version=2, options=0 + hasOptionsSetForHashSigning: false + +2. After applyOptionsForHashSigning: version=2, options=1 + Asserted: version === 2, options bit 0b0001 set, hasOptionsSetForHashSigning === true. + +3. computeHashForSigning length: 32 bytes (keccak-256). + Sign the hash -> verify: true (expected true) + +4. TRAP: account.signTransaction -> account.verifyTransactionSignature: false (expected false!) + Why: computeBytesForSigning is 250 bytes (full JSON) but computeBytesForVerifying is 32 bytes (the hash) — they differ. +``` + +The source's `assert` calls turn this behavior, including the trap, into hard +test assertions, so the run fails loudly if the SDK ever changes. + +## How it works + +Grounded in the installed `@multiversx/sdk-core` v15.4.1 `TransactionComputer` +(`core/transactionComputer.d.ts`) and `Account` (`accounts/account.d.ts`): + +1. **Before.** A default transaction is `version: 2`, `options: 0`. +2. **Opt in and assert.** `applyOptionsForHashSigning(tx)` sets the options bit + `0b0001` and ensures `version >= 2`. The recipe asserts `version === 2`, that + bit `0b0001` is set, and that `hasOptionsSetForHashSigning(tx)` is `true` (the + independent guarded bit `0b0010` stays unset). This is the core deliverable, + proving the flags land. +3. **Sign correctly.** `computeHashForSigning(tx)` returns the 32-byte keccak-256 + digest. `computeBytesForVerifying(tx)` already returns that same hash when the + option is set, so signing the hash and verifying against it line up: `true`. +4. **The trap.** `account.signTransaction(tx)` then + `account.verifyTransactionSignature(tx, sig)` returns `false`. + +## Pitfalls + +:::danger[Pitfall 1: Account.signTransaction does NOT honor the hash-signing option (v15.4.1)] +It serializes with `computeBytesForSigning`, which does not branch on the option +and returns the full ~250-byte JSON. But `computeBytesForVerifying` *does* branch +and returns the 32-byte hash. The two disagree, so a hash-flagged transaction +signed with `account.signTransaction` fails its own `verifyTransactionSignature`, +and would be rejected by the network. **Sign `computeHashForSigning(tx)` +directly instead** (step 3). +::: + +:::warning[Pitfall 2: options is a bitfield, do not overwrite it] +`applyOptionsForHashSigning` OR-s in bit `0b0001`; the guarded flag is `0b0010`. +Setting `tx.options = 1` by hand would clobber a guarded flag. Use the `apply...` +/ `hasOptionsSet...` helpers rather than assigning the field. +::: + +:::note[Pitfall 3: applyOptionsForHashSigning bumps the version if needed] +Options are only honored at `version >= 2`. The helper raises the version for +you; if you set the bit by hand on a `version: 1` transaction, the network +ignores the options entirely. +::: + +## See also + +- [Sign + verify a transaction (offline)](/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-transaction) + is the ordinary (non-hash) signing path, where signing and verifying bytes + match. +- [Sign a message + verify a signature](/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-message) + is message signing, a related but distinct primitive family. +- [Build a relayed v3 transaction](/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction) + is another advanced transaction shape using the `options`/`version` machinery. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/accounts/keystore-save-load.mdx b/docs/sdk-and-tools/sdk-js/cookbook/accounts/keystore-save-load.mdx new file mode 100644 index 000000000..5092dbd79 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/accounts/keystore-save-load.mdx @@ -0,0 +1,182 @@ +--- +title: Save and load a keystore (encrypted JSON) +description: Save a wallet to an encrypted keystore and load it back with a password, for both secret-key and mnemonic keystores, with sdk-core. Fully offline. +difficulty: beginner +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - typescript + - wallet +--- + +The keystore is the safe at-rest wallet format: a JSON file whose key material is +encrypted with your password (scrypt + AES-128-CTR), so the key never touches +disk in plaintext. This recipe saves and reloads both keystore kinds, a single +secret key and a full mnemonic, fully offline. The only I/O is to a throwaway +temp directory it cleans up. + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates its own throwaway keys and writes to a temp + directory it cleans up. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/keystore-save-load +npm install +npm run build +npm start +``` + +## The code + +```ts title="src/index.ts" +// src/index.ts — save a wallet to an encrypted keystore (JSON) and load it +// back, for both keystore kinds: secret-key and mnemonic. Prove the password +// is really required, and show how addressIndex selects an account from a +// mnemonic keystore. +// +// Fully offline. The only I/O is to a throwaway temp directory (cleaned up +// at the end); no devnet, no network. Fresh keys each run, so exact +// addresses differ; the equivalences printed do not. +// +// Grounded in the installed @multiversx/sdk-core v15.4.1: UserWallet +// (wallet/userWallet.d.ts) and Account.newFromKeystore (accounts/account.d.ts). + +import { Account, Mnemonic, UserWallet } from '@multiversx/sdk-core'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +const PASSWORD = 'correct horse battery staple'; + +function main(): void { + const dir = mkdtempSync(join(tmpdir(), 'mvx-keystore-')); + try { + // === 1. Secret-key keystore: encrypt one key. === + const mnemonic = Mnemonic.generate(); + const secretKey = mnemonic.deriveKey(0); + const expectedAddress = secretKey.generatePublicKey().toAddress().toBech32(); + + const wallet = UserWallet.fromSecretKey({ secretKey, password: PASSWORD }); + const keystorePath = join(dir, 'wallet.json'); + wallet.save(keystorePath); + + // The file on disk is encrypted JSON — never plaintext key material. + const onDisk = JSON.parse(readFileSync(keystorePath, 'utf8')) as { + version: number; + kind: string; + crypto: { cipher: string; kdf: string }; + }; + console.log( + `1. Saved keystore: version ${onDisk.version}, kind "${onDisk.kind}", cipher ${onDisk.crypto.cipher}, kdf ${onDisk.crypto.kdf}.`, + ); + + // === 2. Load it back with the password. === + // Account.newFromKeystore handles both keystore kinds and is synchronous. + const loaded = Account.newFromKeystore(keystorePath, PASSWORD); + console.log(`2. Loaded address matches the original: ${loaded.address.toBech32() === expectedAddress}`); + + // UserWallet.loadSecretKey returns the raw key instead of an Account. + const loadedKey = UserWallet.loadSecretKey(keystorePath, PASSWORD); + console.log(` UserWallet.loadSecretKey recovered the same key: ${loadedKey.hex() === secretKey.hex()}`); + + // === 3. The password really is required. === + let wrongPasswordRejected = false; + try { + Account.newFromKeystore(keystorePath, 'wrong password'); + } catch { + wrongPasswordRejected = true; + } + console.log(`3. Loading with the wrong password throws: ${wrongPasswordRejected}`); + + // === 4. Mnemonic keystore: encrypt the whole phrase. === + // A mnemonic keystore holds the seed phrase, so one file yields many + // accounts — addressIndex picks which one on load. + const mnemonicWallet = UserWallet.fromMnemonic({ mnemonic: mnemonic.toString(), password: PASSWORD }); + const mnemonicPath = join(dir, 'mnemonic.json'); + mnemonicWallet.save(mnemonicPath); + + const account0 = Account.newFromKeystore(mnemonicPath, PASSWORD, 0); + const account1 = Account.newFromKeystore(mnemonicPath, PASSWORD, 1); + const derived0 = mnemonic.deriveKey(0).generatePublicKey().toAddress().toBech32(); + const derived1 = mnemonic.deriveKey(1).generatePublicKey().toAddress().toBech32(); + console.log( + `4. Mnemonic keystore, addressIndex 0 and 1 match direct derivation: ${account0.address.toBech32() === derived0 && account1.address.toBech32() === derived1}`, + ); + console.log(` The two indices are different accounts: ${account0.address.toBech32() !== account1.address.toBech32()}`); + + console.log('\nExpected: four "true" confirmations (2, 2b, 3, 4) plus the "different accounts" line.'); + } finally { + // Clean up the temp directory regardless of outcome. + rmSync(dir, { recursive: true, force: true }); + } +} + +main(); +``` + +## Run it + +Keys are generated fresh each run; the pattern of confirmations is stable: + +```text +1. Saved keystore: version 4, kind "secretKey", cipher aes-128-ctr, kdf scrypt. +2. Loaded address matches the original: true + UserWallet.loadSecretKey recovered the same key: true +3. Loading with the wrong password throws: true +4. Mnemonic keystore, addressIndex 0 and 1 match direct derivation: true + The two indices are different accounts: true +``` + +## How it works + +Grounded in the installed `@multiversx/sdk-core` v15.4.1 `UserWallet` +(`wallet/userWallet.d.ts`) and `Account.newFromKeystore`: + +1. **Encrypt a key.** `UserWallet.fromSecretKey({ secretKey, password })` builds + an encrypted wallet; `.save(path)` writes it. On disk it is JSON with + `version: 4`, `kind: "secretKey"`, `cipher: aes-128-ctr`, `kdf: scrypt`. +2. **Load it back.** `Account.newFromKeystore(path, password)` returns an + `Account` (synchronous). `UserWallet.loadSecretKey(path, password)` returns + the raw `UserSecretKey` instead. +3. **The password is required.** Loading with the wrong password throws. +4. **Mnemonic keystore.** `UserWallet.fromMnemonic({ mnemonic, password })` + encrypts the whole phrase, so one file yields many accounts; + `Account.newFromKeystore(path, password, addressIndex)` picks which one. + +## Pitfalls + +:::warning[Pitfall 1: addressIndex only matters for mnemonic keystores] +A `secretKey` keystore holds exactly one key, so `addressIndex` is irrelevant +there. For a `mnemonic` keystore it selects the derived account (0, 1, ...). +`Account.newFromKeystore` handles both kinds with the same call, but only the +mnemonic kind honors the index. +::: + +:::note[Pitfall 2: the JSON's address field is metadata, not proof of the key] +The public `address` field is metadata; the actual key is the encrypted `crypto` +section, recoverable only with the password. Always round-trip (decrypt) to +confirm you hold the right key, as step 2 does. +::: + +:::danger[Pitfall 3: losing the password means losing the key] +There is no recovery path for an encrypted keystore. Scrypt is deliberately slow +to resist brute force, which also means a forgotten password is unrecoverable. +For real keys, back up the mnemonic separately. +::: + +## See also + +- [Save and load a PEM (dev only)](/sdk-and-tools/sdk-js/cookbook/accounts/pem-save-load) + is the unencrypted counterpart; use the keystore for anything real. +- [Create an Account from a KeyPair, secret key, or mnemonic](/sdk-and-tools/sdk-js/cookbook/accounts/account-from-keys) + covers the in-memory constructors. +- [Generate a mnemonic + derive keys](/sdk-and-tools/sdk-js/cookbook/accounts/generate-mnemonic-derive-keys) + is the phrase a mnemonic keystore encrypts. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces.mdx b/docs/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces.mdx new file mode 100644 index 000000000..cd3eafdf1 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces.mdx @@ -0,0 +1,222 @@ +--- +title: Manage nonces (fetch-then-increment) +description: Fetch an account's nonce from the network once, then increment it locally for every transaction sent afterward in a batch, plus recovery from a nonce gap. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - transaction + - typescript + - devnet +--- + +The pattern behind sdk-core's single most emphasized rule: fetch the account's +nonce from the network **once**, then increment it **locally** for every +transaction sent afterward in the same batch — never re-fetch mid-batch. This +recipe makes that pattern concrete with a small burst of sequential sends, plus +the recovery step for when a batch partially fails. + +This is the sdk-core side of a problem that also exists in sdk-dapp's +connected-wallet flow (see +[Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send)'s +Pitfall 1), the failure mode is identical; only the concrete API differs. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet with some devnet EGLD, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites for how to generate and fund a throwaway one. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/manage-nonces +npm install +npm run build +npm start -- ./wallet.pem erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th 3 +``` + +## The pattern + +```ts title="src/batchSend.ts" +// src/batchSend.ts — the actual subject of this recipe: sending more than +// one transaction per process run with a SINGLE nonce fetch, using +// `getNonceThenIncrement()` locally for each one. +// +// The sdk-core nonce rule: you MUST fetch the account nonce from the network +// before sending transactions, then increment locally with +// getNonceThenIncrement(). Stale nonces cause transaction rejection. This +// file is that pattern, applied to a batch. +// +// THE WRONG PATTERN (do not copy this) is calling +// `entrypoint.recallAccountNonce(address)` again before building each +// transaction in the loop below, instead of relying on the account's local +// counter. It looks safer ("always get the freshest nonce!") but does the +// opposite: the network only advances an account's nonce once a +// transaction is PROCESSED, not merely broadcast. If you fire transaction +// #2's nonce fetch before #1 has been processed, both fetches return the +// SAME value — transaction #2 then either overwrites #1 in the mempool +// (same nonce, different hash) or gets rejected outright, depending on +// timing. This is the exact failure mode the "Sign and send a transaction" +// recipe's Pitfall 1 describes for the sdk-dapp + connected-wallet flow; +// the fix is structurally identical here even though the concrete API +// differs (sdk-dapp's AccountType.nonce is read-only with no +// getNonceThenIncrement() equivalent — you'd track a local counter +// yourself; here, Account already has one built in). +// +// Fetch once, per PROCESS RUN (or per logical batch), before the loop — +// not once per transaction inside it. + +import { Address } from '@multiversx/sdk-core'; +import type { Account, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export interface BatchSendResult { + nonce: bigint; + txHash: string; +} + +/** + * Sends one EGLD transfer per entry in `amounts`, all to the same + * `receiver`, using a single local nonce counter incremented once per + * transaction. Does NOT wait for any transaction to confirm before + * building and sending the next — that's the whole point: this is exactly + * the "send a burst, don't wait" scenario where re-fetching the nonce + * per-send would break (see the file header). + * + * `sender.nonce` must already reflect the network's current nonce before + * calling this — see src/account.ts's loadDevnetAccount, called once by the + * caller, not once per entry in `amounts`. + */ +export async function sendBatch( + entrypoint: DevnetEntrypoint, + sender: Account, + receiver: string, + amounts: bigint[], +): Promise { + const controller = entrypoint.createTransfersController(); + const receiverAddress = Address.newFromBech32(receiver); + const results: BatchSendResult[] = []; + + for (const amount of amounts) { + // ONE local increment per transaction. No network call in this loop — + // that's the fix. Capture the nonce used for this transaction before + // the controller call increments it further, so the log below reflects + // reality even though getNonceThenIncrement() already advanced + // sender.nonce internally. + const nonceForThisTx = sender.nonce; + + const transaction = await controller.createTransactionForNativeTokenTransfer( + sender, + sender.getNonceThenIncrement(), + { + receiver: receiverAddress, + nativeAmount: amount, + }, + ); + + const txHash = await entrypoint.sendTransaction(transaction); + results.push({ nonce: nonceForThisTx, txHash }); + } + + return results; +} + +/** + * Recovery for when a batch partially fails and leaves a nonce gap (one + * transaction rejected or expired mid-batch stalls every transaction + * queued behind it, up to the mempool's own timeout). Re-fetches the real + * nonce from the network and overwrites the local one — resyncing rather + * than continuing to trust a local counter that may now be wrong. + * + * There is no way to detect a gap from the client side alone before + * attempting to send; this is a recovery step to call after a send in the + * batch fails, not a pre-check. + */ +export async function resyncNonce(entrypoint: DevnetEntrypoint, sender: Account): Promise { + sender.nonce = await entrypoint.recallAccountNonce(sender.address); +} +``` + +## Run it + +Expected output, three distinct, sequential nonces from a **single** network +fetch at the start: + +```text +Sender: erd1... (starting nonce 42) +Sending 3 transfers of 0.001 EGLD each to erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th… + +nonce 42 -> +nonce 43 -> +nonce 44 -> + +Ending local nonce: 45 +``` + +## How it works + +**One fetch, then N local increments, never N fetches.** `sender.nonce` is set +once from `entrypoint.recallAccountNonce(address)`. Inside `sendBatch()`'s loop, +each iteration calls `sender.getNonceThenIncrement()`, a purely local, +synchronous read-then-increment on the `Account` object, with **no** network +call. Verified directly against real devnet: sending 3 transfers in a row from an +intentionally unfunded wallet produced transactions with `nonce: 0`, `nonce: 1`, +and `nonce: 2` respectively, each rejected for "insufficient funds", never for a +stale or duplicate nonce, confirming no re-fetch happened between iterations. + +**Why re-fetching per-send is actively wrong, not just slower.** The network only +advances an account's nonce once a transaction is *processed*, not merely +broadcast. Calling `recallAccountNonce()` again for transaction #2 before +transaction #1 has been processed returns the **same** value both times; +transaction #2 then collides with #1 or gets rejected, depending on timing. + +**A nonce gap stalls everything queued behind it.** If one transaction in a batch +is rejected or expires, every transaction already built with a *higher* local +nonce is now invalid until that gap is filled or the queued transactions time +out. `resyncNonce()` recovers by re-fetching the real value and overwriting the +local counter — a recovery step to call after a failure, not a defensive pre-check +before every send. + +## Pitfalls + +:::note[Pitfall 1: this recipe does not wait for each transaction to confirm before sending the next] +That's the point, waiting between every send would sidestep the whole problem, at +the cost of one network round-trip per transaction. If your workload can tolerate +that latency, waiting is simpler and this recipe doesn't apply to you. +::: + +:::warning[Pitfall 2: a failure partway through the loop is not automatically retried or detected] +`sendBatch()` throws on the first rejected transaction, aborting the remaining +iterations, verified directly: an intentionally unfunded wallet's first send fails +and the loop never reaches its second or third iteration. A production version +needs its own decision about whether to abort, skip, or resync-and-retry. +::: + +:::warning[Pitfall 3: resyncNonce() is a recovery step, not a pre-check] +There is no way to detect a nonce gap from the client side before attempting to +send. Call `resyncNonce()` after a failure, not defensively before every send, +doing the latter reintroduces the "why re-fetching per-send is wrong" problem +above. +::: + +:::danger[Pitfall 4: concurrent processes sharing one account will race regardless of this pattern] +`getNonceThenIncrement()` only protects against the caller re-fetching nonces it +already knows. If two separate process instances both load the same PEM and both +call `recallAccountNonce()` around the same time, they can still both observe the +same starting value. Use one process (or an external lock) per account for +nonce-sensitive sends. +::: + +## See also + +- [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld) + is the single-send case this recipe extends to a batch. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the same nonce problem, in the sdk-dapp + connected-wallet flow. +- [Send an ESDT](/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt) + is the same Controller pattern for a token transfer. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/accounts/pem-save-load.mdx b/docs/sdk-and-tools/sdk-js/cookbook/accounts/pem-save-load.mdx new file mode 100644 index 000000000..6a308a366 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/accounts/pem-save-load.mdx @@ -0,0 +1,173 @@ +--- +title: Save and load a PEM (dev only) +description: Save a wallet to a PEM file and load it back via Account and UserPem with sdk-core. PEM is unencrypted and dev-only; fully offline, no network. +difficulty: beginner +est_minutes: 5 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - typescript + - wallet +--- + +A PEM file is the quick dev-wallet format: write a key, load it back, done. But +it stores the secret key **unencrypted**, anyone who reads the file has the key. +This recipe covers the full round-trip via both the `Account` API and the +lower-level `UserPem` class, fully offline. + +:::danger[PEM is for development and testing only] +A PEM stores the secret key in plaintext. Never put a mainnet or funded key in a +PEM. For anything real, use an +[encrypted keystore](/sdk-and-tools/sdk-js/cookbook/accounts/keystore-save-load). +::: + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates its own throwaway key and writes to a temp + directory it cleans up. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/pem-save-load +npm install +npm run build +npm start +``` + +## The code + +```ts title="src/index.ts" +// src/index.ts — save a wallet to a PEM file and load it back, via both the +// Account API (saveToPem / newFromPem) and the lower-level UserPem class. +// +// PEM IS FOR DEVELOPMENT AND TESTING ONLY. A PEM file stores the secret key +// UNENCRYPTED — anyone who reads the file has the key. Never put a +// mainnet/funded key in a PEM. For anything real, use an encrypted keystore +// (see the "Save and load a keystore" recipe). +// +// Fully offline. The only I/O is to a throwaway temp directory (cleaned up +// at the end); no devnet, no network. Fresh keys each run. +// +// Grounded in the installed @multiversx/sdk-core v15.4.1: Account.saveToPem / +// Account.newFromPem (accounts/account.d.ts) and UserPem (wallet/userPem.d.ts). + +import { Account, Mnemonic, UserPem } from '@multiversx/sdk-core'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +async function main(): Promise { + const dir = mkdtempSync(join(tmpdir(), 'mvx-pem-')); + try { + // === 1. Write a PEM from an Account. === + const account = Account.newFromMnemonic(Mnemonic.generate().toString(), 0); + const expectedAddress = account.address.toBech32(); + const pemPath = join(dir, 'wallet.pem'); + account.saveToPem(pemPath); + + // A PEM is plain text: a base64 body between BEGIN/END markers, labeled + // with the bech32 address. The secret key sits in that body, unencrypted. + const firstLine = readFileSync(pemPath, 'utf8').split('\n')[0] ?? ''; + console.log(`1. Wrote ${pemPath.split('/').pop() ?? ''}. First line: ${firstLine}`); + + // === 2. Load it back with Account.newFromPem — this one is ASYNC. === + // Unlike newFromMnemonic / newFromKeystore / newFromKeypair (all sync), + // newFromPem returns a Promise and must be awaited. + const loaded = await Account.newFromPem(pemPath); + console.log(`2. Account.newFromPem address matches: ${loaded.address.toBech32() === expectedAddress}`); + + // === 3. UserPem is the lower-level view of the same file. === + // It exposes the label, the secret key, and the public key directly — + // useful when you want the key material rather than a full Account. + const pem = UserPem.fromFile(pemPath); + console.log(`3. UserPem.fromFile label is the address: ${pem.label === expectedAddress}`); + console.log(` UserPem secret key matches the Account's: ${pem.secretKey.hex() === account.secretKey.hex()}`); + + // === 4. Build a PEM from raw key material, no Account needed. === + // new UserPem(label, secretKey).save(path) is the write side of UserPem. + const rebuilt = new UserPem(expectedAddress, account.secretKey); + const rebuiltPath = join(dir, 'rebuilt.pem'); + rebuilt.save(rebuiltPath); + const rebuiltAccount = await Account.newFromPem(rebuiltPath); + console.log(`4. Rebuilt PEM round-trips to the same address: ${rebuiltAccount.address.toBech32() === expectedAddress}`); + + // A PEM file can hold several keys; UserPem.fromFile(path, index) selects + // one, and UserPem.fromFileAll(path) returns them all. Account.newFromPem + // takes the same optional index. + console.log('\nExpected: three "true" confirmations (2, 3, 4) and the matching key line.'); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +Keys are generated fresh each run; the confirmations are stable: + +```text +1. Wrote wallet.pem. First line: -----BEGIN PRIVATE KEY for erd1lss6vxy7pamd6nflt9zuusxdmqutd0mwzu7nrmxvwax3em0kzgjqv63ssn----- +2. Account.newFromPem address matches: true +3. UserPem.fromFile label is the address: true + UserPem secret key matches the Account's: true +4. Rebuilt PEM round-trips to the same address: true +``` + +## How it works + +Grounded in the installed `@multiversx/sdk-core` v15.4.1 `Account.saveToPem` / +`Account.newFromPem` (`accounts/account.d.ts`) and `UserPem` +(`wallet/userPem.d.ts`): + +1. **Write from an Account.** `account.saveToPem(path)`. The file is plain text, + a base64 body between `BEGIN`/`END` markers, labeled with the bech32 address. + The secret key sits in that body, unencrypted. +2. **Load with `Account.newFromPem`, this one is async.** It returns a `Promise` + and must be awaited, unlike the synchronous `newFromMnemonic` / + `newFromKeystore` / `newFromKeypair`. +3. **`UserPem` is the lower-level view.** `UserPem.fromFile(path)` exposes + `label`, `secretKey`, and `publicKey` directly. +4. **Build a PEM from raw material.** `new UserPem(label, secretKey).save(path)` + is the write side of `UserPem`, no `Account` required. + +A PEM can hold several keys; `UserPem.fromFile(path, index)` selects one and +`UserPem.fromFileAll(path)` returns them all. `Account.newFromPem` takes the same +optional index. + +## Pitfalls + +:::danger[Pitfall 1: PEM is unencrypted; treat the file as the key itself] +There is no password. Reading the file is reading the private key. Keep PEMs out +of version control, shared drives, and anything mainnet. This is the whole reason +the keystore format exists. +::: + +:::warning[Pitfall 2: newFromPem is async; saveToPem and the other constructors are not] +Forgetting to `await newFromPem` leaves you holding a `Promise`, whose +`.address` is `undefined`. This is the single most common PEM mistake. +::: + +:::note[Pitfall 3: the label is cosmetic] +The bech32 address in the `BEGIN ... for erd1...` header is a human-readable +hint, not verified against the key on load. The account's real address always +comes from the key, derive it rather than trusting the label. +::: + +## See also + +- [Save and load a keystore](/sdk-and-tools/sdk-js/cookbook/accounts/keystore-save-load) + is the encrypted, production-safe alternative to PEM. +- [Create an Account from a KeyPair, secret key, or mnemonic](/sdk-and-tools/sdk-js/cookbook/accounts/account-from-keys) + covers the in-memory constructors, including the async `newFromPem` note. +- [Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) + is the next step once you have a loaded dev account. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction.mdx b/docs/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction.mdx new file mode 100644 index 000000000..54098c911 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction.mdx @@ -0,0 +1,337 @@ +--- +title: Build a relayed v3 transaction +description: A sender who has no EGLD for gas, and a relayer who pays it, both signing the same transaction, devnet-verified, with V1/V2 deprecation flagged. +difficulty: advanced +est_minutes: 10 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - transaction + - relayed + - typescript + - devnet +--- + +A relayed transaction lets a **relayer** pay the gas fee for a transaction a +**sender** builds and authorizes, the sender never needs any EGLD at all. V3 is +the current, supported iteration; V1 and V2 are being deactivated and this recipe +does not show them. + +Use this for any flow where you want users to interact on-chain without holding +EGLD for gas: a dApp's backend sponsoring its users' first transactions, an +onboarding flow, or a service that pays fees on behalf of its callers. If both +parties are the same entity, you don't need relaying, just +[send normally](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld). + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates two fresh, unfunded accounts and only needs + devnet **read** access before proving the shape via a clean rejection. No devnet + EGLD required. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/relayed-v3-transaction +npm install +npm run build +npm start +``` + +## The code + +First, find a sender and relayer that share a shard, entirely offline: + +```ts title="src/keys.ts" +// src/keys.ts — generate a sender and a relayer that share a shard. +// +// The mx-sdk-js-core cookbook (cookbook/relayed.ts) states the hard +// constraint: "the sender and the relayer must be in the same network +// shard." AddressComputer computes an address's shard purely locally (no +// network call — `computer.getShardOfAddress(addr)`), so this search runs +// entirely offline before this recipe ever touches devnet. +// +// With 3 non-meta shards, trying a handful of derivation indices from two +// independent mnemonics is guaranteed to find a same-shard pair quickly +// (pigeonhole: among any 4 addresses, at least two share one of 3 shards) — +// this recipe searches up to 8 indices per side, far more than needed in +// practice. + +import { Account, AddressComputer, Mnemonic } from '@multiversx/sdk-core'; + +const MAX_INDICES_TO_TRY = 8; + +/** + * Derives up to `MAX_INDICES_TO_TRY` accounts from a mnemonic, paired + * with their shard number. + */ +async function candidatesFromMnemonic( + mnemonic: Mnemonic, + addressComputer: AddressComputer, +): Promise> { + const candidates: Array<{ account: Account; shard: number }> = []; + for (let index = 0; index < MAX_INDICES_TO_TRY; index += 1) { + const account = Account.newFromMnemonic(mnemonic.toString(), index); + const shard = addressComputer.getShardOfAddress(account.address); + candidates.push({ account, shard }); + } + return candidates; +} + +/** + * Generates two fresh, independent, intentionally-unfunded accounts — a + * sender and a relayer — guaranteed to be in the same shard. Two separate + * mnemonics are used (not two indices of one mnemonic) because a relayer + * is realistically a separate party (a dApp's backend, a sponsor + * service), not another account of the sender's own wallet. + */ +export async function generateSameShardPair(): Promise<{ + sender: Account; + relayer: Account; + shard: number; +}> { + const addressComputer = new AddressComputer(); + + const senderCandidates = await candidatesFromMnemonic( + Mnemonic.generate(), + addressComputer, + ); + const relayerCandidates = await candidatesFromMnemonic( + Mnemonic.generate(), + addressComputer, + ); + + for (const senderCandidate of senderCandidates) { + const match = relayerCandidates.find( + (relayerCandidate) => relayerCandidate.shard === senderCandidate.shard, + ); + if (match) { + return { + sender: senderCandidate.account, + relayer: match.account, + shard: senderCandidate.shard, + }; + } + } + + // Statistically shouldn't happen with 8x8=64 pairs across 3 shards, but + // fail loudly rather than silently returning a mismatched pair. + throw new Error( + `No same-shard pair found in ${MAX_INDICES_TO_TRY} tries per side — re-run.`, + ); +} +``` + +Then build, sign (both parties), and broadcast: + +```ts title="src/index.ts" +// src/index.ts — build, sign, and broadcast a relayed v3 transaction. +// +// V1 and V2 relayed transactions existed in earlier protocol versions and +// are being deactivated — the mx-sdk-js-core cookbook (cookbook/relayed.ts) +// says so explicitly: "We are currently on the third iteration (V3) of +// relayed transactions. V1 and V2 will be deactivated soon, so we'll focus +// on V3." This recipe only shows V3 — there is no reason to write new code +// against V1/V2 at this point, and the current cookbook does not document +// their shapes in enough detail to reproduce responsibly. +// +// The four rules for V3, restated from both sources: +// 1. Sender and relayer can sign in any order. +// 2. The `relayer` field must be set on the transaction BEFORE either +// of them signs. +// 3. Relayed transactions need an extra 50,000 gas on top of the usual +// minimum + per-byte cost. +// 4. Sender and relayer must be in the same network shard. +// +// This recipe uses two freshly generated, intentionally UNFUNDED +// accounts (see src/keys.ts) for both roles — like every devnet-touching +// recipe in this Cookbook that doesn't need a funded wallet to prove its +// point, a clean "insufficient funds" rejection from the real network is +// the strongest available evidence that the whole shape (two signatures, +// the relayer field, the gas math) was well-formed enough to reach +// evaluation, without needing real devnet EGLD. + +import { Address, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; +import { generateSameShardPair } from './keys'; + +// Default gas limits: minimum gas limit 50,000, gas per data byte 1,500. +// Relayed transactions need an extra 50,000 gas. This transaction's data is +// the 5-byte string "hello" (the same payload mx-sdk-js-core's own +// relayed.ts example uses), so: +const DATA = 'hello'; +const MIN_GAS_LIMIT = 50_000n; +const GAS_PER_DATA_BYTE = 1_500n; +const RELAYED_V3_EXTRA_GAS = 50_000n; +const GAS_LIMIT = + MIN_GAS_LIMIT + GAS_PER_DATA_BYTE * BigInt(DATA.length) + RELAYED_V3_EXTRA_GAS; + +// A fixed, well-known receiver — the same address this Cookbook's +// manage-nonces recipe uses as a generic example destination. It doesn't +// need to be in the same shard as the sender/relayer — only sender and +// relayer share that constraint. +const RECEIVER = 'erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th'; + +async function main(): Promise { + const { sender, relayer, shard } = await generateSameShardPair(); + console.log(`Sender: ${sender.address.toBech32()} (shard ${shard})`); + console.log(`Relayer: ${relayer.address.toBech32()} (shard ${shard})`); + console.log(`Receiver: ${RECEIVER}`); + + const entrypoint = new DevnetEntrypoint(); + + // Fetch the sender's real nonce — the one genuine network call before + // signing. The relayer does not need its own nonce fetched: only the + // transaction's own sender/nonce pair matters for ordering; the + // relayer is only ever a co-signer, never advances its own nonce via + // this transaction. + sender.nonce = await entrypoint.recallAccountNonce(sender.address); + console.log(`Sender nonce from network: ${sender.nonce}`); + + const transaction = new Transaction({ + chainID: 'D', + sender: sender.address, + receiver: new Address(RECEIVER), + gasLimit: GAS_LIMIT, + data: new Uint8Array(Buffer.from(DATA)), + nonce: sender.getNonceThenIncrement(), + }); + + // Rule 2: set `relayer` BEFORE either party signs. + transaction.relayer = relayer.address; + + // Rule 1: order between these two doesn't matter; sender-then-relayer + // here purely for readability. + transaction.signature = await sender.signTransaction(transaction); + transaction.relayerSignature = await relayer.signTransaction(transaction); + + console.log(`\nGas limit: ${GAS_LIMIT} (${MIN_GAS_LIMIT} min + ${GAS_PER_DATA_BYTE}×${DATA.length} data bytes + ${RELAYED_V3_EXTRA_GAS} relayed v3 extra)`); + console.log(`Sender signature (hex): ${Buffer.from(transaction.signature).toString('hex').slice(0, 24)}…`); + console.log(`Relayer signature (hex): ${Buffer.from(transaction.relayerSignature).toString('hex').slice(0, 24)}…`); + + try { + const txHash = await entrypoint.sendTransaction(transaction); + console.log(`\nBroadcast succeeded unexpectedly. Hash: ${txHash}`); + console.log(`Explorer: https://devnet-explorer.multiversx.com/transactions/${txHash}`); + } catch (err) { + console.log('\nBroadcast rejected, as expected for two unfunded accounts:'); + console.log(err instanceof Error ? err.message : err); + } +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +A real, captured run (addresses, shard, and signatures differ every run): + +```text +Sender: erd1k3v5vllugtnexmtpwqk57352gw6qcrzf0swnya8q4ssmmftf5xmshasf5f (shard 1) +Relayer: erd1s6l6jx6lfa67lnl925nwe8zn4ksqtywmlax5e8gj388nq3tqum5sswla9a (shard 1) +Receiver: erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th + +Sender nonce from network: 0 + +Gas limit: 107500 (50000 min + 1500×5 data bytes + 50000 relayed v3 extra) +Sender signature (hex): 2afc2adc826cfcaae78687b6… +Relayer signature (hex): 9e14bcda390b6cbf8ebbb130… + +Broadcast rejected, as expected for two unfunded accounts: +Request error on url [transactions]: [transaction generation failed: insufficient funds for address erd1s6l6jx6lfa67lnl925nwe8zn4ksqtywmlax5e8gj388nq3tqum5sswla9a] +``` + +:::tip[The rejection names the relayer, not the sender] +That's the network confirming the entire point of a relayed transaction: the +*relayer* is who needs funds for gas. If the gas math, signature order, or +`relayer` field were wrong, the network would reject with a generic +malformed-transaction or bad-signature error instead of this specific, +address-targeted message. +::: + +## How it works + +`src/keys.ts` generates two fresh, **independent** mnemonics (a sender and a +relayer are realistically separate parties, not two accounts of the same wallet) +and derives a handful of indices from each until it finds a pair sharing a shard. +Shard computation is a pure local calculation, so this whole search runs offline +before the recipe ever touches devnet. + +`src/index.ts` then, following the mx-sdk-js-core cookbook (`cookbook/relayed.ts`): + +1. Fetches only the **sender's** nonce from devnet. +2. Builds the transaction with `gasLimit` computed precisely: `50,000` minimum + + `1,500` per data byte + a flat `50,000` extra for relayed v3, `107,500` total, + matching the logged value exactly. +3. Sets `transaction.relayer` **before** either party signs, required ordering. +4. Signs with the sender (`transaction.signature`), then the relayer + (`transaction.relayerSignature`). The two can sign in either order. +5. Broadcasts via `entrypoint.sendTransaction(transaction)`. + +## V1/V2 deprecation + +`cookbook/relayed.ts` states the current guidance directly: "We are currently on +the third iteration (V3) of relayed transactions. V1 and V2 will be deactivated +soon, so we'll focus on V3." This recipe only shows V3, there is no reason to +write new code against V1/V2 at this point, and the current SDK cookbook does not +document their shapes in enough detail to reproduce them responsibly. + +## Creating relayed transactions via Controllers and Factories + +The manual `new Transaction({...})` shape above is the most explicit, but every +Controller in sdk-core also accepts a `relayer` argument directly: + +```ts +const transaction = await controller.createTransactionForIssuingFungible( + alice, alice.getNonceThenIncrement(), + { tokenName: "NEWFNG", tokenTicker: "FNG", /* ... */, relayer: frank.address }, +); +transaction.relayerSignature = await frank.signTransaction(transaction); +``` + +Factories, by contrast, have **no** `relayer` parameter at creation time, set +`transaction.relayer` after the factory builds the transaction, then sign both +parties. + +## Pitfalls + +:::danger[Pitfall 1: sender and relayer must share a shard] +Or the transaction is rejected regardless of funds. This recipe searches for a +matching pair entirely offline before ever calling devnet. +::: + +:::warning[Pitfall 2: set relayer before either signature, not after] +Signing before the relayer field is set produces a signature over the wrong +bytes. +::: + +:::note[Pitfall 3: the relayer pays gas, not the sender] +Confirmed by this recipe's own devnet rejection naming the relayer's address +specifically. Don't assume "insufficient funds" always points at the sender in a +relayed flow. +::: + +:::warning[Pitfall 4: relayed transactions cost an extra flat 50,000 gas] +On top of the usual minimum + per-data-byte cost, easy to under-budget if you +compute gas the same way you would for a non-relayed send. +::: + +:::note[Pitfall 5: Controllers accept relayer at construction time; Factories do not] +Set it on the built transaction afterward for the Factory path. +::: + +## See also + +- [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld) + is the non-relayed case, where sender and payer are the same account. +- [Manage nonces (fetch-then-increment)](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) + is the same nonce-fetch pattern this recipe uses for the sender only. +- [Sign a message + verify a signature](/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-message) + is another accounts-and-signing recipe, fully offline instead of + devnet-verified. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/accounts/set-guardian.mdx b/docs/sdk-and-tools/sdk-js/cookbook/accounts/set-guardian.mdx new file mode 100644 index 000000000..1cc4f89ce --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/accounts/set-guardian.mdx @@ -0,0 +1,208 @@ +--- +title: Set a guardian on an account +description: Nominate a guardian for an account with SetGuardian via sdk-core's AccountController and AccountTransactionsFactory, payload verified on devnet. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - guardian + - transaction + - gas + - devnet + - typescript +--- + +A guardian is a second key that must co-sign an account's transactions once the +account is *guarded*. Turning that protection on is a two-step sequence: + +1. **`SetGuardian`** nominates the guardian (this recipe). +2. **`GuardAccount`** activates guardianship (see + [Guard and unguard an account](/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account)). + +This recipe builds the `SetGuardian` transaction with sdk-core's +`AccountController` (and the matching `AccountTransactionsFactory`), decodes the +payload, and broadcasts it. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites. **No devnet EGLD required**, see "How it works". + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/set-guardian +npm install +npm run build +npm start -- ./wallet.pem erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th MyGuardianService +``` + +## The code + +```ts title="src/guardian.ts" +// src/guardian.ts — the subject of this recipe: nominating a guardian for an +// account with the `SetGuardian` builtin, via sdk-core's AccountController +// and AccountTransactionsFactory. +// +// A guardian is a second key that must co-sign your transactions once the +// account is guarded. Setting one is a two-transaction dance: +// 1. SetGuardian — nominate the guardian (this recipe). The nomination +// only becomes active after a cooldown (unless the account is already +// guarded, in which case the current guardian co-signs and it is +// immediate). +// 2. GuardAccount — activate guardianship (see guard-unguard-account). +// +// SetGuardian is a builtin function executed in the caller's own account +// context, so the transaction's receiver is the sender itself. +// +// SetGuardianInput = { guardianAddress: Address; serviceID: string }. The +// serviceID identifies the guardian SERVICE (e.g. a Trusted Co-Signer +// Service). It is written to the wire verbatim as hex; the exact value +// depends on the service you use, so this recipe takes it as a parameter. + +import { Address } from '@multiversx/sdk-core'; +import type { Account, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +export interface SetGuardianInput { + guardianAddress: Address; + serviceID: string; +} + +/** + * Build a SetGuardian transaction. + * + * Controller path: `createTransactionForSettingGuardian` sets the nonce and + * signs (it takes the whole Account). Factory path: it only builds; the + * caller owns nonce + signature. Same method name on both. + */ +export async function setGuardian( + entrypoint: DevnetEntrypoint, + sender: Account, + input: SetGuardianInput, + useFactory: boolean, +): Promise { + if (useFactory) { + const factory = entrypoint.createAccountTransactionsFactory(); + const transaction = await factory.createTransactionForSettingGuardian(sender.address, { + guardianAddress: input.guardianAddress, + serviceID: input.serviceID, + }); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; + } + + const controller = entrypoint.createAccountController(); + return controller.createTransactionForSettingGuardian(sender, sender.getNonceThenIncrement(), { + guardianAddress: input.guardianAddress, + serviceID: input.serviceID, + }); +} + +/** + * Decode a SetGuardian transaction's `data` field: + * `SetGuardian@@`. The guardian argument is + * a raw 32-byte public key (hex), NOT bech32. + */ +export function describeSetGuardianPayload(transaction: Transaction): { + function: string; + receiver: string; + guardian: string; + serviceID: string; + gasLimit: string; +} { + const parts = Buffer.from(transaction.data).toString().split('@'); + const guardianHex = parts[1] ?? ''; + const serviceHex = parts[2] ?? ''; + return { + function: parts[0] ?? '', + receiver: transaction.receiver.toBech32(), + guardian: guardianHex ? Address.newFromHex(guardianHex).toBech32() : '(none)', + serviceID: Buffer.from(serviceHex, 'hex').toString(), + gasLimit: transaction.gasLimit.toString(), + }; +} +``` + +## Run it + +```bash +npm start -- [serviceID] [--factory] +``` + +A real captured run (unfunded wallet): + +```text +SetGuardian payload: + function: SetGuardian + receiver: erd18l2cww3g3g6uc35wy60efd87ss85xdqj9gh9qn6ga6halz3jscaqd4dl3k + guardian: erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th + serviceID: MyGuardianService + gasLimit: 466500 + +Broadcasting... +Rejected (expected for an unfunded wallet): ... insufficient funds for address erd18l2c... +``` + +## How it works + +`SetGuardian` is a builtin function on the caller's own account, so the +transaction's **receiver is the sender itself**. Its data is +`SetGuardian@@`. The guardian argument is a +raw 32-byte public key in hex, *not* bech32, this recipe's decoder converts it +back to a readable address. The `serviceID` identifies the guardian **service** +(for example a Trusted Co-Signer Service) and is written verbatim as hex; its +exact value depends on the service you use, so the recipe takes it as a +parameter. + +Gas was `466500`, matching `50,000 + 1,500 × 111 (data bytes) + 250,000 +(gasLimitSetGuardian)` to the unit. The unfunded broadcast is rejected with a +clean `insufficient funds` keyed to the sender, the payload is well-formed. + +:::note[Activation is delayed by design] +A newly set guardian is not usable immediately: on an unguarded account the +nomination becomes active only after a protocol cooldown. If the account is +*already* guarded, the current guardian co-signs the `SetGuardian` and the change +is immediate. Either way, `SetGuardian` only nominates, +[`GuardAccount`](/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account) +is what activates protection. +::: + +For built-in function detail, see +[docs.multiversx.com/developers/built-in-functions](https://docs.multiversx.com/developers/built-in-functions/). + +## Pitfalls + +:::warning[Pitfall 1: the guardian argument is a public key, not bech32] +On the wire `SetGuardian` carries the guardian's raw 32-byte public key in hex. +The SDK converts your `Address` for you, but if you hand-decode the payload, read +it back with `Address.newFromHex`, not `newFromBech32`. +::: + +:::note[Pitfall 2: serviceID is service-specific, not a fixed constant] +This recipe uses an illustrative `serviceID`. The real value is defined by the +guardian service you register with; passing the wrong one nominates a guardian +the service cannot co-sign for. +::: + +:::note[Pitfall 3: setting a guardian does not guard the account] +`SetGuardian` only nominates. Until you send +[`GuardAccount`](/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account), +transactions still sign with the primary key alone. +::: + +## See also + +- [Guard and unguard an account](/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account) + activates the guardian you nominated here, and removes it. +- [Apply a guardian to a transaction](/sdk-and-tools/sdk-js/cookbook/accounts/apply-guardian-to-transaction) + co-signs an individual transaction once guardianship is active. +- [Build a relayed v3 transaction](/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction) + is the other multi-signature transaction shape (a relayer paying gas, rather + than a guardian co-signing). diff --git a/docs/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-message.mdx b/docs/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-message.mdx new file mode 100644 index 000000000..572f87adb --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-message.mdx @@ -0,0 +1,265 @@ +--- +title: Sign a message + verify a signature +description: Sign a message with an account or secret key and verify it with a UserVerifier, fully offline, including proof the check fails on a tampered signature. +difficulty: beginner +est_minutes: 7 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - typescript +--- + +Message signing and verification, fully offline. Unlike every other +transaction-shaped recipe in this Cookbook, there is no network call anywhere in +this one: signing and verifying a message is pure local Ed25519 cryptography over +a deterministic byte encoding. No devnet wallet, no funds, no gas. + +Signing a message proves "this address controls this key and endorsed this exact +text", it does not touch the blockchain, cost gas, or change any state. Common +uses: proving wallet ownership to a backend (see +[Native auth](/sdk-and-tools/sdk-js/cookbook/wallets/native-auth) for the +token-based version of this idea), authorizing an off-chain action, or signing +structured data for a service to verify later. If you need to move funds or call +a contract, see +[Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) +instead. + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates its own throwaway keypair and never touches + the network. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/sign-verify-message +npm install +npm run build +npm start +``` + +## The code + +```ts title="src/index.ts" +// src/index.ts — sign a message two ways (Account, then raw SecretKey), +// verify it two ways (UserVerifier, then Account.verifyMessageSignature), +// then prove verification actually checks something by tampering with +// both the message and the signature and showing verify() return false. +// +// Fully offline. No devnet, no network call of any kind — signing and +// verifying a message is pure local cryptography (Ed25519 over a +// deterministic byte encoding), unlike every transaction-sending recipe +// in this Cookbook. A fresh keypair is generated on every run via +// Mnemonic.generate(), so exact addresses/signatures differ run to run; +// the shape of the output (which booleans print true vs false) does not. +// +// Modeled on the mx-sdk-js-core cookbook source (cookbook/signingObjects.ts +// and cookbook/verifySignatures.ts), which shows both the Account-level and +// the raw-SecretKey-level APIs side by side. + +import { + Account, + Message, + MessageComputer, + Mnemonic, + UserVerifier, +} from '@multiversx/sdk-core'; + +async function main(): Promise { + // --- Setup: a fresh, offline keypair. No PEM file, no network. ----- + const mnemonic = Mnemonic.generate(); + const account = Account.newFromMnemonic(mnemonic.toString(), 0); + console.log(`Address: ${account.address.toBech32()}`); + + const plaintext = 'Hello MultiversX'; + const messageComputer = new MessageComputer(); + + // === 1. Sign using an Account (the common case — cookbook/signingObjects.ts's + // first message example). === + const message = new Message({ + data: new Uint8Array(Buffer.from(plaintext)), + address: account.address, + }); + message.signature = await account.signMessage(message); + console.log( + `\nSigned with Account. Signature (hex): ${Buffer.from(message.signature).toString('hex').slice(0, 24)}…`, + ); + + // === 2. Verify using a UserVerifier built from the address — + // cookbook/verifySignatures.ts's "Verifying Message signature + // using a UserVerifier" section. === + const verifier = UserVerifier.fromAddress(account.address); + const bytesToVerify = messageComputer.computeBytesForVerifying(message); + const isValid = await verifier.verify(bytesToVerify, message.signature); + console.log(`UserVerifier.verify() on the untampered message: ${isValid}`); + + // === 3. The same check via Account's own convenience method — no + // separate UserVerifier needed if you already have an Account + // for the signer (cookbook/verifySignatures.ts's "Sending + // messages over boundaries" section uses this exact method + // after an unpack). === + const isValidViaAccount = await account.verifyMessageSignature( + message, + message.signature, + ); + console.log(`account.verifyMessageSignature() on the same message: ${isValidViaAccount}`); + + // === 4. Prove verification actually checks something: tamper with the + // message text, keep the original signature, and verify again. + // A signature scheme that returned true here would be useless — + // this is the check a recipe that only ever shows the "true" + // path can't prove. === + const tamperedMessage = new Message({ + data: new Uint8Array(Buffer.from(`${plaintext} — but edited`)), + address: account.address, + }); + const tamperedBytes = messageComputer.computeBytesForVerifying(tamperedMessage); + const isTamperedValid = await verifier.verify(tamperedBytes, message.signature); + console.log(`\nVerify a tampered message against the original signature: ${isTamperedValid}`); + + // === 5. Same idea, the other direction: original message, corrupted + // signature byte. === + const corruptedSignature = new Uint8Array(message.signature); + const firstByte = corruptedSignature[0] ?? 0; + corruptedSignature[0] = firstByte ^ 0xff; + const isCorruptedValid = await verifier.verify(bytesToVerify, corruptedSignature); + console.log(`Verify the original message against a corrupted signature: ${isCorruptedValid}`); + + // === 6. Signing directly with a SecretKey derived from the mnemonic, + // no Account wrapper — cookbook/signingObjects.ts's "Signing a + // Message using an SecretKey" section (that source uses + // UserSecretKey.fromString(hex); this recipe derives from the + // same fresh mnemonic at a second index instead, to stay + // offline without a hardcoded key). Useful when you're holding + // raw key material rather than a PEM/keystore-backed Account. === + const secretKey = mnemonic.deriveKey(1); + const publicKey = secretKey.generatePublicKey(); + const rawMessage = new Message({ + data: new Uint8Array(Buffer.from(plaintext)), + address: publicKey.toAddress(), + }); + const serialized = messageComputer.computeBytesForSigning(rawMessage); + rawMessage.signature = await secretKey.sign(serialized); + const rawVerifier = new UserVerifier(publicKey); + const rawIsValid = await rawVerifier.verify( + messageComputer.computeBytesForVerifying(rawMessage), + rawMessage.signature, + ); + console.log(`\nSigned + verified via raw SecretKey/UserVerifier (no Account): ${rawIsValid}`); + + // === 7. Packing/unpacking for sending across a boundary (a service + // call, a file, a QR code) — cookbook/verifySignatures.ts's + // "Sending messages over boundaries" section. === + const packed = messageComputer.packMessage(message); + const unpacked = messageComputer.unpackMessage(packed); + // Message.signature is typed Uint8Array | undefined (a Message can + // exist unsigned, before signing) — a real tsc --strict error if you + // pass it straight through without narrowing first. + if (!unpacked.signature) { + throw new Error('Unpacked message has no signature.'); + } + const isUnpackedValid = await account.verifyMessageSignature( + unpacked, + unpacked.signature, + ); + console.log(`\nPack -> unpack -> verify round-trip: ${isUnpackedValid}`); + + console.log( + '\nExpected: five "true" lines above (2, 3, 6, 7) and two "false" lines (4, 5).', + ); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +Addresses and signatures are freshly generated every run; the pattern of +`true`/`false` lines is not: + +```text +Address: erd1xg4xglsa43epnp5v2k45anqcejzdqwug86kug52tt3mq7k7vtt2qm0spu4 + +Signed with Account. Signature (hex): e997e384490ad843899b1047… +UserVerifier.verify() on the untampered message: true +account.verifyMessageSignature() on the same message: true + +Verify a tampered message against the original signature: false +Verify the original message against a corrupted signature: false + +Signed + verified via raw SecretKey/UserVerifier (no Account): true + +Pack -> unpack -> verify round-trip: true +``` + +The two `false` lines are the important ones: they are the proof this recipe's +verification step actually checks something, rather than always returning `true`. + +## How it works + +Modeled on the mx-sdk-js-core cookbook source (`cookbook/signingObjects.ts` and +`cookbook/verifySignatures.ts`), which shows both an Account-level and a +raw-SecretKey-level path side by side: + +1. **Generate a fresh keypair.** `Mnemonic.generate()` + `Account.newFromMnemonic()`. +2. **Sign with the Account.** `account.signMessage(message)`, the address is part + of what gets signed, via `MessageComputer`. +3. **Verify with a `UserVerifier`.** Built from the address alone, the path a + *different* party (a backend, a service) uses, never needing the key. +4. **Verify with `account.verifyMessageSignature()`.** A shortcut when the + verifying code already holds the same `Account`. +5. **Tamper with the message, then the signature.** Both correctly return `false`. +6. **Sign directly with a `SecretKey`**, no `Account` wrapper, the lower-level + path for raw key material. +7. **Pack, unpack, verify.** Round-trips a message for transmission across a + boundary (a service call, a QR code, a file). + +## In a dApp (browser wallet, not verified live here) + +`mx-template-dapp`'s `SignMessage` widget shows the same call from a connected +browser wallet: `provider.signMessage(messageToSign)` opens the wallet's own +confirmation UI instead of signing silently. This recipe doesn't exercise that +path end-to-end, it needs a real wallet extension or xPortal session. The +verification side (`UserVerifier`) is identical regardless of which side signed. + +## Pitfalls + +:::warning[Pitfall 1: Message.signature is Uint8Array | undefined, not always present] +A `Message` can exist unsigned. Passing an unsigned message's `.signature` +straight into a function expecting `Uint8Array` is a real `tsc --strict` failure, +narrow with a null check first. +::: + +:::note[Pitfall 2: the address is part of the signed payload, not a side channel] +`MessageComputer` folds the message's `address` field into the bytes that get +signed/verified. A verifier built from the wrong address reports `false` even +against an untampered message + signature pair. +::: + +:::tip[Pitfall 3: a UserVerifier only needs the address, never the secret key] +That's the entire point of asymmetric signing, don't design a verification flow +that requires shipping key material to the verifying party. +::: + +:::danger[Pitfall 4: this recipe doesn't sign a transaction] +Message signing and transaction signing use related but distinct primitives +(`MessageComputer` vs `TransactionComputer`), see +[Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) +for the transaction case. +::: + +## See also + +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the transaction-signing counterpart to this recipe. +- [Native auth: token issuance, expiry, auto-logout](/sdk-and-tools/sdk-js/cookbook/wallets/native-auth) + is a productized use of message-adjacent signing. +- [Build a relayed v3 transaction](/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction) + is another accounts-and-signing recipe, devnet-verified instead of offline. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-transaction.mdx b/docs/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-transaction.mdx new file mode 100644 index 000000000..dcaa77b08 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-transaction.mdx @@ -0,0 +1,209 @@ +--- +title: Sign + verify a transaction (offline) +description: Sign a transaction offline with a raw secret key and verify it three ways with sdk-core, including tamper checks. No devnet, no broadcast. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - typescript + - transaction +--- + +Sign a `Transaction` offline with a raw secret key and verify the signature three +ways: `Account.verifyTransactionSignature`, a `UserVerifier` built from the +address, and the raw `UserPublicKey`. Then prove the check works by tampering +with the transaction and watching verification fail. This is the +transaction-signing counterpart to +[Sign a message + verify a signature](/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-message): +no devnet, no broadcast, pure local Ed25519. + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates its own throwaway keys and never touches + the network. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/sign-verify-transaction +npm install +npm run build +npm start +``` + +## The code + +```ts title="src/index.ts" +// src/index.ts — sign arbitrary data with a UserSigner, then sign a +// Transaction OFFLINE with a raw secret key and verify the signature three +// ways (UserVerifier, UserPublicKey.verify, Account.verifyTransactionSignature). +// Prove the check works by tampering with the transaction and watching +// verification fail. +// +// Fully offline. No devnet, no broadcast — signing and verifying are pure +// local Ed25519 cryptography. This is the transaction-signing counterpart to +// the message-signing recipe. Fresh keys each run; the true/false pattern of +// the output is stable. +// +// Grounded in the installed @multiversx/sdk-core v15.4.1: UserSigner / +// UserVerifier (wallet/), TransactionComputer (core/transactionComputer.d.ts). + +import { + Account, + Address, + Mnemonic, + Transaction, + TransactionComputer, + UserSigner, + UserVerifier, +} from '@multiversx/sdk-core'; + +async function main(): Promise { + const account = Account.newFromMnemonic(Mnemonic.generate().toString(), 0); + const other = Account.newFromMnemonic(Mnemonic.generate().toString(), 0); + const secretKey = account.secretKey; + const publicKey = account.publicKey; + + // === 1. Sign arbitrary bytes with a UserSigner, verify with UserVerifier. === + // UserSigner wraps a secret key; its sign() is ASYNC (returns a Promise). + // UserVerifier needs only the public key to check the signature. (Step 2 + // below uses the raw secretKey.sign, which is SYNC — the contrast matters.) + const data = new Uint8Array(Buffer.from('arbitrary bytes to authenticate')); + const signer = new UserSigner(secretKey); + const dataSignature = await signer.sign(data); + const verifier = new UserVerifier(publicKey); + console.log(`1. UserSigner-signed data verified by UserVerifier: ${await verifier.verify(data, dataSignature)}`); + + // === 2. Build a transaction and sign it offline with the raw key. === + // account.signTransaction would work too, but doing it by hand shows what + // that method does internally: serialize with computeBytesForSigning, then + // sign those bytes. No network, no nonce fetch — this is a local signature. + const transactionComputer = new TransactionComputer(); + const transaction = new Transaction({ + nonce: 42n, + value: 1000000000000000000n, // 1 EGLD + sender: account.address, + receiver: other.address, + gasLimit: 50000n, + chainID: 'D', + }); + transaction.signature = secretKey.sign(transactionComputer.computeBytesForSigning(transaction)); + console.log(`2. Transaction signed. Signature length: ${transaction.signature.length} bytes.`); + + // === 3. Verify the transaction signature three ways. === + // (a) Account convenience method. + const viaAccount = await account.verifyTransactionSignature(transaction, transaction.signature); + // (b) UserVerifier built from the sender's address alone — the path a + // third party uses, needing only the public address. + const viaVerifier = await UserVerifier.fromAddress(account.address).verify( + transactionComputer.computeBytesForVerifying(transaction), + transaction.signature, + ); + // (c) The raw public key. + const viaPublicKey = await publicKey.verify( + transactionComputer.computeBytesForVerifying(transaction), + transaction.signature, + ); + console.log(`3. Verified via account / UserVerifier / publicKey: ${viaAccount} / ${viaVerifier} / ${viaPublicKey}`); + + // === 4. Tamper with the transaction, keep the signature. === + // Changing any signed field (here, the value) makes the recomputed bytes + // no longer match the signature — verification must return false. + transaction.value = 2000000000000000000n; // 2 EGLD, was 1 + const afterTamper = await account.verifyTransactionSignature(transaction, transaction.signature); + console.log(`4. Verify after changing the value: ${afterTamper} (expected false)`); + + // === 5. A different key cannot verify the (untampered) signature. === + // Restore the value, then check the signature against the wrong address. + transaction.value = 1000000000000000000n; + const wrongSigner = await UserVerifier.fromAddress(other.address).verify( + transactionComputer.computeBytesForVerifying(transaction), + transaction.signature, + ); + console.log(`5. Verify against a different address: ${wrongSigner} (expected false)`); + + // A convenience helper so the "wrong-key" address is unmistakably distinct. + const distinct = !account.address.equals(Address.newFromBech32(other.address.toBech32())); + console.log(` (signer and other address are distinct: ${distinct})`); + + console.log('\nExpected: steps 1 and 3 all true; steps 4 and 5 false.'); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +Keys are generated fresh each run; the pattern of `true`/`false` is stable: + +```text +1. UserSigner-signed data verified by UserVerifier: true +2. Transaction signed. Signature length: 64 bytes. +3. Verified via account / UserVerifier / publicKey: true / true / true +4. Verify after changing the value: false (expected false) +5. Verify against a different address: false (expected false) + (signer and other address are distinct: true) +``` + +The two `false` lines are the point: they prove verification actually checks +something, rather than always returning `true`. + +## How it works + +Grounded in the installed `@multiversx/sdk-core` v15.4.1: `UserSigner` / +`UserVerifier` (`wallet/`) and `TransactionComputer` +(`core/transactionComputer.d.ts`): + +1. **The primitive.** A `UserSigner` wraps the secret key and signs arbitrary + data (async); `new UserVerifier(publicKey).verify(bytes, signature)` checks it, + needing only the public key. +2. **Sign a transaction offline.** Build a `Transaction`, serialize it with + `transactionComputer.computeBytesForSigning(tx)`, and sign those bytes with the + raw key. This is exactly what `account.signTransaction` does internally, no + network, no nonce fetch. +3. **Verify three ways.** `account.verifyTransactionSignature`; a + `UserVerifier.fromAddress(sender)` (the path a third party uses, needing only + the public address); and the raw `publicKey.verify`. +4. **Tamper.** Change the transaction's value; verification returns `false`. +5. **Wrong key.** The signature does not verify against a different address. + +## Pitfalls + +:::warning[Pitfall 1: secretKey.sign is synchronous; the verify methods are async] +`UserSecretKey.sign(bytes)` returns a `Uint8Array` directly (no `await`). But +`UserVerifier.verify`, `UserPublicKey.verify`, `account.verifyTransactionSignature`, +and `account.sign` all return `Promise`s. Mixing these up is an easy +`tsc --strict` error or a silently unawaited promise. +::: + +:::note[Pitfall 2: computeBytesForSigning and computeBytesForVerifying match only when the transaction is NOT hash-signed] +For an ordinary transaction (options bit unset) they return identical bytes, +which is why signing one and verifying against the other works here. The moment +you set the hash-signing option, they diverge, see +[Hash-signing a transaction](/sdk-and-tools/sdk-js/cookbook/accounts/hash-signing-transaction). +::: + +:::tip[Pitfall 3: any signed field is covered] +Changing the value, receiver, nonce, gas, data, or chainID after signing +invalidates the signature. There is no "unsigned metadata" on a transaction, if +it is serialized, it is signed. +::: + +## See also + +- [Sign a message + verify a signature](/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-message) + is the message-signing counterpart, using `MessageComputer` instead of + `TransactionComputer`. +- [Hash-signing a transaction](/sdk-and-tools/sdk-js/cookbook/accounts/hash-signing-transaction) + is the options-bit variant, where signing and verifying bytes deliberately + differ. +- [Build a relayed v3 transaction](/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction) + is another accounts-and-signing recipe, devnet-verified. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/agents.mdx b/docs/sdk-and-tools/sdk-js/cookbook/agents.mdx new file mode 100644 index 000000000..f8c9779fe --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/agents.mdx @@ -0,0 +1,44 @@ +--- +title: Agent or agentic engineer? Start here +sidebar_label: Agent? Start here +description: Two ways into the MultiversX cookbook. Developers copy recipes by hand; coding agents read the same CI-verified recipes from a machine-readable surface. +slug: /sdk-and-tools/sdk-js/cookbook/agents +--- + +Two ways in: copy the recipes by hand, or point a coding agent at them. Same +verified recipes either way. + +## If you are a developer + +Browse the full set on the [cookbook overview](./index.mdx), or jump straight to +a common starting point: + +- [Minimal sdk-dapp v5 app in Vite + React](./start-here/vite-react-minimal.mdx): the smallest working dApp setup. +- [Sign and send a transaction](./start-here/sign-and-send.mdx): the canonical sign, send, and track flow. +- [Send EGLD to an address](./transactions/send-egld.mdx): the backend and script transfer pattern. +- [Call a contract endpoint](./smart-contracts-call/call-contract-endpoint.mdx): invoke a contract with typed arguments. +- [Issue a fungible token](./tokens/issue-fungible-token.mdx): mint your own ESDT. + +Each recipe shows every file it needs and states the SDK versions it was verified +against. The teal badge on a page means its code compiled green in CI on the date +shown. + +## If you are a coding agent (or wiring one up) + +Point your coding agent at these docs. Each recipe is self-contained and +version-pinned, so the code it lifts runs against the SDKs the page names. + +### The machine-readable surface + +MultiversX publishes the whole documentation set in the +[`llms.txt`](https://llmstxt.org) format, so a model can load it as context: + +- [`https://docs.multiversx.com/llms.txt`](https://docs.multiversx.com/llms.txt): the index. Every page as a titled, described link; cookbook recipes carry an inline `[difficulty, verified DATE]` tag, so an agent sees each recipe's difficulty and its last green CI date without opening the page. +- [`https://docs.multiversx.com/llms-full.txt`](https://docs.multiversx.com/llms-full.txt): the same set with full page content inlined, for one-shot ingestion. + +### The wider MultiversX agent stack + +For the skills collection, see [AI agents](../../../learn/ai-agents.md). That page +links the [`mx-ai-skills`](https://github.com/multiversx/mx-ai-skills) repository, a +structured collection of MultiversX-specific skills, roles, and documentation for +equipping agents to build, audit, and optimize on MultiversX. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/delegation/claim-and-redelegate-rewards.mdx b/docs/sdk-and-tools/sdk-js/cookbook/delegation/claim-and-redelegate-rewards.mdx new file mode 100644 index 000000000..72a0fc624 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/delegation/claim-and-redelegate-rewards.mdx @@ -0,0 +1,237 @@ +--- +title: Claim and re-delegate rewards +description: Claim delegation rewards to your wallet or re-delegate (compound) them into the same MultiversX staking contract, with sdk-core DelegationController. +difficulty: intermediate +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - delegation + - transaction + - devnet + - typescript +--- + +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.13.1. +- For the default `parse` and `payload` demos: 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 + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/claim-and-redelegate-rewards +npm install +npm run build +``` + +## Claiming and re-delegating + +```ts title="src/rewards.ts" +// 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + const transactionOnNetwork = await entrypoint.getTransaction(txHash); + const parser = new DelegationTransactionsOutcomeParser(); + return parser.parseRedelegateRewards(transactionOnNetwork)[0]?.amount ?? 0n; +} +``` + +## Run it + +```bash +# 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`: + +```text +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 + +:::warning[Pitfall 1: parseClaimRewards often returns 0] +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. +::: + +:::note[Pitfall 2: re-delegating may be gated by the contract's cap check] +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. +::: + +:::note[Pitfall 3: claiming with zero pending rewards is a successful no-op] +`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](/sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake) + is the stake that earns these rewards. +- [Undelegate and withdraw](/sdk-and-tools/sdk-js/cookbook/delegation/undelegate-and-withdraw) + is the exit path, when you want the principal back too. +- [Read a delegation contract's state](/sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract) + reads your claimable rewards before claiming. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/delegation/create-delegation-contract.mdx b/docs/sdk-and-tools/sdk-js/cookbook/delegation/create-delegation-contract.mdx new file mode 100644 index 000000000..8920a086c --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/delegation/create-delegation-contract.mdx @@ -0,0 +1,247 @@ +--- +title: Create a delegation contract +description: Create a new MultiversX delegation (staking-provider) contract with sdk-core DelegationController and factory, then parse the outcome for its address. +difficulty: advanced +est_minutes: 9 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - delegation + - transaction + - devnet + - typescript +--- + +A delegation contract is a staking provider: it pools EGLD from many delegators +and stakes it across validator nodes, sharing rewards. You register one by +sending a `createNewDelegationContract` transaction to the delegation **manager** +system contract, carrying an initial stake (at least 1,250 EGLD) plus two +arguments, the total delegation cap and the service fee. This recipe builds that +transaction both ways (controller and factory) and parses the completed +transaction for the new contract's address. + +The default `npm start` parses a real, already-completed devnet create, so you +see the new-address parse work without a funded wallet. + +## Prerequisites + +- Node.js >= 20.13.1. +- For the default `parse` and `payload` demos: devnet network access only. +- For an actual create: a devnet PEM wallet holding at least 1,250 EGLD (the + protocol minimum) plus gas. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/create-delegation-contract +npm install +npm run build +``` + +## Creating + +```ts title="src/createDelegation.ts" +// src/createDelegation.ts - the subject of this recipe: creating a brand-new +// delegation (staking-provider) contract with `createNewDelegationContract`, +// two ways (controller and factory), then parsing the outcome to recover the +// new contract's address. +// +// A "delegation contract" is a staking provider: it collects EGLD from many +// delegators and stakes it across validator nodes. You create one by sending +// a `createNewDelegationContract` transaction to the DELEGATION MANAGER system +// contract, carrying an initial stake (>= 1250 EGLD) plus two arguments: +// - totalDelegationCap: the max EGLD the contract may accept (0 = uncapped); +// - serviceFee: the operator's cut, as an integer over 10,000 (1000 = 10%). +// +// Two verified SDK facts baked into this recipe (see the recipe page Pitfalls): +// 1. The receiver is the delegation manager, whose address the factory +// derives itself as +// `erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6`. +// Do not hardcode it from memory - a hand-copied value can carry a +// typo that fails the bech32 checksum. +// 2. The wire payload is `createNewDelegationContract@@`, +// with the initial stake carried in the transaction's `value`, not the +// data. + +import { Account } from '@multiversx/sdk-core'; +import type { DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +/** The protocol minimum initial stake to create a delegation contract: 1250 EGLD. */ +export const MIN_DELEGATION_STAKE_WEI = 1250n * 10n ** 18n; + +export interface CreateDelegationInput { + /** Max EGLD the contract may accept, in wei. 0n = uncapped. */ + totalDelegationCap: bigint; + /** Operator fee as an integer over 10,000 (1000 = 10%). */ + serviceFee: bigint; + /** Initial stake to seed the contract with, in wei (>= 1250 EGLD). */ + amount: bigint; +} + +/** + * Create path 1 - the controller. `createTransactionForNewDelegationContract` + * builds the transaction, sets the nonce, AND signs it (it takes the whole + * `Account`). Use this for scripts. + */ +export async function createViaController( + entrypoint: DevnetEntrypoint, + sender: Account, + input: CreateDelegationInput, +): Promise { + const controller = entrypoint.createDelegationController(); + return controller.createTransactionForNewDelegationContract(sender, sender.getNonceThenIncrement(), input); +} + +/** + * Create path 2 - the factory. The factory only BUILDS the unsigned + * transaction; the caller sets the nonce and signs. Use this when a wallet or + * hardware device signs. + */ +export async function createViaFactory( + entrypoint: DevnetEntrypoint, + sender: Account, + input: CreateDelegationInput, +): Promise { + const factory = entrypoint.createDelegationTransactionsFactory(); + const transaction = await factory.createTransactionForNewDelegationContract(sender.address, input); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; +} + +export interface CreatePayload { + function: string; + totalDelegationCap: bigint; + serviceFee: bigint; + receiver: string; + valueWei: bigint; + gasLimit: bigint; +} + +/** Decode a create transaction's wire fields, for inspection before sending. */ +export function describeCreatePayload(transaction: Transaction): CreatePayload { + const parts = Buffer.from(transaction.data).toString().split('@'); + const capHex = parts[1] ?? ''; + const feeHex = parts[2] ?? ''; + return { + function: parts[0] ?? '', + totalDelegationCap: capHex ? BigInt('0x' + capHex) : 0n, + serviceFee: feeHex ? BigInt('0x' + feeHex) : 0n, + receiver: transaction.receiver.toBech32(), + valueWei: transaction.value, + gasLimit: transaction.gasLimit, + }; +} + +/** + * Parse a completed create transaction for the new contract's address. + * `awaitCompletedCreateNewDelegationContract` waits and parses in one call; + * `parseCreateNewDelegationContract` parses a transaction you already fetched + * (used by this recipe's default mode against a historical create, so it + * needs no funded wallet). + */ +export async function parseNewContractAddress( + entrypoint: DevnetEntrypoint, + txHash: string, +): Promise { + const controller = entrypoint.createDelegationController(); + const transactionOnNetwork = await entrypoint.getTransaction(txHash); + const outcome = controller.parseCreateNewDelegationContract(transactionOnNetwork); + const first = outcome[0]; + if (!first) { + throw new Error(`Transaction ${txHash} created no delegation contract.`); + } + return first.contractAddress; +} + +// The delegation manager system contract (hex 0000...0004ffff) - the receiver +// of every `createNewDelegationContract`. This is the exact bech32 the factory +// derives internally; the CLI asserts the built transaction's receiver equals +// it. A hand-copied value (`...ylllslmq4y`) can carry a typo that fails the +// bech32 checksum; the correct value is below. +export const DELEGATION_MANAGER_ADDRESS = + 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6'; +``` + +## Run it + +```bash +# Parse a real completed devnet create - no wallet, no funds: +npm start + +# Inspect the create wire payload offline: +npm start -- payload + +# Actually create (needs a funded devnet PEM >= 1,250 EGLD); add --factory: +npm start -- send ./wallet.pem +``` + +Output of the default `parse` mode, and of `payload`: + +```text +Parsing completed create d615857f...aab9278c ... + new delegation contract: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdhllllsfymgpz + +function: createNewDelegationContract +totalDelegationCap: 7500000000000000000000 wei (7500 EGLD) +serviceFee: 1000 (10%) +value (initial): 1250000000000000000000 wei (1250 EGLD) +receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6 +receiver is manager: true +gasLimit: 60129500 +``` + +## How it works + +**Controller vs factory.** +`controller.createTransactionForNewDelegationContract(account, nonce, input)` +builds, sets the nonce, and signs in one call. +`factory.createTransactionForNewDelegationContract(address, input)` only builds +the unsigned transaction; you set `nonce` and `signature`. Both are async and take +the same `{ totalDelegationCap, serviceFee, amount }` input. + +**The three inputs.** `amount` is the initial stake, carried in the transaction's +`value` (>= 1,250 EGLD). `totalDelegationCap` is the maximum EGLD the contract may +later accept, in wei (`0n` = uncapped). `serviceFee` is the operator's cut as an +integer over 10,000 (so `1000` = 10%). + +**Parsing the new address.** +`controller.awaitCompletedCreateNewDelegationContract(txHash)` waits and parses in +one call; `parseCreateNewDelegationContract(transactionOnNetwork)` parses a +transaction you already fetched. The default `npm start` uses the second form +against a historical create, which is why it needs no funds. Both read the +`SCDeploy` event the manager emits. + +## Pitfalls + +:::warning[Pitfall 1: do not hardcode the delegation manager address from memory] +Every create is addressed to the delegation manager, hex `0000...0004ffff`, which +the SDK renders as +`erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6`. The factory +derives this itself. A hand-copied value (`...ylllslmq4y`) is a typo that fails +bech32 checksum, so the recipe asserts the built transaction's receiver equals the +correct address instead of trusting a copied constant. +::: + +:::warning[Pitfall 2: the 1,250 EGLD minimum is the initial value, not an argument] +The initial stake travels in the transaction's `value`, not the data. The wire +payload is only `createNewDelegationContract@@`. Send less than +1,250 EGLD and the manager rejects it; the SDK will not catch that for you. +::: + +:::note[Pitfall 3: service fee is over 10,000, not a percentage or basis points of 100] +`serviceFee: 1000` means 10%, because the protocol expresses it as parts per +10,000. Passing `10` for "10%" would set a 0.1% fee. +::: + +## See also + +- [Delegate (stake) EGLD](/sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake) + stakes into the contract once it exists. +- [Read a delegation contract's state](/sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract) + confirms the new contract's config and stake. +- [Deploy a smart contract](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/deploy-contract) + follows the same predict-then-parse shape for an ordinary contract. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake.mdx b/docs/sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake.mdx new file mode 100644 index 000000000..ec984e760 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake.mdx @@ -0,0 +1,200 @@ +--- +title: Delegate (stake) EGLD +description: Delegate (stake) EGLD to an existing MultiversX delegation contract with sdk-core DelegationController and factory, then parse the staked amount. +difficulty: intermediate +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - delegation + - transaction + - devnet + - typescript +--- + +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.13.1. +- 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 + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/delegate-stake +npm install +npm run build +``` + +## Delegating + +```ts title="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 { + 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 { + 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 { + const transactionOnNetwork = await entrypoint.getTransaction(txHash); + const parser = new DelegationTransactionsOutcomeParser(); + const outcome = parser.parseDelegate(transactionOnNetwork); + return outcome[0]?.amount ?? 0n; +} +``` + +## Run it + +```bash +# 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`: + +```text +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 + +:::warning[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. +::: + +:::note[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). +::: + +:::note[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 + +- [Create a delegation contract](/sdk-and-tools/sdk-js/cookbook/delegation/create-delegation-contract) + makes the contract you delegate into. +- [Claim and re-delegate rewards](/sdk-and-tools/sdk-js/cookbook/delegation/claim-and-redelegate-rewards) + handles the rewards this stake earns. +- [Read a delegation contract's state](/sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract) + checks your active stake after delegating. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract.mdx b/docs/sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract.mdx new file mode 100644 index 000000000..c9df25a38 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract.mdx @@ -0,0 +1,257 @@ +--- +title: Read a delegation contract's state +description: "Read a MultiversX staking-provider contract state with read-only queries: total stake, service fee, a delegator active stake and claimable rewards." +difficulty: intermediate +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - delegation + - network-provider + - devnet + - typescript +--- + +A delegation contract is a normal smart contract, so you read its state with the +same read-only VM queries as any other, no wallet, no nonce, no gas. This recipe +queries a live devnet staking provider for its config (owner, service fee), its +total active stake and delegator count, and one delegator's active stake and +claimable rewards, decoding the raw return bytes by hand because the delegation +system contract does not ship an ABI with sdk-core. + +The whole recipe runs against real devnet data; `npm start` needs +nothing but network access. + +## Prerequisites + +- Node.js >= 20.13.1. +- Devnet network access. No wallet, no funds. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/query-delegation-contract +npm install +npm run build +``` + +## Querying + +```ts title="src/queryDelegation.ts" +// src/queryDelegation.ts - the actual subject of this recipe: reading a +// staking-provider (delegation) contract's state with read-only VM queries. +// No wallet, no nonce, no gas, no signing - every function here is a plain +// query against a live devnet delegation contract. A view function does not +// modify contract state, so there is no transaction to send. +// +// A delegation contract is a normal smart contract that happens to be +// created by the delegation manager. Its views are queried exactly like any +// other contract's, through `SmartContractController.query()`. We construct +// the controller WITHOUT an ABI (the delegation system contract does not +// ship one with sdk-core), so: +// - query results come back as RAW `Uint8Array[]` return-data parts, which +// we decode by hand (a top-level BigUint is just big-endian bytes; +// an empty part is zero); +// - address arguments must be passed as `TypedValue`s (an `AddressValue`), +// because without an ABI there is no NativeSerializer to convert a plain +// bech32 string. See the Pitfalls in the recipe page. +// +// The example contract and delegator below are real and live on devnet; the +// recipe page shows the actual values they returned. + +import { Address, AddressValue } from '@multiversx/sdk-core'; +import type { DevnetEntrypoint } from '@multiversx/sdk-core'; + +/** A real, live devnet staking-provider contract (666 delegators at the time of writing). */ +export const EXAMPLE_DELEGATION_CONTRACT = + 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllllscktaww'; + +/** A real delegator with active stake in the contract above (its owner). */ +export const EXAMPLE_DELEGATOR = + 'erd1kv5mkar6fvt6vhqj7evfqr9jnmmlqps3q9dp0t0gr9tcpqupyrsshlnvd0'; + +/** + * Decode a top-level `BigUint` returned by a view. On MultiversX a top-level + * (top-encoded) BigUint is its minimal big-endian byte string, and an empty + * return part means zero - so `[]` decodes to `0n`, not an error. + */ +export function decodeBigUint(part: Uint8Array): bigint { + if (part.length === 0) { + return 0n; + } + return BigInt('0x' + Buffer.from(part).toString('hex')); +} + +/** Query a zero-argument view that returns a single BigUint. */ +async function queryBigUintView( + entrypoint: DevnetEntrypoint, + contract: string, + functionName: string, +): Promise { + const controller = entrypoint.createSmartContractController(); // no ABI + const parts = (await controller.query({ + contract: Address.newFromBech32(contract), + function: functionName, + arguments: [], + })) as Uint8Array[]; + return decodeBigUint(parts[0] ?? new Uint8Array()); +} + +/** Query a view that takes one delegator address and returns a single BigUint. */ +async function queryDelegatorView( + entrypoint: DevnetEntrypoint, + contract: string, + functionName: string, + delegator: string, +): Promise { + const controller = entrypoint.createSmartContractController(); // no ABI + const parts = (await controller.query({ + contract: Address.newFromBech32(contract), + function: functionName, + // Without an ABI, arguments must be TypedValues (or raw buffers), never a + // plain bech32 string. An AddressValue encodes to the 32-byte public key. + arguments: [new AddressValue(Address.newFromBech32(delegator))], + })) as Uint8Array[]; + return decodeBigUint(parts[0] ?? new Uint8Array()); +} + +/** Total EGLD actively staked into this contract (all delegators). */ +export async function queryTotalActiveStake( + entrypoint: DevnetEntrypoint, + contract: string, +): Promise { + return queryBigUintView(entrypoint, contract, 'getTotalActiveStake'); +} + +/** Number of distinct delegators in this contract. */ +export async function queryNumUsers(entrypoint: DevnetEntrypoint, contract: string): Promise { + return queryBigUintView(entrypoint, contract, 'getNumUsers'); +} + +/** One delegator's currently-active (staked) amount, in wei. */ +export async function queryUserActiveStake( + entrypoint: DevnetEntrypoint, + contract: string, + delegator: string, +): Promise { + return queryDelegatorView(entrypoint, contract, 'getUserActiveStake', delegator); +} + +/** One delegator's unclaimed rewards, in wei. */ +export async function queryClaimableRewards( + entrypoint: DevnetEntrypoint, + contract: string, + delegator: string, +): Promise { + return queryDelegatorView(entrypoint, contract, 'getClaimableRewards', delegator); +} + +export interface DelegationConfig { + /** The contract owner (the staking provider operator). */ + owner: string; + /** Service fee in basis points of 10,000 (e.g. 1000 = 10%). */ + serviceFeePerTenThousand: number; +} + +/** + * Read the contract's configuration. `getContractConfig` returns many parts; + * the first is the owner's 32-byte public key and the second is the service + * fee as a BigUint over 10,000. We decode just those two here; the remaining + * parts (delegation cap, activation flags, etc.) decode the same way. + */ +export async function queryContractConfig( + entrypoint: DevnetEntrypoint, + contract: string, +): Promise { + const controller = entrypoint.createSmartContractController(); // no ABI + const parts = (await controller.query({ + contract: Address.newFromBech32(contract), + function: 'getContractConfig', + arguments: [], + })) as Uint8Array[]; + + const ownerBytes = parts[0] ?? new Uint8Array(); + const serviceFeeBytes = parts[1] ?? new Uint8Array(); + return { + owner: new Address(ownerBytes).toBech32(), + serviceFeePerTenThousand: Number(decodeBigUint(serviceFeeBytes)), + }; +} +``` + +## Run it + +```bash +# Query the built-in example contract + delegator: +npm start + +# Or point it at any delegation contract and delegator: +npm start -- erd1qqq...delegationContract erd1...delegator +``` + +Real output against the example contract (values move as the chain lives): + +```text +Delegation contract: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllllscktaww + owner: erd1kv5mkar6fvt6vhqj7evfqr9jnmmlqps3q9dp0t0gr9tcpqupyrsshlnvd0 + service fee: 10% + total active stake: 162444.3154 EGLD (162444315455774609992988 wei) + delegators: 666 + +Delegator: erd1kv5mkar6fvt6vhqj7evfqr9jnmmlqps3q9dp0t0gr9tcpqupyrsshlnvd0 + active stake: 10000.0000 EGLD (10000000000000000000000 wei) + claimable rewards: 19559.2005 EGLD (19559200521796778772671 wei) +``` + +## How it works + +**No ABI, so decode by hand.** `entrypoint.createSmartContractController()` with +no ABI makes `query()` return the raw `Uint8Array[]` return-data parts. A top-level +`BigUint` is just its big-endian bytes, and an empty part is zero, so +`getUserActiveStake` and friends decode with a one-line `BigInt('0x' + hex)` (or +`0n` when empty). + +**Address arguments must be TypedValues.** Without an ABI there is no +NativeSerializer, so a delegator argument cannot be a plain bech32 string. Wrap it +as `new AddressValue(Address.newFromBech32(...))`, which encodes to the 32-byte +public key the view expects. + +**The views used.** `getContractConfig` returns many parts (part 0 is the owner's +public key, part 1 is the service fee over 10,000); `getTotalActiveStake` and +`getNumUsers` take no arguments; `getUserActiveStake` and `getClaimableRewards` +take one delegator address. + +For the same pattern with an ABI that auto-decodes results, see the +query-contract-view recipe. + +## Pitfalls + +:::warning[Pitfall 1: without an ABI you get bytes, and address args must be typed] +`query()` with no ABI returns `Uint8Array[]`, not decoded values, and rejects +plain-string arguments with "cannot encode arguments: when ABI is not available, +they must be either typed values or buffers." Pass an `AddressValue` (or a raw +buffer) and decode the results yourself. +::: + +:::note[Pitfall 2: an empty return part means zero, not an error] +A delegator with no stake or no rewards makes the contract return an empty part for +that view. Decode it as `0n`; do not treat the empty `Uint8Array` as a failure. +::: + +:::note[Pitfall 3: service fee is over 10,000] +`getContractConfig` returns the service fee as an integer over 10,000 (e.g. `1000` += 10%), matching the value you pass when creating a contract. Divide by 100 for a +percentage. +::: + +## See also + +- [Delegate (stake) EGLD](/sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake) + acts on the state you just read. +- [Claim and re-delegate rewards](/sdk-and-tools/sdk-js/cookbook/delegation/claim-and-redelegate-rewards) + uses the claimable rewards this recipe reads. +- [Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) + is the same read pattern with an ABI for automatic decoding. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/delegation/undelegate-and-withdraw.mdx b/docs/sdk-and-tools/sdk-js/cookbook/delegation/undelegate-and-withdraw.mdx new file mode 100644 index 000000000..2b44ea9df --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/delegation/undelegate-and-withdraw.mdx @@ -0,0 +1,240 @@ +--- +title: Undelegate and withdraw +description: "Exit a MultiversX delegation: unDelegate an amount, wait out the unbonding period, then withdraw it, using sdk-core DelegationController and factory." +difficulty: intermediate +est_minutes: 7 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - delegation + - transaction + - devnet + - typescript +--- + +Exiting a delegation is a two-step flow with a mandatory wait in between. First +`unDelegate(amount)` asks the contract to unstake a given amount, but the EGLD is +not returned yet; it enters an unbonding period (10 epochs, about 10 days on +mainnet). Then, after that period elapses, `withdraw()` pulls all matured EGLD +back to your wallet. This recipe builds both, both ways (controller and factory), +and parses a completed `unDelegate`. + +The default `npm start` parses a real, already-completed devnet `unDelegate`, so +you see the parse work without a funded wallet. + +## Prerequisites + +- Node.js >= 20.13.1. +- For the default `parse` and `payload` demos: devnet network access only. +- For an actual exit: a devnet PEM wallet with an active delegation, plus gas. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/undelegate-and-withdraw +npm install +npm run build +``` + +## Undelegating and withdrawing + +```ts title="src/exit.ts" +// src/exit.ts - the subject of this recipe: exiting a delegation, which is a +// TWO-STEP flow with a mandatory unbonding wait in between: +// 1. unDelegate(amount) -> ask the contract to unstake `amount`. The EGLD is +// not returned yet; it enters an unbonding period (10 epochs, ~10 days on +// mainnet). Unlike `delegate`, `unDelegate` DOES carry an argument (the +// amount) on the wire: `unDelegate@`, with no value. +// 2. withdraw() -> after the unbonding period elapses, pull all matured +// unbonded EGLD back to your wallet. No arguments, no value; the contract +// pays out whatever has finished unbonding. +// Calling `withdraw` before anything has matured is a no-op (nothing to pay). + +import { Account, Address, DelegationTransactionsOutcomeParser } from '@multiversx/sdk-core'; +import type { DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +/** Undelegate (unstake) `amount` - controller path. */ +export async function undelegateViaController( + entrypoint: DevnetEntrypoint, + sender: Account, + delegationContract: string, + amount: bigint, +): Promise { + const controller = entrypoint.createDelegationController(); + return controller.createTransactionForUndelegating(sender, sender.getNonceThenIncrement(), { + delegationContract: Address.newFromBech32(delegationContract), + amount, + }); +} + +/** Undelegate (unstake) `amount` - factory path. */ +export async function undelegateViaFactory( + entrypoint: DevnetEntrypoint, + sender: Account, + delegationContract: string, + amount: bigint, +): Promise { + const factory = entrypoint.createDelegationTransactionsFactory(); + const transaction = await factory.createTransactionForUndelegating(sender.address, { + delegationContract: Address.newFromBech32(delegationContract), + amount, + }); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; +} + +/** Withdraw all matured unbonded EGLD - controller path. */ +export async function withdrawViaController( + entrypoint: DevnetEntrypoint, + sender: Account, + delegationContract: string, +): Promise { + const controller = entrypoint.createDelegationController(); + return controller.createTransactionForWithdrawing(sender, sender.getNonceThenIncrement(), { + delegationContract: Address.newFromBech32(delegationContract), + }); +} + +/** Withdraw all matured unbonded EGLD - factory path. */ +export async function withdrawViaFactory( + entrypoint: DevnetEntrypoint, + sender: Account, + delegationContract: string, +): Promise { + const factory = entrypoint.createDelegationTransactionsFactory(); + const transaction = await factory.createTransactionForWithdrawing(sender.address, { + delegationContract: Address.newFromBech32(delegationContract), + }); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; +} + +export interface UndelegatePayload { + function: string; + /** The unstake amount, decoded from the wire argument. */ + amount: bigint; + receiver: string; + valueWei: bigint; + gasLimit: bigint; +} + +/** Decode an unDelegate transaction: `unDelegate@`. */ +export function describeUndelegatePayload(transaction: Transaction): UndelegatePayload { + const parts = Buffer.from(transaction.data).toString().split('@'); + const amountHex = parts[1] ?? ''; + return { + function: parts[0] ?? '', + amount: amountHex ? BigInt('0x' + amountHex) : 0n, + receiver: transaction.receiver.toBech32(), + valueWei: transaction.value, + gasLimit: transaction.gasLimit, + }; +} + +export interface WithdrawPayload { + function: string; + receiver: string; + valueWei: bigint; + gasLimit: bigint; +} + +/** Decode a withdraw transaction (no arguments). */ +export function describeWithdrawPayload(transaction: Transaction): WithdrawPayload { + 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 unDelegate transaction for the amount that entered + * unbonding. Reads the `unDelegate` log event. + */ +export async function parseUndelegatedAmount( + entrypoint: DevnetEntrypoint, + txHash: string, +): Promise { + const transactionOnNetwork = await entrypoint.getTransaction(txHash); + const parser = new DelegationTransactionsOutcomeParser(); + return parser.parseUndelegate(transactionOnNetwork)[0]?.amount ?? 0n; +} +``` + +## Run it + +```bash +# Parse a real completed devnet unDelegate - no wallet, no funds: +npm start + +# Inspect both wire payloads offline: +npm start -- payload + +# Actually undelegate (needs a funded devnet PEM with active delegation): +npm start -- send ./wallet.pem +# ...or withdraw matured funds instead: +npm start -- send ./wallet.pem --withdraw +``` + +Output of the default `parse` mode, and of `payload`: + +```text +Parsing completed unDelegate f1f50870...3c0392b3 ... + unbonding amount: 5000000000000000000 wei (5 EGLD) + +unDelegate: function=unDelegate amountArg=2000000000000000000 wei (2 EGLD) + value=0 receiver=erd1qqq...scktaww gasLimit=11090500 +withdraw: function=withdraw value=0 receiver=erd1qqq...scktaww gasLimit=11062000 +``` + +## How it works + +**Controller vs factory.** `controller.createTransactionForUndelegating` / +`createTransactionForWithdrawing` build, set the nonce, and sign in one call; the +factory equivalents build only. `unDelegate` takes `{ delegationContract, amount }`; +`withdraw` takes just `{ delegationContract }`. + +**unDelegate carries an argument; withdraw does not.** The `unDelegate` wire is +`unDelegate@` (the amount to unstake, with no `value`), while `withdraw` +is bare. This is the one delegation write besides create that puts a real argument +in the data. + +**Withdraw is settle-all, not per-request.** `withdraw()` pays out every unbonding +entry that has matured; it takes no amount. Calling it before anything has matured +is a successful no-op that moves 0 EGLD. + +## Pitfalls + +:::warning[Pitfall 1: withdraw does nothing until the unbonding period elapses] +`unDelegate` starts a ~10-epoch unbonding timer per entry. A `withdraw` before any +entry matures completes successfully but returns 0 EGLD. Sequencing the two +back-to-back in one script will not return your funds. +::: + +:::warning[Pitfall 2: unDelegate takes an amount; withdraw does not] +`unDelegate` needs the amount to unstake in its input (`amount`), encoded on the +wire as `unDelegate@`. `withdraw` takes no amount, it settles everything +matured. Do not expect a per-request withdraw. +::: + +:::note[Pitfall 3: partial exits must respect the provider's minimum] +Undelegating part of your stake can leave a remainder below the provider's minimum +delegation, which some contracts reject. If in doubt, undelegate the full active +amount (read it first with the query recipe). +::: + +## See also + +- [Delegate (stake) EGLD](/sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake) + is the inverse operation. +- [Claim and re-delegate rewards](/sdk-and-tools/sdk-js/cookbook/delegation/claim-and-redelegate-rewards) + handles rewards without exiting. +- [Read a delegation contract's state](/sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract) + reads your active stake to know how much to undelegate. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/governance/create-proposal.mdx b/docs/sdk-and-tools/sdk-js/cookbook/governance/create-proposal.mdx new file mode 100644 index 000000000..1c43e2933 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/governance/create-proposal.mdx @@ -0,0 +1,242 @@ +--- +title: Create a governance proposal +description: Create a MultiversX governance proposal with sdk-core's GovernanceController and factory, reading the live proposal fee from getConfig first. +difficulty: advanced +est_minutes: 9 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - governance + - transaction + - devnet +--- + +A governance proposal points at a Git commit (its `commitHash`) and opens a voting +window between two epochs. Creating one costs a proposal fee in EGLD, held by the +governance system contract until the proposal closes. This recipe reads the live +fee and config with `getConfig()`, then creates a proposal both ways, the +controller and the factory, against the real governance contract. + +The default `npm start` reads the live governance config (fee, thresholds, last +proposal nonce), so you see real output without a funded wallet, and you learn the +fee you would need before spending it. + +## Prerequisites + +- Node.js >= 20.13.1. +- For the default config read and `payload` demos: devnet network access only. +- For an actual proposal: a devnet PEM holding at least the proposal fee (500 EGLD + on devnet at time of writing) plus gas. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/governance-create-proposal +npm install +npm run build +``` + +## Creating a proposal + +```ts title="src/proposal.ts" +// src/proposal.ts - the subject of this recipe: creating a MultiversX +// governance proposal with the GovernanceController and its factory. +// +// A governance proposal points at a Git commit (its `commitHash`) and opens a +// voting window between two epochs. Creating one costs a proposal fee in EGLD, +// which the governance system contract holds until the proposal closes. The +// fee is not a constant - read it from the live config with `getConfig()` +// rather than hard-coding it. +// +// The proposal is sent to the governance system contract (a protocol contract +// at a fixed address the SDK knows), not to a contract you deploy. Unlike the +// multisig writes, governance transactions set their own gas limit from the +// SDK's config defaults, so you do not pass one. + +import { Account } from '@multiversx/sdk-core'; +import type { DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +export interface GovernanceConfigView { + proposalFeeWei: bigint; + lostProposalFeeWei: bigint; + minQuorum: number; + minPassThreshold: number; + minVetoThreshold: number; + lastProposalNonce: number; +} + +/** Read the live governance config, including the current proposal fee. */ +export async function readGovernanceConfig(entrypoint: DevnetEntrypoint): Promise { + const controller = entrypoint.createGovernanceController(); + const config = await controller.getConfig(); + return { + proposalFeeWei: config.proposalFee, + lostProposalFeeWei: config.lostProposalFee, + minQuorum: config.minQuorum, + minPassThreshold: config.minPassThreshold, + minVetoThreshold: config.minVetoThreshold, + lastProposalNonce: config.lastProposalNonce, + }; +} + +export interface ProposalInput { + commitHash: string; + startVoteEpoch: number; + endVoteEpoch: number; + feeWei: bigint; +} + +/** + * Create a proposal, path 1 - the controller. `createTransactionForNewProposal` + * builds, sets the nonce, and signs in one call. The fee travels in the + * transaction's `value`. + */ +export async function createProposalViaController( + entrypoint: DevnetEntrypoint, + proposer: Account, + input: ProposalInput, +): Promise { + const controller = entrypoint.createGovernanceController(); + return controller.createTransactionForNewProposal(proposer, proposer.getNonceThenIncrement(), { + commitHash: input.commitHash, + startVoteEpoch: input.startVoteEpoch, + endVoteEpoch: input.endVoteEpoch, + nativeTokenAmount: input.feeWei, + }); +} + +/** + * Create a proposal, path 2 - the factory. Builds the unsigned transaction + * only; the caller sets the nonce and signs. + */ +export async function createProposalViaFactory( + entrypoint: DevnetEntrypoint, + proposer: Account, + input: ProposalInput, +): Promise { + const factory = entrypoint.createGovernanceTransactionsFactory(); + const transaction = await factory.createTransactionForNewProposal(proposer.address, { + commitHash: input.commitHash, + startVoteEpoch: input.startVoteEpoch, + endVoteEpoch: input.endVoteEpoch, + nativeTokenAmount: input.feeWei, + }); + transaction.nonce = proposer.getNonceThenIncrement(); + transaction.signature = await proposer.signTransaction(transaction); + return transaction; +} + +export interface ProposalPayload { + function: string; + commitHashHex: string; + startEpochHex: string; + endEpochHex: string; + receiver: string; + feeWei: bigint; + gasLimit: bigint; +} + +/** Decode a proposal transaction's wire fields. */ +export function describeProposalPayload(transaction: Transaction): ProposalPayload { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + function: parts[0] ?? '', + commitHashHex: parts[1] ?? '', + startEpochHex: parts[2] ?? '', + endEpochHex: parts[3] ?? '', + receiver: transaction.receiver.toBech32(), + feeWei: transaction.value, + gasLimit: transaction.gasLimit, + }; +} +``` + +## Run it + +```bash +# Read the live governance config (fee, thresholds) - no wallet, no funds: +npm start + +# Inspect the proposal wire payload offline: +npm start -- payload + +# Actually create a proposal (needs a devnet PEM holding the fee); add --factory: +npm start -- propose ./wallet.pem +``` + +Output of the default config read, and of `payload`: + +```text +Governance config (live devnet): + proposal fee: 500000000000000000000 wei (500 EGLD) + lost-proposal fee: 10000000000000000000 wei (10 EGLD) + min quorum: 0.2 + min pass threshold: 0.6667 + min veto threshold: 0.33 + last proposal nonce: 138 + +function: proposal +commitHash: 616263...363738396162636465663031 (hex of the 40-char commit string) +startEpoch: 0x1850 = 6224 +endEpoch: 0x1855 = 6229 +receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla (the governance system contract) +value (fee): 500000000000000000000 wei (500 EGLD) +gasLimit: 50198500 (set automatically by the SDK) +``` + +## How it works + +**Controller vs factory.** +`controller.createTransactionForNewProposal(account, nonce, { commitHash, startVoteEpoch, endVoteEpoch, nativeTokenAmount })` +builds, sets the nonce, and signs. The factory form only builds; you set `nonce` +and `signature`. The governance controller and factory take no ABI, the governance +contract's interface is baked into the SDK. + +**Read the fee, do not hard-code it.** `nativeTokenAmount` is the proposal fee and +travels in the transaction's `value`. The fee is a network parameter, so the +recipe reads it from `getConfig().proposalFee` (500 EGLD on devnet) rather than +assuming a constant that could drift. + +**The receiver is a system contract.** Proposals go to `erd1qqq...rlllsrujgla`, the +governance system contract, whose address the SDK derives from its own constants +(`GOVERNANCE_CONTRACT_ADDRESS_HEX`). You never pass it. The wire payload is +`proposal@@@`. + +## Pitfalls + +:::warning[Pitfall 1: the commit hash must be exactly 40 characters] +Governance expects a full 40-character Git commit hash (a SHA-1). The SDK encodes +whatever string you pass as-is (`StringValue`), so a wrong length is not caught +when building, it is rejected on-chain. Pass the real 40-char commit of the change +you are proposing. +::: + +:::note[Pitfall 2: governance sets its own gas; multisig does not] +Unlike the multisig writes (which require an explicit `gasLimit`), governance +transactions set their gas limit automatically from the SDK config +(`gasLimitForProposal`, 50,000,000 plus data). Do not pass a `gasLimit`, there is +no parameter for it here. +::: + +:::note[Pitfall 3: the fee is at stake, not just a deposit] +The proposal fee travels in `value` and is held by the contract. If the proposal +fails to pass, part of it is kept as the `lostProposalFee` (10 EGLD on devnet); +only the rest is returned when the proposal is closed. Proposing is not free even +when it works. +::: + +:::note[Pitfall 4: vote epochs must be in the future] +`startVoteEpoch` / `endVoteEpoch` open the window; a window in the past is rejected +on-chain. This recipe reads the current epoch from the network and offsets from it, +rather than hard-coding epoch numbers that go stale. +::: + +## See also + +- [Vote and close a proposal](/sdk-and-tools/sdk-js/cookbook/governance/vote-close-proposal) + covers the next steps for the proposal this recipe creates. +- [Delegate (stake) EGLD](/sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake) + is what gives an address the voting power to back a proposal. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/governance/vote-close-proposal.mdx b/docs/sdk-and-tools/sdk-js/cookbook/governance/vote-close-proposal.mdx new file mode 100644 index 000000000..b9a8e79f9 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/governance/vote-close-proposal.mdx @@ -0,0 +1,252 @@ +--- +title: Vote and close a proposal +description: Vote on and close a MultiversX governance proposal with sdk-core's GovernanceController and factory, reading a live proposal's tallies first. +difficulty: advanced +est_minutes: 9 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - governance + - transaction + - devnet +--- + +After a proposal is created (see +[Create a governance proposal](/sdk-and-tools/sdk-js/cookbook/governance/create-proposal)), +it needs votes while its window is open, and closing once the window ends. This +recipe **votes** (yes / no / abstain / veto, weighted by staked voting power) and +**closes** a proposal, each via the controller and the factory, and reads a live +proposal's tallies and status first. + +The default `npm start` reads the latest real proposal on devnet, its vote counts, +whether it is closed, and whether it passed, so you see real governance data +without a wallet. + +## Prerequisites + +- Node.js >= 20.13.1. +- For the default read and `payload` demos: devnet network access only. +- For an actual vote: a devnet PEM with staked or delegated EGLD (voting power). A + wallet with no stake has zero voting power and cannot vote. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/governance-vote-close-proposal +npm install +npm run build +``` + +## Voting and closing + +```ts title="src/voteClose.ts" +// src/voteClose.ts - the subject of this recipe: the rest of a governance +// proposal's life after it is created (see the create-proposal recipe): +// VOTING on it while its window is open, and CLOSING it once the window ends. +// +// vote -> cast yes / no / abstain / veto, weighted by your staked +// voting power. One transaction per voter. +// closeProposal -> after the end epoch, settle the proposal and release the +// fee back to the proposer (if it was not lost). +// +// Both go to the governance system contract and set their own gas limit from +// the SDK config. Voting requires staked or delegated EGLD - a wallet with no +// stake has zero voting power and its vote (and `getVotingPower`) is rejected. + +import { Account, Vote } from '@multiversx/sdk-core'; +import type { Address, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +export interface ProposalView { + nonce: number; + commitHash: string; + issuer: string; + startVoteEpoch: number; + endVoteEpoch: number; + numYesVotes: bigint; + numNoVotes: bigint; + numAbstainVotes: bigint; + numVetoVotes: bigint; + isClosed: boolean; + isPassed: boolean; +} + +/** Read one proposal's tallies and status by its nonce. */ +export async function readProposal(entrypoint: DevnetEntrypoint, proposalNonce: number): Promise { + const controller = entrypoint.createGovernanceController(); + const info = await controller.getProposal(proposalNonce); + return { + nonce: info.nonce, + commitHash: info.commitHash, + issuer: info.issuer.toBech32(), + startVoteEpoch: info.startVoteEpoch, + endVoteEpoch: info.endVoteEpoch, + numYesVotes: info.numYesVotes, + numNoVotes: info.numNoVotes, + numAbstainVotes: info.numAbstainVotes, + numVetoVotes: info.numVetoVotes, + isClosed: info.isClosed, + isPassed: info.isPassed, + }; +} + +/** The address's current governance voting power (needs staked/delegated EGLD). */ +export async function readVotingPower(entrypoint: DevnetEntrypoint, address: Address): Promise { + const controller = entrypoint.createGovernanceController(); + return controller.getVotingPower(address); +} + +/** Vote on a proposal, path 1 - the controller. */ +export async function voteViaController( + entrypoint: DevnetEntrypoint, + voter: Account, + proposalNonce: number, + vote: Vote, +): Promise { + const controller = entrypoint.createGovernanceController(); + return controller.createTransactionForVoting(voter, voter.getNonceThenIncrement(), { proposalNonce, vote }); +} + +/** Vote on a proposal, path 2 - the factory (you set nonce + signature). */ +export async function voteViaFactory( + entrypoint: DevnetEntrypoint, + voter: Account, + proposalNonce: number, + vote: Vote, +): Promise { + const factory = entrypoint.createGovernanceTransactionsFactory(); + const transaction = await factory.createTransactionForVoting(voter.address, { proposalNonce, vote }); + transaction.nonce = voter.getNonceThenIncrement(); + transaction.signature = await voter.signTransaction(transaction); + return transaction; +} + +/** Close a proposal whose window has ended, path 1 - the controller. */ +export async function closeViaController( + entrypoint: DevnetEntrypoint, + closer: Account, + proposalNonce: number, +): Promise { + const controller = entrypoint.createGovernanceController(); + return controller.createTransactionForClosingProposal(closer, closer.getNonceThenIncrement(), { proposalNonce }); +} + +/** Close a proposal whose window has ended, path 2 - the factory. */ +export async function closeViaFactory( + entrypoint: DevnetEntrypoint, + closer: Account, + proposalNonce: number, +): Promise { + const factory = entrypoint.createGovernanceTransactionsFactory(); + const transaction = await factory.createTransactionForClosingProposal(closer.address, { proposalNonce }); + transaction.nonce = closer.getNonceThenIncrement(); + transaction.signature = await closer.signTransaction(transaction); + return transaction; +} + +export interface GovernancePayload { + function: string; + args: string[]; + receiver: string; + gasLimit: bigint; +} + +/** Decode a vote / closeProposal transaction's wire fields. */ +export function describeGovernancePayload(transaction: Transaction): GovernancePayload { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + function: parts[0] ?? '', + args: parts.slice(1), + receiver: transaction.receiver.toBech32(), + gasLimit: transaction.gasLimit, + }; +} +``` + +## Run it + +```bash +# Read the latest live proposal (tallies + status) - no wallet, no funds: +npm start + +# Inspect the vote and close wire payloads offline: +npm start -- payload + +# Actually vote yes on the latest proposal (needs a staked devnet PEM); --close +# to close, --factory for the factory path, --no/--abstain/--veto to change vote: +npm start -- vote ./wallet.pem +``` + +Output of the default read mode, and of `payload`: + +```text +Latest proposal nonce: 138 + +Proposal #138 + commitHash: c8b6f734c248d5620aa6a045b8975b6d5e119314 + issuer: erd107uaynrvf80g4zuym4fqqh5pqzvaczdryj49zr2qew57wqe3mvusupj8xh + vote window: epoch 5133 .. 5133 + yes: 2633194709716022500000 + no: 120000000000000000000000 + abstain: 1349700998243378049064 + veto: 0 + closed: true passed: false + +vote: vote@8a@796573 (nonce hex | vote string hex, "yes" = 796573) gasLimit 5171000 +closeProposal: closeProposal@8a (nonce hex) gasLimit 50074000 +receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla (the governance system contract, for both) +``` + +## How it works + +**Controller vs factory.** +`controller.createTransactionForVoting(account, nonce, { proposalNonce, vote })` +builds, sets the nonce, and signs; the factory form only builds. `closeProposal` +is the same shape with just `{ proposalNonce }`. Neither takes an ABI, and both set +their own gas from the SDK config. + +**The vote is a typed enum.** Pass `Vote.YES` / `Vote.NO` / `Vote.ABSTAIN` / +`Vote.VETO` (imported from `@multiversx/sdk-core`). On the wire it is the utf8 +string of that value, `vote@@796573` is a yes vote (`796573` = "yes"). Your +vote's weight is your staked voting power, not one-address-one-vote. + +**Reading a proposal.** `getProposal(nonce)` returns the issuer, commit hash, vote +window, the four vote tallies, and `isClosed` / `isPassed`. The recipe reads +`getConfig().lastProposalNonce` to find the latest proposal, then reads it. + +## Pitfalls + +:::warning[Pitfall 1: voting needs voting power, and getVotingPower throws without it] +Governance votes are weighted by staked or delegated EGLD. An address with no stake +has zero voting power, and its vote is rejected on-chain. Worse, +`getVotingPower(address)` does not return `0` for such an address, it throws `not +enough stake/delegate to vote` (sdk-core v15.4.1). Guard the call, and stake before +voting. +::: + +:::note[Pitfall 2: vote only while open, close only after the window ends] +A vote lands only between `startVoteEpoch` and `endVoteEpoch`; `closeProposal` +works only after `endVoteEpoch`. Reading the proposal (as this recipe does) tells +you which phase it is in, `isClosed` and the vote window, before you spend gas. +::: + +:::note[Pitfall 3: anyone can close, and closing releases the fee] +Closing is not restricted to the proposer; any account can close a proposal whose +window has ended. Closing settles the result and returns the proposal fee (minus +the lost-proposal fee if it did not pass) to the issuer. +::: + +:::note[Pitfall 4: governance sets its own gas] +As with creating a proposal, `vote` and `closeProposal` set their gas limit +automatically (`gasLimitForVote` + a voting extra, and +`gasLimitForClosingProposal`). There is no `gasLimit` parameter to pass. +::: + +## See also + +- [Create a governance proposal](/sdk-and-tools/sdk-js/cookbook/governance/create-proposal) + is where the proposal you vote on comes from. +- [Read a delegation contract's state](/sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract) + reads the delegated stake that is one source of the voting power a vote needs. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/index.mdx b/docs/sdk-and-tools/sdk-js/cookbook/index.mdx new file mode 100644 index 000000000..efd42a4db --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/index.mdx @@ -0,0 +1,25 @@ +--- +title: MultiversX SDK cookbook +sidebar_label: Overview +description: Recipes for building on MultiversX with the JavaScript, TypeScript, and Rust SDKs. One task each, every file you need, checked in CI. +slug: /sdk-and-tools/sdk-js/cookbook +hide_table_of_contents: true +--- + +import CookbookIndex from "@site/src/components/cookbook/CookbookIndex"; +import manifest from "@site/src/data/cookbook-manifest.json"; + +Recipes for building on MultiversX with the JavaScript, TypeScript, and Rust +SDKs. Each one does a single thing, shows every file it needs, and drops straight +into your project. + +The code on every page is checked in CI, so the **Verified** badge tells you it +compiled clean and when. + +:::tip[Building with a coding agent?] +Point your agent at the docs and let it read the recipes directly. +[Agent or agentic engineer? Start here](./agents.mdx) covers the machine-readable +surface (`llms.txt`) and how to wire the cookbook into an agentic workflow. +::: + + diff --git a/docs/sdk-and-tools/sdk-js/cookbook/migration/dapp-provider-to-initapp.mdx b/docs/sdk-and-tools/sdk-js/cookbook/migration/dapp-provider-to-initapp.mdx new file mode 100644 index 000000000..457ae2344 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/migration/dapp-provider-to-initapp.mdx @@ -0,0 +1,352 @@ +--- +title: "Migration: DappProvider to initApp, side-by-side" +description: The full-app version of the DappProvider removal, every file that touches it, including the session-restore state v4 quietly handled for you. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-dapp: "^5.6.0" +tags: + - sdk-dapp + - migration + - v4-to-v5 + - react + - typescript +--- + +This Cookbook's +[Migrate v4 to v5: hook-by-hook diffs](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration) +shows the `` to `initApp()` change as a single before/after +component. That is the compressed version. Here is the same change at +the scale it actually happens at, a whole entry point plus its provider tree, +because that is where the real migration cost lives: not the line that changed, +but everything that now has to be sequenced around it. + +Come here once you are actually doing the `` removal and want to +see every file that touches it. Start with the hook-by-hook overview first if you +have not already. + +## Prerequisites + +- An existing sdk-dapp v4.x codebase using ``. +- Familiarity with React `useEffect` and component lifecycles. +- Node.js >= 20.13.1. + +## Before (v4): what DappProvider did implicitly + +The v4 entry point wraps the app in a declarative component. These two files are +v4 code and are shown for contrast only; they are not part of the compiled recipe. + +```tsx +// before/App.tsx (v4) +``` + +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed in this +// recipe (we only install v5 packages — see package.json). This file is +// reference material, not compiled or linted. Excluded via tsconfig.json +// `exclude` and .eslintrc.cjs `ignorePatterns`. +// +// before/App.tsx — the entire v4 app tree, wrapped once at the root. +// +// did five things under the hood that v5 makes explicit +// (see after/Providers.tsx's header comment for the v5 side of each): +// 1. Read `environment` and picked the matching network config. +// 2. Restored a previous session from sessionStorage/localStorage, if any. +// 3. Registered the WebSocket transaction-status listener. +// 4. Made every hook in `hooks/` work anywhere inside the tree. +// 5. Exposed `customNetworkConfig` for WalletConnect's project ID and any +// API-address overrides. +// +// None of this was awaited by the caller — rendered +// synchronously and hooks like useGetIsLoggedIn() simply returned `false` +// (or stale-but-safe defaults) until the async restore finished, then +// re-rendered. There was no explicit "not ready yet" state to handle. + +import { DappProvider } from '@multiversx/sdk-dapp/wrappers'; +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/types'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/hooks'; +import { ExtensionLoginButton } from '@multiversx/sdk-dapp/UI'; + +const customNetworkConfig = { + name: 'customConfig', + walletConnectV2ProjectId: 'multiversx-cookbook-demo', +}; + +function Dashboard() { + // This hook "just works" here because it's rendered inside + // , below in the tree. v4 didn't distinguish + // "provider mounted" from "session restore finished" — both + // logged-out and still-restoring render as isLoggedIn === false. + const isLoggedIn = useGetIsLoggedIn(); + + if (!isLoggedIn) { + return ; + } + + return

Connected.

; +} + +export function App() { + return ( + + + + ); +} +``` + +```tsx +// before/index.tsx (v4) +``` + +```tsx +// @ts-nocheck — illustrative v4 code; see App.tsx's header comment. +// +// before/index.tsx — v4 entry point. +// +// Nothing app-specific happens here beyond the standard React root mount. +// All of the SDK setup is inside because it's a component +// (), not an imperative call — that's the whole point of +// this recipe. Contrast with after/index.tsx, which is almost as short but +// for a different reason: the setup moved into Providers.tsx instead. + +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error('No #root element found in index.html'); +} + +createRoot(rootElement).render( + + + , +); +``` + +## After (v5): initApp made explicit + +The v5 entry point calls `initApp()` inside a `Providers` wrapper, then renders. +These three files compile against the installed v5 SDK. + +```tsx title="after/Providers.tsx" +// after/Providers.tsx — v5 imperative init, side-by-side with before/App.tsx. +// +// Same verified pattern as recipes/vite-react-minimal/src/providers.tsx — +// copied here (not cross-imported; each Cookbook recipe is self-contained) +// because the WHOLE POINT of this recipe is to show this file existing at +// all, where v4 had no equivalent. Read this alongside before/App.tsx: +// every one of 's five under-the-hood jobs (listed in that +// file's header comment) now has an explicit, visible line of code here: +// +// 1. Environment selection -> dappConfig.dAppConfig.environment +// 2. Session restore -> awaited inside initApp(); ready gate +// below makes the "still restoring" +// state explicit, unlike v4. +// 3. WebSocket listener -> registered internally by initApp() +// when a restored session exists +// (during initApp() startup). +// 4. Hooks work anywhere -> still true, but only for descendants +// of , and only after `ready` +// flips true. +// 5. WalletConnect project ID -> dappConfig.dAppConfig.providers.walletConnect +// +// The one thing v4 did NOT make you handle: an explicit "not ready yet" +// render. v5's initApp() is a Promise; until it resolves, hooks return +// defaults that are indistinguishable from "logged out" (same as v4 during +// restore) — but now the developer decides whether to show a loading +// state instead of silently rendering "logged out" for a few hundred ms. +// This recipe chooses to show one (see the `!ready` branch below); +// omitting it is also valid and matches v4's actual behavior more closely. + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + // theme takes one of the enum's string values; the short `'dark'` fails — see + // node_modules/@multiversx/sdk-dapp/out/methods/initApp/initApp.types.d.ts. + // A `theme: 'dark' as const` cast (what the naive port of the v4 prop + // would look like) does not satisfy InitAppType; this was a real, + // confirmed tsc --strict failure found while verifying this Cookbook's + // start-here recipes. + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: 'multiversx-cookbook-demo', + }, + }, + }, +}; + +// Module-scope flag prevents double-init under React Strict Mode, which +// deliberately double-invokes effects in dev. v4 needed no equivalent +// because was a component, not an effect. +let initStarted = false; + +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; + + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + setReady(true); + }); + + return () => { + cancelled = true; + }; + }, []); + + if (!ready) { + return
Initializing…
; + } + return <>{children}; +} +``` + +```tsx title="after/App.tsx" +// after/App.tsx — the v5 half of the side-by-side, structurally identical +// to before/App.tsx's child: read login state, render a login +// button or a connected message. The difference this recipe is actually +// about is entirely in Providers.tsx / index.tsx — this file is here to +// prove the hook still "just works" once it's inside , same +// promise v4 made inside . +// +// For the full connect/disconnect button and account-field walkthrough, +// see the wallets/wallet-login-button and wallets/read-connected-account +// recipes — this file stays intentionally minimal so the provider/init +// contrast above isn't buried under unrelated UI code. + +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; + +export function App(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + + const handleConnect = (): void => { + // openUnlockPanel() returns Promise; this handler is a sync + // onClick, so the promise is explicitly discarded to satisfy + // @typescript-eslint/no-floating-promises. + void UnlockPanelManager.getInstance().openUnlockPanel(); + }; + + if (!isLoggedIn) { + return ( + + ); + } + + return

Connected.

; +} +``` + +```tsx title="after/index.tsx" +// after/index.tsx — v5 entry point. UnlockPanelManager also needs a one- +// time init() call somewhere; the Next.js / Vite minimal recipes do this +// inside Providers.tsx right after initApp() resolves. This recipe's +// Providers.tsx keeps that out to stay focused purely on the initApp() +// side of the story — see wallets/wallet-login-button for the +// UnlockPanelManager.init({ loginHandler, onClose }) call this app would +// need before openUnlockPanel() does anything useful in a real app. + +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; +import { Providers } from './Providers'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error('No #root element found in index.html'); +} + +createRoot(rootElement).render( + + + + + , +); +``` + +## What `` actually did, made explicit + +`` did five things implicitly that v5 makes explicit: + +| v4 (``, implicit) | v5 (`initApp()`, explicit) | +| --- | --- | +| Environment selection | `dappConfig.dAppConfig.environment` | +| Session restore | Awaited inside `initApp()`; this recipe's `ready` gate makes the "still restoring" state visible | +| WebSocket listener | Registered internally by `initApp()` when a restored session exists | +| Hooks work anywhere | Still true, but only for descendants of ``, and only after `ready` flips `true` | +| WalletConnect project ID | `dappConfig.dAppConfig.providers.walletConnect.walletConnectV2ProjectId` | + +The one thing v4 never made you handle explicitly: a distinct "not ready yet" +render. `initApp()` is a Promise; until it resolves, hooks return defaults +indistinguishable from "logged out", same as v4 during restore, but v5 hands you +the choice of showing a loading state instead of letting that ambiguous instant +flash by. + +:::tip[Reused, not reinvented] +`after/Providers.tsx` is copied from the already-verified +[Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) +pattern, not written fresh for this recipe. That pattern is proven to compile and run. +::: + +## Pitfalls + +:::danger[Pitfall 1: module-scope initApp() calls crash under SSR] +If you lift the `useEffect` body to module scope (or call `initApp()` directly in +a Next.js Server Component), it crashes during server-side rendering. `initApp()` +touches `sessionStorage`, which does not exist on the server. See +[Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) +Pitfall 3 for the `"use client"` plus `useEffect` fix. +::: + +:::warning[Pitfall 2: React Strict Mode double-invokes useEffect in dev] +Without the module-scope `initStarted` guard, `initApp()` fires twice on first +mount in development. `initApp()` is idempotent, but the guard avoids a redundant +round of session-restore work. +::: + +:::note[Pitfall 3: v4 hid that restore is async; v5 does not have to] +The `ready` gate in `Providers.tsx` is optional. Skipping it means your app's +first render always shows "logged out", then flips to "logged in" a moment later +for a returning user. Decide deliberately whether that flash is acceptable, rather +than inheriting v4's behavior by default. +::: + +## See also + +- [Migrate v4 to v5: hook-by-hook diffs](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration) + is the compressed six-pair overview this recipe expands on. +- [Migration: useGetAccountInfo v4 vs v5](/sdk-and-tools/sdk-js/cookbook/migration/get-account-info-v4-vs-v5) + is the next thing that breaks once your provider tree compiles. +- [Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) + is the target shape `after/` is drawn from directly. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/migration/get-account-info-v4-vs-v5.mdx b/docs/sdk-and-tools/sdk-js/cookbook/migration/get-account-info-v4-vs-v5.mdx new file mode 100644 index 000000000..d528af26a --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/migration/get-account-info-v4-vs-v5.mdx @@ -0,0 +1,224 @@ +--- +title: "Migration: useGetAccountInfo v4 vs v5" +description: The same hook name still resolves in v5 but returns less data, silently. The easiest migration bug to miss because nothing forces a second look at it. +difficulty: intermediate +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-dapp: "^5.6.0" +tags: + - sdk-dapp + - migration + - v4-to-v5 + - react + - typescript +--- + +The +[hook-by-hook migration overview](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration) +shows the v4 shape and the v5-recommended replacement for `useGetAccountInfo`. +This recipe adds the piece that overview leaves out: **`useGetAccountInfo` is not +removed in v5**. The same import name still resolves, still compiles, and still +returns real data. It just returns *less* data, silently, which makes this one of +the easiest migration bugs to miss entirely. + +## Prerequisites + +- An existing sdk-dapp v4.x codebase calling `useGetAccountInfo()`. +- Node.js >= 20.13.1. + +## Before (v4): one hook returned everything + +v4's `useGetAccountInfo()` returned address, balance, nonce, `isLoggedIn`, and +`tokenLogin` from a single call. This is v4 code, shown for contrast only. + +```tsx +// before.tsx (v4) +``` + +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed. +// +// before.tsx — v4's useGetAccountInfo(): one hook, one big object. +// address + balance/nonce + login flag + auth token, all in one place. + +import { useGetAccountInfo } from '@multiversx/sdk-dapp/hooks'; + +export function AccountSummary() { + const { + address, + account: { balance, nonce }, + isLoggedIn, + tokenLogin, + } = useGetAccountInfo(); + + if (!isLoggedIn) { + return

Not logged in.

; + } + + return ( +
+

Address: {address}

+

Balance: {balance}

+

Nonce: {nonce}

+

Token: {tokenLogin?.nativeAuthToken}

+
+ ); +} +``` + +## After (v5): same name, thinner shape + +The same import name still resolves under its new `out/react/account/...` path, +same call-site shape, but `isLoggedIn` and `tokenLogin` are gone from the return +type. `tsc --strict` only catches this if you actually destructure those fields. + +```tsx title="after-shim.tsx" +// after-shim.tsx — the SAME import name resolves in v5, but the shape it +// returns changed. This is the trap this recipe exists to name: a v4 +// codebase that only bulk-renamed import paths (the "smallest viable +// patch" from the v4-to-v5-migration recipe) will still find +// `useGetAccountInfo` at +// `@multiversx/sdk-dapp/out/react/account/useGetAccountInfo` and will +// still compile a call to it — right up until it reaches for +// `.isLoggedIn` or `.tokenLogin`, which no longer exist on the return +// type. `tsc --strict` catches this the moment you destructure a field +// that's gone; it will NOT catch it if you only ever access `.address` or +// `.account`, which still work identically. That's what makes this a +// sneaky migration bug rather than an obvious one: the hook doesn't +// disappear, so nothing forces you to look at it. +// +// Confirmed against the real, installed +// `@multiversx/sdk-dapp v5` `.d.ts`: the v5 shape is +// { address, account, publicKey, ledgerAccount, walletConnectAccount, +// websocketEvent, websocketBatchEvent } +// — no `isLoggedIn`, no `tokenLogin`. Both moved to useGetLoginInfo() / +// useGetIsLoggedIn() — see after-recommended.tsx. + +import { useGetAccountInfo } from '@multiversx/sdk-dapp/out/react/account/useGetAccountInfo'; + +export function AccountSummaryShim(): JSX.Element { + const { address, account } = useGetAccountInfo(); + + // The next two lines are what the v4 code looked like. Uncommenting + // either one is a real, reproducible tsc --strict failure against the + // installed v5 types — left here as comments rather than deleted, so + // the failure is visible without needing to break the build to see it: + // + // const { isLoggedIn } = useGetAccountInfo(); + // // error TS2339: Property 'isLoggedIn' does not exist on type + // // 'AccountInfoType'. + // + // const { tokenLogin } = useGetAccountInfo(); + // // error TS2339: Property 'tokenLogin' does not exist on type + // // 'AccountInfoType'. + + return ( +
+

Address: {address}

+

Balance: {account.balance}

+

Nonce: {account.nonce}

+

+ Not shown: login flag and auth token — this shim no longer carries + them. See after-recommended.tsx. +

+
+ ); +} +``` + +## After (v5): the recommended three-hook split + +The recommended replacement: `useGetAccount()` plus `useGetIsLoggedIn()` plus +`useGetLoginInfo()`. + +```tsx title="after-recommended.tsx" +// after-recommended.tsx — the v5-recommended replacement: three hooks, +// one per concern, instead of reaching for the still-exists-but-thinner +// useGetAccountInfo() shim in after-shim.tsx. +// +// Migration rule of thumb: +// address, balance, nonce, shard, username -> useGetAccount() +// isLoggedIn -> useGetIsLoggedIn() +// tokenLogin, providerType, expires, loginMethod -> useGetLoginInfo() + +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetLoginInfo } from '@multiversx/sdk-dapp/out/react/loginInfo/useGetLoginInfo'; + +export function AccountSummaryRecommended(): JSX.Element { + const { address, balance, nonce } = useGetAccount(); + const isLoggedIn = useGetIsLoggedIn(); + const { tokenLogin } = useGetLoginInfo(); + + if (!isLoggedIn) { + return

Not logged in.

; + } + + return ( +
+

Address: {address}

+

Balance: {balance}

+

Nonce: {nonce}

+

Token: {tokenLogin?.nativeAuthToken}

+
+ ); +} +``` + +## Why the shim is the dangerous one + +A codebase migrated only via the "smallest viable patch" (bulk find-and-replace +of import paths) keeps calling `useGetAccountInfo()` under its new +`out/react/account/...` path. If that codebase only ever reads `.address` or +`.account`, **it keeps compiling and keeps working**. Nothing forces a second look +at this hook. The bug surfaces only when code reaches for `.isLoggedIn` or +`.tokenLogin`, at which point `tsc --strict` reports a real, specific error (see +the commented-out lines in `after-shim.tsx` for the exact message). + +Confirmed against the real, installed `@multiversx/sdk-dapp v5` `.d.ts`: the +v5 return type is +`{ address, account, publicKey, ledgerAccount, walletConnectAccount, websocketEvent, websocketBatchEvent }`, +with no `isLoggedIn` and no `tokenLogin`. + +**Practical takeaway:** grep your v4 codebase for every `useGetAccountInfo()` call +site and check what it destructures, rather than trusting that "it still compiles" +means "it still works the same way." + +## How it works + +`useGetAccount()`, `useGetIsLoggedIn()`, and `useGetLoginInfo()` read three +different Zustand store slices, each with a different update cadence: account +balance/nonce change per transaction, the login flag changes twice per session, +and the login info (auth token, provider type) changes only on login. v4's +`useGetAccountInfo()` bundled all three into one subscription, so any change to any +of the three re-rendered every consumer. v5's split means a component that only +cares about the balance does not re-render when the auth token refreshes, a real +performance improvement that falls out of doing the migration properly instead of +leaning on the shim indefinitely. + +## Pitfalls + +:::danger[Pitfall 1: "it still compiles" is not proof the migration is done] +`useGetAccountInfo` surviving the rename means nothing here forces a re-read. +Audit every call site's destructuring, not just whether the import resolves. +::: + +:::warning[Pitfall 2: isLoggedIn and tokenLogin move to different hooks entirely] +There is no `useGetAccountInfo().isLoggedIn` replacement shortcut; you need the +separate `useGetIsLoggedIn()` / `useGetLoginInfo()` calls. +::: + +:::note[Pitfall 3: don't assume every surviving v4 hook kept its full shape] +This Cookbook found the same "same name, thinner shape" pattern is worth checking +case-by-case per hook, not assumed either way. +::: + +## See also + +- [Migrate v4 to v5: hook-by-hook diffs](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration) + is the six-pair overview this recipe expands on. +- [Migration: DappProvider to initApp](/sdk-and-tools/sdk-js/cookbook/migration/dapp-provider-to-initapp) + is the structural change that usually comes first in a real migration. +- [Read the connected account](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + is the full `useGetAccount()` field reference. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/migration/send-transactions-to-transaction-manager.mdx b/docs/sdk-and-tools/sdk-js/cookbook/migration/send-transactions-to-transaction-manager.mdx new file mode 100644 index 000000000..813f4eafd --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/migration/send-transactions-to-transaction-manager.mdx @@ -0,0 +1,275 @@ +--- +title: "Migration: useSendTransactions to TransactionManager.send" +description: The v4 hook this migration is named for does not exist in the real package. What does exist is batch sends and a v5 nested-array trigger. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-dapp: "^5.6.0" +tags: + - sdk-dapp + - migration + - v4-to-v5 + - transaction + - typescript +--- + +## A correction before this recipe starts + +Some migration notes name the v4 hook this recipe replaces as +`useSendTransactions()`. While authoring this recipe we checked the real, +published `@multiversx/sdk-dapp@4.6.4` (the last v4 release, fetched from the npm +registry and unpkg): **no hook by that exact name exists anywhere in it.** The two +real v4 surfaces for sending transactions are: + +- the plain `sendTransactions()` function from + `services/transactions/sendTransactions`, already covered by + [Migrate v4 to v5: hook-by-hook diffs](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration)'s + hook-by-hook pair, for the single-array case. +- `useSendBatchTransactions()`, a dedicated hook for grouped/sequential sends, + which that pair does not cover. + +This recipe covers the second one, in depth, since it maps onto a genuinely +distinct v5 capability (nested-array batch sends). + +## Prerequisites + +- An existing sdk-dapp v4.x codebase using `useSendBatchTransactions()`. +- Node.js >= 20.13.1. +- Familiarity with + [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send); + this recipe assumes the single-transaction flow and focuses on what changes for groups. + +## Before (v4): the dedicated batch-sending hook + +This is v4 code, shown for contrast only. + +```tsx +// before.tsx (v4) — useSendBatchTransactions() +``` + +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed. +// +// before.tsx — v4's dedicated batch-sending hook. +// +// A note on the name of this recipe: some migration notes say +// "useSendTransactions() replaced by +// TransactionManager.getInstance().send()". Checked against the real, +// published @multiversx/sdk-dapp@4.6.4 (the last v4 release, fetched from +// the npm registry / unpkg while authoring this recipe): there is no hook +// literally named useSendTransactions anywhere in that package. The two +// real v4 surfaces for sending transactions are: +// - the plain `sendTransactions()` function from +// `services/transactions/sendTransactions` — already covered by +// the v4-to-v5-migration recipe's migrations/04-send-transactions/ +// pair, for the single-array case. +// - `useSendBatchTransactions()`, shown below — a dedicated HOOK for +// grouped/sequential sends, which pair 04 does not cover at all. +// This recipe covers the batch hook specifically, since it's the one +// real gap pair 04 leaves open, and it maps onto a genuinely distinct v5 +// capability (see after.tsx) rather than repeating pair 04's content. + +import { useSendBatchTransactions } from '@multiversx/sdk-dapp/hooks/transactions/batch/useSendBatchTransactions'; + +export function useBatchStakeFlow() { + const { send, batchId } = useSendBatchTransactions(); + + const submitBatch = async (transactions: unknown[]) => { + const result = await send({ + transactions, + transactionsDisplayInfo: { + processingMessage: 'Processing batch…', + successMessage: 'Batch complete', + errorMessage: 'Batch failed', + }, + }); + + if ('error' in result && result.error) { + throw new Error(result.error); + } + + return result.batchId; + }; + + return { submitBatch, batchId }; +} +``` + +## After (v5): TransactionManager with a nested array + +```tsx title="after.tsx" +// after.tsx — v5's grouped-send path: a nested Transaction[][] fed to the +// SAME TransactionManager.send()/.track() calls the flat, single-group +// case uses (see v4-to-v5-migration/migrations/04-send-transactions/after.tsx +// for that simpler case). +// +// Confirmed directly from the installed +// node_modules/@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager.d.ts +// and its compiled .cjs: +// +// send(signedTransactions: Transaction[] | Transaction[][]): +// Promise +// track(sentTransactions: SignedTransactionType[] | SignedTransactionType[][], options?): +// Promise +// +// `isBatchTransaction(t)` (the internal helper send() calls first) is +// exactly `Array.isArray(t[0])` — a NESTED array is what selects batch +// mode. Concretely, from reading the compiled source: +// - Flat Transaction[] -> send() POSTs each transaction individually +// to `${apiAddress}/transactions` (parallel, +// one request per tx). +// - Nested Transaction[][] -> send() POSTs the WHOLE nested structure +// ONCE to `${apiAddress}/batch`, as +// { transactions: [[...], [...]], id: batchId }. +// The outer arrays are what the source calls "sequential" groups +// (`getIsSequential` / `sequentialToFlatArray`) — that naming strongly +// implies the API processes group 1 before group 2, but this recipe only +// verifies the CLIENT-SIDE contract (the type signature, the batch +// trigger, and the endpoint choice) by reading the compiled source; it +// does not independently confirm server-side group ordering against a +// live multi-group broadcast, which would need funded wallets and is out +// of scope here. Treat "sequential" as the SDK's own claim, not this +// recipe's independently-verified one. +// +// track() ALWAYS flattens the structure back down +// (`sequentialToFlatArray`, triggered when every top-level element is +// itself an array) before building the transaction session — so whether +// you sent flat or grouped, you get exactly ONE sessionId covering every +// transaction in the call, not one per group. + +import { Transaction } from '@multiversx/sdk-core'; +import { TransactionManager } from '@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; + +/** + * Splits a flat array back into the group sizes it came from. Used below + * to re-nest a signed, flat array of transactions after signing — signing + * itself only accepts a flat Transaction[] (see Pitfall 1), so grouping + * has to happen before signing (to know the sizes) and again after (to + * rebuild the nested shape send()/track() expect for batch mode). + */ +function regroup(flat: T[], groupSizes: number[]): T[][] { + const groups: T[][] = []; + let offset = 0; + for (const size of groupSizes) { + groups.push(flat.slice(offset, offset + size)); + offset += size; + } + return groups; +} + +/** + * Sends `groups` — e.g. [[approveTx1, approveTx2], [claimTx]] — as one + * grouped submission, and tracks all of it under a single sessionId. + * + * Replaces v4's useSendBatchTransactions() hook (see before.tsx). Unlike + * the hook, this is plain async code — usable from anywhere + * getAccountProvider() works (a click handler, a CLI-adjacent browser + * script, a service worker), not tied to a React render. + */ +export async function sendGrouped(groups: Transaction[][]): Promise { + const groupSizes = groups.map((g) => g.length); + const flat = groups.flat(); + + // 1. Sign. signTransactions() only accepts a flat array — see Pitfall 1. + const provider = getAccountProvider(); + const signedFlat = await provider.signTransactions(flat); + + // 2. Re-nest using the ORIGINAL group sizes, now that everything is + // signed. The order signTransactions() returns is stable (one + // signed transaction per input, same index), so slicing by the + // sizes we recorded before flattening reconstructs the same groups. + const signedGroups = regroup(signedFlat, groupSizes); + + // 3. Send. A nested array here (signedGroups is Transaction[][]) makes + // isBatchTransaction() return true, which routes this call to the + // dedicated POST /batch endpoint instead of N individual + // POST /transactions calls. + const txManager = TransactionManager.getInstance(); + const sent = await txManager.send(signedGroups); + + // 4. Track. One call, one sessionId, even though the input was nested — + // track() flattens internally before building the session. + const sessionId = await txManager.track(sent, { + transactionsDisplayInfo: { + processingMessage: 'Processing batch…', + successMessage: 'Batch complete', + errorMessage: 'Batch failed', + }, + }); + + return sessionId; +} +``` + +## The v5 mechanism: a nested array picks batch mode + +Confirmed directly from the installed +`node_modules/@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager.d.ts` +and its compiled source: + +```ts +send(signedTransactions: Transaction[] | Transaction[][]): + Promise + +track(sentTransactions: SignedTransactionType[] | SignedTransactionType[][], options?): + Promise +``` + +The internal `isBatchTransaction(t)` check is exactly `Array.isArray(t[0])`. +Passing a **nested** array selects batch mode: + +| Input shape | What `send()` actually does | +| --- | --- | +| Flat `Transaction[]` | POSTs each transaction individually to `${apiAddress}/transactions` (parallel, one request per tx) | +| Nested `Transaction[][]` | POSTs the whole nested structure **once** to `${apiAddress}/batch`, as `{ transactions: [[...], [...]], id: batchId }` | + +The outer groups are what the compiled source's own internal naming calls +"sequential" (`getIsSequential` / `sequentialToFlatArray`), naming that implies the +API processes group 1 before group 2. This recipe verifies the **client-side +contract** (the type signature, the batch trigger, and the endpoint choice) by +reading the compiled source directly; it does not independently confirm +server-side group ordering against a live multi-group broadcast. + +`track()` always flattens the structure back down before building the transaction +session, so whether you sent flat or grouped, you get exactly **one** `sessionId` +covering every transaction in the call, not one per group. + +## Pitfalls + +:::danger[Pitfall 1: signTransactions() only accepts a flat array] +There is no nested-array overload for signing, confirmed from +`node_modules/@multiversx/sdk-dapp/out/providers/DappProvider/DappProvider.d.ts`: +`signTransactions(transactions: Transaction[], options?): Promise`. +This is why `after.tsx` flattens before signing and re-nests afterward, rather +than trying to sign group-by-group. +::: + +:::warning[Pitfall 2: re-nesting depends on signTransactions() preserving order and count] +`regroup()` in `after.tsx` slices the signed, flat result using the group sizes +recorded before flattening. This is correct as long as the signed array has +exactly one entry per input transaction, in the same order. +::: + +:::warning[Pitfall 3: batch mode requires a resolvable logged-in address] +Reading `TransactionManager.cjs`'s `sendSignedBatchTransactions`: it builds the +batch id from `getAccount().address` and returns an explicit error if that is +empty. The flat-array path has no equivalent check at this layer. +::: + +:::note[Pitfall 4: don't treat "sequential" as independently proven here] +See "The v5 mechanism" above; this recipe verified the client contract, not +on-chain group ordering. +::: + +## See also + +- [Migrate v4 to v5: hook-by-hook diffs](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration) + is the six-pair overview, including the flat single-array send/track case. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the canonical single-transaction sign, send, track flow this recipe extends + to groups. +- [Migration: useGetAccountInfo v4 vs v5](/sdk-and-tools/sdk-js/cookbook/migration/get-account-info-v4-vs-v5) + is another "the old name still resolves, but check the real shape" trap from the + same migration. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/multisig/propose-action.mdx b/docs/sdk-and-tools/sdk-js/cookbook/multisig/propose-action.mdx new file mode 100644 index 000000000..f5d28d611 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/multisig/propose-action.mdx @@ -0,0 +1,267 @@ +--- +title: Propose a multisig action +description: Propose a multisig action, a transfer-execute or an add-board-member, with sdk-core's MultisigController and factory, then parse the new action id. +difficulty: advanced +est_minutes: 9 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - multisig + - smart-contract + - transaction + - devnet +--- + +A multisig contract does nothing until someone proposes an action. A proposal is +just a transaction to the multisig naming what it should eventually do; it does +not execute yet. It becomes a pending action with an id, waiting for board members +to sign it to quorum and for someone to perform it (see +[Sign and perform](/sdk-and-tools/sdk-js/cookbook/multisig/sign-perform-action)). +This recipe proposes two representative actions, `proposeTransferExecute` (move +EGLD out, optionally calling a function) and `proposeAddBoardMember` (change the +board), each through the real `MultisigController` and +`MultisigTransactionsFactory`, then parses the new action id. + +The default `npm start` parses a real, already-completed devnet propose for its +action id, so you see parsing work without a funded wallet. + +## Prerequisites + +- Node.js >= 20.13.1. +- For the default `parse` and `payload` demos: devnet network access only. +- For an actual propose: a devnet PEM whose address is a proposer or board member + on the target multisig, with a little EGLD for gas. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/multisig-propose-action +npm install +npm run build +``` + +## Proposing + +```ts title="src/propose.ts" +// src/propose.ts - the subject of this recipe: proposing an action on an +// existing multisig contract, two ways (controller and factory), then parsing +// the resulting action id. +// +// A multisig contract does nothing until a *proposer* or *board member* +// proposes an action. The proposal is just a transaction to the multisig +// contract naming what it should eventually do; it does not execute yet. It +// sits as a pending action (with an id) until board members sign it to quorum +// and someone performs it (see the sign-and-perform recipe). +// +// This recipe shows two representative proposals: +// - proposeTransferExecute: move EGLD out of the multisig (optionally calling +// a function on the receiver). The everyday "spend" proposal. +// - proposeAddBoardMember: change the board itself. The everyday "governance +// of the multisig" proposal. +// Both go through the real MultisigController / MultisigTransactionsFactory. + +import { Account, Address } from '@multiversx/sdk-core'; +import type { Abi, DevnetEntrypoint, Transaction, TransactionOnNetwork } from '@multiversx/sdk-core'; + +// Gas is REQUIRED for every multisig write: the factory throws without it and +// the controller silently builds a gasLimit:0 transaction (see Pitfalls). A +// simple propose comfortably fits in 10M gas. +const PROPOSE_GAS_LIMIT = 10_000_000n; + +/** + * Propose a transfer-execute, path 1 - the controller. + * `createTransactionForProposeTransferExecute` builds, sets the nonce, and + * signs in one call. Passing only `to` + `nativeTokenAmount` (no `functionName`) + * proposes a plain EGLD transfer out of the multisig. + */ +export async function proposeTransferViaController( + entrypoint: DevnetEntrypoint, + abi: Abi, + proposer: Account, + multisigContract: string, + to: string, + amount: bigint, +): Promise { + const controller = entrypoint.createMultisigController(abi); + return controller.createTransactionForProposeTransferExecute(proposer, proposer.getNonceThenIncrement(), { + multisigContract: Address.newFromBech32(multisigContract), + to: Address.newFromBech32(to), + nativeTokenAmount: amount, + gasLimit: PROPOSE_GAS_LIMIT, + }); +} + +/** + * Propose a transfer-execute, path 2 - the factory. Builds the unsigned + * transaction only; the caller sets the nonce and signs. Use this when a + * wallet or hardware device signs. + */ +export async function proposeTransferViaFactory( + entrypoint: DevnetEntrypoint, + abi: Abi, + proposer: Account, + multisigContract: string, + to: string, + amount: bigint, +): Promise { + const factory = entrypoint.createMultisigTransactionsFactory(abi); + const transaction = await factory.createTransactionForProposeTransferExecute(proposer.address, { + multisigContract: Address.newFromBech32(multisigContract), + to: Address.newFromBech32(to), + nativeTokenAmount: amount, + gasLimit: PROPOSE_GAS_LIMIT, + }); + transaction.nonce = proposer.getNonceThenIncrement(); + transaction.signature = await proposer.signTransaction(transaction); + return transaction; +} + +/** + * Propose adding a board member (controller). A different action type that + * changes the board rather than spending funds - but the propose/sign/perform + * lifecycle is identical. + */ +export async function proposeAddBoardMemberViaController( + entrypoint: DevnetEntrypoint, + abi: Abi, + proposer: Account, + multisigContract: string, + newBoardMember: string, +): Promise { + const controller = entrypoint.createMultisigController(abi); + return controller.createTransactionForProposeAddBoardMember(proposer, proposer.getNonceThenIncrement(), { + multisigContract: Address.newFromBech32(multisigContract), + boardMember: Address.newFromBech32(newBoardMember), + gasLimit: PROPOSE_GAS_LIMIT, + }); +} + +export interface ProposePayload { + function: string; + args: string[]; + receiver: string; + valueWei: bigint; + gasLimit: bigint; +} + +/** Decode a propose transaction's wire fields. */ +export function describeProposePayload(transaction: Transaction): ProposePayload { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + function: parts[0] ?? '', + args: parts.slice(1), + receiver: transaction.receiver.toBech32(), + valueWei: transaction.value, + gasLimit: transaction.gasLimit, + }; +} + +/** + * Parse a completed propose transaction for the new action id. Every propose + * endpoint returns the freshly-assigned action id; the board will use it to + * sign and perform the action. `parseProposeAction` reads it back. + */ +export function parseProposedActionId( + entrypoint: DevnetEntrypoint, + abi: Abi, + transactionOnNetwork: TransactionOnNetwork, +): number { + const controller = entrypoint.createMultisigController(abi); + return controller.parseProposeAction(transactionOnNetwork); +} +``` + +## Run it + +```bash +# Parse a real completed devnet propose for its action id - no wallet, no funds: +npm start + +# Inspect the two propose wire payloads offline: +npm start -- payload + +# Actually propose (needs a proposer/board-member devnet PEM); add --factory or +# --add-board-member: +npm start -- propose ./wallet.pem +``` + +Output of the default `parse` mode, and of `payload`: + +```text +Parsing completed propose 6f4992b6...05a83cfc437 ... + proposed action id: 4 + (board members now sign this id; then someone performs it) + +proposeTransferExecute: + function: proposeTransferExecute + args: 6d40...e45a | 0de0b6b3a7640000 | (to hex | amount hex | empty gas option) + receiver: erd1qqqqqqqqqqqqqpgqvjcswsav2rw4ccvguyuk0f0u6qycpsuvu3dq9rse9w (the multisig contract) + value: 0 wei (0 - the moved EGLD is an argument, paid later on perform) + gasLimit: 10000000 +proposeAddBoardMember: + function: proposeAddBoardMember + args: 6d40...e45a (the new board member's public key) +``` + +## How it works + +**Controller vs factory.** +`controller.createTransactionForProposeTransferExecute(account, nonce, options)` +builds, sets the nonce, and signs in one call. +`factory.createTransactionForProposeTransferExecute(address, options)` only builds +the unsigned transaction; you set `nonce` and `signature`. Both take +`{ multisigContract, to, nativeTokenAmount, gasLimit }` and both need the multisig +ABI at construction (`entrypoint.createMultisigController(abi)`). + +**The moved amount is an argument, not the value.** `proposeTransferExecute` puts +the receiver and the EGLD amount in the data; the proposing transaction's own +`value` is `0`. The EGLD moves out of the multisig's balance later, when the action +is performed, not out of the proposer's wallet now. + +**Parsing the outcome.** Every propose endpoint returns the freshly assigned action +id. `controller.parseProposeAction(transactionOnNetwork)` reads it back; the default +`npm start` runs it against a historical propose, which is why it needs no funds. +Keep this id, it is what you sign and perform. + +## Pitfalls + +:::warning[Pitfall 1: gas is required; the controller does NOT protect you] +Every multisig write needs an explicit `gasLimit`. The factory throws `Either +provide a gasLimit parameter or initialize the factory with a gasLimitEstimator` +when it is missing, but the **controller silently coerces a missing gasLimit to +`0n`** and builds a broadcastable `gasLimit: 0` transaction that the network then +rejects for too-low gas. Confirmed against sdk-core v15.4.1. Always pass `gasLimit`. +::: + +:::note[Pitfall 2: only proposers and board members may propose] +The multisig contract accepts a proposal only from an address with the proposer or +board-member role. An unauthorized proposer is rejected by the contract at +execution, not by the SDK when building. Check roles first with +[Read a multisig's state](/sdk-and-tools/sdk-js/cookbook/multisig/read-multisig-state). +::: + +:::note[Pitfall 3: proposeTransferExecute's value is always 0] +The EGLD that a transfer-execute will move is encoded as the `nativeTokenAmount` +argument and paid from the multisig's own balance on perform. The proposing +transaction carries `value: 0`. Do not attach EGLD to the proposal expecting it to +be the transferred amount. +::: + +:::note[Pitfall 4: the propose payload's trailing empty part is the gas option] +`proposeTransferExecute@to@amount@` ends with an empty part: it is the +`Option` gas argument set to `None` (an empty top-level arg). A plain EGLD +transfer with no function call adds nothing after it. Pass `optGasLimit` / +`functionName` to fill it. +::: + +## See also + +- [Sign and perform a multisig action](/sdk-and-tools/sdk-js/cookbook/multisig/sign-perform-action) + is what happens to the action id this recipe creates. +- [Read a multisig's state](/sdk-and-tools/sdk-js/cookbook/multisig/read-multisig-state) + checks roles, quorum, and pending actions before proposing. +- [Deploy a smart contract](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/deploy-contract) + is how the multisig itself, being a contract, gets deployed. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/multisig/read-multisig-state.mdx b/docs/sdk-and-tools/sdk-js/cookbook/multisig/read-multisig-state.mdx new file mode 100644 index 000000000..8e8aa5725 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/multisig/read-multisig-state.mdx @@ -0,0 +1,214 @@ +--- +title: Read a multisig's state +description: Read a MultiversX multisig's quorum, board members, and pending actions with their signers using sdk-core's MultisigController, no wallet needed. +difficulty: intermediate +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - multisig + - smart-contract + - devnet +--- + +Before you sign or perform anything on a multisig, you read it: who is on the +board, how many signatures the quorum needs, and what is currently pending. This +recipe reads all of that with the `MultisigController`'s view helpers. None of it +needs a wallet; it is a read-only `queryContract`, decoded through the +multisig ABI. + +The reads split in two: the **board configuration** (quorum, board members, +proposers) and the **pending actions** (what has been proposed but not performed, +and for each, its signers and whether it has reached quorum). The default +`npm start` reads a real devnet multisig, so you get live output right away. + +## Prerequisites + +- Node.js >= 20.13.1 and devnet network access. No wallet, ever. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/multisig-read-state +npm install +npm run build +``` + +## Reading + +```ts title="src/read.ts" +// src/read.ts - the subject of this recipe: reading a multisig contract's +// state with the MultisigController's view helpers. None of this needs a +// wallet - it is all read-only `queryContract` under the hood, decoded through +// the multisig ABI. +// +// What you can read splits into two groups: +// - the board configuration: quorum, board members, proposers; +// - the pending actions: what has been proposed but not yet performed, and +// for each, who has signed and whether it has reached quorum. +// +// Together these answer "who controls this multisig, and what is it about to +// do?" - the questions you ask before you sign or perform anything. + +import { Address } from '@multiversx/sdk-core'; +import type { Abi, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export interface MultisigBoard { + quorum: number; + numBoardMembers: number; + numProposers: number; + actionLastIndex: number; + boardMembers: string[]; + proposers: string[]; +} + +/** Read the board configuration: quorum and membership. */ +export async function readBoard( + entrypoint: DevnetEntrypoint, + abi: Abi, + multisigAddress: string, +): Promise { + const controller = entrypoint.createMultisigController(abi); + const [quorum, numBoardMembers, numProposers, actionLastIndex, boardMembers, proposers] = await Promise.all([ + controller.getQuorum({ multisigAddress }), + controller.getNumBoardMembers({ multisigAddress }), + controller.getNumProposers({ multisigAddress }), + controller.getActionLastIndex({ multisigAddress }), + controller.getAllBoardMembers({ multisigAddress }), + controller.getAllProposers({ multisigAddress }), + ]); + return { quorum, numBoardMembers, numProposers, actionLastIndex, boardMembers, proposers }; +} + +export interface PendingAction { + actionId: number; + groupId: number; + type: string; + signers: string[]; + quorumReached: boolean; +} + +/** + * Read every pending action, and for each, its signers and whether it has + * reached quorum. `getPendingActionFullInfo` returns the action id, group id, + * decoded action data (its `type` names what it will do), and the raw signer + * list; `quorumReached` tells you if it can be performed now. + */ +export async function readPendingActions( + entrypoint: DevnetEntrypoint, + abi: Abi, + multisigAddress: string, +): Promise { + const controller = entrypoint.createMultisigController(abi); + const pending = await controller.getPendingActionFullInfo({ multisigAddress }); + + const result: PendingAction[] = []; + for (const action of pending) { + const quorumReached = await controller.quorumReached({ multisigAddress, actionId: action.actionId }); + result.push({ + actionId: action.actionId, + groupId: action.groupId, + type: action.actionData.type, + // `signers` here is `Address[]` (from FullMultisigAction), unlike the + // `getActionSigners` helper whose declared type is wrong - see Pitfalls. + signers: action.signers.map((address: Address) => address.toBech32()), + quorumReached, + }); + } + return result; +} + +/** Look up one user's role on the multisig: None, Proposer, or BoardMember. */ +export async function readUserRole( + entrypoint: DevnetEntrypoint, + abi: Abi, + multisigAddress: string, + userAddress: string, +): Promise { + const controller = entrypoint.createMultisigController(abi); + const role = await controller.getUserRole({ multisigAddress, userAddress }); + return role.valueOf(); +} +``` + +## Run it + +```bash +# Read the bundled example multisig - live, no wallet: +npm start + +# Read any multisig you pass: +npm start -- erd1qqqqqqqqqqqqqpgq... +``` + +Expected output (a live snapshot, the pending action changes as the board acts): + +```text +Multisig: erd1qqqqqqqqqqqqqpgqvjcswsav2rw4ccvguyuk0f0u6qycpsuvu3dq9rse9w + +Board configuration: + quorum: 2 of 2 board members + proposers (non-board): 0 + actions ever created: 5 + board[0]: erd1d4q9a3qx5695gwd02rap2qqad3hv7lga7s06y5e6m3shuy6pu3dqagj7qe + board[1]: erd1c8fjemhv646hwlladfna6ce4m85y5hr4esqlysceelqgl987y92sp248zr + role of board[0]: BoardMember + +Pending actions: 1 + action 4 (group 0): SendTransferExecuteEgld + signers: erd1d4q9a3qx5695gwd02rap2qqad3hv7lga7s06y5e6m3shuy6pu3dqagj7qe + quorum reached: false +``` + +## How it works + +**One controller, many views.** `entrypoint.createMultisigController(abi)` gives +you `getQuorum`, `getNumBoardMembers`, `getAllBoardMembers`, `getAllProposers`, +`getActionLastIndex`, `getUserRole`, `getPendingActionFullInfo`, `quorumReached`, +and more, all read-only queries. The ABI is required so the controller can decode +the raw query responses into numbers, addresses, and typed action data. + +**Pending actions carry decoded action data.** `getPendingActionFullInfo` returns +each pending action's id, group id, signer list (`Address[]`), and an `actionData` +whose `type` names what it will do (`SendTransferExecuteEgld`, `AddBoardMember`, +`ChangeQuorum`, ...). That `type` is how you tell a spend proposal from a board +change before you sign. + +**Roles gate everything.** `getUserRole` returns `None`, `Proposer`, or +`BoardMember`. Only proposers and board members may propose; only board members' +signatures count toward quorum. + +## Pitfalls + +:::note[Pitfall 1: the ABI is required even for reads] +`createMultisigController(abi)` needs the multisig ABI to decode query responses. +Without it, the controller cannot turn raw returned bytes into the board list or the +pending `actionData`. Each recipe bundles `multisig-full.abi.json`; ship the ABI +with your app too. +::: + +:::warning[Pitfall 2: getActionSigners returns Address objects, not strings] +If you reach for `getActionSigners(...)` (used in the sign-and-perform recipe) +rather than `getPendingActionFullInfo(...).signers`, note it is declared +`Promise` but actually returns `Address[]` at runtime (sdk-core v15.4.1). +The `signers` on a `FullMultisigAction` here is correctly typed `Address[]`. +::: + +:::note[Pitfall 3: the output is a live snapshot] +Quorum and board membership are stable, but pending actions come and go as the board +signs, performs, and discards. The example's `action 4` may be gone by the time you +run this; pass your own multisig address as an argument to read a contract you +control. +::: + +## See also + +- [Propose a multisig action](/sdk-and-tools/sdk-js/cookbook/multisig/propose-action) + creates a pending action to read here. +- [Sign and perform a multisig action](/sdk-and-tools/sdk-js/cookbook/multisig/sign-perform-action) + acts on what you read. +- [Read a delegation contract's state](/sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract) + is the same read-only, no-wallet pattern for delegation. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/multisig/sign-perform-action.mdx b/docs/sdk-and-tools/sdk-js/cookbook/multisig/sign-perform-action.mdx new file mode 100644 index 000000000..f188c9862 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/multisig/sign-perform-action.mdx @@ -0,0 +1,280 @@ +--- +title: Sign and perform a multisig action +description: Sign a proposed multisig action to quorum and perform it with sdk-core's MultisigController and factory, reading its live signer state first. +difficulty: advanced +est_minutes: 9 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - multisig + - smart-contract + - transaction + - devnet +--- + +Once an action is proposed (see +[Propose a multisig action](/sdk-and-tools/sdk-js/cookbook/multisig/propose-action)), +it sits pending until board members **sign** it up to the contract's **quorum**, +and then anyone can **perform** it, the moment it actually executes and funds move +or the board changes. Both `sign` and `performAction` take only the action id. +You get each via the controller and factory, plus the read helpers you +use to decide whether an action is ready. + +The default `npm start` reads the live signer-versus-quorum state of a real devnet +multisig's first pending action, so you see the readiness check work without a +funded wallet. + +## Prerequisites + +- Node.js >= 20.13.1. +- For the default read and `payload` demos: devnet network access only. +- For an actual sign or perform: a devnet PEM whose address is a board member on + the target multisig, with a little EGLD for gas. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/multisig-sign-perform-action +npm install +npm run build +``` + +## Signing and performing + +```ts title="src/signPerform.ts" +// src/signPerform.ts - the subject of this recipe: the second half of the +// multisig lifecycle. Once an action has been proposed (see the propose +// recipe), board members must SIGN it until it reaches quorum, and then anyone +// can PERFORM it - which is the moment it actually executes. +// +// propose -> action id N created, 1 signer (the proposer, if a board member) +// sign -> each board member adds their signature to id N +// perform -> once signers >= quorum, execute id N (the funds move / the +// board changes / the call fires) +// +// Both `sign` and `performAction` take just the action id. This file shows +// each via the controller and the factory, plus the read helpers you use to +// decide whether an action is ready to perform. + +import { Account, Address } from '@multiversx/sdk-core'; +import type { Abi, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +// Signing is cheap; performing must also fund the wrapped call, so give it +// more. Gas is REQUIRED for both (see Pitfalls) - never rely on a default. +const SIGN_GAS_LIMIT = 8_000_000n; +const PERFORM_GAS_LIMIT = 12_000_000n; + +/** Sign (approve) an action, path 1 - the controller. */ +export async function signViaController( + entrypoint: DevnetEntrypoint, + abi: Abi, + signer: Account, + multisigContract: string, + actionId: number, +): Promise { + const controller = entrypoint.createMultisigController(abi); + return controller.createTransactionForSignAction(signer, signer.getNonceThenIncrement(), { + multisigContract: Address.newFromBech32(multisigContract), + actionId, + gasLimit: SIGN_GAS_LIMIT, + }); +} + +/** Sign (approve) an action, path 2 - the factory (you set nonce + signature). */ +export async function signViaFactory( + entrypoint: DevnetEntrypoint, + abi: Abi, + signer: Account, + multisigContract: string, + actionId: number, +): Promise { + const factory = entrypoint.createMultisigTransactionsFactory(abi); + const transaction = await factory.createTransactionForSignAction(signer.address, { + multisigContract: Address.newFromBech32(multisigContract), + actionId, + gasLimit: SIGN_GAS_LIMIT, + }); + transaction.nonce = signer.getNonceThenIncrement(); + transaction.signature = await signer.signTransaction(transaction); + return transaction; +} + +/** Perform (execute) an action that has reached quorum, path 1 - the controller. */ +export async function performViaController( + entrypoint: DevnetEntrypoint, + abi: Abi, + performer: Account, + multisigContract: string, + actionId: number, +): Promise { + const controller = entrypoint.createMultisigController(abi); + return controller.createTransactionForPerformAction(performer, performer.getNonceThenIncrement(), { + multisigContract: Address.newFromBech32(multisigContract), + actionId, + gasLimit: PERFORM_GAS_LIMIT, + }); +} + +/** Perform (execute) an action that has reached quorum, path 2 - the factory. */ +export async function performViaFactory( + entrypoint: DevnetEntrypoint, + abi: Abi, + performer: Account, + multisigContract: string, + actionId: number, +): Promise { + const factory = entrypoint.createMultisigTransactionsFactory(abi); + const transaction = await factory.createTransactionForPerformAction(performer.address, { + multisigContract: Address.newFromBech32(multisigContract), + actionId, + gasLimit: PERFORM_GAS_LIMIT, + }); + transaction.nonce = performer.getNonceThenIncrement(); + transaction.signature = await performer.signTransaction(transaction); + return transaction; +} + +export interface SignPerformPayload { + function: string; + actionIdHex: string; + receiver: string; + gasLimit: bigint; +} + +/** Decode a sign/perform transaction: the function name plus the action id. */ +export function describeSignPerformPayload(transaction: Transaction): SignPerformPayload { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + function: parts[0] ?? '', + actionIdHex: parts[1] ?? '', + receiver: transaction.receiver.toBech32(), + gasLimit: transaction.gasLimit, + }; +} + +export interface ActionReadiness { + quorum: number; + validSigners: number; + signers: string[]; + quorumReached: boolean; +} + +/** + * Read whether an action is ready to perform: how many valid board-member + * signatures it has versus the quorum. This is exactly what you check before + * spending gas on a perform that would revert. + */ +export async function readActionReadiness( + entrypoint: DevnetEntrypoint, + abi: Abi, + multisigAddress: string, + actionId: number, +): Promise { + const controller = entrypoint.createMultisigController(abi); + const [quorum, validSigners, signers, quorumReached] = await Promise.all([ + controller.getQuorum({ multisigAddress }), + controller.getActionValidSignerCount({ multisigAddress, actionId }), + controller.getActionSigners({ multisigAddress, actionId }), + controller.quorumReached({ multisigAddress, actionId }), + ]); + // SDK trap: getActionSigners is declared `Promise` but actually + // returns `Address[]` at runtime (v15.4.1). Handle both so this stays correct + // whichever way the SDK settles it. See Pitfalls on the recipe page. + const signerAddresses = signers as Array; + return { + quorum, + validSigners, + signers: signerAddresses.map((address) => (typeof address === 'string' ? address : address.toBech32())), + quorumReached, + }; +} +``` + +## Run it + +```bash +# Read the first pending action's signers vs quorum - no wallet, no funds: +npm start + +# Inspect the sign and perform wire payloads offline: +npm start -- payload + +# Actually sign action 4 (needs a board-member devnet PEM); --perform to perform, +# --factory for the factory path: +npm start -- sign ./wallet.pem +``` + +Output of the default read mode, and of `payload`: + +```text +Reading pending actions of erd1qqqqqqqqqqqqqpgqvjcswsav2rw4ccvguyuk0f0u6qycpsuvu3dq9rse9w ... + first pending action: id 4 (SendTransferExecuteEgld) + valid signers / quorum: 1 / 2 + quorum reached (ready to perform): false + signers so far: erd1d4q9a3qx5695gwd02rap2qqad3hv7lga7s06y5e6m3shuy6pu3dqagj7qe + +sign: sign@04 (actionId 4 = 0x04) gasLimit 8000000 +performAction: performAction@04 (actionId 4 = 0x04) gasLimit 12000000 +receiver: erd1qqqqqqqqqqqqqpgqvjcswsav2rw4ccvguyuk0f0u6qycpsuvu3dq9rse9w (the multisig contract, for both) +``` + +## How it works + +**Controller vs factory.** +`controller.createTransactionForSignAction(account, nonce, { multisigContract, actionId, gasLimit })` +builds, sets the nonce, and signs; the factory form only builds and you sign. +`performAction` is identical in shape. Both need the multisig ABI at construction. + +**Check readiness before performing.** `getActionValidSignerCount` and +`quorumReached` tell you whether an action can be performed. Performing below quorum +reverts and wastes gas, so the recipe reads `validSigners / quorum` first. Note the +proposer's own signature usually counts as the first one when they are a board +member. + +**Perform is the moment of execution.** Signing only records approval; +`performAction` is what fires the wrapped call, moves the EGLD, or applies the board +change. Give perform enough gas to also run whatever it wraps (this recipe uses +12M). + +## Pitfalls + +:::warning[Pitfall 1: getActionSigners is typed as a string array but returns Address objects] +`MultisigController.getActionSigners` is declared `Promise`, but at +runtime (sdk-core v15.4.1) it returns an array of `Address` objects, not bech32 +strings. TypeScript will let you write `.length` and index it, then `String(x)` +yields `[object Object]`. The recipe handles both forms defensively. +`getPendingActionFullInfo(...).signers`, by contrast, is correctly typed +`Address[]`. +::: + +:::warning[Pitfall 2: gas is required; the controller does NOT protect you] +As with proposing, both `sign` and `performAction` need an explicit `gasLimit`. The +factory throws without one; the **controller silently coerces a missing gasLimit to +`0n`** and builds a `gasLimit: 0` transaction the network rejects. Confirmed against +sdk-core v15.4.1. Always pass `gasLimit`. +::: + +:::note[Pitfall 3: performing below quorum reverts] +`performAction` succeeds only when valid signers >= quorum. Calling it early reverts +on-chain (the SDK builds it happily). Read `quorumReached` first. For the final +signer, `createTransactionForSignAndPerform` signs and performs in one transaction, +saving a round trip. +::: + +:::note[Pitfall 4: only current board members' signatures count] +`getActionValidSignerCount` counts only signers who are still board members; a +signer removed from the board no longer counts toward quorum. That is why it can be +lower than the raw `getActionSigners` length. +::: + +## See also + +- [Propose a multisig action](/sdk-and-tools/sdk-js/cookbook/multisig/propose-action) + creates the action id this recipe signs and performs. +- [Read a multisig's state](/sdk-and-tools/sdk-js/cookbook/multisig/read-multisig-state) + is the full read surface behind the readiness check here. +- [Delegate (stake) EGLD](/sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake) + is another sdk-core controller/factory write, for comparison. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/network-providers/await-account-on-condition.mdx b/docs/sdk-and-tools/sdk-js/cookbook/network-providers/await-account-on-condition.mdx new file mode 100644 index 000000000..6b26907b0 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/network-providers/await-account-on-condition.mdx @@ -0,0 +1,208 @@ +--- +title: Await an account on a custom condition +description: Block until an account matches a predicate you write with awaitAccountOnCondition, tuning the poll interval and timeout via AwaitingOptions. +difficulty: intermediate +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - network-provider + - typescript +--- + +Sometimes you need to wait on an **account**, not a transaction: block until a +deposit lands (balance crosses a threshold), or until a batch of your own sends +has confirmed (nonce reaches a target). That is `awaitAccountOnCondition`, the +same poll-until-predicate machinery as +[Await a transaction on a condition](/sdk-and-tools/sdk-js/cookbook/network-providers/await-transaction-on-condition), +but the predicate receives an `AccountOnNetwork`. + +`awaitAccountOnCondition(address, predicate, options?)` is on `INetworkProvider`, +so an Api or Proxy provider both work. This recipe reads a live, funded mainnet +account, so every condition is checked against real state, with no wallet, no +funds, and no broadcast. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/await-account-on-condition +npm install +npm run build +npm start +``` + +## The awaiters + +```ts title="src/awaitAccount.ts" +// src/awaitAccount.ts — the subject of this recipe: blocking until an ACCOUNT +// reaches a state you define. Same poll-until-predicate shape as the +// transaction awaiter, but the predicate receives an AccountOnNetwork. +// +// Real uses: wait for a deposit to land (balance crosses a threshold), or wait +// for a sequence of your own transactions to confirm (nonce reaches a target). +// +// `awaitAccountOnCondition` is on INetworkProvider, so an Api or Proxy provider +// both work. This recipe reads a live, funded mainnet account, so every +// condition is checked against real state — no wallet, no funds, no broadcast. + +import type { + INetworkProvider, + AccountOnNetwork, + AwaitingOptions, + Address, +} from '@multiversx/sdk-core'; + +/** Block until `condition(account)` is true, using the SDK default poll/timeout. */ +export async function awaitAccount( + provider: INetworkProvider, + address: Address, + condition: (account: AccountOnNetwork) => boolean, +): Promise { + return provider.awaitAccountOnCondition(address, condition); +} + +/** + * The same, with your own `AwaitingOptions`. On timeout the promise REJECTS + * (it does not resolve with the last-seen account), so wrap it in try/catch + * when a timeout is a realistic outcome. + */ +export async function awaitAccountWithOptions( + provider: INetworkProvider, + address: Address, + condition: (account: AccountOnNetwork) => boolean, + options: AwaitingOptions, +): Promise { + return provider.awaitAccountOnCondition(address, condition, options); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — three awaits against one live, funded mainnet account: +// 1. nonce reaches its current value (monotonic, so already true), +// 2. balance is above zero (the "deposit landed" shape, already true), +// 3. nonce reaches a value far in the future, with a short timeout, to show +// the reject path. +// No wallet, no gas. +// +// Usage: +// npm run build && npm start [bech32Address] + +import { ApiNetworkProvider, Address, AwaitingOptions } from '@multiversx/sdk-core'; +import { awaitAccount, awaitAccountWithOptions } from './awaitAccount'; + +const MAINNET_API = 'https://api.multiversx.com'; +const DEFAULT_ADDRESS = 'erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th'; + +async function main(): Promise { + const addressArg = process.argv[2] ?? DEFAULT_ADDRESS; + const provider = new ApiNetworkProvider(MAINNET_API, { clientName: 'mvx-cookbook' }); + const address = Address.newFromBech32(addressArg); + + const current = await provider.getAccount(address); + console.log(`Account ${address.toBech32()}`); + console.log(` current nonce ${current.nonce}, balance ${current.balance}`); + + // 1. Nonce is monotonic, so "reach the current nonce" holds on the first poll. + // In real use this is how you wait for a batch of your sends to confirm. + const reached = await awaitAccount(provider, address, (a) => a.nonce >= current.nonce); + console.log(`1. awaited nonce >= ${current.nonce}: resolved at nonce ${reached.nonce}`); + + // 2. The "deposit landed" shape — balance above a threshold. + const funded = await awaitAccount(provider, address, (a) => a.balance > 0n); + console.log(`2. awaited balance > 0: resolved with balance ${funded.balance}`); + + // 3. A nonce far in the future the account will not reach in 3s — shows the + // reject-on-timeout path and AwaitingOptions. + const target = current.nonce + 1_000_000n; + const options = new AwaitingOptions(); + options.pollingIntervalInMilliseconds = 500; + options.timeoutInMilliseconds = 3000; + const startedAt = Date.now(); + try { + await awaitAccountWithOptions(provider, address, (a) => a.nonce >= target, options); + console.log('3. unexpected: condition matched'); + } catch { + console.log(`3. awaited nonce >= ${target} timed out after ~${Math.round((Date.now() - startedAt) / 1000)}s (rejected, as expected)`); + } +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start # a known funded mainnet account +npm start # await any account you like +``` + +Expected output (live values, so nonce and balance will differ when you run it): + +```text +Account erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th + current nonce 89, balance 1000010000000 +1. awaited nonce >= 89: resolved at nonce 89 +2. awaited balance > 0: resolved with balance 1000010000000 +3. awaited nonce >= 1000089 timed out after ~3s (rejected, as expected) +``` + +## How it works + +**A predicate is just `(account) => boolean`.** The SDK re-fetches the account on +each poll and hands your predicate the fresh `AccountOnNetwork`: `nonce` (bigint), +`balance` (bigint), `userName`, `isGuarded`, and the contract fields. Return +`true` to resolve. + +**Nonce is monotonic; balance is not.** "Reach nonce N" is a safe, one-way +condition, ideal for waiting on a sequence of your own transactions (see +[Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces)). A balance +condition captures the "deposit landed" case, but a balance can also decrease, so +write the condition for the direction you actually mean. + +**`AwaitingOptions` tunes the loop.** `pollingIntervalInMilliseconds`, +`timeoutInMilliseconds`, `patienceInMilliseconds` (defaults `600` / `9000` / `0`). +This recipe uses a 500ms poll and a 3s timeout for the demo. + +## Pitfalls + +:::danger[Pitfall 1: timeout REJECTS, it does not resolve] +If the account never satisfies the predicate within `timeoutInMilliseconds`, the +promise rejects — it does not hand you the last-seen account. Wrap the await in +try/catch wherever a timeout is realistic, as step 3 does. +::: + +:::warning[Pitfall 2: do not busy-poll a tiny interval against a public API] +A 100ms poll against `api.multiversx.com` will get you rate-limited. Keep the +interval realistic (the SDK default is 600ms), or run your own observing squad / +gateway if you truly need tight polling. +::: + +:::note[Pitfall 3: read the baseline before you await a relative condition] +This recipe calls `getAccount()` once up front to capture the current +nonce/balance, then writes conditions relative to that snapshot. If you hard-code +an absolute threshold instead, make sure it reflects the account's real starting +state or the await either returns instantly or never. +::: + +## See also + +- [Await a transaction on a custom condition](/sdk-and-tools/sdk-js/cookbook/network-providers/await-transaction-on-condition) + is the transaction-shaped sibling of this recipe. +- [Fetch an account's on-chain state](/sdk-and-tools/sdk-js/cookbook/accounts/fetch-account-state) + is the single-shot `getAccount` read the predicate here polls repeatedly. +- [Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) + shows why "wait until nonce reaches N" is a natural way to sequence your own + sends. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/network-providers/await-transaction-on-condition.mdx b/docs/sdk-and-tools/sdk-js/cookbook/network-providers/await-transaction-on-condition.mdx new file mode 100644 index 000000000..8c2122758 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/network-providers/await-transaction-on-condition.mdx @@ -0,0 +1,221 @@ +--- +title: Await a transaction on a custom condition +description: Block until a transaction matches a predicate you write with awaitTransactionOnCondition, tuning the poll interval and timeout via AwaitingOptions. +difficulty: intermediate +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - network-provider + - transaction + - typescript +--- + +You broadcast a transaction. Now you need to block until it reaches a state you +care about — not just "completed", but maybe "completed **and** produced a +smart-contract result", or "reached a specific status". That is +`awaitTransactionOnCondition`. + +The provider gives you two awaiters, both on `INetworkProvider`: + +- `awaitTransactionCompleted(hash, options?)`, the common case, a fixed condition. +- `awaitTransactionOnCondition(hash, predicate, options?)`, **any** predicate you + write against the live `TransactionOnNetwork`. + +Both poll the network on an interval until the predicate holds or the timeout +elapses. You would normally call these right after sending a transaction; this +recipe points them at an already-final historical transaction so the whole thing +runs read-only. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/await-transaction-on-condition +npm install +npm run build +npm start +``` + +## The awaiters + +```ts title="src/awaitTransaction.ts" +// src/awaitTransaction.ts — the subject of this recipe: blocking until a +// transaction reaches a state YOU define, not just "completed". +// +// The provider gives you two awaiters: +// - awaitTransactionCompleted(hash, options?) — the common case +// - awaitTransactionOnCondition(hash, predicate, options?) — any predicate +// +// Both poll the network on an interval until the predicate holds or the timeout +// elapses. Both are on INetworkProvider, so an Api or a Proxy provider works. +// +// You normally call these right after broadcasting a transaction, to block your +// script until the tx reaches a state you care about. This recipe instead +// points them at an already-final historical transaction so the whole thing +// runs read-only — no wallet, no funds, no broadcast. + +import type { INetworkProvider, TransactionOnNetwork, AwaitingOptions } from '@multiversx/sdk-core'; + +/** Block until `condition(tx)` is true, using the SDK's default poll/timeout. */ +export async function awaitTransaction( + provider: INetworkProvider, + txHash: string, + condition: (tx: TransactionOnNetwork) => boolean, +): Promise { + return provider.awaitTransactionOnCondition(txHash, condition); +} + +/** + * The same, but with your own `AwaitingOptions` — poll faster/slower, or fail + * sooner. On timeout the promise REJECTS (it does not resolve with a partial + * result), so wrap it in try/catch if a timeout is expected. + */ +export async function awaitTransactionWithOptions( + provider: INetworkProvider, + txHash: string, + condition: (tx: TransactionOnNetwork) => boolean, + options: AwaitingOptions, +): Promise { + return provider.awaitTransactionOnCondition(txHash, condition, options); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — three awaits against one already-final mainnet transaction: +// 1. a plain "is it completed" condition (returns on the first poll), +// 2. a custom condition ("has smart-contract results"), +// 3. a condition that is never true, with a short timeout, to show the +// reject path. +// No wallet, no gas. +// +// Usage: +// npm run build && npm start [txHash] + +import { ApiNetworkProvider, AwaitingOptions } from '@multiversx/sdk-core'; +import { awaitTransaction, awaitTransactionWithOptions } from './awaitTransaction'; + +const MAINNET_API = 'https://api.multiversx.com'; +// A real, final, permanent mainnet transaction (a successful call with +// smart-contract results). Override with your own hash as argv[2]. +const DEFAULT_TX = 'd537da3f347191cf906602a83bcf3207b0c7d55e6b120649d224b7ee255bfbbb'; + +async function main(): Promise { + const txHash = process.argv[2] ?? DEFAULT_TX; + const provider = new ApiNetworkProvider(MAINNET_API, { clientName: 'mvx-cookbook' }); + + // 1. Default awaiter — block until the tx is completed. + const completed = await awaitTransaction(provider, txHash, (tx) => tx.status.isCompleted()); + console.log('1. awaited status.isCompleted():'); + console.log(` status ${completed.status.toString()}, nonce ${completed.nonce}, scResults ${completed.smartContractResults.length}`); + + // 2. Custom predicate — block until the tx has produced smart-contract results. + const withResults = await awaitTransaction( + provider, + txHash, + (tx) => tx.smartContractResults.length > 0, + ); + console.log('2. awaited a custom predicate (smartContractResults.length > 0):'); + console.log(` resolved with ${withResults.smartContractResults.length} smart-contract results`); + + // 3. A condition that can never be true for this successful tx, with a short + // timeout — demonstrates the reject-on-timeout path and AwaitingOptions. + const options = new AwaitingOptions(); + options.pollingIntervalInMilliseconds = 500; + options.timeoutInMilliseconds = 3000; + const startedAt = Date.now(); + try { + await awaitTransactionWithOptions(provider, txHash, (tx) => tx.status.isInvalid(), options); + console.log('3. unexpected: condition matched'); + } catch { + console.log(`3. never-true condition timed out after ~${Math.round((Date.now() - startedAt) / 1000)}s (rejected, as expected)`); + } +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start # uses a known permanent mainnet tx +npm start # await any transaction you like +``` + +Expected output: + +```text +1. awaited status.isCompleted(): + status success, nonce 20052, scResults 3 +2. awaited a custom predicate (smartContractResults.length > 0): + resolved with 3 smart-contract results +3. never-true condition timed out after ~4s (rejected, as expected) +``` + +## How it works + +**A predicate is just `(tx) => boolean`.** The SDK re-fetches the transaction on +each poll and hands your predicate the fresh `TransactionOnNetwork`. Return `true` +to resolve. `tx.status` exposes `isCompleted()`, `isSuccessful()`, `isPending()`, +`isFailed()`, `isInvalid()`, `isNotExecutableInBlock()`; and +`tx.smartContractResults`, `tx.logs`, `tx.nonce` and the rest are all fair game +for a condition. + +**`AwaitingOptions` tunes the loop.** Three fields: +`pollingIntervalInMilliseconds`, `timeoutInMilliseconds`, +`patienceInMilliseconds`. The SDK defaults (confirmed at runtime) are `600` / +`9000` / `0`. This recipe sets a 500ms poll and a 3s timeout for the demo. + +**When to reach for the custom condition over `awaitTransactionCompleted`.** Use +the plain completed-awaiter for "did my tx land". Use +`awaitTransactionOnCondition` when "done" for your app means more than the +protocol's notion of completed, e.g. a specific event was logged, or a cross-shard +smart-contract result arrived. For the browser/dApp equivalent (WebSocket-driven +status, no polling loop of your own), see +[Track a transaction](/sdk-and-tools/sdk-js/cookbook/transactions/track-transaction-status). + +## Pitfalls + +:::danger[Pitfall 1: timeout REJECTS, it does not resolve] +If your predicate never holds within `timeoutInMilliseconds`, the promise rejects, +it does not resolve with a partial/last-seen transaction. Any code path where a +timeout is realistic must wrap the await in try/catch, as step 3 of this recipe +does. +::: + +:::note[Pitfall 2: the thrown error is status-specific, not the documented one] +The `INetworkProvider` doc comment says these throw `ErrAwaitConditionNotReached`, +but the transaction awaiter actually throws +`ErrExpectedTransactionStatusNotReached`. Catch broadly (`catch (e: unknown)`) +rather than matching one class name. +::: + +:::warning[Pitfall 3: a slow poll can miss short-lived states] +The predicate only sees the transaction at each poll boundary. A transient status +that appears and disappears between two polls will be missed. For "did it ever +pass through state X" you need the transaction's logs/results after the fact, not +a live poll. +::: + +## See also + +- [Track a transaction (WebSocket + polling fallback)](/sdk-and-tools/sdk-js/cookbook/transactions/track-transaction-status) + is the sdk-dapp, browser-side way to watch a transaction, without writing your + own poll loop. +- [Await an account on a custom condition](/sdk-and-tools/sdk-js/cookbook/network-providers/await-account-on-condition) + applies the same poll-until-predicate pattern to an account instead of a + transaction. +- [Simulate and estimate a transaction](/sdk-and-tools/sdk-js/cookbook/network-providers/simulate-estimate-transaction) + checks what a transaction will do before you send and await it. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider.mdx b/docs/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider.mdx new file mode 100644 index 000000000..30acb3bf2 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider.mdx @@ -0,0 +1,252 @@ +--- +title: Configure a network provider (Api vs Proxy) +description: Construct an ApiNetworkProvider or ProxyNetworkProvider, tune clientName and timeout, or get one from an entrypoint. Both share one INetworkProvider interface. +difficulty: beginner +est_minutes: 5 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - network-provider + - api + - proxy + - typescript +--- + +Every read-side call in sdk-core (fetching an account, a block, a token +balance, network config) goes through an `INetworkProvider`. Before you can read +anything, you need one. There are two implementations, and this recipe builds +both. + +- **`ApiNetworkProvider`** talks to the MultiversX HTTP API (for example + `api.multiversx.com`), backed by Elasticsearch. It has the richest, indexed + endpoints: token lists per account, transaction history, token metadata. This + is the default choice for most apps and backends. +- **`ProxyNetworkProvider`** talks to a gateway / observing-squad proxy (for + example `gateway.multiversx.com`). It sits closer to the protocol and exposes + a few endpoints the API does not, most notably block-by-nonce. + +Both implementations share one `INetworkProvider` interface, so your code should +depend on `INetworkProvider`, not the concrete class. You can swap one for the +other without touching a call site. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/configure-network-provider +npm install +npm run build +npm start +``` + +## Constructing the providers + +```ts title="src/providers.ts" +// src/providers.ts — the subject of this recipe: how to construct a network +// provider. Everything read-side in sdk-core goes through the same +// `INetworkProvider` interface, and there are two implementations: +// +// - ApiNetworkProvider → the MultiversX HTTP API (e.g. api.multiversx.com), +// backed by Elasticsearch. Richer, indexed endpoints: token lists per +// account, transaction history, token metadata. +// - ProxyNetworkProvider → a gateway / observing-squad proxy (e.g. +// gateway.multiversx.com). Closer to the protocol; exposes a few +// endpoints the API does not (notably block-by-nonce). +// +// Both implement `INetworkProvider`, so application code should depend on the +// interface, not the concrete class: you can swap Api for Proxy without +// touching a call site. + +import { + ApiNetworkProvider, + ProxyNetworkProvider, + DevnetEntrypoint, +} from '@multiversx/sdk-core'; +import type { INetworkProvider, NetworkProviderConfig } from '@multiversx/sdk-core'; + +// A `clientName` is recommended on every provider — it identifies your app in +// the network's request metrics. Omit it and the SDK logs a recommendation to +// the console on every construction. +const CLIENT_NAME = 'mvx-cookbook'; + +/** The API provider — the default choice for most apps and backends. */ +export function createApiProvider(url: string): INetworkProvider { + const config: NetworkProviderConfig = { clientName: CLIENT_NAME }; + return new ApiNetworkProvider(url, config); +} + +/** The Proxy provider — same interface, different backend. */ +export function createProxyProvider(url: string): INetworkProvider { + const config: NetworkProviderConfig = { clientName: CLIENT_NAME }; + return new ProxyNetworkProvider(url, config); +} + +/** + * `NetworkProviderConfig` extends Axios's `AxiosRequestConfig`, so you set the + * request `timeout` (milliseconds), custom `headers`, proxy agents, etc. in the + * same object as `clientName`. + */ +export function createApiProviderWithConfig(url: string): INetworkProvider { + const config: NetworkProviderConfig = { + clientName: CLIENT_NAME, + timeout: 10_000, + }; + return new ApiNetworkProvider(url, config); +} + +/** + * You rarely construct a provider by hand in a script — an entrypoint already + * holds one. `createNetworkProvider()` hands you the exact same + * `INetworkProvider` the controllers and factories use internally. A default + * `DevnetEntrypoint()` gives you an `ApiNetworkProvider` for devnet. + */ +export function providerFromEntrypoint(): INetworkProvider { + return new DevnetEntrypoint().createNetworkProvider(); +} + +/** + * To get a Proxy-backed provider from an entrypoint, pass `kind: 'proxy'` and a + * gateway URL. Everything else about the entrypoint stays the same. + */ +export function proxyProviderFromEntrypoint(gatewayUrl: string): INetworkProvider { + return new DevnetEntrypoint({ url: gatewayUrl, kind: 'proxy' }).createNetworkProvider(); +} +``` + +## Proving each one is live + +```ts title="src/index.ts" +// src/index.ts — constructs each kind of provider and proves it is live by +// calling getNetworkConfig() on it. No wallet, no PEM, no gas — every call +// below is a plain read. +// +// Usage: +// npm run build && npm start + +import { + createApiProvider, + createProxyProvider, + createApiProviderWithConfig, + providerFromEntrypoint, + proxyProviderFromEntrypoint, +} from './providers'; + +const MAINNET_API = 'https://api.multiversx.com'; +const MAINNET_GATEWAY = 'https://gateway.multiversx.com'; +const DEVNET_GATEWAY = 'https://devnet-gateway.multiversx.com'; + +async function main(): Promise { + const api = createApiProvider(MAINNET_API); + const apiConfig = await api.getNetworkConfig(); + console.log(`ApiNetworkProvider (${MAINNET_API})`); + console.log(` live — chainID ${apiConfig.chainID}, minGasPrice ${apiConfig.minGasPrice}`); + + const proxy = createProxyProvider(MAINNET_GATEWAY); + const proxyConfig = await proxy.getNetworkConfig(); + console.log(`ProxyNetworkProvider (${MAINNET_GATEWAY})`); + console.log(` live — chainID ${proxyConfig.chainID}, minGasPrice ${proxyConfig.minGasPrice}`); + + const tuned = createApiProviderWithConfig(MAINNET_API); + const tunedConfig = await tuned.getNetworkConfig(); + console.log('ApiNetworkProvider + custom timeout/config'); + console.log(` live — chainID ${tunedConfig.chainID}`); + + const fromEntry = providerFromEntrypoint(); + const entryConfig = await fromEntry.getNetworkConfig(); + console.log('DevnetEntrypoint().createNetworkProvider()'); + console.log(` live — chainID ${entryConfig.chainID} (devnet)`); + + const proxyFromEntry = proxyProviderFromEntrypoint(DEVNET_GATEWAY); + const proxyEntryConfig = await proxyFromEntry.getNetworkConfig(); + console.log(`DevnetEntrypoint({ kind: 'proxy' }).createNetworkProvider()`); + console.log(` live — chainID ${proxyEntryConfig.chainID} (devnet gateway)`); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start +``` + +Expected output: + +```text +ApiNetworkProvider (https://api.multiversx.com) + live — chainID 1, minGasPrice 1000000000 +ProxyNetworkProvider (https://gateway.multiversx.com) + live — chainID 1, minGasPrice 1000000000 +ApiNetworkProvider + custom timeout/config + live — chainID 1 +DevnetEntrypoint().createNetworkProvider() + live — chainID D (devnet) +DevnetEntrypoint({ kind: 'proxy' }).createNetworkProvider() + live — chainID D (devnet gateway) +``` + +## How it works + +**Depend on the interface, not the class.** Every function in `providers.ts` is +typed to return `INetworkProvider`. `ApiNetworkProvider` and +`ProxyNetworkProvider` both implement it, so a function that accepts an +`INetworkProvider` takes either. This recipe proves it by running the identical +`getNetworkConfig()` call against all five providers and getting the same +`NetworkConfig` shape back. + +**`NetworkProviderConfig` extends Axios's `AxiosRequestConfig`.** The second +constructor argument is where `clientName`, `timeout` (milliseconds), custom +`headers`, and proxy agents all live. It is one object, typed as +`interface NetworkProviderConfig extends AxiosRequestConfig { clientName?: string }`. + +**An entrypoint already holds a provider.** In a real script you rarely `new` a +provider up by hand. `DevnetEntrypoint().createNetworkProvider()` hands you the +exact same `INetworkProvider` the controllers and factories use internally. Pass +`kind: 'proxy'` plus a gateway URL to get a proxy-backed one instead. + +## Pitfalls + +:::note[Pitfall 1: set clientName or the SDK nags you] +Omit `clientName` and the SDK logs a recommendation to the console on every +provider construction ("We recommend providing the `clientName`..."). It is used +for the network's request metrics. This recipe always sets it; the +entrypoint-created providers in `index.ts` do not, which is why you will see +that log line when you run it. +::: + +:::warning[Pitfall 2: Api and Proxy are NOT feature-identical] +They share the `INetworkProvider` interface, but a handful of methods differ. +`ApiNetworkProvider.getBlock(hash)` takes a hash; `ProxyNetworkProvider.getBlock({ shard, blockNonce })` +takes a shard plus nonce. Pagination (`{ from, size }`) is honored by the Api +provider but ignored by the Proxy provider's token methods. Pick the provider +whose backend actually exposes what you need. +::: + +:::note[Pitfall 3: the URL is the network, not a constructor flag] +There is no `network: 'mainnet'` option. Mainnet vs devnet vs testnet is +entirely the URL you pass (`api.multiversx.com` vs `devnet-api.multiversx.com`). +The pre-configured `DevnetEntrypoint` / `MainnetEntrypoint` classes just bake the +right URL and chain ID in for you. +::: + +## See also + +- [Fetch network config and status](/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-network-config-status) + is the first thing you read once you have a provider. +- [Custom API/Proxy request](/sdk-and-tools/sdk-js/cookbook/network-providers/custom-api-request) + calls any endpoint the typed methods do not cover, via `doGetGeneric` / + `doPostGeneric`. +- [Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) + uses `createSmartContractController()`, which wraps the same provider this + recipe builds. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/network-providers/custom-api-request.mdx b/docs/sdk-and-tools/sdk-js/cookbook/network-providers/custom-api-request.mdx new file mode 100644 index 000000000..6fadf3ba7 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/network-providers/custom-api-request.mdx @@ -0,0 +1,220 @@ +--- +title: Custom API/Proxy request +description: Call any API or Proxy endpoint the typed provider methods do not cover, via doGetGeneric and doPostGeneric, for economics, stats, and a raw VM query. +difficulty: intermediate +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - network-provider + - api + - proxy + - typescript +--- + +The typed provider methods (`getAccount`, `getNetworkConfig`, +`getTokenOfAccount`, ...) cover the common endpoints. The API and gateway expose +many more. When you need one the SDK does not model, call it raw, both escape +hatches are on `INetworkProvider`: + +- `doGetGeneric(resourceUrl)`, a GET, path relative to the provider's base URL. +- `doPostGeneric(resourceUrl, payload)`, a POST. + +Both return `any`, so **you** own the shape of the result. This recipe wraps +three raw calls, the `economics` and `stats` GETs, and a raw VM query POST, in +typed functions. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access to devnet. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/custom-api-request +npm install +npm run build +npm start +``` + +## The raw calls + +```ts title="src/customRequest.ts" +// src/customRequest.ts — the subject of this recipe: the escape hatch. When a +// provider has no typed method for the endpoint you need, call it raw: +// +// doGetGeneric(resourceUrl) → any (a GET, path relative to the base URL) +// doPostGeneric(resourceUrl, payload) → any (a POST) +// +// Both are on INetworkProvider. They return `any`, so YOU own the shape of the +// result — the SDK does not validate it. This file wraps three raw calls in +// typed functions so the rest of the app gets real types back. + +import type { INetworkProvider } from '@multiversx/sdk-core'; + +/** + * The API's `/economics` endpoint — total/circulating supply, staked, price, + * market cap, APR. There is NO typed provider method for it; raw is the only + * way. This interface is the shape you assert (you are responsible for it). + */ +export interface Economics { + totalSupply: number; + circulatingSupply: number; + staked: number; + price: number; + marketCap: number; + apr: number; +} + +export async function getEconomics(provider: INetworkProvider): Promise { + return (await provider.doGetGeneric('economics')) as Economics; +} + +/** The API's `/stats` endpoint — another untyped GET. */ +export interface NetworkStats { + shards: number; + blocks: number; + accounts: number; + transactions: number; +} + +export async function getStats(provider: INetworkProvider): Promise { + return (await provider.doGetGeneric('stats')) as NetworkStats; +} + +/** + * A raw VM query via POST — the same read `SmartContractController.query` + * wraps, shown at the HTTP level. `returnData` comes back as base64-encoded, + * big-endian bytes; this decodes the first return value to a bigint. + */ +export async function rawQuery( + provider: INetworkProvider, + scAddress: string, + funcName: string, +): Promise { + const result = (await provider.doPostGeneric('query', { + scAddress, + funcName, + args: [], + })) as { returnCode: string; returnData: string[] }; + + const [first] = result.returnData; + if (first === undefined || first === '') { + return 0n; + } + const bytes = Buffer.from(first, 'base64'); + return bytes.length === 0 ? 0n : BigInt(`0x${bytes.toString('hex')}`); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — three raw calls against devnet: two GETs the typed methods do +// not model (economics, stats) and one POST (a VM query) that they DO, to show +// the raw layer underneath. No wallet, no gas. +// +// Usage: +// npm run build && npm start + +import { DevnetEntrypoint } from '@multiversx/sdk-core'; +import { getEconomics, getStats, rawQuery } from './customRequest'; + +// The adder contract on devnet — the same stable fixture the query recipe uses. +const ADDER = 'erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug'; + +async function main(): Promise { + const provider = new DevnetEntrypoint().createNetworkProvider(); + + const econ = await getEconomics(provider); + console.log('GET economics (no typed provider method exists):'); + console.log(` price $${econ.price}, marketCap ${econ.marketCap}, staked ${econ.staked}`); + + const stats = await getStats(provider); + console.log('GET stats:'); + console.log(` shards ${stats.shards}, accounts ${stats.accounts}, transactions ${stats.transactions}`); + + const sum = await rawQuery(provider, ADDER, 'getSum'); + console.log('POST query — adder.getSum() at the raw HTTP level:'); + console.log(` decoded returnData: ${sum}`); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start +``` + +Expected output (economics and the adder sum are live, so they move): + +```text +GET economics (no typed provider method exists): + price $3.26, marketCap 84056590, staked 2406394 +GET stats: + shards 3, accounts 18, transactions 25902940 +POST query — adder.getSum() at the raw HTTP level: + decoded returnData: 84 +``` + +## How it works + +**`economics` is the flagship raw GET.** Total supply, price, market cap, APR, +there is no typed provider method for it, so `doGetGeneric('economics')` is the +only way. `stats` is another. This is exactly what the escape hatch is for: +endpoints that exist on the network but not (yet) in the SDK's typed surface. + +**`doPostGeneric('query', ...)` is `SmartContractController.query` at the HTTP +level.** The raw VM query returns `returnData` as base64-encoded, big-endian +bytes; the recipe decodes the first value to a bigint, adder's `getSum()`, the +same `84` the typed query-contract-view recipe returns. Use the typed controller +in real code; this shows the raw layer underneath, and the shape you would reuse +for any unwrapped POST endpoint. + +**Query params go in the path string.** `doGetGeneric` takes the whole resource +path, so pagination and filters ride along as a query string, e.g. +`doGetGeneric('accounts/erd1.../tokens?from=0&size=10')`. There is no separate +params argument. + +## Pitfalls + +:::warning[Pitfall 1: the response is any; you assert the shape] +`doGetGeneric` / `doPostGeneric` return `any`. Casting to an interface (as this +recipe does) gives your code real types, but nothing validates the cast at +runtime. Treat these responses like any other untrusted input, a renamed field +will not be a compile error, it will be `undefined` at runtime. +::: + +:::note[Pitfall 2: the path is relative and provider-specific] +`doGetGeneric` prepends the provider's base URL, so pass `economics`, not +`/economics` or a full URL. And the Api and Proxy expose different paths, +`economics` and `stats` are API routes; a gateway (Proxy) has its own +`network/...` and `address/...` routes. The escape hatch does not paper over that +difference. +::: + +:::note[Pitfall 3: prefer the typed method when one exists] +If a typed method covers your need, use it, you get decoding, types, and stability +across SDK versions for free. Reach for `doGetGeneric` / `doPostGeneric` only for +endpoints the SDK does not model yet, and consider filing an issue so it gets a +typed method. +::: + +## See also + +- [Configure a network provider](/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider) + is the provider whose `doGetGeneric` / `doPostGeneric` this recipe uses. +- [Fetch an account's token balances](/sdk-and-tools/sdk-js/cookbook/tokens/fetch-account-token-balances) + has typed methods for the token endpoints, so you rarely need the raw layer + there. +- [Fetch token metadata](/sdk-and-tools/sdk-js/cookbook/tokens/fetch-token-metadata) + is another place a typed method saves you from a raw request. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-a-block.mdx b/docs/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-a-block.mdx new file mode 100644 index 000000000..fb171f8bf --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-a-block.mdx @@ -0,0 +1,201 @@ +--- +title: Fetch a block +description: Fetch the latest block and a block by hash via ApiNetworkProvider, and a block by shard and nonce via ProxyNetworkProvider. Verified live against mainnet. +difficulty: intermediate +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - network-provider + - api + - proxy + - typescript +--- + +Fetching a block is the read where the Api and Proxy providers diverge the most, +so it is worth doing deliberately. + +- **`ApiNetworkProvider`** addresses a block by **hash**: `getBlock(blockHash)` + and `getLatestBlock()`. +- **`ProxyNetworkProvider`** addresses a block by **shard + nonce**: + `getBlock({ shard, blockNonce })` and `getLatestBlock(shard)`. + +The API is a cross-shard index, so a hash is a global key. The proxy talks to +observers of one shard at a time, so it needs the shard plus a coordinate within +it. The block methods are **not** on the shared `INetworkProvider` interface, so +the functions in this recipe are typed against the concrete provider classes. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/fetch-a-block +npm install +npm run build +npm start +``` + +## Fetching blocks + +```ts title="src/blocks.ts" +// src/blocks.ts — the subject of this recipe: fetching a block. This is where +// the Api and Proxy providers diverge the most, so the functions below are +// typed against the concrete provider classes, not INetworkProvider (the block +// methods are NOT on the shared interface). +// +// - ApiNetworkProvider identifies a block by HASH: +// getBlock(blockHash) / getLatestBlock() +// - ProxyNetworkProvider identifies a block by SHARD + NONCE: +// getBlock({ shard, blockNonce }) / getLatestBlock(shard) +// +// Why the difference: the API is a cross-shard index, so a hash is a global +// key. The proxy talks to observers of one shard at a time, so it needs the +// shard plus a coordinate (hash or nonce) within it. + +import type { ApiNetworkProvider, ProxyNetworkProvider, BlockOnNetwork } from '@multiversx/sdk-core'; + +/** API: the most recent block the index has seen. */ +export async function fetchLatestBlockApi(api: ApiNetworkProvider): Promise { + return api.getLatestBlock(); +} + +/** API: any block, addressed by its hash. */ +export async function fetchBlockByHashApi( + api: ApiNetworkProvider, + blockHash: string, +): Promise { + return api.getBlock(blockHash); +} + +/** + * Proxy: a block addressed by shard + nonce. Get the nonce from + * `getNetworkStatus(shard)` (use `highestFinalNonce` so the block is final). + */ +export async function fetchBlockByNonceProxy( + proxy: ProxyNetworkProvider, + shard: number, + blockNonce: bigint, +): Promise { + return proxy.getBlock({ shard, blockNonce }); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — fetch the latest block by hash (API), re-fetch that exact +// block by its hash to prove round-trip, then fetch a final block by shard + +// nonce (Proxy). No wallet, no gas. +// +// Usage: +// npm run build && npm start + +import { ApiNetworkProvider, ProxyNetworkProvider } from '@multiversx/sdk-core'; +import { fetchLatestBlockApi, fetchBlockByHashApi, fetchBlockByNonceProxy } from './blocks'; + +const MAINNET_API = 'https://api.multiversx.com'; +const MAINNET_GATEWAY = 'https://gateway.multiversx.com'; + +async function main(): Promise { + const api = new ApiNetworkProvider(MAINNET_API, { clientName: 'mvx-cookbook' }); + const proxy = new ProxyNetworkProvider(MAINNET_GATEWAY, { clientName: 'mvx-cookbook' }); + + // API — latest block, then the same block re-fetched by its hash. + const latest = await fetchLatestBlockApi(api); + console.log('API getLatestBlock():'); + console.log(` shard ${latest.shard}, nonce ${latest.nonce}, epoch ${latest.epoch}`); + console.log(` hash ${latest.hash}`); + + const byHash = await fetchBlockByHashApi(api, latest.hash); + console.log('API getBlock(hash) — round-trip:'); + console.log(` shard ${byHash.shard}, nonce ${byHash.nonce} (same block: ${byHash.hash === latest.hash})`); + + // Proxy — a final block on shard 1, addressed by nonce. + const shard = 1; + const status = await proxy.getNetworkStatus(shard); + const finalNonce = status.highestFinalNonce; + const byNonce = await fetchBlockByNonceProxy(proxy, shard, finalNonce); + console.log(`\nProxy getBlock({ shard: ${shard}, blockNonce: ${finalNonce} }):`); + console.log(` shard ${byNonce.shard}, nonce ${byNonce.nonce}, epoch ${byNonce.epoch}`); + console.log(` hash ${byNonce.hash}`); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start +``` + +Expected output (live values, so nonces and hashes will differ when you run it): + +```text +API getLatestBlock(): + shard 0, nonce 31313292, epoch 2175 + hash 5a3d6c018a5aa443ea558c0cd9c4e23738d1184bbbda3fbe072fda931c50458e +API getBlock(hash) — round-trip: + shard 0, nonce 31313292 (same block: true) + +Proxy getBlock({ shard: 1, blockNonce: 31302554 }): + shard 1, nonce 31302554, epoch 2175 + hash daf493760526846b1d0d571ce801573ddfe1383866b9338ee56afe611fbb1462 +``` + +## How it works + +**Latest, then round-trip by hash.** The recipe calls `getLatestBlock()` on the +Api provider, then feeds that block's `hash` straight back into `getBlock(hash)` +and asserts the two are the same block, a live proof that both API paths agree. + +**Proxy needs a nonce, so read the status first.** For the proxy's by-nonce +lookup, the recipe reads `getNetworkStatus(shard).highestFinalNonce` and requests +that block, so it always asks for a **final** (irreversible) block rather than a +tip that might still reorg. + +**`BlockOnNetwork` fields.** Both paths return the same shape: `shard`, `nonce` +(bigint), `hash`, `previousHash`, `timestamp`, `round`, `epoch`. + +## Pitfalls + +:::danger[Pitfall 1: ProxyNetworkProvider.getLatestBlock() is broken (sdk-core v15.4.1)] +It returns an empty block (`shard: NaN, nonce: 0n, hash: ''`) because it does not +unwrap the gateway's `response.block` envelope, unlike `getBlock()` which does. +Workaround: use `getBlock({ shard, blockNonce })` with a nonce from +`getNetworkStatus(shard)`, which is exactly what this recipe does. The **Api** +provider's `getLatestBlock()` works correctly. +::: + +:::warning[Pitfall 2: ProxyNetworkProvider.getBlock rejects the genesis nonce] +The internal guard is `else if (args.blockNonce)`, and `0n` is falsy, so +requesting block nonce `0` throws `Block hash or block nonce not provided.` If you +genuinely need the genesis block from the proxy, query it by hash instead of +nonce. +::: + +:::note[Pitfall 3: block methods are not on INetworkProvider] +`getBlock` / `getLatestBlock` exist only on the concrete `ApiNetworkProvider` and +`ProxyNetworkProvider` classes, and with different signatures. A variable typed as +`INetworkProvider` cannot call them at all. Type against the concrete provider, as +this recipe does. +::: + +## See also + +- [Fetch network config and status](/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-network-config-status) + is where the `blockNonce` this recipe queries comes from. +- [Configure a network provider](/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider) + covers Api vs Proxy, the distinction this recipe leans on hardest. +- [Custom API/Proxy request](/sdk-and-tools/sdk-js/cookbook/network-providers/custom-api-request) + reaches block sub-resources the typed methods do not model yet. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-network-config-status.mdx b/docs/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-network-config-status.mdx new file mode 100644 index 000000000..f1c598483 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-network-config-status.mdx @@ -0,0 +1,228 @@ +--- +title: Fetch network config and status +description: Read the static network config (gas costs, shard count, round duration) and the live per-shard status (block nonce, epoch, round) via a network provider. +difficulty: beginner +est_minutes: 5 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - network-provider + - api + - gas + - typescript +--- + +Two "about the network itself" reads, side by side. They look similar but answer +different questions and have different lifetimes: + +- **`getNetworkConfig()`** returns the **static** protocol parameters: chain ID, + gas costs (`minGasLimit`, `minGasPrice`, `gasPerDataByte`, `gasPriceModifier`), + shard count, round duration. These change only across protocol upgrades, so you + fetch them once and cache them. Any fee calculation needs them. +- **`getNetworkStatus(shard)`** returns the **live** chain tip: current block + nonce, epoch, round, highest final nonce. This moves every round (~6s on + mainnet), so you re-fetch it whenever you need "where is the chain right now". + +Both are read-only, with no wallet and no gas. You need a provider first; see +[Configure a network provider](/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider). + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/fetch-network-config-status +npm install +npm run build +npm start +``` + +## Reading config and status + +```ts title="src/networkInfo.ts" +// src/networkInfo.ts — the subject of this recipe: reading the two "about the +// network itself" endpoints. They answer different questions and have +// different lifetimes: +// +// - getNetworkConfig() → the STATIC protocol parameters: chain ID, gas costs, +// shard count, round duration. These change only across protocol upgrades, +// so you fetch them once and cache them (a transaction builder needs +// minGasLimit / gasPerDataByte to compute fees). +// - getNetworkStatus(shard) → the LIVE chain tip: current block nonce, epoch, +// round, highest final nonce. This moves every round (~6s on mainnet), so +// you re-fetch it whenever you need "where is the chain right now". + +import type { + INetworkProvider, + ApiNetworkProvider, + ProxyNetworkProvider, + NetworkConfig, + NetworkStatus, +} from '@multiversx/sdk-core'; + +/** + * Static protocol parameters — safe to fetch once and cache. `getNetworkConfig` + * takes no arguments and is on the `INetworkProvider` interface, so an + * ApiNetworkProvider or a ProxyNetworkProvider both work here. + */ +export async function fetchNetworkConfig(provider: INetworkProvider): Promise { + return provider.getNetworkConfig(); +} + +/** + * The live chain tip for a given shard. `getNetworkStatus()` with no argument + * targets the metachain (shard 4294967295); pass a shard number (0, 1, 2) for a + * regular shard. + * + * Note the concrete-type parameter: the `INetworkProvider` interface declares + * `getNetworkStatus()` with NO shard argument, while both implementations + * (ApiNetworkProvider, ProxyNetworkProvider) accept `shard?`. To pass a shard + * under strict typing you must reference a concrete provider, not the + * interface. See the recipe's "Pitfalls". + */ +export async function fetchNetworkStatus( + provider: ApiNetworkProvider | ProxyNetworkProvider, + shard?: number, +): Promise { + return shard === undefined + ? provider.getNetworkStatus() + : provider.getNetworkStatus(shard); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — fetches the network config once and the status for the +// metachain plus each regular shard, then prints them. No wallet, no gas. +// +// Usage: +// npm run build && npm start [apiUrl] +// +// Defaults to mainnet; pass a devnet/testnet API URL to target another network. + +import { ApiNetworkProvider } from '@multiversx/sdk-core'; +import { fetchNetworkConfig, fetchNetworkStatus } from './networkInfo'; + +const DEFAULT_API = 'https://api.multiversx.com'; + +async function main(): Promise { + const apiUrl = process.argv[2] ?? DEFAULT_API; + const provider = new ApiNetworkProvider(apiUrl, { clientName: 'mvx-cookbook' }); + + const config = await fetchNetworkConfig(provider); + console.log(`Network config (${apiUrl}):`); + console.log(` chainID: ${config.chainID}`); + console.log(` minGasLimit: ${config.minGasLimit}`); + console.log(` minGasPrice: ${config.minGasPrice}`); + console.log(` gasPerDataByte: ${config.gasPerDataByte}`); + console.log(` gasPriceModifier:${config.gasPriceModifier}`); + console.log(` numShards: ${config.numShards}`); + console.log(` roundDuration: ${config.roundDuration} ms`); + + // Metachain status (default) then every regular shard. + const meta = await fetchNetworkStatus(provider); + console.log('\nNetwork status — metachain:'); + console.log(` blockNonce ${meta.blockNonce}, epoch ${meta.currentEpoch}, round ${meta.currentRound}`); + + for (let shard = 0; shard < config.numShards; shard++) { + const status = await fetchNetworkStatus(provider, shard); + console.log(`Network status — shard ${shard}:`); + console.log(` blockNonce ${status.blockNonce}, highestFinalNonce ${status.highestFinalNonce}, epoch ${status.currentEpoch}`); + } +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start # mainnet +npm start https://devnet-api.multiversx.com # any other network +``` + +Expected output (mainnet; live values, so the nonces and epoch will differ when +you run it): + +```text +Network config (https://api.multiversx.com): + chainID: 1 + minGasLimit: 50000 + minGasPrice: 1000000000 + gasPerDataByte: 1500 + gasPriceModifier:0.5 + numShards: 3 + roundDuration: 6000 ms + +Network status — metachain: + blockNonce 31295127, epoch 2175, round 31333830 +Network status — shard 0: + blockNonce 31313271, highestFinalNonce 31313271, epoch 2175 +Network status — shard 1: + blockNonce 31302533, highestFinalNonce 31302533, epoch 2175 +Network status — shard 2: + blockNonce 31308060, highestFinalNonce 31308060, epoch 2175 +``` + +## How it works + +**Config is static; status is live.** `getNetworkConfig()` returns numbers that +only change at a protocol upgrade: `gasPerDataByte` `1500`, `minGasLimit` `50000`, +`numShards` `3` on mainnet. Cache them. `getNetworkStatus()` returns the chain +tip, stale within one round, so never cache it. These are exactly the two +categories the SDK models as separate classes (`NetworkConfig` vs +`NetworkStatus`). + +**Status is per-shard.** MultiversX is a sharded chain, so there is no single +"current block". `getNetworkStatus()` with no argument targets the metachain +(shard `4294967295`); pass `0`, `1`, or `2` for a regular shard. This recipe +loops `0..numShards` to show all three plus the metachain. + +**`gasPerDataByte` and `minGasLimit` are the fee formula inputs.** A plain +transfer's gas is `minGasLimit + gasPerDataByte * data.length`. That is why a +fee-computing transaction builder reads `getNetworkConfig()` first. See +[Simulate and estimate a transaction](/sdk-and-tools/sdk-js/cookbook/network-providers/simulate-estimate-transaction) +for letting the network compute the cost for you instead. + +## Pitfalls + +:::warning[Pitfall 1: the shard argument is missing from the interface type] +`INetworkProvider.getNetworkStatus()` is declared with **no** parameter, but both +`ApiNetworkProvider` and `ProxyNetworkProvider` implement +`getNetworkStatus(shard?: number)`. If your variable is typed as +`INetworkProvider`, TypeScript will reject the shard argument. Type it as the +concrete provider (as `fetchNetworkStatus` does) to pass a shard. Verified against +the installed `.d.ts` files in sdk-core v15.4. +::: + +:::note[Pitfall 2: roundDuration is milliseconds, gasPriceModifier is a float] +`roundDuration` is in milliseconds (`6000` = 6s), not seconds. `gasPriceModifier` +is a plain `number` (`0.5`), the fraction of gas price actually charged on the +non-data portion of gas, not a bigint like the other gas fields. +::: + +:::note[Pitfall 3: blockNonce is a bigint] +`blockNonce`, `highestFinalNonce`, and `currentRound` are `bigint`; +`currentEpoch` is a plain `number`. Do not mix them in arithmetic without +converting; TypeScript will stop you, which is the point. +::: + +## See also + +- [Configure a network provider](/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider) + builds the provider these calls run on. +- [Fetch a block](/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-a-block) + goes from the `blockNonce` in the status to the full block. +- [Simulate and estimate a transaction](/sdk-and-tools/sdk-js/cookbook/network-providers/simulate-estimate-transaction) + lets the network compute a transaction's gas cost instead of applying the + config formula by hand. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/network-providers/simulate-estimate-transaction.mdx b/docs/sdk-and-tools/sdk-js/cookbook/network-providers/simulate-estimate-transaction.mdx new file mode 100644 index 000000000..3b5df9bd4 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/network-providers/simulate-estimate-transaction.mdx @@ -0,0 +1,220 @@ +--- +title: Simulate and estimate a transaction +description: Ask the network what a transaction would cost with estimateTransactionCost and what it would do with simulateTransaction, without ever broadcasting it. +difficulty: intermediate +est_minutes: 7 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - network-provider + - transaction + - gas + - typescript +--- + +Before you spend real gas, ask the network two questions about a transaction, +without broadcasting it, without moving funds: + +- **`estimateTransactionCost(tx)`**, how much gas will this need? Returns + `{ gasLimit, status }`. +- **`simulateTransaction(tx)`**, what would this actually do? Returns the full + `TransactionOnNetwork` it would produce: status, smart-contract results, logs, + and a `failReason` if it would fail. + +Both are read-only. This recipe builds a plain transfer from a **throwaway +account** that is generated in-process and never funded or broadcast, so there is +nothing to set up, no PEM, no faucet. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access to devnet. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/simulate-estimate-transaction +npm install +npm run build +npm start +``` + +## The two dry-run reads + +```ts title="src/simulateEstimate.ts" +// src/simulateEstimate.ts — the subject of this recipe: two "dry run" reads +// that ask the network about a transaction WITHOUT broadcasting it. Neither +// changes state or spends funds. +// +// - estimateTransactionCost(tx) → how much gas this transaction needs. +// Returns a TransactionCostResponse { gasLimit, status }. Works on an +// UNSIGNED transaction: the SDK fills in a dummy signature and fetches the +// sender nonce for you internally. +// - simulateTransaction(tx) → what this transaction WOULD do: the full +// TransactionOnNetwork it would produce (status, smart-contract results, +// logs). Runs in a sandbox. Unlike estimate, it requires a signature to be +// PRESENT on the transaction (a throwaway one is fine — nothing is sent). +// +// Both are on INetworkProvider, so an Api or Proxy provider both work. + +import type { + INetworkProvider, + Transaction, + TransactionOnNetwork, + TransactionCostResponse, +} from '@multiversx/sdk-core'; + +/** How much gas will this cost? Works on an unsigned transaction. */ +export async function estimateGas( + provider: INetworkProvider, + tx: Transaction, +): Promise { + return provider.estimateTransactionCost(tx); +} + +/** What would this transaction do? Requires a signature to be present. */ +export async function simulate( + provider: INetworkProvider, + tx: Transaction, +): Promise { + return provider.simulateTransaction(tx); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — build a plain EGLD transfer from a THROWAWAY account (no +// funds, never broadcast), then ask devnet to estimate its gas and simulate its +// execution. No real wallet, no real funds. +// +// Usage: +// npm run build && npm start + +import { DevnetEntrypoint, Account, Mnemonic, Transaction } from '@multiversx/sdk-core'; +import { estimateGas, simulate } from './simulateEstimate'; + +async function main(): Promise { + const entrypoint = new DevnetEntrypoint(); + const provider = entrypoint.createNetworkProvider(); + + // A throwaway account: generated in-process, never funded, never broadcast. + // It only needs to be a valid sender address for the transaction we probe. + const account = Account.newFromMnemonic(Mnemonic.generate().toString()); + const config = await provider.getNetworkConfig(); + + // Send to SELF so the sender and receiver are always in the same shard — + // simulate runs on the sender's shard, so a same-shard tx gives a definitive + // execution status every run (a cross-shard one would only report routing). + const tx = new Transaction({ + sender: account.address, + receiver: account.address, + gasLimit: 50000n, + chainID: config.chainID, + value: 1000000000000000000n, // 1 EGLD — this account does NOT have it; that is the point of a dry run + nonce: 0n, + }); + + console.log(`Throwaway sender/receiver: ${account.address.toBech32()}`); + + // ESTIMATE — runs against the UNSIGNED transaction. + const cost = await estimateGas(provider, tx); + console.log('\nestimateTransactionCost (unsigned):'); + console.log(` gasLimit ${cost.gasLimit}`); + + // SIMULATE — needs a signature present. A throwaway signature from the + // ephemeral key is enough; nothing is broadcast. + tx.signature = await account.signTransaction(tx); + const simulated = await simulate(provider, tx); + const failReason = typeof simulated.raw.failReason === 'string' ? simulated.raw.failReason : ''; + console.log('\nsimulateTransaction (signed with the throwaway key, not broadcast):'); + console.log(` status ${simulated.status.toString()} (the sandbox execution outcome)`); + if (failReason) { + console.log(` failReason: ${failReason}`); + } + console.log(' (an unfunded sender means "fail" here — simulate caught it for free, no gas spent)'); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start +``` + +Expected output (the throwaway address changes every run; everything else is +stable): + +```text +Throwaway sender/receiver: erd1u34zst7xxnkj2f097mw7j3xqx75mxnfd2tut5nyvl7usfewtjgps6yzv0d + +estimateTransactionCost (unsigned): + gasLimit 50000 + +simulateTransaction (signed with the throwaway key, not broadcast): + status fail (the sandbox execution outcome) + failReason: insufficient balance for fees, has: 0, wanted: 50000000000000 + (an unfunded sender means "fail" here — simulate caught it for free, no gas spent) +``` + +## How it works + +**The failure is the point.** The throwaway sender has no funds, so simulate +returns `status: fail` with `failReason: insufficient balance for fees`, the exact +error you would otherwise have paid gas to discover on-chain. Simulate is how you +catch that first. On a funded sender the same call would return `status: success` +and the real smart-contract results. + +**Estimate needs no signature; simulate does.** `estimateTransactionCost` works on +the raw, unsigned transaction, the SDK injects a dummy 64-byte signature and +recalls the nonce for you internally (confirmed in +`node_modules/@multiversx/sdk-core/out/networkProviders/proxyNetworkProvider.js`). +`simulateTransaction` rejects a nil signature even though `checkSignature` +defaults to `false`, so the transaction must carry one, and a throwaway key's +signature is enough because nothing is broadcast. + +**Send to self to keep the simulation same-shard.** Simulate runs on the sender's +shard. A cross-shard transaction's simulate reports only routing (`senderShard` / +`receiverShard`) with no execution status; a same-shard one (sender === receiver +here) always returns a definitive status. That is why `failReason` is populated on +every run. + +## Pitfalls + +:::warning[Pitfall 1: simulate needs a signature, estimate does not] +`simulateTransaction` throws `nil signature while trying to simulate` on a +transaction with no signature, even with `checkSignature=false`. Sign it first (a +throwaway key is fine, nothing is sent). `estimateTransactionCost` has no such +requirement. +::: + +:::note[Pitfall 2: a cross-shard simulate returns routing, not a status] +Simulate executes on the sender's shard only. If sender and receiver are in +different shards, the response is just `{ senderShard, receiverShard }` and +`status` is empty — not a bug, just the limit of a single-shard sandbox. Send to +self (or keep both endpoints in one shard) for a definitive status. +::: + +:::note[Pitfall 3: estimate's status field is not the execution status] +`TransactionCostResponse.status` is empty for a plain transfer, the `/cost` +endpoint returns a `gasLimit`, not an execution outcome. For "would this succeed", +use `simulateTransaction`, not the `status` on the estimate response. +::: + +## See also + +- [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld) + is how you actually send it once simulate says it will succeed. +- [Fetch network config and status](/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-network-config-status) + provides the `gasPerDataByte` / `minGasLimit` formula that estimate spares you + from applying by hand. +- [Configure a network provider](/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider) + builds the provider both dry-run reads run on. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint.mdx b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint.mdx new file mode 100644 index 000000000..ed4bf0022 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint.mdx @@ -0,0 +1,207 @@ +--- +title: Call a contract endpoint with native JS args (NativeSerializer) +description: Call a mutable contract endpoint with plain JS arguments, auto-converted to typed values by sdk-core's NativeSerializer, verified against live devnet. +difficulty: intermediate +est_minutes: 7 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - smart-contract + - abi + - transaction + - typescript + - devnet +--- + +Call a smart contract's mutable endpoint, passing a **plain JS value** as the +argument, with no manual `TypedValue` construction. The ABI (see +[Load an ABI](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi)) is +what lets sdk-core's `NativeSerializer` do this conversion automatically. + +Target: the real, currently-deployed devnet **adder** contract, +`erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug`, the same +contract `mx-sdk-js-core`'s own cookbook uses as its running example, and the one +[Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) +reads from. + +**When NOT to use this recipe:** for a browser dApp where the *end user's own +wallet* signs, see +[Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) +and `mx-template-dapp`'s `PingPongAbi` widget (same ABI plus Factory pattern, +driven from a connected wallet). This recipe is for when **your own code holds +the private key**. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites for how to generate a throwaway one. **No devnet EGLD required**, + see "How it works" below. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/call-contract-endpoint +npm install +npm run build +npm start -- ./wallet.pem 7 +``` + +## Calling the endpoint + +```ts title="src/callAdd.ts" +// src/callAdd.ts — calling a mutable endpoint with a plain JS value as the +// argument, letting sdk-core's NativeSerializer convert it to the ABI's +// declared type (`number` / `bigint` / `BigNumber` -> numerical types, +// including BigUint). +// +// Target: the real, currently-deployed devnet **adder** contract +// (erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug) — the same +// contract mx-sdk-js-core's own cookbook uses as its running example for +// "Calling a smart contract using the controller." +// +// One spelling detail worth being explicit about: the options shape for +// `createTransactionForExecute` is `function` / `arguments`, NOT `functionName` +// / `args`. The installed SDK's `ContractExecuteInput` type +// (out/smartContracts/resources.d.ts) is: +// +// export declare type ContractExecuteInput = { +// contract: Address; +// gasLimit?: bigint; +// function: string; // NOT functionName +// arguments?: any[]; // NOT args +// nativeTransferAmount?: bigint; +// tokenTransfers?: TokenTransfer[]; +// }; +// +// This is the same shape used by both `SmartContractController` and +// `SmartContractTransactionsFactory`, and matches the real mx-sdk-js-core +// cookbook and mx-template-dapp's shipped `PingPongAbi` widget. The +// `function` / `arguments` spelling is used throughout this recipe. + +import { Address } from '@multiversx/sdk-core'; +import type { Abi, Account, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export const ADDER_CONTRACT_ADDRESS = + 'erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug'; + +export interface CallAddOutput { + txHash: string; +} + +/** + * Calls the adder contract's `add(value: BigUint)` endpoint, passing a plain + * JS `number` for `value`. The ABI (loaded by the caller) is what lets + * `NativeSerializer` convert that `number` into a `BigUint` typed value + * automatically. Without an ABI, you would have to pass a `BigUIntValue` (or + * similar `TypedValue`) instead. + */ +export async function callAdd( + entrypoint: DevnetEntrypoint, + abi: Abi, + sender: Account, + amountToAdd: number, +): Promise { + const controller = entrypoint.createSmartContractController(abi); + + const transaction = await controller.createTransactionForExecute(sender, sender.getNonceThenIncrement(), { + contract: Address.newFromBech32(ADDER_CONTRACT_ADDRESS), + function: 'add', + arguments: [amountToAdd], + gasLimit: 5_000_000n, + }); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash }; +} +``` + +## Run it + +```bash +npm start -- [amountToAdd] +``` + +Expected output: + +```text +Sender: erd1... (nonce 0) +Contract: erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug (adder, live devnet) +Calling: add(7) <- plain JS number, converted to BigUint by NativeSerializer + +Sent. Transaction hash: <64-char hex hash> +``` + +(An unfunded wallet gets a clean "insufficient funds" rejection here instead of +a hash, see "How it works.") + +## How it works + +**A plain JS `number` becomes a `BigUint` typed value automatically.** Because +the `SmartContractController` was constructed with the adder ABI, +`NativeSerializer` looks up `add`'s declared input type (`BigUint`) and converts +the plain number `7` into the right typed value before encoding it. Verified +directly: sending this transaction logged a `data` field of base64 `YWRkQDA3`, +which decodes byte-for-byte to `add@07`, the endpoint name, the `@` argument +separator, and `07`, the hex encoding of `7`. + +**The options shape is `function` / `arguments`.** Reading the installed +`out/smartContracts/resources.d.ts` shows the real type: + +```ts +export declare type ContractExecuteInput = { + contract: Address; + gasLimit?: bigint; + function: string; // NOT functionName + arguments?: any[]; // NOT args + nativeTransferAmount?: bigint; + tokenTransfers?: TokenTransfer[]; +}; +``` + +It is identical on both `SmartContractController` and +`SmartContractTransactionsFactory`, and matches the real `mx-sdk-js-core` +cookbook and `mx-template-dapp`'s shipped `PingPongAbi` widget. Older snippets +that show `functionName` / `args` will not compile against the installed SDK. + +**Why an unfunded wallet is enough to verify this end to end.** Sending this +transaction from a freshly generated, intentionally unfunded devnet wallet was +rejected with a clean, specific `insufficient funds` error, never a +malformed-request or bad-signature error. That is the strongest proof available, +without a funded wallet, that the whole pipeline (ABI loading, NativeSerializer +conversion, nonce handling, signing, serialization) produced a well-formed, +correctly-signed transaction. + +## Pitfalls + +:::danger[Pitfall 1: functionName/args will not compile] +Use `function` / `arguments` instead, see "How it works" above. This is a real +difference between older illustrative snippets and the installed SDK, not a style +preference. +::: + +:::warning[Pitfall 2: without an ABI, a plain-number argument would not work] +`NativeSerializer` only knows how to convert a plain JS value because the ABI +told it what type to convert it *to*. Calling the same endpoint without an ABI +requires constructing a `BigUIntValue(7)` (or similar `TypedValue`) yourself. +::: + +:::note[Pitfall 3: this recipe deliberately never succeeds against a real balance] +It proves the request is well-formed via a clean "insufficient funds" rejection, +not a confirmed on-chain state change. Fund the wallet first (real devnet EGLD) +and the same code actually increments the adder's stored sum; nothing in +`callAdd.ts` changes for that case. +::: + +## See also + +- [Load an ABI](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi) + is the ABI-loading step this recipe builds on. +- [Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) + reads back the adder's stored sum, no wallet needed. +- [Call a payable endpoint with EGLD](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-payable-endpoint) + is the same ABI-driven pattern, attaching a payment. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-payable-endpoint.mdx b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-payable-endpoint.mdx new file mode 100644 index 000000000..990de7853 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-payable-endpoint.mdx @@ -0,0 +1,209 @@ +--- +title: Call a payable endpoint with EGLD +description: Call a payable contract endpoint, attaching EGLD via nativeTransferAmount, against a real live devnet contract with sdk-core's controller pattern. +difficulty: intermediate +est_minutes: 7 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - smart-contract + - abi + - transaction + - gas + - devnet + - typescript +--- + +Call a **payable** smart contract endpoint, attaching EGLD via +`nativeTransferAmount` (the same option works for plain EGLD, no token +identifier needed). + +Target: the real, currently-deployed devnet **ping-pong** contract, +`erd1qqqqqqqqqqqqqpgqm6ad6xrsjvxlcdcffqe8w58trpec09ug9l5qde96pq`, the exact +address `mx-template-dapp`'s own `src/config/config.devnet.ts` ships as its +default `contractAddress`. Its own ABI docs: "A contract that allows anyone to +send a fixed sum, locks it for a while and then allows users to take it back... +Only the set amount can be `ping`-ed, no more, no less." This recipe queries that +fixed amount first (`getPingAmount()`, the same pattern as +[Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view)) +rather than hardcoding it. + +**When NOT to use this recipe:** for a browser dApp where the *end user's own +wallet* signs, see +[Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) +and `mx-template-dapp`'s `PingPongAbi` widget (same ABI plus Factory plus +`nativeTransferAmount` pattern, driven from a connected wallet). This recipe is +for when **your own code holds the private key**. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites for how to generate a throwaway one. **No devnet EGLD required**, + see "How it works" below. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/call-payable-endpoint +npm install +npm run build +npm start -- ./wallet.pem +``` + +## Calling the payable endpoint + +```ts title="src/callPing.ts" +// src/callPing.ts — calling a PAYABLE endpoint, attaching EGLD via +// `nativeTransferAmount` (the same option works for plain EGLD, no token +// identifier needed). +// +// Target: the real, currently-deployed devnet **ping-pong** contract — +// erd1qqqqqqqqqqqqqpgqm6ad6xrsjvxlcdcffqe8w58trpec09ug9l5qde96pq — the exact +// contract address mx-template-dapp's own config.devnet.ts ships as its default +// contractAddress, and the same contract mx-template-dapp's PingPongAbi widget +// calls. Its ABI docs: "A contract that allows anyone to send a fixed sum, +// locks it for a while and then allows users to take it back... Only the set +// amount can be `ping`-ed, no more, no less." That fixed amount is itself a +// read-only view (`getPingAmount`) — this recipe queries it first, then uses +// the result as the payment. + +import { Address } from '@multiversx/sdk-core'; +import type { Abi, Account, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export const PING_PONG_CONTRACT_ADDRESS = + 'erd1qqqqqqqqqqqqqpgqm6ad6xrsjvxlcdcffqe8w58trpec09ug9l5qde96pq'; + +/** + * Reads the exact EGLD amount this ping-pong instance requires for `ping()` — a + * read-only view, no wallet needed (same pattern as the "Query a read-only + * view" recipe). This deployed instance returns exactly 1000000000000000000 + * (1 EGLD). + */ +export async function queryPingAmount(entrypoint: DevnetEntrypoint, abi: Abi): Promise { + const controller = entrypoint.createSmartContractController(abi); + const [pingAmount] = await controller.query({ + contract: Address.newFromBech32(PING_PONG_CONTRACT_ADDRESS), + function: 'getPingAmount', + arguments: [], + }); + return BigInt((pingAmount as { toString(base: number): string }).toString(10)); +} + +export interface CallPingOutput { + txHash: string; +} + +/** + * Calls `ping()`, attaching exactly `pingAmountInSmallestDenomination` of EGLD + * via `nativeTransferAmount` — the same option `createTransactionForExecute` + * uses for ANY native EGLD payment, token payments, or both together. `ping` + * itself declares zero ABI inputs (`arguments: []`); the payment is not an + * argument, it is the transaction's attached value. + */ +export async function callPing( + entrypoint: DevnetEntrypoint, + abi: Abi, + sender: Account, + pingAmountInSmallestDenomination: bigint, +): Promise { + const controller = entrypoint.createSmartContractController(abi); + + const transaction = await controller.createTransactionForExecute(sender, sender.getNonceThenIncrement(), { + contract: Address.newFromBech32(PING_PONG_CONTRACT_ADDRESS), + function: 'ping', + arguments: [], + gasLimit: 6_000_000n, + nativeTransferAmount: pingAmountInSmallestDenomination, + }); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash }; +} +``` + +## Run it + +```bash +npm start -- +``` + +Expected output: + +```text +Contract: erd1qqqqqqqqqqqqqpgqm6ad6xrsjvxlcdcffqe8w58trpec09ug9l5qde96pq (ping-pong, live devnet) +Required ping amount (from getPingAmount()): 1000000000000000000 (smallest denomination) +Sender: erd1... (nonce 0) +Calling: ping() <- payable, attaching 1000000000000000000 EGLD via nativeTransferAmount + +Sent. Transaction hash: <64-char hex hash> +``` + +(An unfunded wallet gets a clean "insufficient funds" rejection here instead of +a hash, see "How it works.") + +## How it works + +**`nativeTransferAmount` attaches EGLD to a call without it being an ABI +argument.** `ping()`'s ABI declares zero inputs; the payment is not a function +argument, it is the transaction's attached value, same as sending EGLD to any +address, except this transaction also carries `data: "ping"` so the contract's +VM execution knows which endpoint to run. Verified directly: sending this call +logged `"value":"1000000000000000000","data":"cGluZw=="`, where +`1000000000000000000` is exactly 1 EGLD and `cGluZw==` decodes to the literal +string `"ping"` (no `@`-separated arguments, since none are declared). + +**The required amount is discovered, not hardcoded.** `queryPingAmount()` calls +`getPingAmount()`, a read-only view, no wallet needed, before `callPing()` builds +a transaction. This deployed instance requires exactly `1000000000000000000` +(1 EGLD). A different ping-pong deployment could require a different fixed +amount, set once at deploy time; querying it is what makes this recipe correct +for whichever instance you point it at. + +**Why an unfunded wallet is enough to verify this end to end.** Sending this +transaction from a freshly generated, intentionally unfunded devnet wallet was +rejected with a clean, specific `insufficient funds` error, never a +malformed-request or bad-signature error. That is proof the whole pipeline +(querying the live required amount, building the payable call, attaching +`nativeTransferAmount`, signing) produced a well-formed transaction, without +needing to fund the wallet with 1+ EGLD. + +The crowdfunding tutorial contract in `mx-sdk-rs` +(`contracts/examples/crowdfunding`) has a `fund()` endpoint of the same shape, +payable, accepts EGLD, no arguments, if you want the same pattern in a Rust +contract you deploy yourself. + +## Pitfalls + +:::danger[Pitfall 1: sending the wrong amount is an on-chain-enforced failure for this contract] +ping-pong's own logic accepts only its exact configured `ping_amount`, "no more, +no less." Always query the real value (`getPingAmount()`) instead of assuming a +round number; this recipe's default is 1 EGLD only because that is what this +particular deployed instance happens to require. +::: + +:::warning[Pitfall 2: nativeTransferAmount is separate from arguments] +It is easy to assume payment has to be encoded as an ABI argument somehow. It +does not. A payable endpoint with real arguments would use both `arguments` +(for the declared inputs) and `nativeTransferAmount` (for the payment) together. +::: + +:::note[Pitfall 3: this recipe deliberately never succeeds against a real balance] +It proves the request is well-formed via a clean "insufficient funds" rejection, +not a confirmed on-chain state change. Funding the wallet with at least 1 EGLD +(plus gas) lets the same code actually `ping` the contract for real; nothing in +`callPing.ts` changes for that case. +::: + +## See also + +- [Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) + is the `getPingAmount()` query pattern this recipe depends on. +- [Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint) + is the non-payable counterpart, calling adder's `add(value)` instead. +- [Load an ABI](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi) + is the ABI-loading step both of the above build on. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/decode-contract-events.mdx b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/decode-contract-events.mdx new file mode 100644 index 000000000..96386e574 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/decode-contract-events.mdx @@ -0,0 +1,194 @@ +--- +title: Decode contract events +description: Decode a smart contract's emitted events into named typed fields with sdk-core's TransactionEventsParser and an ABI, verified against a real event. +difficulty: intermediate +est_minutes: 7 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - smart-contract + - abi + - events + - typescript +--- + +Decode the events a smart contract emits into named, typed fields, using an ABI +and `TransactionEventsParser`. A raw event is a bag of base64 topics and data +bytes; the parser uses the ABI's event definition to turn those bytes into a +usable object. + +This recipe decodes a **real** `pongEvent` from the ping-pong contract, captured +verbatim from devnet transaction `922dbae7...`. The decoded `user` field must +equal the real sender of that transaction, which is how the recipe verifies +itself. + +## Prerequisites + +- Node.js >= 20.13.1. +- For the default (offline) decode: nothing, the raw event is baked in. +- For the `live` mode: devnet network access. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/decode-contract-events +npm install +npm run build +``` + +## Decoding + +```ts title="src/decodeEvents.ts" +// src/decodeEvents.ts - decoding the events a smart contract emits, using an +// ABI and `TransactionEventsParser`. A raw event is a bag of base64 topics and +// data bytes; the parser uses the ABI's event definition to turn those bytes +// into named, typed fields. +// +// Target: the real **ping-pong** contract's `pongEvent`, which has one indexed +// input `user: Address`. This recipe decodes a REAL pongEvent captured verbatim +// from devnet transaction +// 922dbae7f85c949add1c5971f7cb88ab2f806760851539e53cdf48e055d12740 +// (whose sender pong-ed the contract). The decoded `user` must equal that +// sender: erd179ktm46cy3hvcg6r0c9khafgtlwxq3rswj578x7wl2ltkv6k5suq7mn47d. +// +// KEY DETAIL: the parser matches the event to the ABI by its FIRST TOPIC +// ("pongEvent"), which is the ABI event's identifier. Note the log's own +// `identifier` field here is "pong" (the endpoint name), which is different. +// `firstTopicIsIdentifier` defaults to true, so the first topic wins. + +import { + TransactionEvent, + TransactionEventsParser, + findEventsByFirstTopic, +} from '@multiversx/sdk-core'; +import type { Abi, DevnetEntrypoint } from '@multiversx/sdk-core'; + +// A real pongEvent, exactly as the devnet API returned it (base64 topics). +// topics[0] "cG9uZ0V2ZW50" is base64 for "pongEvent" (the ABI event id); +// topics[1] is the 32-byte pubkey of the indexed `user` field. +export const CAPTURED_PONG_EVENT = { + address: 'erd1qqqqqqqqqqqqqpgqm6ad6xrsjvxlcdcffqe8w58trpec09ug9l5qde96pq', + identifier: 'pong', + topics: ['cG9uZ0V2ZW50', '8Wy911gkbswjQ34La/UoX9xgRHB0qeObzvq+uzNWpDg='], + data: '', + additionalData: [''], +} as const; + +export const PONG_EVENT_TX_HASH = + '922dbae7f85c949add1c5971f7cb88ab2f806760851539e53cdf48e055d12740'; + +export interface DecodedPongEvent { + /** The bech32 address of the `user` who pong-ed. */ + user: string; +} + +/** + * Decodes the real captured pongEvent above. Fully offline and deterministic: + * the raw bytes are baked in, so this always produces the same result and never + * depends on the transaction still being retrievable from the network. + */ +export function decodeCapturedPongEvent(abi: Abi): DecodedPongEvent { + const event = TransactionEvent.fromHttpResponse({ + address: CAPTURED_PONG_EVENT.address, + identifier: CAPTURED_PONG_EVENT.identifier, + topics: [...CAPTURED_PONG_EVENT.topics], + data: CAPTURED_PONG_EVENT.data, + additionalData: [...CAPTURED_PONG_EVENT.additionalData], + }); + + const parser = new TransactionEventsParser({ abi }); + const decoded = parser.parseEvent({ event }) as { user: { toBech32(): string } }; + return { user: decoded.user.toBech32() }; +} + +/** + * The production path: fetch a live transaction, gather its pongEvent(s), and + * decode them. `findEventsByFirstTopic(tx, "pongEvent")` pulls exactly the + * events whose first topic is the ABI event identifier, across the main log and + * every smart-contract-result log. Returns one decoded object per event. + * + * Devnet may prune old transactions, so this is the "how you'd do it against + * your own fresh transaction" path; the captured decode above is the durable + * one this recipe verifies against. + */ +export async function decodePongEventsFromTransaction( + entrypoint: DevnetEntrypoint, + abi: Abi, + txHash: string, +): Promise { + const transactionOnNetwork = await entrypoint.getTransaction(txHash); + const events = findEventsByFirstTopic(transactionOnNetwork, 'pongEvent'); + + const parser = new TransactionEventsParser({ abi }); + const decoded = parser.parseEvents({ events }) as Array<{ user: { toBech32(): string } }>; + return decoded.map((d) => ({ user: d.user.toBech32() })); +} +``` + +## Run it + +```bash +# Decode the captured real event - offline, deterministic: +npm start + +# Fetch a live transaction and decode its pongEvent(s): +npm start -- live +``` + +Expected output of the default mode: + +```text +Decoding the captured pongEvent (offline) from tx 922dbae7f85c949add1c5971f7cb88ab2f806760851539e53cdf48e055d12740... + decoded user: erd179ktm46cy3hvcg6r0c9khafgtlwxq3rswj578x7wl2ltkv6k5suq7mn47d + expected user: erd179ktm46cy3hvcg6r0c9khafgtlwxq3rswj578x7wl2ltkv6k5suq7mn47d + match: true +``` + +## How it works + +**The ABI event definition drives the decode.** ping-pong's ABI declares +`pongEvent` with one indexed input, `user: Address`. +`new TransactionEventsParser({ abi }).parseEvent({ event })` reads the event's +topics and returns `{ user }` as a real `Address`. The captured event is a +verbatim copy of a real on-chain pongEvent, so decoding it offline is +deterministic and genuine. + +**Getting events from a live transaction.** The `live` mode fetches with +`entrypoint.getTransaction(txHash)`, then `findEventsByFirstTopic(tx, +"pongEvent")` pulls matching events across the main log and every +smart-contract-result log, and `parseEvents({ events })` decodes them. +`gatherAllEvents(tx)` is the unfiltered version. + +## Pitfalls + +:::warning[Pitfall 1: the parser matches on the FIRST TOPIC, not the log identifier] +For `pongEvent`, the first topic is base64 `"cG9uZ0V2ZW50"` = `"pongEvent"`, the +ABI event's identifier. The log's own `identifier` field is `"pong"` (the +endpoint name), which is different. `firstTopicIsIdentifier` defaults to `true`, +so the first topic wins. Load the ABI that actually declares the event, or there +is nothing to match, and an ABI that lacks the event decodes nothing. +::: + +:::note[Pitfall 2: topics and data are base64 on the wire] +`TransactionEvent.fromHttpResponse({ topics, data, additionalData })` +base64-decodes each into a `Buffer` for you. Pass the API's base64 strings +straight through; do not decode them yourself first. +::: + +:::note[Pitfall 3: devnet prunes old transactions] +The `live` mode points at a real tx by hash; if devnet has pruned it, pass your +own recent pong transaction. The captured offline decode never rots, which is why +it is the default and the verification anchor. +::: + +## See also + +- [Decode contract return data (ABI codec)](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/decode-return-data) + is the codec that also powers event field decoding. +- [Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) + reads live state from the same ping-pong contract. +- [Load an ABI](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi) + is where the event definition comes from. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/decode-return-data.mdx b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/decode-return-data.mdx new file mode 100644 index 000000000..bf7a8b2ec --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/decode-return-data.mdx @@ -0,0 +1,189 @@ +--- +title: Decode contract return data (ABI codec) +description: Decode raw contract return data and encode or decode custom struct and enum types with sdk-core's BinaryCodec, getStruct, and getEnum, fully offline. +difficulty: advanced +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - smart-contract + - abi + - codec + - typescript +--- + +Use an ABI plus the low-level `BinaryCodec` to decode raw contract bytes into +typed values, and to encode typed values back into bytes. This is the layer +beneath `controller.parseQueryResponse` / `parseExecute`: reach for it when you +hold raw return data, a struct, or an enum and want to convert it by hand. + +Three real demonstrations: decode `getPingAmount`'s raw return data into a +`BigUint`, encode the multisig `EsdtTokenPayment` struct, and decode the multisig +`Action` enum. All offline and deterministic. + +## Prerequisites + +- Node.js >= 20.13.1. +- No wallet, no gas, no network. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/decode-return-data +npm install +npm run build +npm start +``` + +## Decoding and encoding + +```ts title="src/decodeReturnData.ts" +// src/decodeReturnData.ts - using an ABI plus the low-level `BinaryCodec` to +// decode raw contract bytes into typed values, and to encode typed values back +// into bytes. This is the layer beneath `controller.parseQueryResponse` / +// `parseExecute`: reach for it when you hold raw return data, a struct, or an +// enum and want to convert by hand. +// +// Three real, verified demonstrations: +// 1. Decode raw RETURN DATA: ping-pong's getPingAmount returns a BigUint; +// the raw bytes 0de0b6b3a7640000 decode to 1000000000000000000 (1 EGLD), +// the same value the live query returns. +// 2. Encode a STRUCT: the multisig contract's EsdtTokenPayment struct, +// via abi.getStruct(...) + Struct/Field + codec.encodeNested. +// 3. Decode an ENUM: the multisig contract's Action enum, via +// abi.getEnum(...) + codec.decodeNested, from a real encoded action. + +import { + BinaryCodec, + Struct, + Field, + TokenIdentifierValue, + U64Value, + BigUIntValue, +} from '@multiversx/sdk-core'; +import type { Abi } from '@multiversx/sdk-core'; + +const codec = new BinaryCodec(); + +/** + * Decode raw contract RETURN DATA into a value, using the output type the ABI + * declares for a given endpoint. `decodeTopLevel` is the top-level form (a whole + * return-data part); `decodeNested` is for values embedded inside a larger + * buffer. + * + * Returns the decimal string form, since a decoded BigUint is a BigNumber. + */ +export function decodeReturnValue(abi: Abi, endpointName: string, returnDataHex: string): string { + const endpoint = abi.getEndpoint(endpointName); + const outputType = endpoint.output[0]?.type; + if (!outputType) { + throw new Error(`Endpoint ${endpointName} declares no output type.`); + } + const decoded = codec.decodeTopLevel(Buffer.from(returnDataHex, 'hex'), outputType); + return String(decoded.valueOf()); +} + +/** + * Encode a custom STRUCT (EsdtTokenPayment) into binary, using the struct type + * looked up from the ABI. Each `Field` pairs a typed value with its field name. + * Returns the nested-encoding hex. + */ +export function encodeEsdtTokenPayment( + abi: Abi, + tokenIdentifier: string, + tokenNonce: bigint, + amount: bigint, +): string { + const paymentType = abi.getStruct('EsdtTokenPayment'); + const paymentStruct = new Struct(paymentType, [ + new Field(new TokenIdentifierValue(tokenIdentifier), 'token_identifier'), + new Field(new U64Value(tokenNonce), 'token_nonce'), + new Field(new BigUIntValue(amount), 'amount'), + ]); + return codec.encodeNested(paymentStruct).toString('hex'); +} + +/** + * Decode a custom ENUM (Action) from binary, using the enum type looked up from + * the ABI. Returns the variant name and the decoded fields object. + */ +export function decodeAction(abi: Abi, dataHex: string): { name: string; value: unknown } { + const actionType = abi.getEnum('Action'); + const [decoded] = codec.decodeNested(Buffer.from(dataHex, 'hex'), actionType); + const value = decoded.valueOf() as { name: string }; + return { name: value.name, value }; +} +``` + +## Run it + +```bash +npm start +``` + +Expected output: + +```text +1) Decode raw return data (ping-pong getPingAmount -> BigUint): + 0de0b6b3a7640000 -> 1000000000000000000 + equals 1 EGLD (1e18): true + +2) Encode a struct (multisig EsdtTokenPayment): + encoded: 0000000b544553542d3862303238660000000000000000000000022710 + matches expected: true + +3) Decode an enum (multisig Action): + variant: SendTransferExecuteEgld +``` + +## How it works + +**Decode raw return data with the ABI's declared output type.** A contract's +return data is just bytes. `abi.getEndpoint("getPingAmount").output[0].type` is +the type the ABI declares for that endpoint's result (`BigUint`), and +`codec.decodeTopLevel(buffer, type)` turns raw bytes into a typed value. +`0de0b6b3a7640000` decodes to `1000000000000000000` (1 EGLD), the same value the +live [query recipe](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) +returns. + +**Encode a struct.** `abi.getStruct("EsdtTokenPayment")` returns the struct type; +build a `Struct` from `Field`s (each a typed value plus its field name) and call +`codec.encodeNested(struct)`. + +**Decode an enum.** `abi.getEnum("Action")` returns the enum type; +`codec.decodeNested(buffer, type)` returns the decoded value and the number of +bytes consumed. The real `Action` bytes decode to the `SendTransferExecuteEgld` +variant. + +## Pitfalls + +:::note[Pitfall 1: a decoded value's static type is TypedValue] +Call `.valueOf()` and convert explicitly. A decoded `BigUint` is a `BigNumber`, +so this recipe returns `String(decoded.valueOf())`. `strict` mode will not narrow +the shape for you, the same caveat as query results. +::: + +:::warning[Pitfall 2: decodeTopLevel vs decodeNested are not interchangeable] +Top-level and nested encodings differ (nested values are length-prefixed where +top-level ones are not). Decode return-data parts with `decodeTopLevel`; decode a +value pulled from inside a larger buffer with `decodeNested`. Using the wrong one +yields garbage or throws. +::: + +:::note[Pitfall 3: getStruct / getEnum need a name that exists in the ABI] +They throw if the type name is missing. `adder` and `ping-pong` declare no custom +types, which is why this recipe uses the multisig ABI for the struct and enum +demonstrations. +::: + +## See also + +- [Decode contract events](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/decode-contract-events) + decodes event fields with the same codec. +- [Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) + is the high-level `parseQueryResponse` that sits on top of this codec. +- [Load an ABI](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi) + is where `getStruct` / `getEnum` / `getEndpoint` come from. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi.mdx b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi.mdx new file mode 100644 index 000000000..b602bccce --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi.mdx @@ -0,0 +1,257 @@ +--- +title: Load an ABI +description: Load a contract's ABI from a file, a URL, or by hand with sdk-core's Abi/AbiRegistry, then introspect its endpoints and argument types. +difficulty: beginner +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - abi + - smart-contract + - typescript +--- + +Load a contract's ABI (Application Binary Interface), the description of its +endpoints, custom types, and events, three ways: from a local file, from a URL, +and by hand when no ABI file exists at all. This is the prerequisite every other +recipe in this category builds on: +[Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint), +[Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view), +and [Call a payable endpoint with EGLD](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-payable-endpoint) +all load an ABI first, the same way. + +This recipe never sends a transaction or calls a devnet API. Loading and +inspecting an ABI is entirely local, plus one plain HTTPS fetch for the "from a +URL" case. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access to fetch one ABI JSON file over HTTPS. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/load-abi +npm install +npm run build +npm start +``` + +## Loading + +```ts title="src/loadAbi.ts" +// src/loadAbi.ts — three ways to load a contract's ABI, plus the lookup +// methods each one gives you afterward. +// +// A naming subtlety worth being explicit about: `AbiRegistry` is the class +// with the real implementation — a protected constructor plus the static +// `create()` factory used below. `Abi` is a subclass that adds its own public +// constructor (taking already-built `EndpointDefinition`/`CustomType` objects, +// not raw JSON, which is not what you want for loading a JSON file) but does +// NOT override `create()`, so it inherits AbiRegistry's version as-is. +// `create()` always does `new AbiRegistry(...)` internally, regardless of +// which class name you called it through — so `Abi.create(json)` does not +// actually give you an `Abi` instance despite the name; both `Abi.create(json)` +// and `AbiRegistry.create(json)` return the exact same `AbiRegistry` instance +// (this is also the declared TypeScript return type of `create()` on both +// classes). The mx-sdk-js-core cookbook uses `Abi.create(...)`; mx-template-dapp's +// shipped widgets use `AbiRegistry.create(...)`. Both are correct and +// interchangeable — this recipe uses `Abi.create(...)`. + +import * as fs from 'fs'; +import axios from 'axios'; +import { Abi } from '@multiversx/sdk-core'; +import type { AbiRegistry } from '@multiversx/sdk-core'; + +/** + * Loads a contract's ABI from a local JSON file. + */ +export function loadAbiFromFile(filePath: string): AbiRegistry { + const json = fs.readFileSync(filePath, { encoding: 'utf8' }); + return Abi.create(JSON.parse(json) as Record); +} + +/** + * Loads a contract's ABI from a URL — e.g. straight from a GitHub raw content + * URL, the same source this recipe's own src/adder.abi.json was copied from. + */ +export async function loadAbiFromUrl(url: string): Promise { + const response = await axios.get>(url); + return Abi.create(response.data); +} + +/** + * Manually constructs an ABI when no ABI file is available but the endpoint + * names and argument types are known. `foo` and `bar` are illustrative names + * only; this does not correspond to any real deployed contract. + */ +export function buildAbiManually(): AbiRegistry { + return Abi.create({ + endpoints: [ + { + name: 'foo', + inputs: [{ type: 'BigUint' }, { type: 'u32' }, { type: 'Address' }], + outputs: [{ type: 'u32' }], + }, + { + name: 'bar', + inputs: [{ type: 'counted-variadic' }, { type: 'variadic' }], + outputs: [], + }, + ], + }); +} + +export interface EndpointSummary { + name: string; + mutability: string; + inputs: string[]; + outputs: string[]; +} + +/** + * Summarizes every endpoint an ABI declares: name, mutability (readonly vs + * mutable, from `EndpointModifiers.isReadonly()`), and each parameter's type + * name — using `abi.getEndpoints()` and `EndpointDefinition.input` / `.output`. + * Those field names are singular, each an array; the ABI JSON itself uses the + * plural `inputs`/`outputs`, but the parsed `EndpointDefinition` object does not. + */ +export function summarizeEndpoints(abi: AbiRegistry): EndpointSummary[] { + return abi.getEndpoints().map((endpoint) => ({ + name: endpoint.name, + mutability: endpoint.modifiers.isReadonly() ? 'readonly' : 'mutable', + inputs: endpoint.input.map((param) => `${param.name}: ${param.type.getName()}`), + outputs: endpoint.output.map((param) => param.type.getName()), + })); +} +``` + +## Run it + +```ts title="src/index.ts" +// src/index.ts — CLI entry point. Loads the same ABI three ways and prints its +// endpoint summary each time (should be identical for the file/URL cases — +// same JSON, two sources), then shows the manual-construct path with its own +// illustrative endpoints. +// +// Usage: +// npm run build && npm start +// +// No wallet, no PEM, no network write — this recipe only reads a JSON file and +// (for the URL case) fetches one over HTTPS. The recipe bundles src/adder.abi.json. + +import * as path from 'path'; +import { loadAbiFromFile, loadAbiFromUrl, buildAbiManually, summarizeEndpoints } from './loadAbi'; + +const ADDER_ABI_URL = + 'https://raw.githubusercontent.com/multiversx/mx-sdk-js-core/main/src/testdata/adder.abi.json'; + +async function main(): Promise { + console.log('--- Loading from a local file (src/adder.abi.json) ---'); + const fromFile = loadAbiFromFile(path.join(__dirname, '..', 'src', 'adder.abi.json')); + console.log(JSON.stringify(summarizeEndpoints(fromFile), null, 2)); + + console.log(`\n--- Loading the same ABI from a URL (${ADDER_ABI_URL}) ---`); + const fromUrl = await loadAbiFromUrl(ADDER_ABI_URL); + console.log(JSON.stringify(summarizeEndpoints(fromUrl), null, 2)); + + console.log('\n--- Manually constructed ABI (no file, illustrative "foo"/"bar" only) ---'); + const manual = buildAbiManually(); + console.log(JSON.stringify(summarizeEndpoints(manual), null, 2)); + + console.log('\n--- Single-endpoint lookup: abi.getEndpoint("add") ---'); + const addEndpoint = fromFile.getEndpoint('add'); + console.log(`add(${addEndpoint.input.map((p) => `${p.name}: ${p.type.getName()}`).join(', ')})`); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +Expected output (abridged): + +```text +--- Loading from a local file (src/adder.abi.json) --- +[ + { "name": "getSum", "mutability": "readonly", "inputs": [], "outputs": ["BigUint"] }, + { "name": "add", "mutability": "mutable", "inputs": ["value: BigUint"], "outputs": [] } +] + +--- Loading the same ABI from a URL (...) --- +[ ...identical output... ] + +--- Manually constructed ABI (no file, illustrative "foo"/"bar" only) --- +[ + { "name": "foo", "mutability": "mutable", "inputs": ["?: BigUint", "?: u32", "?: Address"], "outputs": ["u32"] }, + { "name": "bar", "mutability": "mutable", "inputs": ["?: Variadic", "?: Variadic"], "outputs": [] } +] + +--- Single-endpoint lookup: abi.getEndpoint("add") --- +add(value: BigUint) +``` + +## How it works + +**`Abi.create(json)` and `AbiRegistry.create(json)` are the same inherited +method.** `AbiRegistry` is the class with the real implementation: a protected +constructor plus the static `create()` factory. `Abi extends AbiRegistry` and +adds its own public constructor (for already-built `EndpointDefinition` / +`CustomType` objects, not raw JSON), but it does not override `create()`, which +always runs `new AbiRegistry(...)` internally. So `Abi.create(json)` does not +actually give you an `Abi` instance; both spellings return the same +`AbiRegistry` instance. Both work identically; this recipe follows the +`Abi.create(...)` naming. + +**`src/adder.abi.json` is the real ABI of a real, currently-deployed devnet +contract**, `erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug`, +copied from `mx-sdk-js-core`'s own `src/testdata/adder.abi.json`, the exact file +its cookbook uses as its running example. +[Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint) +and [Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) +call this same live contract. + +**`abi.getEndpoints()` / `abi.getEndpoint(name)`** return `EndpointDefinition` +objects whose parameter arrays are named `input` / `output` (singular). The raw +ABI JSON uses the plural `inputs`/`outputs`; the parsed object does not carry +that naming through. Each parameter's `.type` is a `Type` object, and +`.getName()` gives its readable type name. + +## Pitfalls + +:::note[Pitfall 1: a freshly-loaded ABI's types start out raw, not known] +`AbiRegistry.remapToKnownTypes()` increases the specificity of a registry's +field and parameter types on a best-effort basis. `Abi.create()` / +`AbiRegistry.create()` already call this internally before returning, so you do +not need to call it yourself for the common case of loading from JSON, only if +you construct a registry through some other path. +::: + +:::warning[Pitfall 2: this recipe never touches devnet] +Loading an ABI is a local operation, plus (for the URL case) one plain HTTPS GET +for a JSON file, nothing MultiversX-specific about that request, and no wallet +or devnet EGLD is needed. If you expected a devnet wallet requirement here, you +are thinking of +[Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint). +::: + +:::danger[Pitfall 3: the manually-constructed ABI is illustrative only] +`foo` and `bar` are not endpoints on any real deployed contract. This shape +exists only to show what `Abi.create()` expects when you know a contract's +interface but do not have its ABI JSON file. Do not copy it expecting it to call +anything real. +::: + +## See also + +- [Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint) + uses this same adder ABI to call `add(value)` on the real deployed contract. +- [Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) + queries the same contract's `getSum()`. +- [Call a payable endpoint with EGLD](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-payable-endpoint) + is the ABI-driven pattern applied to a payable endpoint. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view.mdx b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view.mdx new file mode 100644 index 000000000..c7dc5f25a --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view.mdx @@ -0,0 +1,201 @@ +--- +title: Query a read-only view +description: Query a smart contract's read-only view with sdk-core's SmartContractController, no wallet, no gas, no transaction, verified against live devnet. +difficulty: beginner +est_minutes: 5 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - smart-contract + - abi + - devnet + - typescript +--- + +Query a smart contract's read-only view function. A view function does not +modify the state of the contract, so there is no transaction to send: no wallet, +no nonce, no gas, no signing, no devnet EGLD. This is the cheapest possible way +to read on-chain state. + +This recipe queries two real, currently-deployed devnet contracts: **adder** +(`getSum()`, zero arguments, the same contract as +[Load an ABI](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi) and +[Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint)) +and **ping-pong** (`didUserPing(address)`, which itself takes a native JS +argument, the same contract as +[Call a payable endpoint with EGLD](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-payable-endpoint)). + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access to devnet. No wallet, no PEM, no devnet EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/query-contract-view +npm install +npm run build +npm start +``` + +## Querying + +```ts title="src/queryView.ts" +// src/queryView.ts — querying a read-only view function. A view function does +// not modify the state of the contract, so there is no transaction to send: +// no wallet, no nonce, no gas, no signing. +// +// Targets two real, currently-deployed devnet contracts: +// - adder (erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug) +// `getSum()` — zero-argument view, the mx-sdk-js-core cookbook's own example. +// - ping-pong (erd1qqqqqqqqqqqqqpgqm6ad6xrsjvxlcdcffqe8w58trpec09ug9l5qde96pq) +// `didUserPing(address)` — a view that itself takes a native JS argument +// (an `Address`), to show query arguments go through the same +// NativeSerializer conversion as mutating-endpoint arguments do. + +import { Address } from '@multiversx/sdk-core'; +import type { Abi, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export const ADDER_CONTRACT_ADDRESS = + 'erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug'; +export const PING_PONG_CONTRACT_ADDRESS = + 'erd1qqqqqqqqqqqqqpgqm6ad6xrsjvxlcdcffqe8w58trpec09ug9l5qde96pq'; + +/** + * Queries adder's `getSum()` — the one-step way, via + * `SmartContractController.query()`. Returns already-decoded native values + * (each with a `.valueOf()`/`.toString()` you can read directly) because the + * controller was constructed with the ABI. + */ +export async function queryAdderSum(entrypoint: DevnetEntrypoint, abi: Abi): Promise { + const controller = entrypoint.createSmartContractController(abi); + + const [sum] = await controller.query({ + contract: Address.newFromBech32(ADDER_CONTRACT_ADDRESS), + function: 'getSum', + arguments: [], + }); + + return BigInt((sum as { toString(base: number): string }).toString(10)); +} + +/** + * The same query, split into its three granular steps — create, run, parse. + * Produces the same result; useful when you want to inspect or cache the raw + * `SmartContractQuery` / `SmartContractQueryResponse` in between. + */ +export async function queryAdderSumGranular(entrypoint: DevnetEntrypoint, abi: Abi): Promise { + const controller = entrypoint.createSmartContractController(abi); + + const query = controller.createQuery({ + contract: Address.newFromBech32(ADDER_CONTRACT_ADDRESS), + function: 'getSum', + arguments: [], + }); + const response = await controller.runQuery(query); + const [sum] = controller.parseQueryResponse(response); + + return BigInt((sum as { toString(base: number): string }).toString(10)); +} + +/** + * Queries ping-pong's `didUserPing(address)` — a view that takes a native JS + * argument (an `Address` instance, or equally a bech32 string, per + * NativeSerializer's `Address` conversion rule) instead of none. + */ +export async function queryDidUserPing( + entrypoint: DevnetEntrypoint, + abi: Abi, + userAddress: string, +): Promise { + const controller = entrypoint.createSmartContractController(abi); + + const [didPing] = await controller.query({ + contract: Address.newFromBech32(PING_PONG_CONTRACT_ADDRESS), + function: 'didUserPing', + arguments: [Address.newFromBech32(userAddress)], + }); + + return Boolean((didPing as { valueOf(): boolean }).valueOf()); +} +``` + +## Run it + +```bash +npm start [addressToCheck] +``` + +Expected output: + +```text +Querying getSum() on adder (erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug)... + one-step controller.query(): 84 + three-step create/run/parseQueryResponse(): 84 + (both queries hit the same live contract; equal: true) + +Querying didUserPing(erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th) on ping-pong (erd1qqqqqqqqqqqqqpgqm6ad6xrsjvxlcdcffqe8w58trpec09ug9l5qde96pq)... + didUserPing: false +``` + +(The adder sum grows over time as other cookbook readers call `add()`; expect a +different number, not necessarily `84`.) + +## How it works + +**Two ways to run the same query.** +`controller.query({ contract, function, arguments })` creates the query, runs +it, and parses the result in one call. The three-step form (`createQuery()`, +`runQuery()`, `parseQueryResponse()`) does the same work, split apart for when +you want to inspect or cache the raw `SmartContractQuery` / +`SmartContractQueryResponse` in between. Both are verified here to return +identical results against the real, live adder contract. + +**Query arguments use the exact same `NativeSerializer` conversion as +mutating-endpoint arguments.** `didUserPing(address)` takes an `Address`; this +recipe passes `Address.newFromBech32(userAddress)` directly in the `arguments` +array. + +**No wallet is constructed anywhere in this recipe.** `SmartContractController` +only needs a `chainID` and a network provider (both supplied internally by +`DevnetEntrypoint`) plus the ABI: no `IAccount`, no nonce, no signing. Compare +with [Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint), +whose `createTransactionForExecute` takes a `sender: IAccount` because it builds +a transaction that must be signed; `query()` never does. + +## Pitfalls + +:::note[Pitfall 1: a query result's static type is any] +The SDK decodes the result using the ABI at runtime, but TypeScript's static +type is not narrowed to `bigint` / `boolean` / etc. This recipe casts explicitly +rather than trusting an implicit `any`, worth doing in your own code too, since +`strict` mode will not catch a wrong assumption about a query result's shape for +you. +::: + +:::warning[Pitfall 2: a query reflects state at the moment it runs, not a fixed snapshot] +Two calls to the same query moments apart can return different values if someone +else's transaction lands in between. Nothing wrong with the code, that is what +"live" state means. Neither query here waits for or depends on a particular +block. +::: + +:::note[Pitfall 3: this recipe reads from contracts it does not own] +Both adder and ping-pong are used here only because they are real, stable, +publicly queryable devnet fixtures, the same ones `mx-sdk-js-core`'s cookbook and +`mx-template-dapp`'s default config already point at. This recipe never deploys +or controls either one. +::: + +## See also + +- [Load an ABI](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi) + is the ABI-loading step this recipe builds on. +- [Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint) + is the mutating-endpoint counterpart to this read-only recipe. +- [Call a payable endpoint with EGLD](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-payable-endpoint) + uses this recipe's query pattern to discover how much EGLD to attach. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/compute-contract-address.mdx b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/compute-contract-address.mdx new file mode 100644 index 000000000..ea47afccd --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/compute-contract-address.mdx @@ -0,0 +1,143 @@ +--- +title: Compute a contract address before deploy +description: Predict a smart contract's address before deploying it from the deployer address and deployment nonce with sdk-core's AddressComputer, fully offline. +difficulty: beginner +est_minutes: 5 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - smart-contract + - address + - typescript +--- + +A smart contract's address is a deterministic function of two things: the +deployer's address and the nonce of the deploy transaction. You can compute it +**before** you broadcast the deploy, with no network call, no wallet, and no gas. +`AddressComputer.computeContractAddress` reproduces the exact rule the protocol +uses to assign the address on deploy. + +This is useful when you want to log or store the upcoming address, wire it into a +follow-up transaction in the same batch, or assert after the fact that the +address the network reports matches what you predicted (the +[deploy recipe](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/deploy-contract) +does exactly that). + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe is pure computation and runs fully offline. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/compute-contract-address +npm install +npm run build +npm start +``` + +## Computing + +```ts title="src/computeAddress.ts" +// src/computeAddress.ts - deriving a smart contract's address BEFORE you deploy +// it. A contract's address is a pure function of (deployer address, deployment +// nonce) - no network, no wallet, no signing, no gas. +// `AddressComputer.computeContractAddress` reproduces the exact rule the +// protocol uses when it assigns the address on deploy. +// +// Why you'd want this: you can log/store/print the upcoming contract address, +// wire it into a follow-up transaction in the same batch, or (as the sibling +// deploy recipe does) assert the address the network reports after deploy +// matches what you predicted. +// +// CRITICAL: the address depends on the EXACT nonce the deploy transaction is +// broadcast with. Compute it with the same nonce you will actually use, not a +// stale or placeholder value. See the Pitfalls in the recipe page. + +import { Address, AddressComputer } from '@multiversx/sdk-core'; + +/** + * Computes the (upcoming) address of a smart contract that `deployer` would + * create with a deploy transaction sent at `deploymentNonce`. + * + * `deploymentNonce` is a `bigint` because account nonces are `bigint` + * throughout sdk-core. + */ +export function predictContractAddress(deployer: Address, deploymentNonce: bigint): Address { + const computer = new AddressComputer(); + return computer.computeContractAddress(deployer, deploymentNonce); +} + +/** + * Returns the shard a given address lives in (0, 1, 2, or the metachain). A + * contract is created in the same shard as its deployer, so the predicted + * contract address and the deployer share a shard - this recipe prints both to + * show that. + */ +export function shardOf(address: Address): number { + const computer = new AddressComputer(); + return computer.getShardOfAddress(address); +} +``` + +## Run it + +```bash +npm start [deployerBech32] [nonce] +``` + +Expected output (default deployer, shard 1): + +```text +Deployer: erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th (shard 1) + nonce 0 -> erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3 (shard 1, isSmartContract=true) + nonce 1 -> erd1qqqqqqqqqqqqqpgq2j4t5v0lu0cvrwapl9z5zr88zfcepvjsd8ssc6sfq6 (shard 1, isSmartContract=true) + nonce 42 -> erd1qqqqqqqqqqqqqpgq3ytm9m8dpeud35v3us20vsafp77smqghd8ss4jtm0q (shard 1, isSmartContract=true) +``` + +## How it works + +**The address is `(deployer, deploymentNonce)`, nothing else.** +`computeContractAddress` returns the `Address` the network will assign to a +contract that `deployer` creates with that exact nonce. Because it is +deterministic, you can predict it, store it, or reference it before the deploy is +even signed. Every result is a valid smart contract address, so +`Address.isSmartContract()` returns `true`. + +**A contract lives in its deployer's shard.** `getShardOfAddress` on the +predicted address returns the same shard as the deployer, which this recipe +prints for both. That is why every address above is in shard 1: the deployer is +in shard 1. + +## Pitfalls + +:::warning[Pitfall 1: the nonce must be the one you actually deploy with] +The address changes with every nonce. If you predict at nonce 7 but the deploy +transaction lands at nonce 8 (because another transaction from the same account +went first), the real contract address will not match. A transaction built by +`SmartContractTransactionsFactory` has `nonce = 0n` until you set it, so predict +**after** you assign the intended nonce, not before. +::: + +:::note[Pitfall 2: deploymentNonce is a bigint] +Pass `7n`, not `7`. Account nonces are `bigint` throughout sdk-core. +::: + +:::note[Pitfall 3: AddressComputer defaults to 3 shards] +The constructor takes an optional `numberOfShardsWithoutMeta` (default `3`), +which matches mainnet, devnet, and testnet. Only change it for a custom network +with a different shard count. +::: + +## See also + +- [Deploy a smart contract](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/deploy-contract) + uses this prediction and confirms it against the deployed address. +- [Upgrade a smart contract](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/upgrade-contract) + is the other half of the contract lifecycle. +- [Load an ABI](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi) + is the ABI you pass to the deploy factory or controller. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/deploy-contract.mdx b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/deploy-contract.mdx new file mode 100644 index 000000000..d4b671f93 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/deploy-contract.mdx @@ -0,0 +1,280 @@ +--- +title: Deploy a smart contract +description: Deploy a smart contract from WASM bytecode and constructor arguments with sdk-core's controller and factory, then parse the outcome for the new address. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - smart-contract + - deploy + - devnet + - typescript +--- + +Deploy a smart contract from its WASM bytecode plus constructor arguments, then +parse the deploy outcome to recover the new contract's address. You get +both the controller path (build, nonce, sign in one call) and the factory path +(build only, you sign), against the real **adder** example contract +(`init(initial_value: BigUint)`) using the exact 687-byte WASM and ABI that +`mx-sdk-js-core` ships in its own tests. + +The default `npm start` parses a real, already-completed devnet deploy, so you +can see deploy-outcome parsing work without a funded wallet. + +## Prerequisites + +- Node.js >= 20.13.1. +- For the default `parse` demo: devnet network access only. +- For an actual deploy: a devnet PEM wallet with a little EGLD for gas (see + [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send)). + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/deploy-contract +npm install +npm run build +``` + +## Deploying + +```ts title="src/deploy.ts" +// src/deploy.ts - deploying a smart contract from its WASM bytecode plus +// constructor arguments, two ways (controller and factory), then parsing the +// deploy outcome to recover the new contract's address. +// +// Target contract: the real **adder** example contract, whose constructor is +// `init(initial_value: BigUint)`. Its 687-byte WASM (src/adder.wasm) and ABI +// (src/adder.abi.json) are the exact fixtures mx-sdk-js-core uses in its own +// tests and cookbook. +// +// Two verified SDK facts baked into this recipe (see the Pitfalls below for +// detail): +// 1. WITH an ABI, constructor arguments are plain JS values (`[42]`). +// WITHOUT an ABI, they must be TypedValue objects (`[new BigUIntValue(42)]`), +// or the factory throws "Can't convert args to TypedValues". +// 2. The deploy transaction's `data` is `@@@`. +// The default codeMetadata built by the SDK is `0504` = +// Upgradeable + Readable + PayableBySmartContract. + +import { + Account, + Address, + AddressComputer, + SmartContractTransactionsOutcomeParser, +} from '@multiversx/sdk-core'; +import type { Abi, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +export interface DeployResult { + txHash: string; + /** Address predicted BEFORE broadcasting, from (sender, nonce). */ + predictedAddress: string; +} + +/** + * Deploy path 1 - the controller. `createTransactionForDeploy` builds the + * transaction, sets the nonce, AND signs it (the controller takes the whole + * `Account`). We predict the contract address from the transaction's own sender + * and nonce before sending, so it lines up with whatever nonce the controller + * consumed. + */ +export async function deployViaController( + entrypoint: DevnetEntrypoint, + abi: Abi, + sender: Account, + bytecode: Uint8Array, + initialValue: number, +): Promise { + const controller = entrypoint.createSmartContractController(abi); + + const transaction = await controller.createTransactionForDeploy(sender, sender.getNonceThenIncrement(), { + bytecode, + gasLimit: 6_000_000n, + arguments: [initialValue], // plain JS value - allowed because we passed the ABI + }); + + const predictedAddress = predictAddress(transaction); + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash, predictedAddress }; +} + +/** + * Deploy path 2 - the factory. The factory only BUILDS the transaction; the + * caller must set the nonce and sign it. Use this when the signing happens + * elsewhere (a wallet, a hardware device, a dApp). + */ +export async function deployViaFactory( + entrypoint: DevnetEntrypoint, + abi: Abi, + sender: Account, + bytecode: Uint8Array, + initialValue: number, +): Promise { + const factory = entrypoint.createSmartContractTransactionsFactory(abi); + + const transaction = await factory.createTransactionForDeploy(sender.address, { + bytecode, + gasLimit: 6_000_000n, + arguments: [initialValue], + }); + + // The developer owns nonce + signing with the factory. + transaction.nonce = sender.getNonceThenIncrement(); + const predictedAddress = predictAddress(transaction); + transaction.signature = await sender.signTransaction(transaction); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash, predictedAddress }; +} + +/** Predict the contract address from a built deploy transaction. */ +function predictAddress(transaction: Transaction): string { + const computer = new AddressComputer(); + return computer.computeContractAddress(transaction.sender, transaction.nonce).toBech32(); +} + +/** + * Parse path 1 - the controller's one-liner. `awaitCompletedDeploy` waits for + * the transaction to complete and parses it, returning the deployed contract(s). + * Use this right after your own deploy. + */ +export async function awaitDeployedAddress( + entrypoint: DevnetEntrypoint, + abi: Abi, + txHash: string, +): Promise { + const controller = entrypoint.createSmartContractController(abi); + const outcome = await controller.awaitCompletedDeploy(txHash); + return outcome.contracts[0]!.address.toBech32(); +} + +/** + * Parse path 2 - fetch the completed transaction yourself, then parse it with + * `SmartContractTransactionsOutcomeParser`. Works on ANY completed deploy + * transaction (yours or a historical one), which is why this recipe can show + * real parse output without needing a funded wallet. + */ +export async function parseDeployFromHash( + entrypoint: DevnetEntrypoint, + txHash: string, +): Promise<{ address: string; owner: string; codeHashHex: string; returnCode: string }> { + const transactionOnNetwork = await entrypoint.getTransaction(txHash); + const parser = new SmartContractTransactionsOutcomeParser(); + const outcome = parser.parseDeploy({ transactionOnNetwork }); + + const first = outcome.contracts[0]; + if (!first) { + throw new Error(`Transaction ${txHash} deployed no contract (returnCode: ${outcome.returnCode}).`); + } + return { + address: first.address.toBech32(), + owner: first.ownerAddress.toBech32(), + codeHashHex: Buffer.from(first.codeHash).toString('hex'), + returnCode: outcome.returnCode, + }; +} + +/** Decode a deploy transaction's `data` field into its four wire parts. */ +export function describeDeployPayload(transaction: Transaction): { + codeHex: string; + vmType: string; + codeMetadata: string; + args: string[]; +} { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + codeHex: parts[0] ?? '', + vmType: parts[1] ?? '', + codeMetadata: parts[2] ?? '', + args: parts.slice(3), + }; +} + +/** Convenience: the system deploy address every deploy is addressed to. */ +export const SYSTEM_DEPLOY_ADDRESS = Address.newFromBech32( + 'erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu', +).toBech32(); +``` + +## Run it + +```bash +# Parse a real completed devnet deploy - no wallet, no funds: +npm start + +# Inspect the deploy wire payload offline: +npm start -- payload + +# Actually deploy (needs a funded devnet PEM); add --factory for the factory path: +npm start -- deploy ./wallet.pem 42 +``` + +Expected output of the default `parse` mode: + +```text +Parsing completed deploy a957cf3038a79517b1a11ee45094259989b0c7f67b61e1a39f3502ed041b964d ... + returnCode: ok + contract: erd1qqqqqqqqqqqqqpgqs6reg0rjc7tmdcz65qg9namphcat5hvk8cfs4mfuj2 + owner: erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede + codeHash (hex): ... +``` + +## How it works + +**Controller vs factory.** `controller.createTransactionForDeploy(account, +nonce, options)` builds, sets the nonce, and signs in one call. +`factory.createTransactionForDeploy(address, options)` only builds the unsigned +transaction; you set `nonce` and `signature`. Use the controller for scripts, the +factory when a wallet or hardware device signs. Both are async. + +**Predict, then confirm.** Both paths compute the upcoming contract address with +`AddressComputer.computeContractAddress(tx.sender, tx.nonce)` before sending, and +this recipe asserts it equals the address the network reports after completion. +See [Compute a contract address before deploy](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/compute-contract-address). + +**Parsing, two ways.** `controller.awaitCompletedDeploy(txHash)` waits and parses +in one call; `SmartContractTransactionsOutcomeParser.parseDeploy({ transactionOnNetwork })` +parses a transaction you already fetched. The default `npm start` uses the second +form so it can show real output against a historical deploy without funds. + +## Pitfalls + +:::warning[Pitfall 1: without an ABI, arguments must be TypedValue objects] +With the ABI passed to the controller or factory, `arguments: [42]` works. +Without an ABI, the same call throws `Err: Can't convert args to TypedValues`; +you must pass `arguments: [new BigUIntValue(42)]`. +::: + +:::warning[Pitfall 2: the default code metadata is permissive] +The SDK builds deploy/upgrade transactions with `codeMetadata = 0504` = +Upgradeable + Readable + PayableBySmartContract. `isPayableBySmartContract` +defaults to `true` in the factory. For a locked-down contract, pass +`isUpgradeable: false` / `isPayableBySmartContract: false` explicitly. Run +`npm start -- payload` to see the raw metadata bytes. +::: + +:::note[Pitfall 3: the receiver is the system deploy address, not the contract] +A deploy is addressed to +`erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu`. The contract +address only exists after the network assigns it. The `data` field is +`@@@`, with vmType `0500` = WASM VM. +::: + +:::note[Pitfall 4: the predicted address depends on the exact deploy nonce] +If another transaction from the same account lands first, the deploy nonce shifts +and the address changes. This recipe predicts from the transaction's own `nonce` +after it is set. +::: + +## See also + +- [Compute a contract address before deploy](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/compute-contract-address) + is the prediction this recipe confirms. +- [Upgrade a smart contract](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/upgrade-contract) + is the same bytecode-plus-args shape, for an existing contract. +- [Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint) + calls the contract once it is deployed. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/upgrade-contract.mdx b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/upgrade-contract.mdx new file mode 100644 index 000000000..ec1db6d9f --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/upgrade-contract.mdx @@ -0,0 +1,203 @@ +--- +title: Upgrade a smart contract +description: Upgrade a deployed smart contract to new WASM bytecode with sdk-core's controller and factory, and read the upgradeContract builtin wire payload. +difficulty: intermediate +est_minutes: 7 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - smart-contract + - upgrade + - devnet + - typescript +--- + +Upgrade an already-deployed smart contract to new WASM bytecode, two ways +(controller and factory). An upgrade is almost identical to a deploy: it carries +new WASM plus constructor arguments. The differences are that the contract +address is already known and the transaction is addressed to the contract, with a +`data` field that starts with the `upgradeContract` builtin function. This recipe +targets the real **adder** fixtures. + +The default `npm start` builds an upgrade and prints its wire payload without +sending anything, so you can see the exact shape offline. + +## Prerequisites + +- Node.js >= 20.13.1. +- For the default payload demo: nothing (it builds offline). +- For an actual upgrade: a devnet PEM wallet that **owns** the target contract + and has a little EGLD for gas. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/upgrade-contract +npm install +npm run build +``` + +## Upgrading + +```ts title="src/upgrade.ts" +// src/upgrade.ts - upgrading an already-deployed smart contract to new bytecode. +// An upgrade is almost identical to a deploy (it carries new WASM plus +// constructor arguments), with two differences: +// 1. The contract address is already known, so you pass it in `contract`. +// 2. The transaction is addressed to the CONTRACT (not the system deploy +// address), and its `data` starts with the `upgradeContract` builtin +// function: `upgradeContract@@@`. +// +// The contract must have been deployed as upgradeable (codeMetadata bit set) and +// the caller must be its owner, otherwise the network rejects the upgrade at +// execution time. The default deploy code metadata `0504` DOES set the +// Upgradeable bit (see the deploy recipe). +// +// Targets the real **adder** contract fixtures (adder.wasm + adder.abi.json). +// Adder declares a dedicated `upgradeConstructor` in its ABI, also taking +// `initial_value: BigUint`, which is what the upgrade arguments feed. + +import type { Abi, Account, Address, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +/** + * Upgrade path 1 - the controller. `createTransactionForUpgrade` builds, sets + * the nonce, and signs, taking the whole `Account`. Same shape as + * `createTransactionForDeploy` plus the `contract` field. + */ +export async function upgradeViaController( + entrypoint: DevnetEntrypoint, + abi: Abi, + sender: Account, + contract: Address, + bytecode: Uint8Array, + newInitialValue: number, +): Promise { + const controller = entrypoint.createSmartContractController(abi); + + const transaction = await controller.createTransactionForUpgrade(sender, sender.getNonceThenIncrement(), { + contract, + bytecode, + gasLimit: 6_000_000n, + arguments: [newInitialValue], // plain JS value - allowed because we passed the ABI + }); + + return entrypoint.sendTransaction(transaction); +} + +/** + * Upgrade path 2 - the factory. Builds the unsigned transaction only; the caller + * sets the nonce and signs. Use when a wallet or hardware device signs. + */ +export async function upgradeViaFactory( + entrypoint: DevnetEntrypoint, + abi: Abi, + sender: Account, + contract: Address, + bytecode: Uint8Array, + newInitialValue: number, +): Promise { + const factory = entrypoint.createSmartContractTransactionsFactory(abi); + + const transaction = await factory.createTransactionForUpgrade(sender.address, { + contract, + bytecode, + gasLimit: 6_000_000n, + arguments: [newInitialValue], + }); + + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + + return entrypoint.sendTransaction(transaction); +} + +/** Decode an upgrade transaction's `data` field into its wire parts. */ +export function describeUpgradePayload(transaction: Transaction): { + builtinFunction: string; + codeHex: string; + codeMetadata: string; + args: string[]; + receiver: string; +} { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + builtinFunction: parts[0] ?? '', + codeHex: parts[1] ?? '', + codeMetadata: parts[2] ?? '', + args: parts.slice(3), + receiver: transaction.receiver.toBech32(), + }; +} +``` + +## Run it + +```bash +# Build an upgrade and print its wire payload - no wallet, offline: +npm start + +# Actually upgrade a contract you own; add --factory for the factory path: +npm start -- upgrade ./wallet.pem erd1qqqqqqqqqqqqqpgq...your-contract 42 +``` + +Expected output of the default payload demo: + +```text +Upgrade transaction wire payload (built, not sent): + receiver: erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug (the contract itself) + builtinFunction: upgradeContract + code === wasm: true + codeMetadata: 0504 (0504 = Upgradeable + Readable + PayableBySmartContract) + args: 2a (2a = 42) +``` + +## How it works + +**Upgrade is deploy with a known address.** +`controller.createTransactionForUpgrade(account, nonce, { contract, bytecode, gasLimit, arguments })` +and `factory.createTransactionForUpgrade(address, { contract, ... })` take the +same options as their deploy counterparts plus `contract`. The controller signs; +the factory only builds. + +**The wire shape differs from deploy.** A deploy is addressed to the system +deploy address with `data` = `@@@`. An +upgrade is addressed to the **contract** with `data` = +`upgradeContract@@@` (no vmType part; the +`upgradeContract` builtin replaces it). + +**Storage survives, code is replaced.** Adder declares a dedicated +`upgradeConstructor` in its ABI (also `initial_value: BigUint`), which the upgrade +arguments feed. Existing storage persists across the upgrade unless the new code +changes the layout. + +## Pitfalls + +:::warning[Pitfall 1: the contract must be upgradeable and you must own it] +If the Upgradeable code metadata bit was not set at deploy, or the caller is not +the owner, the network rejects the upgrade at execution time. The default deploy +metadata `0504` does set the Upgradeable bit. +::: + +:::warning[Pitfall 2: without an ABI, arguments must be TypedValue objects] +Same trap as deploy: with the ABI, `arguments: [42]` works; without it you must +pass `arguments: [new BigUIntValue(42)]` or the SDK throws `Can't convert args to +TypedValues`. +::: + +:::note[Pitfall 3: codeMetadata is rebuilt, not inherited] +Whatever metadata you pass (or the `0504` default) replaces the old metadata. +Upgrading with the defaults keeps the contract Upgradeable; passing +`isUpgradeable: false` locks it against future upgrades. +::: + +## See also + +- [Deploy a smart contract](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/deploy-contract) + is the deploy counterpart with the same bytecode-plus-args shape. +- [Compute a contract address before deploy](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/compute-contract-address) + is how the address you upgrade was assigned. +- [Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint) + calls the upgraded contract. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/new-contract-from-template.mdx b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/new-contract-from-template.mdx new file mode 100644 index 000000000..3014cdd6e --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/new-contract-from-template.mdx @@ -0,0 +1,304 @@ +--- +title: New contract from sc-meta new --template empty +description: The real, unedited output of sc-meta new --template empty, then the smallest real customization on top, with every command actually run and captured. +difficulty: intermediate +est_minutes: 10 +last_validated: "2026-07-16" +sdk_versions: + sdk-rs: "^0.64.1" +tags: + - sdk-rs + - rust + - smart-contract +--- + +The real, unedited output of `sc-meta new --template empty`, then the smallest +real customization on top (one storage mapper, one endpoint, one view), because a +genuinely empty contract does not prove the workflow actually works end to end. +Every command in this recipe was run for real with `rustc 1.93.0`, +`cargo 1.93.0`, and `multiversx-sc-meta 0.64.1`. + +## Prerequisites + +- Rust via `rustup`, with the `wasm32v1-none` target installed (see "A version + note" below for why not `wasm32-unknown-unknown`). +- `sc-meta` (`cargo install multiversx-sc-meta`). +- Optional: `wasm-opt`, for size-optimized release builds (this recipe was + verified without it). + +## Step 1: the bare scaffold + +```bash +sc-meta new --template empty --name greeter +``` + +This does more than copy files: it renames the trait, the crate names, the module +paths, and the scenario references throughout, all in one pass. Two things stand +out immediately, both confirmed by actually running the command rather than +reading about it: + +- **There is no `sc-config.toml`, no `wasm/`, and no `output/` yet.** A bare + `sc-meta new` output does not have these; project-anatomy overviews often show + them as if every contract project always does. +- **The scenario test files' function names do not get renamed.** The *file* is + renamed and the scenario path inside is updated, but the test function itself is + still `fn empty_go()`, not `fn greeter_go()`. Not a bug, just something to + expect when you diff a freshly generated project. + +## Step 2: add a storage mapper, an endpoint, and a proxy + +```rust title="greeter/src/greeter.rs" +#![no_std] + +use multiversx_sc::imports::*; + +pub mod greeter_proxy; + +/// A minimal greeter contract. Starts from `sc-meta new --template empty +/// --name greeter` (the exact, unmodified output of that command) with one +/// endpoint and one storage mapper added on top — the smallest real next step +/// after scaffolding, and the natural bridge into the +/// storage-mapper-decision-table recipe. +#[multiversx_sc::contract] +pub trait Greeter { + #[init] + fn init(&self) {} + + #[upgrade] + fn upgrade(&self) {} + + /// Stores a greeting message for the calling address. Overwrites any + /// previous greeting for the same caller. + #[endpoint(setGreeting)] + fn set_greeting(&self, message: ManagedBuffer) { + let caller = self.blockchain().get_caller(); + self.greeting(&caller).set(message); + } + + /// Reads back the greeting message stored for `address`. Returns an + /// empty ManagedBuffer if that address never called setGreeting. + #[view(getGreeting)] + #[storage_mapper("greeting")] + fn greeting(&self, address: &ManagedAddress) -> SingleValueMapper; +} +``` + +**Generating the proxy has a real chicken-and-egg order to it.** Adding +`pub mod greeter_proxy;` before the file exists fails the build +(`error[E0583]: file not found for module`). The working order: + +1. Add `sc-config.toml`: `[[proxy]]` / `path = "src/greeter_proxy.rs"`. +2. Write the contract logic WITHOUT the `pub mod greeter_proxy;` line yet. +3. Run `sc-meta all proxy`, which compiles the contract (which does not yet + reference the proxy module) and generates `src/greeter_proxy.rs` from the + resulting ABI. +4. Add `pub mod greeter_proxy;` now that the file exists. + +## Step 3: build + +```bash +sc-meta all build +``` + +Real output, captured from a build: + +```text +Building greeter.wasm in .../greeter/wasm ... +RUSTFLAGS="-C link-arg=-s -C link-arg=-zstack-size=131072" cargo +1.93-aarch64-apple-darwin build --target=wasm32v1-none --release ... + Finished `release` profile [optimized] target(s) in 6.78s +Copying .../target/wasm32v1-none/release/greeter_wasm.wasm to ../output/greeter.wasm ... +Warning: wasm-opt not installed. +Packing ../output/greeter.mxsc.json ... +Contract size: 996 bytes. +``` + +`sc-meta all build` also regenerates `wasm/src/lib.rs`'s endpoint list on every +run, confirmed by diffing it before and after adding `setGreeting` / `getGreeting`: +it picked both up automatically with no manual edit. + +## Step 4: test + +```rust title="greeter/tests/greeter_blackbox_test.rs" +// tests/greeter_blackbox_test.rs — a hand-written blackbox test for the +// endpoint added on top of the bare `empty` scaffold. The two +// `greeter_scenario_*_test.rs` files (unmodified from `sc-meta new +// --template empty`) only exercise deploy; this test exercises the actual +// custom logic, following the recommended blackbox-test pattern. + +use multiversx_sc_scenario::imports::*; + +use greeter::greeter_proxy; + +const OWNER: TestAddress = TestAddress::new("owner"); +const CONTRACT: TestSCAddress = TestSCAddress::new("greeter-contract"); +const CODE_PATH: MxscPath = MxscPath::new("output/greeter.mxsc.json"); + +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + blockchain.register_contract(CODE_PATH, greeter::ContractBuilder); + blockchain +} + +#[test] +fn set_and_get_greeting() { + let mut world = world(); + world.account(OWNER).nonce(1); + + // Deploy. + world + .tx() + .from(OWNER) + .typed(greeter_proxy::GreeterProxy) + .init() + .code(CODE_PATH) + .new_address(CONTRACT) + .run(); + + // Before any call, the greeting for OWNER is empty. + world + .query() + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .greeting(OWNER.to_address()) + .returns(ExpectValue(ManagedBuffer::::new())) + .run(); + + // Call setGreeting as OWNER. + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .set_greeting(ManagedBuffer::::from(b"hello devnet")) + .run(); + + // getGreeting now returns what we stored, keyed by the caller address. + world + .query() + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .greeting(OWNER.to_address()) + .returns(ExpectValue(ManagedBuffer::::from(b"hello devnet"))) + .run(); +} + +#[test] +fn greeting_is_keyed_per_caller() { + let mut world = world(); + world.account(OWNER).nonce(1); + let other: TestAddress = TestAddress::new("other-caller"); + world.account(other).nonce(1); + + world + .tx() + .from(OWNER) + .typed(greeter_proxy::GreeterProxy) + .init() + .code(CODE_PATH) + .new_address(CONTRACT) + .run(); + + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .set_greeting(ManagedBuffer::::from(b"from owner")) + .run(); + + // A different caller who never called setGreeting still reads back + // empty — this is what a parameterized storage mapper gives you: the key + // includes the parameter automatically, so each address gets its own + // storage slot, not a shared one. + world + .query() + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .greeting(other.to_address()) + .returns(ExpectValue(ManagedBuffer::::new())) + .run(); + + world + .query() + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .greeting(OWNER.to_address()) + .returns(ExpectValue(ManagedBuffer::::from(b"from owner"))) + .run(); +} +``` + +```bash +cargo test +``` + +```text +running 2 tests +test greeting_is_keyed_per_caller ... ok +test set_and_get_greeting ... ok +test result: ok. 2 passed; 0 failed; ... + +running 1 test +test empty_go ... ok + +running 1 test +test empty_rs ... ok +``` + +All 4 tests pass: the 2 new blackbox tests plus the 2 scaffold-provided scenario +tests. + +## A version note + +Three different version numbers showed up while working through this recipe, and +they disagree: + +| Source | `multiversx-sc` version | +| --- | --- | +| Commonly documented "current" version | 0.65.0 | +| `mx-sdk-rs` GitHub `master`, `contracts/examples/empty/Cargo.toml` | 0.66.2 | +| This machine's installed `sc-meta` (0.64.1), and what it actually generated | 0.64.1 | + +`sc-meta new` bundles its own template, versioned to match whatever +`multiversx-sc-meta` release you have installed; it does not fetch the latest +framework version from anywhere. Run `sc-meta upgrade` after scaffolding for the +latest framework version, or pass `sc-meta new --tag ` to pin one +explicitly. + +Also: the actual build command targets `wasm32v1-none`, not +`wasm32-unknown-unknown` as some older references list, confirmed directly from +the real `sc-meta all build` invocation logged above. + +## Pitfalls + +:::danger[Pitfall 1: declaring the proxy module before generating it breaks the build] +See Step 2's ordering: generate first, declare the module second. +::: + +:::note[Pitfall 2: a bare sc-meta new output has no sc-config.toml] +Project-anatomy diagrams often show one unconditionally; you only need it once +you want proxy generation or a multi-contract build. +::: + +:::tip[Pitfall 3: wasm-opt not being installed does not fail the build] +It is a warning, and `sc-meta all build` still produces a valid, deployable +`.wasm` / `.mxsc.json`, just without size optimization. Install it before +shipping to mainnet if contract size matters. +::: + +:::note[Pitfall 4: scenario test function names surviving a rename are not a sign something went wrong] +See Step 1's second bullet. +::: + +:::warning[Pitfall 5: sc-meta new's bundled template version may lag the framework's documented current version] +Check what actually got generated (`cat Cargo.toml`) rather than assuming it +matches the newest release. +::: + +## See also + +- [Storage mappers: which to pick, when](/sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/storage-mapper-decision-table) + is the natural next recipe: this one introduces exactly one mapper; that one + covers the rest. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the dApp side that would eventually call an endpoint like `setGreeting`. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/storage-mapper-decision-table.mdx b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/storage-mapper-decision-table.mdx new file mode 100644 index 000000000..c4d7573d6 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/storage-mapper-decision-table.mdx @@ -0,0 +1,530 @@ +--- +title: "Storage mappers: which to pick, when" +description: A working, tested contract exercising six storage mappers side by side, with a decision table grounded in the real multiversx-sc crate source. +difficulty: intermediate +est_minutes: 9 +last_validated: "2026-07-16" +sdk_versions: + sdk-rs: "^0.64.1" +tags: + - sdk-rs + - rust + - smart-contract +--- + +A working, tested contract exercising six storage mappers side by side, plus a +decision table built from the real `multiversx-sc` crate source doc comments. Two +easy-to-get-wrong entries (a mapper's `contains()` complexity and its storage +cost) are called out below in "Two clarifications from the crate source". + +## Prerequisites + +- Rust via `rustup`, with the `wasm32v1-none` target installed. +- `sc-meta` (`cargo install multiversx-sc-meta`). +- Familiarity with + [New contract from sc-meta new --template empty](/sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/new-contract-from-template), + this recipe assumes that scaffolding workflow. + +## The contract + +```rust title="storage-mappers/src/storage_mappers.rs" +#![no_std] + +use multiversx_sc::imports::*; + +pub mod storage_mappers_proxy; + +/// One endpoint pair per mapper type this recipe covers with working, tested +/// code. Method names and complexity claims below are taken directly from the +/// real `multiversx-sc` crate source doc comments +/// (`~/.cargo/registry/.../multiversx-sc-0.64.2/src/storage/mappers/*.rs`). +#[multiversx_sc::contract] +pub trait StorageMappers { + #[init] + fn init(&self) {} + + #[upgrade] + fn upgrade(&self) {} + + // ---- SingleValueMapper: one value, no parameters. Simplest mapper, + // baseline for storage cost (1 entry). ---- + #[endpoint(setCounter)] + fn set_counter(&self, value: BigUint) { + self.counter().set(value); + } + + #[view(getCounter)] + #[storage_mapper("counter")] + fn counter(&self) -> SingleValueMapper; + + // ---- VecMapper: ordered, 1-indexed, allows duplicates, random + // access by index. ---- + #[endpoint(pushItem)] + fn push_item(&self, item: ManagedBuffer) -> usize { + self.items().push(&item) + } + + #[view(getItem)] + fn get_item(&self, index: usize) -> ManagedBuffer { + self.items().get(index) + } + + #[view(itemCount)] + fn item_count(&self) -> usize { + self.items().len() + } + + #[storage_mapper("items")] + fn items(&self) -> VecMapper; + + // ---- SetMapper: ordered (insertion order) set. The crate source confirms + // O(1) contains via an internal value->node_id lookup. ---- + #[endpoint(addToOrderedSet)] + fn add_to_ordered_set(&self, value: u64) -> bool { + self.ordered_set().insert(value) + } + + #[view(orderedSetContains)] + fn ordered_set_contains(&self, value: u64) -> bool { + self.ordered_set().contains(&value) + } + + #[view(orderedSetLen)] + fn ordered_set_len(&self) -> usize { + self.ordered_set().len() + } + + #[storage_mapper("ordered_set")] + fn ordered_set(&self) -> SetMapper; + + // ---- UnorderedSetMapper: no ordering guarantee, O(1) contains via + // VecMapper + a reverse index lookup (2N+1 entries total). ---- + #[endpoint(addToUnorderedSet)] + fn add_to_unordered_set(&self, value: u64) -> bool { + self.unordered_set().insert(value) + } + + #[view(unorderedSetContains)] + fn unordered_set_contains(&self, value: u64) -> bool { + self.unordered_set().contains(&value) + } + + #[view(unorderedSetLen)] + fn unordered_set_len(&self) -> usize { + self.unordered_set().len() + } + + #[storage_mapper("unordered_set")] + fn unordered_set(&self) -> UnorderedSetMapper; + + // ---- WhitelistMapper: membership-only, no iteration, most + // space-efficient of the set-shaped mappers. ---- + #[endpoint(addToWhitelist)] + fn add_to_whitelist(&self, address: ManagedAddress) { + self.whitelist().add(&address); + } + + #[view(isWhitelisted)] + fn is_whitelisted(&self, address: ManagedAddress) -> bool { + self.whitelist().contains(&address) + } + + #[storage_mapper("whitelist")] + fn whitelist(&self) -> WhitelistMapper; + + // ---- MapMapper: key-value with iteration, HashMap-like API. Uses a + // SetMapper internally for key tracking plus separate value + // storage — the crate source confirms the 4N+1 entries this costs. ---- + #[endpoint(setBalance)] + fn set_balance(&self, address: ManagedAddress, amount: BigUint) { + self.balances().insert(address, amount); + } + + #[view(getBalance)] + fn get_balance(&self, address: ManagedAddress) -> BigUint { + self.balances().get(&address).unwrap_or_default() + } + + #[view(hasBalanceEntry)] + fn has_balance_entry(&self, address: ManagedAddress) -> bool { + self.balances().contains_key(&address) + } + + #[storage_mapper("balances")] + fn balances(&self) -> MapMapper; +} +``` + +## The decision table + +| Mapper | Ordering | Membership check | Storage entries for N items | Iterable? | Pick it when | +| --- | --- | --- | --- | --- | --- | +| `SingleValueMapper` | n/a (one value) | n/a | 1 | n/a | You need exactly one value: a counter, a config flag, a total. | +| `VecMapper` | Insertion order | Linear scan only | N + 1 | Yes, 1 to `len()` | Ordered, indexable, append-friendly storage without fast membership checks. **Indexes start at 1, not 0.** | +| `SetMapper` | Insertion order (doubly-linked internally) | **O(1)**, confirmed from the crate source | ~3N + 1 | Yes, in insertion order, plus `next()`/`previous()` | You need both ordered iteration AND fast membership checks. | +| `UnorderedSetMapper` | None | O(1) | 2N + 1 (the reverse-lookup keys are easy to undercount as N+1) | Yes, arbitrary order | Fast membership checks, order does not matter: deduping, a processed-IDs set. | +| `WhitelistMapper` | n/a | O(1), most storage-efficient | N | **No**, cannot enumerate at all | "Is X allowed?" only, never "list everyone allowed." | +| `MapMapper` | Insertion order of keys | O(1) via `contains_key()` | ~4N + 1 | Yes: `.iter()`, `.keys()`, `.values()` | A real key-value store with iteration: balances, per-user settings. | +| `LinkedListMapper` | Insertion order, efficient front/back ops | Not built in | ~2N + 1 | Yes | Efficient push/pop from both ends. Not exercised with working code here, see "What this recipe did not test." | + +The storage-cost column is expressed in unique storage keys, not bytes; it +captures the relative ordering between choices for the same logical data, not an +absolute gas number. + +## Two clarifications from the crate source + +Read directly from `multiversx-sc-0.64.2`'s source doc comments: + +:::danger[SetMapper's contains() is O(1), not O(n)] +It is easy to assume `SetMapper.contains()` is O(n) because the mapper keeps +insertion order, but the crate source's doc comment states plainly: +`"Contains: contains(value) - Checks membership. O(1) with one storage read."`, +listing `"O(1) insert, remove, and contains"` as a Pro. `SetMapper` maintains a +separate value→node-ID lookup specifically to make `contains()` O(1); that is why +its storage layout is more complex than a plain ordered list. +::: + +:::danger[UnorderedSetMapper costs 2N+1 entries, not N+1] +It is easy to undercount this as N+1. The real storage layout has value storage +(`.len` + `.item{index}`, the N+1) AND a separate `.index{encoded_value}` +reverse-lookup key per element, which is what actually delivers O(1) +`contains()`. You cannot get O(1) membership testing from N+1 keys with no reverse +index. +::: + +Everything else this recipe independently checked against the crate source held +up, including `MapMapper`'s "4N+1 entries (expensive!)", confirmed by reading how +it is built on top of `SetMapper` internally plus its own value storage. + +## Tests + +```rust title="storage-mappers/tests/storage_mappers_blackbox_test.rs" +// tests/storage_mappers_blackbox_test.rs — one test per mapper this recipe +// covers, each proving the specific behavioral claim its decision-table entry +// makes (not just "it compiles"). + +use multiversx_sc_scenario::imports::*; + +use storage_mappers::storage_mappers_proxy; + +const OWNER: TestAddress = TestAddress::new("owner"); +const CONTRACT: TestSCAddress = TestSCAddress::new("storage-mappers-contract"); +const CODE_PATH: MxscPath = MxscPath::new("output/storage-mappers.mxsc.json"); + +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + blockchain.register_contract(CODE_PATH, storage_mappers::ContractBuilder); + blockchain +} + +fn deploy(world: &mut ScenarioWorld) { + world.account(OWNER).nonce(1); + world + .tx() + .from(OWNER) + .typed(storage_mappers_proxy::StorageMappersProxy) + .init() + .code(CODE_PATH) + .new_address(CONTRACT) + .run(); +} + +#[test] +fn single_value_mapper_set_and_get() { + let mut world = world(); + deploy(&mut world); + + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .set_counter(BigUint::::from(42u64)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .counter() + .returns(ExpectValue(BigUint::::from(42u64))) + .run(); +} + +#[test] +fn vec_mapper_is_one_indexed() { + let mut world = world(); + deploy(&mut world); + + // Push three items; VecMapper's own doc comment says indexes start + // at 1 — confirm index 1 is the FIRST push, not the second. + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .push_item(ManagedBuffer::::from(b"first")) + .run(); + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .push_item(ManagedBuffer::::from(b"second")) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .item_count() + .returns(ExpectValue(2usize)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .get_item(1usize) + .returns(ExpectValue(ManagedBuffer::::from(b"first"))) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .get_item(2usize) + .returns(ExpectValue(ManagedBuffer::::from(b"second"))) + .run(); +} + +#[test] +fn set_mapper_contains_and_ordering() { + let mut world = world(); + deploy(&mut world); + + for value in [30u64, 10u64, 20u64] { + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .add_to_ordered_set(value) + .run(); + } + + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .ordered_set_contains(10u64) + .returns(ExpectValue(true)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .ordered_set_contains(99u64) + .returns(ExpectValue(false)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .ordered_set_len() + .returns(ExpectValue(3usize)) + .run(); +} + +#[test] +fn unordered_set_mapper_contains_after_insert_and_absent_value() { + let mut world = world(); + deploy(&mut world); + + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .add_to_unordered_set(7u64) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .unordered_set_contains(7u64) + .returns(ExpectValue(true)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .unordered_set_contains(8u64) + .returns(ExpectValue(false)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .unordered_set_len() + .returns(ExpectValue(1usize)) + .run(); +} + +#[test] +fn whitelist_mapper_membership_only() { + let mut world = world(); + deploy(&mut world); + let allowed: TestAddress = TestAddress::new("allowed-user"); + let stranger: TestAddress = TestAddress::new("stranger"); + world.account(allowed).nonce(1); + world.account(stranger).nonce(1); + + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .add_to_whitelist(allowed.to_address()) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .is_whitelisted(allowed.to_address()) + .returns(ExpectValue(true)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .is_whitelisted(stranger.to_address()) + .returns(ExpectValue(false)) + .run(); +} + +#[test] +fn map_mapper_insert_get_and_contains_key() { + let mut world = world(); + deploy(&mut world); + let holder: TestAddress = TestAddress::new("balance-holder"); + let nobody: TestAddress = TestAddress::new("no-balance"); + world.account(holder).nonce(1); + world.account(nobody).nonce(1); + + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .set_balance(holder.to_address(), BigUint::::from(500u64)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .get_balance(holder.to_address()) + .returns(ExpectValue(BigUint::::from(500u64))) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .has_balance_entry(holder.to_address()) + .returns(ExpectValue(true)) + .run(); + + // A key that was never inserted: contains_key is false, and the + // convenience getter's unwrap_or_default() reads as zero rather than + // erroring — a real design choice worth testing explicitly, since it + // means "balance of zero" and "never had an entry" are + // indistinguishable through get_balance alone. + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .has_balance_entry(nobody.to_address()) + .returns(ExpectValue(false)) + .run(); + + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .get_balance(nobody.to_address()) + .returns(ExpectValue(BigUint::::from(0u64))) + .run(); +} +``` + +```bash +cargo test +``` + +8/8 passing: the 6 tests above (one per mapper, each proving the specific claim in +the table: `VecMapper`'s first push lands at index 1, +`SetMapper` / `UnorderedSetMapper.contains()` returns correctly for present and +absent values, `WhitelistMapper` checked via `contains()` only, `MapMapper`'s +zero-default trap), plus the 2 scaffold-provided scenario tests. + +Every method name used (`.insert()`, `.contains()`, `.contains_key()`, `.add()`, +`.push()`) was copied from the real crate source's own doc-comment examples, worth +calling out since, for instance, `SetMapper` / `UnorderedSetMapper` both use +`.contains()` while `MapMapper` uses `.contains_key()` instead, an easy name to +get wrong by assuming symmetry. + +## Pitfalls + +:::danger[Pitfall 1: VecMapper is 1-indexed] +Index 0 is invalid and panics. Confirmed directly: this recipe's test pushes two +items and reads back index 1 as the first one pushed. +::: + +:::warning[Pitfall 2: MapMapper.get() returning a default value looks identical to a real stored zero] +If "never set" and "set to zero" need to be distinguishable, check +`contains_key()` explicitly rather than trusting a default-valued read. +::: + +:::note[Pitfall 3: SetMapper and UnorderedSetMapper both offer O(1) contains()] +The deciding factor between them is ordering and storage cost, not lookup speed. +Pick `UnorderedSetMapper` unless you specifically need insertion-order iteration +or `next()`/`previous()` navigation. +::: + +:::danger[Pitfall 4: WhitelistMapper cannot list its members at all] +Not even inefficiently. If you might ever need to enumerate, use `SetMapper` or +`UnorderedSetMapper` from the start; there is no way to add enumeration later +without migrating storage. +::: + +:::note[Pitfall 5: method names differ across the mapper family] +`SetMapper` / `UnorderedSetMapper.contains()` vs `MapMapper.contains_key()`, check +the exact mapper's own method names rather than assuming consistency. +::: + +## What this recipe did not test + +`LinkedListMapper`, `QueueMapper`, `UserMapper`, `UniqueIdMapper`, and `BiDiMapper` +are real, exported mapper types, but this recipe does not ship working code +exercising them: six mappers with real, tested endpoints was already substantial +scope. `FungibleTokenMapper` / `NonFungibleTokenMapper` / `TokenAttributesMapper` +need real ESDT system contract interaction to demonstrate meaningfully, which +belongs in a token-issuance-from-a-contract recipe, not a storage-mapper +comparison. + +## See also + +- [New contract from sc-meta new --template empty](/sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/new-contract-from-template) + is the scaffolding this recipe's contract builds on. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the dApp side that would eventually call an endpoint reading from one of + these mappers. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup.mdx b/docs/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup.mdx new file mode 100644 index 000000000..9e5722711 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup.mdx @@ -0,0 +1,407 @@ +--- +title: Local HTTPS for dApp dev (mkcert + Vite + Next.js) +description: Wallet providers refuse to connect over plain HTTP. Set up trusted HTTPS for localhost using mkcert with Vite, Next.js, or framework-agnostic plain Node. +difficulty: beginner +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-dapp: "^5.6.0" +tags: + - https + - mkcert + - vite + - nextjs + - wallet +--- + +The first time a builder hits the wallet button over plain HTTP, they get a wall +of silence. The DeFi extension does nothing. Ledger does nothing. Most docs say +"make sure you run your app on https" and stop there. This recipe is the missing how. + +The short version: install mkcert, run `setup.sh`, copy the matching framework +snippet. Five minutes total. The longer version walks the per-framework +configuration, Vite (two options), Next.js (two options), framework-agnostic plain +Node, plus the "I can't install root certs on this corp laptop" Cloudflare Tunnel +escape hatch. + +## Why HTTPS in dev + +Most wallet providers require a secure context to connect: + +| Provider | Works under HTTP? | HTTPS required? | +| --- | --- | --- | +| Extension (DeFi) | No | Yes | +| Ledger | No | Yes | +| WalletConnect / xPortal | Yes | Optional | +| Web Wallet (cross-window) | Yes | Optional | +| Passkey / WebAuthn | No | Yes (browser API requires secure context) | +| InMemory (dev) | Yes | Optional | + +If you only test against xPortal during dev, plain HTTP works. The moment a +teammate tries the DeFi extension, things break, and it does not fail with a useful +message; it just silently does not connect. + +## Prerequisites + +- macOS, Linux, or Windows. +- A package manager (Homebrew on macOS / apt on Linux / Chocolatey on Windows). +- Admin rights for the one-time root cert install. + +## Step 1: Install mkcert and trust the local CA (one-time per machine) + +The bundled `setup.sh` does the install, trust, and cert generation in one shot: + +```bash +#!/usr/bin/env bash +# setup.sh — install mkcert and generate localhost certs for the current +# directory. Idempotent: re-running is safe. +# +# Usage: +# bash setup.sh +# +# What this does: +# 1. Detects OS (macOS / Linux / Windows-via-Git-Bash) and installs +# mkcert via the appropriate package manager if it's not present. +# 2. Runs `mkcert -install` to add the local CA to the OS trust store. +# Requires admin rights on first run; later runs are no-ops. +# 3. Generates cert + key files in the current directory: +# ./localhost+2.pem (cert) +# ./localhost+2-key.pem (key) +# 4. Adds both file globs to .gitignore so the key is never committed. +# +# After running this, configure your dev server to use the cert/key — +# see snippets/vite.config.mkcert.ts or snippets/server.mkcert.js. + +set -euo pipefail + +echo ">>> Local HTTPS setup for dApp dev" +echo + +# ---- 1. Install mkcert ---- +if command -v mkcert >/dev/null 2>&1; then + echo "mkcert already installed: $(mkcert --version 2>&1)" +else + echo "Installing mkcert..." + if [[ "$OSTYPE" == "darwin"* ]]; then + if ! command -v brew >/dev/null 2>&1; then + echo "Homebrew not found. Install from https://brew.sh and re-run." + exit 1 + fi + brew install mkcert nss + elif [[ "$OSTYPE" == "linux"* ]]; then + echo "Linux detected. Install mkcert and libnss3-tools manually:" + echo " Debian/Ubuntu: sudo apt install libnss3-tools && \\" + echo " curl -JLO 'https://dl.filippo.io/mkcert/latest?for=linux/amd64' && \\" + echo " chmod +x mkcert-* && sudo mv mkcert-* /usr/local/bin/mkcert" + echo " Arch: sudo pacman -S mkcert nss" + echo " Fedora: sudo dnf install mkcert nss-tools" + exit 1 + elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then + if ! command -v choco >/dev/null 2>&1; then + echo "Chocolatey not found. Install from https://chocolatey.org and re-run, or install mkcert directly:" + echo " choco install mkcert (PowerShell as admin)" + echo " scoop bucket add extras && scoop install mkcert" + exit 1 + fi + choco install -y mkcert + else + echo "Unsupported OS: $OSTYPE" + echo "Install mkcert manually from https://github.com/FiloSottile/mkcert" + exit 1 + fi +fi + +# ---- 2. Trust the local CA ---- +echo +echo ">>> Installing local CA (requires admin rights on first run)" +mkcert -install + +# ---- 3. Generate the cert ---- +echo +echo ">>> Generating cert for localhost, 127.0.0.1, ::1" +mkcert localhost 127.0.0.1 ::1 + +# ---- 4. .gitignore safety ---- +echo +echo ">>> Adding cert globs to .gitignore" +GITIGNORE=".gitignore" +touch "$GITIGNORE" +for pattern in "localhost*.pem" "localhost*-key.pem"; do + if ! grep -qx "$pattern" "$GITIGNORE"; then + echo "$pattern" >> "$GITIGNORE" + echo " added: $pattern" + else + echo " already present: $pattern" + fi +done + +echo +echo ">>> Done." +echo "Configure your dev server to use ./localhost+2.pem and ./localhost+2-key.pem." +echo "See:" +echo " snippets/vite.config.mkcert.ts (Vite)" +echo " snippets/server.mkcert.js (Next.js)" +echo " snippets/server.plain-node.ts (other)" +``` + +Run it from your project root: + +```bash +bash setup.sh +``` + +It detects the OS, installs mkcert via the appropriate package manager, runs +`mkcert -install` (adds a root CA to your OS trust store, admin rights required +first time), generates `./localhost+2.pem` plus `./localhost+2-key.pem`, and adds +both globs to `.gitignore`. + +## Step 2: Configure your dev server + +### Vite (mkcert): fully trusted, no browser warning (recommended) + +```ts +// snippets/vite.config.mkcert.ts — Vite HTTPS via mkcert-generated certs. +// +// Pros: fully trusted certificate. No browser warning. Same cert can be +// trusted on the phone via mkcert.dev's mobile guide for end-to-end +// trusted dev. +// +// Cons: one-time `mkcert -install` per machine; cert + key files in the +// project root that MUST be gitignored (otherwise you've shipped a key +// that signs as your hostname). +// +// Generation steps (one-time per project): +// mkcert localhost 127.0.0.1 ::1 +// → produces ./localhost+2.pem and ./localhost+2-key.pem +// +// Add both to .gitignore: +// localhost*.pem +// localhost*-key.pem + +import { readFileSync } from 'node:fs'; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// readFileSync at config-load time: Vite's config is evaluated in Node, +// so this is fine. It runs once at server start, not per request. +const cert = readFileSync('./localhost+2.pem'); +const key = readFileSync('./localhost+2-key.pem'); + +export default defineConfig({ + plugins: [react()], + server: { + https: { cert, key }, + port: 5173, + }, +}); +``` + +### Vite (basic-ssl): zero-config but self-signed + +```bash +npm install --save-dev @vitejs/plugin-basic-ssl +``` + +```ts +// snippets/vite.config.basic-ssl.ts — minimum-config Vite HTTPS via the +// @vitejs/plugin-basic-ssl plugin. +// +// Pros: zero filesystem fingerprint. No certs to generate, manage, or +// gitignore. Good for solo work. +// +// Cons: the cert is self-signed and untrusted. Your browser shows a +// "not private" warning every dev session; the wallet extensions still +// connect, but the warning is annoying and can confuse new contributors +// to your project. +// +// Do NOT also set `server.https` yourself. Two independent reasons: +// 1. Vite's `server.https` type is `https.ServerOptions | undefined` +// (node_modules/vite/dist/node/index.d.ts) — it has never accepted a +// bare `boolean`. `https: true` fails `tsc --strict` with "no overload +// matches this call" on the actually-installed vite@5.4.21. +// 2. The plugin's own README (node_modules/@vitejs/plugin-basic-ssl/README.md) +// shows usage as `plugins: [basicSsl()]` with no `server.https` at all — +// the plugin injects the generated cert into `server.https` itself via +// its Vite `config` hook. Setting it yourself risks the two configs +// merging in an order you don't control. + +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import basicSsl from '@vitejs/plugin-basic-ssl'; + +export default defineConfig({ + plugins: [react(), basicSsl()], + server: { + port: 5173, + }, +}); +``` + +### Next.js (mkcert): fully trusted, via a custom server + +Used by `mx-template-dapp-nextjs/server.js`. + +```js +// snippets/server.mkcert.js — Next.js custom HTTPS dev server with mkcert. +// +// Run with `node server.mkcert.js` instead of `next dev`. +// +// Mirrors the pattern used by mx-template-dapp-nextjs/server.js. +// The Node `https` module wraps the Next.js request handler; mkcert +// supplies the trusted certificate so the browser doesn't show a +// warning. +// +// Generation steps (one-time per project): +// mkcert localhost 127.0.0.1 ::1 +// → produces ./localhost+2.pem and ./localhost+2-key.pem +// +// Add both to .gitignore: +// localhost*.pem +// localhost*-key.pem +// +// This file is .js (CommonJS) deliberately — Next 14 expects a CJS +// custom server. If you'd rather use ESM, rename to .mjs and switch to +// `import` syntax. + +const { createServer } = require('node:https'); +const { parse } = require('node:url'); +const { readFileSync } = require('node:fs'); +const next = require('next'); + +const port = 3000; +const dev = process.env.NODE_ENV !== 'production'; +const app = next({ dev, hostname: 'localhost', port }); +const handle = app.getRequestHandler(); + +const httpsOptions = { + key: readFileSync('./localhost+2-key.pem'), + cert: readFileSync('./localhost+2.pem'), +}; + +void app.prepare().then(() => { + createServer(httpsOptions, (req, res) => { + const parsedUrl = parse(req.url ?? '/', true); + void handle(req, res, parsedUrl); + }).listen(port, () => { + // eslint-disable-next-line no-console + console.log(`> Ready on https://localhost:${port}`); + }); +}); +``` + +Then point your `dev` script at it: + +```json +{ + "scripts": { + "dev": "node server.js", + "build": "next build", + "start": "NODE_ENV=production node server.js" + } +} +``` + +### Next.js (experimental): zero-config, browser warns + +```json +{ + "scripts": { + "dev": "next dev --experimental-https" + } +} +``` + +`npm run dev` starts the server on `https://localhost:3000` with a self-signed cert. + +### Plain Node: Express, Fastify, Koa, or vanilla + +Same pattern, different request handler. + +```ts +// snippets/server.plain-node.ts — framework-agnostic HTTPS dev server. +// +// For the rare reader who isn't on Vite or Next.js — Express, Fastify, +// Koa, or vanilla Node. The pattern is the same: load the cert, hand it +// to https.createServer. The framework's request handler slots in the +// same way the Next.js custom server does. +// +// This snippet is illustrative. In production you'd put a real reverse +// proxy (Cloudflare, NGINX, Caddy) in front of your service — local +// HTTPS only matters for the dev loop. + +import { createServer } from 'node:https'; +import { readFileSync } from 'node:fs'; +import type { IncomingMessage, ServerResponse } from 'node:http'; + +const httpsOptions = { + key: readFileSync('./localhost+2-key.pem'), + cert: readFileSync('./localhost+2.pem'), +}; + +// Replace this with your framework's request handler. For Express: +// const app = express(); +// ... +// const handler = app; +const handler = (_req: IncomingMessage, res: ServerResponse): void => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('hello over https'); +}; + +createServer(httpsOptions, handler).listen(3000, () => { + // eslint-disable-next-line no-console + console.log('> Ready on https://localhost:3000'); +}); +``` + +## Pitfalls + +:::warning[Pitfall 1: mkcert -install fails silently on locked-down corp laptops] +On macOS, IT can lock the System Keychain so `mkcert` can add to `nss` but not +Keychain. Browsers then do not trust the cert. Escape hatch: Cloudflare Tunnel. + +```bash +cloudflared tunnel --url http://localhost:3000 +``` + +A free public HTTPS URL that wallets accept. The tunnel URL changes per session +unless you register a stable hostname through the Cloudflare Zero Trust dashboard. +::: + +:::warning[Pitfall 2: committing the cert key] +The cert and key files are .gitignored, but only after you set them up that way. +`setup.sh` does it; the manual flow does not unless you remember. If you committed +the key, **regenerate** with a fresh `mkcert localhost 127.0.0.1` and rotate. +Anyone with the committed key can MITM your traffic on the local network. +::: + +:::note[Pitfall 3: mkcert leaf certificates expire after about two years] +The cert mkcert issues for your hostnames is short-lived; the root CA it installs +lasts much longer. When the cert expires you will start seeing "not private" +warnings on a setup that used to work. Re-run the `mkcert` command from the setup +step to reissue it. You should not need to reinstall the root CA (`mkcert -install`). +::: + +:::note[Pitfall 4: mobile testing] +Run `mkcert -CAROOT` to find the CA cert location (typically +`~/Library/Application Support/mkcert/rootCA.pem` on macOS), copy it to the iOS / +Android device, and trust manually. mkcert.dev has the per-platform trust +instructions. +::: + +:::note[Pitfall 5: staging is not dev] +The "not private" click-through is fine for `localhost`. Staging deploys should use +a real cert (Let's Encrypt via Caddy / Cloudflare / whatever your hosting provides), +not mkcert. mkcert root CAs are local-machine-only; they do not propagate to +teammates' machines. +::: + +## See also + +- [Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) + uses `next dev --experimental-https` by default; switch to mkcert via `server.js` + for fully trusted. +- [Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) + uses `@vitejs/plugin-basic-ssl` by default; switch to mkcert for fully trusted. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the wallet flow this recipe makes accessible. +- [mkcert](https://github.com/FiloSottile/mkcert) is the upstream tool. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal.mdx b/docs/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal.mdx new file mode 100644 index 000000000..440cf1a81 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal.mdx @@ -0,0 +1,526 @@ +--- +title: Minimal sdk-dapp v5 app in Next.js (App Router) +description: A working Next.js 14 App Router starter with sdk-dapp v5. Client-only init wrapper, the real next.config.js, login button, account hook, compiles strict. +difficulty: beginner +est_minutes: 10 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" + sdk-dapp: "^5.6.0" +tags: + - sdk-dapp + - nextjs + - react + - typescript + - wallet + - devnet +--- + +This recipe gives you the smallest working sdk-dapp v5 setup in a Next.js 14 App +Router app. It is the path the official sdk-dapp docs describe in passing without +ever showing a complete working file. Use this as your starting point if you are +scaffolding a new dApp on Next.js: clone the `recipes/nextjs-minimal` directory +and rename it. + +This recipe is **not** the right starting point if you are on Vite + React (use +[Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal)) +or if you have an existing v4 codebase to upgrade (use +[Migrate v4 to v5](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration)). + +## Prerequisites + +- Node.js >= 20.13.1 (sdk-dapp's stated minimum). +- pnpm or npm. +- A devnet wallet, either the DeFi browser extension or xPortal (mobile via + WalletConnect). +- Familiarity with the Next.js App Router (`app/` directory). + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/nextjs-minimal +npm install +npm run dev +# open https://localhost:3000 (note the s, see Pitfalls below) +``` + +## package.json: pin the SDK and peer deps + +`bignumber.js` and `protobufjs` are peer dependencies of sdk-core and must be +installed explicitly. The Next.js template installs them as direct deps; we do +the same. + +```json +{ + "name": "cookbook-recipe-nextjs-minimal", + "version": "0.1.0", + "private": true, + "description": "Cookbook recipe — minimal sdk-dapp v5 in Next.js 14 App Router. Compiles strict.", + "scripts": { + "dev": "next dev --experimental-https", + "build": "next build", + "start": "next start", + "typecheck": "tsc --noEmit --strict", + "lint": "next lint --max-warnings=0" + }, + "dependencies": { + "@multiversx/sdk-core": "^15.4.0", + "@multiversx/sdk-dapp": "^5.6.0", + "bignumber.js": "^9.1.2", + "next": "^14.2.18", + "protobufjs": "^7.4.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^20.17.6", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "eslint": "^8.57.1", + "eslint-config-next": "^14.2.18", + "typescript": "^5.6.3" + }, + "engines": { + "node": ">=20.13.1" + } +} +``` + +## next.config.js: the real config, not the README's + +This matches the actual `next.config.js` shipped in `mx-template-dapp-nextjs`, not +the simplified version in that repo's README. The four entries do four things: +`transpilePackages: ['@multiversx/sdk-dapp-ui']` (sdk-dapp-ui ships untranspiled +web components); `webpack(config) { config.resolve.fallback = { fs: false } }` +(silences the `fs` lookup in transitive deps that do not run server-side anyway); +`config.externals.push('pino-pretty', 'lokijs', 'encoding', ...)` (Node-only code +pulled in via the WalletConnect transitive chain); and the `bufferutil` / +`utf-8-validate` externals (optional WebSocket performance binaries). + +```js +// next.config.js — sdk-dapp v5 + Next.js 14 App Router +// +// This file mirrors the actual configuration shipped in +// https://github.com/multiversx/mx-template-dapp-nextjs/blob/main/next.config.js +// (NOT the simplified version shown in that repo's README). +// +// What each entry does: +// transpilePackages: sdk-dapp-ui ships ESM web components untranspiled. +// Without this, Next.js's default Babel pipeline rejects the syntax. +// webpack().resolve.fallback.fs: false: silences `fs` lookups in +// transitive deps that don't actually run server-side. +// externals: pino-pretty, lokijs, encoding pull in Node-only code via +// the WalletConnect transitive chain. Externalising them avoids +// bundling them and the warnings they produce. +// bufferutil / utf-8-validate: optional WebSocket native acceleration — +// graceful fallback if absent, but webpack tries to bundle them +// unless externalised. + +/** @type {import('next').NextConfig} */ +const nextConfig = { + transpilePackages: ['@multiversx/sdk-dapp-ui'], + webpack: (config) => { + config.resolve.fallback = { ...(config.resolve.fallback || {}), fs: false }; + config.externals.push( + 'pino-pretty', + 'lokijs', + 'encoding', + { + bufferutil: 'bufferutil', + 'utf-8-validate': 'utf-8-validate', + }, + ); + return config; + }, +}; + +module.exports = nextConfig; +``` + +## app/layout.tsx: root layout, just the shell + +`layout.tsx` is a server component (no `"use client"`). It imports ``, +which is the client-only wrapper. + +```tsx +// app/layout.tsx — Next.js 14 App Router root layout. +// +// This is a server component (no "use client" directive). It does nothing +// blockchain-related — it only renders the html/body shell and delegates +// to which is the client-only sdk-dapp init wrapper. +// +// Why this split matters: sdk-dapp's initApp() touches sessionStorage, +// which is undefined during SSR / build. Putting initApp in a server +// component crashes the build. Putting it in a "use client" component that +// runs in useEffect makes it safe. + +import type { Metadata, Viewport } from 'next'; +import type { ReactNode } from 'react'; +import { Providers } from './providers'; + +export const metadata: Metadata = { + title: 'Cookbook recipe — Minimal sdk-dapp v5 in Next.js', + description: + 'A working sdk-dapp v5 + Next.js 14 App Router setup that compiles under TypeScript strict mode.', +}; + +export const viewport: Viewport = { + width: 'device-width', + initialScale: 1, +}; + +export default function RootLayout({ + children, +}: { + children: ReactNode; +}): JSX.Element { + return ( + + + {children} + + + ); +} +``` + +## app/providers.tsx: the client-only init wrapper + +This is the file the docs leave you to invent. Three things happen here: +`"use client"` keeps `initApp()` off the server (it touches `sessionStorage`, +which crashes during SSR); `useEffect` runs `initApp(config)` once on mount and +gates rendering on `ready`; `UnlockPanelManager.init()` sets up the wallet +selection panel, with an **`async`** `onClose` handler (see Pitfall 1). + +```tsx title="app/providers.tsx" +'use client'; + +// app/providers.tsx — client-only sdk-dapp v5 init wrapper. +// +// This component is the file the official docs leave to the reader to +// invent. It does three things: +// +// 1. Marks itself "use client" so it never runs on the server. sdk-dapp's +// initApp reads from sessionStorage, which is undefined during SSR. +// 2. Calls initApp(config) once on mount inside useEffect. Renders a +// lightweight loader until init resolves; then renders children. +// 3. Configures the UnlockPanelManager — the v5 replacement for the +// per-provider login button components from v4. +// +// IMPORTANT: the OnCloseUnlockPanelType requires Promise. The docs +// example uses sync `() => navigate(...)` +// which fails strict-mode compile. We use `async` callbacks. + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { dappConfig } from '../lib/multiversx'; + +// Module-scope flag prevents double-init if React Strict Mode mounts the +// component twice (it does, deliberately, in dev). initApp is idempotent +// in v5 but skipping the second call avoids redundant work. +let initStarted = false; + +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; + + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + + // UnlockPanelManager.init must be called AFTER initApp resolves. + // Both callbacks return Promise — see Pitfall 1 on the recipe page. + UnlockPanelManager.init({ + loginHandler: async (): Promise => { + // Replace with your post-login navigation (e.g. router.push). + // For this minimal recipe we just leave the user on /. + // eslint-disable-next-line no-console + console.log('Logged in.'); + }, + onClose: async (): Promise => { + // Called if the user dismisses the unlock panel without logging in. + // eslint-disable-next-line no-console + console.log('Unlock panel closed.'); + }, + }); + + setReady(true); + }); + + return () => { + cancelled = true; + }; + }, []); + + if (!ready) { + return ( +
+ Initializing wallet SDK… +
+ ); + } + + return <>{children}; +} +``` + +## app/page.tsx: landing page with login and account info + +`useGetIsLoggedIn()` and `useGetAccount()` are sdk-dapp's React hooks. The +component renders a connect button when logged out and the account address plus +EGLD balance when logged in. + +```tsx title="app/page.tsx" +'use client'; + +// app/page.tsx — landing page. +// +// Two states: +// 1. Logged out → "Connect wallet" button that opens the unlock panel. +// 2. Logged in → address (bech32) and EGLD balance. +// +// All blockchain state comes from sdk-dapp React hooks. These read from a Zustand store that initApp populates, +// so they MUST run inside — they return empty defaults until +// init has resolved. + +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import { formatAmount } from '@multiversx/sdk-dapp/out/lib/sdkDappUtils'; + +export default function Home(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const account = useGetAccount(); + const { network } = useGetNetworkConfig(); + + // The UnlockPanelManager is a singleton. .getInstance() works once it has + // been initialised inside . Calling .openUnlockPanel() raises + // the wallet selector. + const handleConnect = (): void => { + // openUnlockPanel() returns Promise (see + // node_modules/@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager.d.ts). + // This handler is a sync onClick, so we explicitly discard the promise — + // matching the eslint no-floating-promises gate. + void UnlockPanelManager.getInstance().openUnlockPanel(); + }; + + const handleLogout = async (): Promise => { + const provider = getAccountProvider(); + await provider.logout(); + }; + + return ( +
+

Minimal sdk-dapp v5 in Next.js

+

+ Network: {network.chainId} · API: {network.apiAddress} +

+ + {!isLoggedIn ? ( + + ) : ( +
+

Connected

+

+ Address: {account.address} +

+

+ Balance:{' '} + + {formatAmount({ + input: account.balance, + decimals: 18, + digits: 4, + showLastNonZeroDecimal: true, + })}{' '} + {network.egldLabel} + +

+ +
+ )} +
+ ); +} +``` + +## lib/multiversx.ts: environment config + +Centralized config so swapping environments is a one-line change. Exports +`dappConfig`, consumed by ``. + +```ts title="lib/multiversx.ts" +// lib/multiversx.ts — environment configuration for sdk-dapp's initApp. +// +// Centralising config here means that swapping environments +// (devnet -> testnet -> mainnet) is a one-file change. It also lets us keep +// secret-ish values (WalletConnect project IDs) in a single place and read +// them from environment variables. +// +// The full `dAppConfig` shape accepts more keys than this. We +// only set the keys we actually use; sdk-dapp fills the rest with defaults. + +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +// `WalletConnect` requires a project ID from https://cloud.walletconnect.com. +// Read from env so the same code ships across environments without leaks. +// Fallback is the demo project ID — works in dev but rate-limited; replace it +// with your own before going to production. +const WALLET_CONNECT_PROJECT_ID = + process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; + +// `dappConfig` is consumed by `app/providers.tsx` -> `initApp(...)`. +export const dappConfig = { + storage: { + // sessionStorage on the client. SSR-safe because only calls + // initApp inside a useEffect. + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + // nativeAuth: true issues a JWT-shaped token on login that backends can + // verify. Set to false for the + // simplest possible flow. + nativeAuth: true, + // `theme` takes one of the enum's string values; the short `'dark'` fails, + // so use ThemesEnum. sdk-dapp's own + // InitAppType JSDoc example uses `theme: ThemesEnum.light` (see + // node_modules/@multiversx/sdk-dapp/out/methods/initApp/initApp.types.d.ts). + // The runtime value is the literal 'mvx:dark-theme'; the web components + // key off that exact string. + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + }, +}; + +// Re-export EnvironmentsEnum so consumers don't need a deep import. +export { EnvironmentsEnum }; +``` + +## Run it + +```bash +npm run dev +``` + +Then open `https://localhost:3000` (HTTPS, see Pitfall 2). You should see a +"Connect wallet" button; after clicking, the sdk-dapp unlock panel listing your +installed providers; after connecting, the account address (bech32) and the EGLD +balance, formatted via `formatAmount`. + +## How it works + +Three sdk-dapp v5 concepts are in play here. + +**`initApp()` is imperative, not declarative.** Unlike wagmi or +`@solana/wallet-adapter-react`, sdk-dapp's setup is a function call you must +`await` once before any hook works. The `` component wraps that call +so React does not render until it resolves. This is the workaround for the +SSR-unsafe init; the docs show a top-level `initApp(...).then(render)` which works +in Vite but breaks in Next.js. + +**`UnlockPanelManager` replaces v4 login button components.** In v4 you imported +``, ``, and so on, one component +per provider. In v5 you call `UnlockPanelManager.init({ loginHandler })` and then +`.openUnlockPanel()` from any button. The panel itself is a web component rendered +by `@multiversx/sdk-dapp-ui`. + +**Hooks read from a Zustand store.** `useGetAccount()`, `useGetIsLoggedIn()`, and +`useGetNetworkConfig()` are thin wrappers over `useSelector` against a Zustand +store initialized inside `initApp()`. That is why they return empty/default values +until init completes, and why `` gates rendering on the `ready` flag. + +## Pitfalls + +:::warning[Pitfall 1: UnlockPanelManager.init's onClose must be async] +The docs example uses sync callbacks. That fails under TypeScript strict mode with +`error TS2322: Type '() => void' is not assignable to type 'OnCloseUnlockPanelType'`. +The `OnCloseUnlockPanelType` requires `Promise`. Use `async () => { ... }` +or `() => Promise.resolve(...)`. The sample code in `app/providers.tsx` does this +correctly. +::: + +:::warning[Pitfall 2: wallet providers require HTTPS, even in dev] +Stock `next dev` serves plain `http://localhost:3000`, and the DeFi extension and +Ledger providers refuse to connect over HTTP. That is why this recipe's dev script +adds `--experimental-https` to serve `https://localhost:3000`. WalletConnect +(xPortal) **does** work over HTTP, since the connection runs over the WalletConnect +relay rather than the dApp origin. See +[Local HTTPS for dApp dev](/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup) +for the mkcert workflow that makes the certificate *trusted*. +::: + +:::warning[Pitfall 3: initApp() must run client-side only] +`initApp()` reads from `sessionStorage` during rehydration. On the server (during +Next.js SSR or build), `sessionStorage` is undefined and the call throws. The +`"use client"` plus `useEffect` pattern in `app/providers.tsx` is mandatory; you +cannot put `initApp()` at module scope. +::: + +:::note[Pitfall 4: expect two build warnings] +Building this recipe surfaces two non-blocking warnings: +`Attempted import error: 'firstValueFrom' is not exported from 'rxjs'` and +`Critical dependency: the request of a dependency is an expression`. The first is +the `@ledgerhq/hw-transport-web-ble` package pulling an older `rxjs`; the second is +`protobufjs/inquire` doing a runtime `require` lookup. Neither breaks the build. +::: + +## See also + +- [Minimal sdk-dapp v5 in Vite + React](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) + is the same recipe, Vite path, no SSR concerns. +- [Local HTTPS for dApp dev](/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup) + is how to make `https://localhost:3000` work for both Next.js and Vite. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the next thing you will do once login works. +- [Migrate v4 to v5](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration) + if you have an existing v4 Next.js codebase. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send.mdx b/docs/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send.mdx new file mode 100644 index 000000000..46f58ae6a --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send.mdx @@ -0,0 +1,674 @@ +--- +title: Sign and send a transaction (the working path) +description: The canonical sdk-dapp v5 sign, send, and track flow. provider.signTransactions then TransactionManager.send and .track, with proper nonce handling. +difficulty: beginner +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" + sdk-dapp: "^5.6.0" +tags: + - sdk-dapp + - sdk-core + - transaction + - typescript + - devnet +--- + +This is the canonical "I have a wallet, now what?" recipe, the foundation every +other transaction recipe links from. Build a `Transaction` with sdk-core, sign +with the connected provider, send and track with `TransactionManager`. The whole +pattern is three calls. + +In v4, signing was a React hook (`useSignTransactions`). In v5, signing is on the +provider itself (`provider.signTransactions(txs)`), a deliberate change because +the provider already knows the user's signing surface (extension popup, Ledger +USB, WalletConnect mobile prompt). This recipe is the v5-current pattern. + +If you do not yet have a working sdk-dapp v5 setup, start with +[Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) +or +[Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal), +both of which end where this recipe begins. + +## Prerequisites + +- A working sdk-dapp v5 setup (Next.js or Vite, see the start-here recipes). +- A logged-in account on devnet. +- A few devnet EGLD ([faucet](https://r3d4.fr/faucet) or + [devnet wallet](https://devnet-wallet.multiversx.com)). + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/sign-and-send +npm install +npm run dev +# open https://localhost:3000 +``` + +The recipe is a Next.js app, but the load-bearing file (`lib/transactions.ts`) is +framework-agnostic. Copy it into a Vite app, a Remix loader, a CLI script, or +anywhere the sdk-dapp store has been initialized. + +## The three-step pattern + +The whole flow lives in `lib/transactions.ts`. Read this once and you have the v5 +transaction pattern. + +```ts title="lib/transactions.ts" +// lib/transactions.ts — the canonical sign / send / track flow for sdk-dapp v5. +// +// This file demonstrates the three-step pattern that every transaction in a +// v5 dApp follows: +// +// 1. BUILD with sdk-core's Transaction constructor. +// Pull sender, nonce, and chainID from the sdk-dapp +// store via getAccount() and getNetworkConfig() — both are non-React +// synchronous reads. +// 2. SIGN via the connected provider. +// In v5 signing is on the provider, not a hook — this is the v4 → v5 +// semantic change documented in the v5 changelog. +// 3. SEND + TRACK with TransactionManager. +// The returned sessionId is the React-hook key for +// useGetPendingTransactions(sessionId). +// +// The function below is intentionally framework-agnostic. It reads from the +// sdk-dapp store directly (no hooks), so you can call it from a React +// onClick, a server action, a CLI script, or anywhere else. + +import { Address, Transaction } from '@multiversx/sdk-core'; +import { GAS_PRICE, GAS_LIMIT } from '@multiversx/sdk-dapp/out/constants/mvx.constants'; +import { TransactionManager } from '@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager'; +import { getAccount } from '@multiversx/sdk-dapp/out/methods/account/getAccount'; +import { getNetworkConfig } from '@multiversx/sdk-dapp/out/methods/network/getNetworkConfig'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import type { SignedTransactionType } from '@multiversx/sdk-dapp/out/types/transactions.types'; + +/** + * Inputs for an EGLD transfer. Amount is in the smallest denomination — + * 1 EGLD = 10^18. + * + * Use bigint everywhere. Never `Number` — JavaScript loses precision past + * 2^53 and EGLD amounts go higher than that all the time. + */ +export interface SendEgldInput { + /** Bech32 address of the recipient. */ + receiver: string; + /** Amount in the smallest denomination (10^18 = 1 EGLD). */ + amountInSmallestDenomination: bigint; + /** Optional UTF-8 data field. Note: data adds gas. */ + data?: string; +} + +/** + * Output of a successful sign + send + track call. The sessionId is the key + * the React hooks (useGetPendingTransactions, etc.) use to render UI state. + */ +export interface SendEgldOutput { + sessionId: string; + transactionHash: string; +} + +/** + * Build, sign, send, and start tracking an EGLD transfer. + * + * Throws if no account is connected. The caller is responsible for ensuring + * useGetIsLoggedIn() is true before invoking — the function does not + * gracefully wait for login. + */ +export async function sendEgld(input: SendEgldInput): Promise { + const account = getAccount(); + const { network } = getNetworkConfig(); + + if (!account.address) { + throw new Error('No account connected. Call after a successful login.'); + } + + // Step 1 — BUILD. + // Note: account.nonce is the network's last-known nonce. If you sent any + // transactions earlier in this session you must increment locally — see + // Pitfall 1 on this page. + // + // The canonical Transaction constructor shape: value/gasLimit/nonce are + // bigint, data is Uint8Array. + const tx = new Transaction({ + sender: Address.newFromBech32(account.address), + receiver: Address.newFromBech32(input.receiver), + value: input.amountInSmallestDenomination, + gasLimit: BigInt(computeGasLimit(input.data)), + gasPrice: BigInt(GAS_PRICE), + chainID: network.chainId, + nonce: BigInt(account.nonce), + // Version 1 is the unsigned-transaction default; sdk-dapp managers handle + // hash-signing version bumps internally where needed. + version: 1, + // data is omitted via spread when undefined to play nicely with + // exactOptionalPropertyTypes (strict mode rejects passing + // `undefined` to a property that is `Uint8Array` not `Uint8Array | undefined`). + ...(input.data ? { data: new Uint8Array(Buffer.from(input.data)) } : {}), + }); + + // Step 2 — SIGN. + // The provider knows the connected wallet's signing surface (extension + // popup / Ledger USB / WalletConnect mobile prompt). signTransactions + // returns the same array shape it was given, with each tx now carrying a + // .signature field. + const provider = getAccountProvider(); + const [signed] = await provider.signTransactions([tx]); + + if (!signed) { + // Defensive: if the user cancelled the wallet popup, signTransactions + // throws or returns an empty array depending on the provider. + throw new Error('User cancelled signing.'); + } + + // Step 3 — SEND + TRACK. + // TransactionManager.send()'s type signature is shape-symmetric: pass a + // flat Transaction[] (one batch), get a flat SignedTransactionType[] back; + // pass Transaction[][] (multiple batches), get SignedTransactionType[][] + // back (node_modules/@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager.d.ts). + // We always pass a single flat batch, so the nested variant cannot occur + // here — narrow explicitly (isFlatSentTransactions below) rather than + // casting, so a real shape change fails loudly instead of miscompiling. + // + // track() takes that SAME array (not a single unwrapped element) — see the + // TransactionManager.d.ts JSDoc example, which calls + // `txManager.track(sentTransactions, {...})` on the whole array. + const txManager = TransactionManager.getInstance(); + const sentTransactions = await txManager.send([signed]); + + if (!isFlatSentTransactions(sentTransactions)) { + throw new Error('Unexpected response shape from TransactionManager.send().'); + } + + const [sent] = sentTransactions; + if (!sent) { + throw new Error('Send failed: TransactionManager returned no transactions.'); + } + + const sessionId = await txManager.track(sentTransactions, { + transactionsDisplayInfo: { + processingMessage: 'Sending EGLD…', + successMessage: 'Transfer complete.', + errorMessage: 'Transfer failed.', + }, + }); + + // `SignedTransactionType` already carries `hash: string` (see + // node_modules/@multiversx/sdk-dapp/out/types/transactions.types.d.ts). + // sdk-dapp's own status-polling joins on this same field internally, so + // it is the authoritative hash — no separate computation needed. + // + // Earlier drafts of this recipe called + // `new TransactionComputer().computeTransactionHash(sent)` and hex-encoded + // the result via `Buffer.from(...)`. Both are wrong for this SDK version: + // computeTransactionHash() expects an sdk-core `Transaction` instance, not + // the `SignedTransactionType` plain object `send()` returns here (that's a + // real tsc --strict error, not just a style nit) — and its return value is + // already a hex `string`, not a `Uint8Array` + // (node_modules/@multiversx/sdk-core/out/core/transactionComputer.js), so + // re-wrapping it in `Buffer.from(str).toString('hex')` would silently + // double-encode it into a wrong, longer hex string. + const transactionHash = sent.hash; + + return { + sessionId, + transactionHash, + }; +} + +/** + * Type guard narrowing TransactionManager.send()'s union return type down to + * the flat (single-batch) variant. We only ever call send() with a single + * flat Transaction[] batch, so this should always hold at runtime; it exists + * so the compiler (not an assertion) enforces that invariant. + */ +function isFlatSentTransactions( + value: SignedTransactionType[] | SignedTransactionType[][], +): value is SignedTransactionType[] { + return value.length === 0 || !Array.isArray(value[0]); +} + +/** + * Compute a reasonable gas limit for a plain or data-bearing EGLD transfer. + * + * The default GAS_LIMIT (50_000) covers the empty-data case. For data + * transactions, the network charges 1500 gas per byte. + */ +function computeGasLimit(data: string | undefined): number { + const baseLimit = GAS_LIMIT; + if (!data) { + return baseLimit; + } + const dataBytes = Buffer.byteLength(data, 'utf8'); + // 1500 gas per byte of data. Add some headroom (10%) so a slight protocol + // change doesn't immediately invalidate every recipe — CI catches drift. + return Math.ceil((baseLimit + dataBytes * 1500) * 1.1); +} +``` + +The three steps: + +1. **Build** with sdk-core. `Transaction` is the value type; + `Address.newFromBech32()` parses the bech32 strings. `getAccount()` and + `getNetworkConfig()` are non-React reads of the sdk-dapp store; they work from + any context as long as `initApp()` has resolved. +2. **Sign** with the provider. `getAccountProvider()` returns the connected + provider; calling `.signTransactions([tx])` opens the wallet UI and resolves + with the same array, now signature-bearing. +3. **Send and track** with `TransactionManager`. The singleton's `.send()` POSTs + to the network; `.track()` starts the WebSocket-or-polling tracker that updates + the Zustand store as the tx transitions pending to success/fail. The returned + `sessionId` is the lookup key for the React hooks. + +## The React hook + +A thin wrapper around `sendEgld()` with status / error state for `onClick` use: + +```ts title="lib/useSendEgld.ts" +// lib/useSendEgld.ts — React hook around the framework-agnostic sendEgld(). +// +// Wraps lib/transactions.ts in a stateful hook so a component can render a +// "Send" button, drive it from local state, and surface in-flight / success / +// error UI. +// +// Why a hook? sendEgld() is callable from anywhere, but most UI calls want: +// - a loading flag while the wallet popup is open +// - the sessionId, so other parts of the page can use +// useGetPendingTransactions(sessionId) to render per-tx state +// - a stable reference to the send function for use in onClick + +import { useCallback, useState } from 'react'; +import { sendEgld, type SendEgldInput, type SendEgldOutput } from './transactions'; + +export interface UseSendEgldState { + status: 'idle' | 'signing' | 'sending' | 'tracking' | 'done' | 'error'; + sessionId: string | null; + transactionHash: string | null; + error: string | null; +} + +const initialState: UseSendEgldState = { + status: 'idle', + sessionId: null, + transactionHash: null, + error: null, +}; + +export interface UseSendEgld { + state: UseSendEgldState; + send: (input: SendEgldInput) => Promise; + reset: () => void; +} + +export function useSendEgld(): UseSendEgld { + const [state, setState] = useState(initialState); + + const send = useCallback( + async (input: SendEgldInput): Promise => { + setState({ ...initialState, status: 'signing' }); + try { + // The status doesn't get a chance to update between signing and + // sending in any visually meaningful way; we keep it simple. + const out = await sendEgld(input); + setState({ + status: 'done', + sessionId: out.sessionId, + transactionHash: out.transactionHash, + error: null, + }); + return out; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + setState({ + status: 'error', + sessionId: null, + transactionHash: null, + error: message, + }); + return null; + } + }, + [], + ); + + const reset = useCallback((): void => { + setState(initialState); + }, []); + + return { state, send, reset }; +} +``` + +## The page + +The form: receiver address, amount in EGLD (decimal, converted internally to the +smallest denomination), submit button. It pulls login state and pending-transaction +count from the sdk-dapp hooks, so the UI reflects the same store the manager writes to. + +```tsx title="app/page.tsx" +'use client'; + +// app/page.tsx — sign + send + track demo page. +// +// Three states: +// 1. Logged out → "Connect wallet" button (delegates to UnlockPanelManager). +// 2. Logged in, idle → form: receiver address + amount in EGLD. +// 3. Logged in, post-send → status display: hash, sessionId, success/error. +// +// The form's amount input is in EGLD (decimal), but lib/transactions.ts +// expects the smallest denomination (10^18). We do the conversion inline — +// see toSmallestDenomination() below — so the user can think in EGLD while +// the SDK still gets a precise bigint. + +import { useState } from 'react'; +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { useGetPendingTransactions } from '@multiversx/sdk-dapp/out/react/transactions/useGetPendingTransactions'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import { useSendEgld } from '../lib/useSendEgld'; + +export default function SendPage(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const account = useGetAccount(); + const { network } = useGetNetworkConfig(); + const { state, send, reset } = useSendEgld(); + + // useGetPendingTransactions() returns SignedTransactionType[] — a flat + // array of currently-pending transactions, NOT a sessionId-keyed map (see + // node_modules/@multiversx/sdk-dapp/out/react/transactions/useGetPendingTransactions.d.ts). + // If you need session-keyed + // lookups instead, use useGetPendingTransactionsSessions(), which returns + // Record. + const pending = useGetPendingTransactions(); + const pendingCount = pending.length; + + const [receiver, setReceiver] = useState(''); + const [amountEgld, setAmountEgld] = useState('0.001'); + + const handleConnect = (): void => { + // openUnlockPanel() returns Promise (see + // node_modules/@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager.d.ts). + // This handler is a sync onClick, so we explicitly discard the promise — + // matching the eslint no-floating-promises gate. + void UnlockPanelManager.getInstance().openUnlockPanel(); + }; + + const handleLogout = async (): Promise => { + const provider = getAccountProvider(); + await provider.logout(); + }; + + const handleSubmit = async (e: React.FormEvent): Promise => { + e.preventDefault(); + let amountInSmallest: bigint; + try { + amountInSmallest = toSmallestDenomination(amountEgld); + } catch (err) { + // eslint-disable-next-line no-console + console.error('Bad amount:', err); + return; + } + await send({ + receiver, + amountInSmallestDenomination: amountInSmallest, + }); + }; + + return ( +
+

Sign and send a transaction

+

+ Network: {network.chainId} +

+ + {!isLoggedIn ? ( + + ) : ( + <> +

+ Connected as {account.address} +

+ + +
{ + void handleSubmit(e); + }} + style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }} + > + + + +
+ + + + )} +
+ ); +} + +interface ResultProps { + state: ReturnType['state']; + reset: () => void; + pending: number; +} + +function Result({ state, reset, pending }: ResultProps): JSX.Element | null { + if (state.status === 'idle') { + return null; + } + return ( +
+

Result

+

+ Status: {state.status} · Pending in store:{' '} + {pending} +

+ {state.transactionHash && ( +

+ Hash: {state.transactionHash} +

+ )} + {state.sessionId && ( +

+ Session ID: {state.sessionId} +

+ )} + {state.error && ( +

+ Error: {state.error} +

+ )} + +
+ ); +} + +/** + * Convert a decimal EGLD string to the smallest denomination (10^18). + * + * Avoid `parseFloat` + `* 1e18` — it loses precision for any meaningful + * balance. We do the multiplication on the integer part with bigint, then + * pad the fractional part with zeros to 18 digits. + */ +function toSmallestDenomination(decimalEgld: string): bigint { + const trimmed = decimalEgld.trim(); + if (trimmed === '' || !/^\d+(\.\d+)?$/.test(trimmed)) { + throw new Error(`Invalid amount: "${decimalEgld}"`); + } + const dotIndex = trimmed.indexOf('.'); + const integerPart = dotIndex === -1 ? trimmed : trimmed.slice(0, dotIndex); + const rawFraction = dotIndex === -1 ? '' : trimmed.slice(dotIndex + 1); + if (rawFraction.length > 18) { + throw new Error('Amount has more than 18 decimal places.'); + } + const paddedFraction = rawFraction.padEnd(18, '0'); + return BigInt(integerPart) * 10n ** 18n + BigInt(paddedFraction || '0'); +} + +const buttonStyle: React.CSSProperties = { + padding: '0.6rem 1.1rem', + fontSize: '1rem', + cursor: 'pointer', +}; + +const inputStyle: React.CSSProperties = { + display: 'block', + width: '100%', + marginTop: '0.25rem', + padding: '0.5rem', + fontSize: '0.95rem', + fontFamily: 'monospace', +}; +``` + +## How it works + +**Why signing is on the provider, not a hook.** v4 had `useSignTransactions()`, a +hook that paid the cost of being a hook (Rules of Hooks, render-phase identity) +without using any of the benefits. The signing call is fundamentally imperative: +"user clicks Send, wallet pops up". Putting it on the provider makes the call site +honest about that. + +**Why sending is via TransactionManager, not the provider.** Sending is a separate +concern from signing. `TransactionManager` handles batching, gas-bumping retries, +and integrates with the WebSocket-or-polling tracker. The provider is just a +signature surface; the manager is the orchestrator. + +**What `sessionId` is for.** `useGetPendingTransactions()`, +`useGetSuccessfulTransactions()`, and `useGetFailedTransactions()` take **no +arguments**. Each returns the flat `SignedTransactionType[]` for *every* +transaction in that state, not just yours: + +```ts +const pending = useGetPendingTransactions(); // every pending tx, all sessions +const successful = useGetSuccessfulTransactions(); +const failed = useGetFailedTransactions(); +``` + +To look up **one** session (the one `sessionId` from `send()` refers to), use the +`...Sessions()` variants instead. They return +`Record`: + +```ts +const sessions = useGetPendingTransactionsSessions(); +const mySession = sessions[sessionId]; // SessionTransactionType | undefined +``` + +## Pitfalls + +:::warning[Pitfall 1: the nonce trap (silent rejection)] +`getAccount()` returns the network's last-known nonce. If you fire two +transactions in a session, the second is rejected unless you increment the nonce +locally: + +```ts +const nonce = BigInt(getAccount().nonce); +// First transaction with nonce, signed and sent. +// For the second: +const tx2 = new Transaction({ ...rest, nonce: nonce + 1n }); +``` + +The store updates the nonce after a confirmed transaction, but if you fire two +before the first confirms you must manage the nonce yourself. +::: + +:::warning[Pitfall 2: amount is in the smallest denomination (10^18)] +`value: 1n` means **0.000000000000000001** EGLD, not 1 EGLD. The recipe's UI takes +a decimal string and converts via `toSmallestDenomination()`. Avoid +`parseFloat * 1e18`, JavaScript loses precision past 2^53. Use bigint arithmetic. +::: + +:::warning[Pitfall 3: gas limit scales with data] +The default `GAS_LIMIT` (50,000) covers an empty-data transfer. Each byte of +`data` adds 1500 gas. If you hand-roll a `Transaction` and skip the math, the +network rejects with "insufficient gas". The recipe's `computeGasLimit()` does +this, copy it. +::: + +:::warning[Pitfall 4: chain ID mismatch] +The signed transaction's `chainID` must match the network you are sending to. +Pulling from `getNetworkConfig().network.chainId` ensures consistency. Never +hard-code `"D"`, `"T"`, or `"1"`; it makes the recipe environment-bound and +silently breaks when someone switches via a config change. +::: + +:::warning[Pitfall 5: sender mismatch] +`tx.sender` MUST equal the logged-in account's bech32 address. The wallet refuses +to sign otherwise (it would sign with a different key than the tx's sender field, +then the network rejects on mismatch). +::: + +:::warning[Pitfall 6: useGetPendingTransactions() has no per-session overload] +You might assume `useGetPendingTransactions()` accepts the `sessionId` +from `send()` and filters to just that transaction. It does not; the signature is +`(): SignedTransactionType[]`, no parameters, verified against +`node_modules/@multiversx/sdk-dapp/out/react/transactions/useGetPendingTransactions.d.ts` +on `@multiversx/sdk-dapp v5`. It returns *every* pending transaction across +every session. For a single session's status, index into +`useGetPendingTransactionsSessions()` by `sessionId` instead. +::: + +## See also + +- [Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) + is the wallet-connect setup this recipe builds on. +- [Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) + is the same pattern, Vite path. +- [Migration: useSendTransactions to TransactionManager.send](/sdk-and-tools/sdk-js/cookbook/migration/send-transactions-to-transaction-manager) + extends this single-transaction flow to grouped/batch sends. +- [Read the connected account](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + is how to read the account this recipe signs for. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration.mdx b/docs/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration.mdx new file mode 100644 index 000000000..cc686b0b2 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration.mdx @@ -0,0 +1,640 @@ +--- +title: "Migrate sdk-dapp v4 to v5: hook-by-hook diffs" +description: Side-by-side v4 vs v5 diffs for every removed hook, component, and import path. The shortest viable upgrade path for an existing v4 dApp. +difficulty: intermediate +est_minutes: 12 +last_validated: "2026-07-16" +sdk_versions: + sdk-dapp: "^5.6.0" +tags: + - sdk-dapp + - migration + - v4-to-v5 + - typescript + - react +--- + +Many teams have stayed on v4 because the migration cost was unclear. The official +guide is a patch-notes-style doc that lists renames; here you get runnable +diffs for the six load-bearing changes plus the rename table, so you can see the +shape of your migration before you start. + +Effort estimate, very loosely: + +| Codebase size | Estimated effort | +| --- | --- | +| Small dApp: login plus a handful of transactions | Half a day | +| Medium: 20+ pages, custom login UI, multi-step txs | 2 to 3 days | +| Large: heavily customized provider tree, custom hooks | 1 to 2 weeks | + +Most of the time goes into hook renames and the `` to `initApp()` +restructure. The actual code shape is similar; the imports and the +wrapper-vs-imperative shift are what take time. + +In each pair below, the "before (v4)" block is v4 code shown for contrast only. It +imports v4-only paths and is not part of the compiled recipe; the "after (v5)" +block compiles against the installed v5 SDK. + +## The structural change: `` is gone + +v4 wrapped your app in a declarative React component; v5 calls a function before +rendering. + +```tsx +// before (v4): wraps the app +``` + +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed. +// +// migrations/01-dapp-provider/before.tsx — v4 declarative provider. +// +// In v4 you wrapped the entire app tree with . The provider +// component took the network environment as a prop and handled +// initialisation under the hood. Hooks worked anywhere inside the tree. + +import { DappProvider } from '@multiversx/sdk-dapp/wrappers'; +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/types'; + +const customNetworkConfig = { + walletConnectV2ProjectId: 'multiversx-cookbook-demo', +}; + +export function App() { + return ( + + + + ); +} +``` + +```tsx title="migrations/01-dapp-provider/after.tsx" +// migrations/01-dapp-provider/after.tsx — v5 imperative init. +// +// v5 replaces with an imperative initApp() call. The +// component below mirrors the structure of the Next.js / Vite minimal +// recipes' Providers wrapper: useEffect-driven init, ready-gate, +// children pass-through. +// +// Key changes from v4: +// - wrapping is gone — call initApp(config) instead. +// - The config shape is { storage, dAppConfig: { environment, providers, ... } }. +// - initApp must run AFTER the component mounts. In Next.js this means +// a "use client" component; in Vite there is no SSR concern. +// - Hooks still work, but only after init resolves. + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; + +const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + providers: { + walletConnect: { + walletConnectV2ProjectId: 'multiversx-cookbook-demo', + }, + }, + }, +}; + +let initStarted = false; + +export function Providers({ children }: { children: ReactNode }): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; + + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + setReady(true); + }); + + return () => { + cancelled = true; + }; + }, []); + + if (!ready) { + return
Initializing…
; + } + return <>{children}; +} +``` + +The mechanism changes (`` becomes `await initApp(config)`), where it +runs changes (client-side, in a `useEffect`, behind a `"use client"` directive and +a re-init guard), and SSR behavior changes (v5's `initApp()` hits `sessionStorage` +during rehydration and crashes server-side, so it must be gated). See +[Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) +Pitfall 3 for the canonical workaround. + +## `useGetAccountInfo` to `useGetAccount` + `useGetIsLoggedIn` + `useGetLoginInfo` + +v5 splits the v4 monolith into three hooks. Login state and on-chain account state +live in different store slices and have different update cadences. + +```tsx +// before (v4): useGetAccountInfo returned everything +``` + +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed. +// +// migrations/02-get-account-info/before.tsx — v4 useGetAccountInfo. +// +// In v4, useGetAccountInfo() returned everything: address, balance, +// nonce, plus assorted "loginInfo" fields like isLoggedIn and the +// auth token. One hook, one big object. + +import { useGetAccountInfo } from '@multiversx/sdk-dapp/hooks'; + +export function AccountSummary() { + const { + address, + account: { balance, nonce }, + isLoggedIn, + tokenLogin, + } = useGetAccountInfo(); + + if (!isLoggedIn) { + return

Not logged in.

; + } + + return ( +
+

Address: {address}

+

Balance: {balance}

+

Nonce: {nonce}

+

Token: {tokenLogin?.nativeAuthToken}

+
+ ); +} +``` + +```tsx title="migrations/02-get-account-info/after.tsx" +// migrations/02-get-account-info/after.tsx — v5 split: useGetAccount + useGetLoginInfo. +// +// v5 splits the v4 monolith. The login state and the on-chain account +// state live in different store slices and have different update +// cadences (login info changes rarely; account balance / nonce change +// per transaction), so they got their own hooks. +// +// Migration rule of thumb: +// address, balance, nonce, shard, username → useGetAccount() +// isLoggedIn → useGetIsLoggedIn() +// tokenLogin, providerType, expires, loginMethod → useGetLoginInfo() + +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetLoginInfo } from '@multiversx/sdk-dapp/out/react/loginInfo/useGetLoginInfo'; + +export function AccountSummary(): JSX.Element { + const { address, balance, nonce } = useGetAccount(); + const isLoggedIn = useGetIsLoggedIn(); + const { tokenLogin } = useGetLoginInfo(); + + if (!isLoggedIn) { + return

Not logged in.

; + } + + return ( +
+

Address: {address}

+

Balance: {balance}

+

Nonce: {nonce}

+

Token: {tokenLogin?.nativeAuthToken}

+
+ ); +} +``` + +Migration rule of thumb: `address`, `balance`, `nonce`, `shard`, `username` go to +`useGetAccount()`; `isLoggedIn` goes to `useGetIsLoggedIn()`; `tokenLogin`, +`providerType`, `expires`, `loginMethod` go to `useGetLoginInfo()`. See +[Migration: useGetAccountInfo v4 vs v5](/sdk-and-tools/sdk-js/cookbook/migration/get-account-info-v4-vs-v5) +for why the shim that survives the rename is the dangerous case. + +## `useSignTransactions` to `provider.signTransactions(txs)` + +In v5 signing is on the provider, not a hook. The signing call is fundamentally +imperative ("user clicks Send, wallet pops up"); putting it on the provider makes +the call site honest about that. + +```tsx +// before (v4): useSignTransactions hook +``` + +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed. +// +// migrations/03-sign-transactions/before.tsx — v4 useSignTransactions. +// +// In v4, signing was a hook. It returned a signTransactions function +// plus a "signing state" object you could destructure to render +// signing-pending UI. + +import { useSignTransactions } from '@multiversx/sdk-dapp/hooks'; +import { Transaction } from '@multiversx/sdk-core'; + +export function useV4SignFlow() { + const { signTransactions } = useSignTransactions(); + + const handleSign = async (txs: Transaction[]) => { + const signedTransactions = await signTransactions(txs); + return signedTransactions; + }; + + return { handleSign }; +} +``` + +```tsx title="migrations/03-sign-transactions/after.tsx" +// migrations/03-sign-transactions/after.tsx — v5 provider.signTransactions. +// +// v5 moves signing onto the provider. The signing call is fundamentally +// imperative — "user clicks Send, wallet pops up". Putting it on the +// provider makes the call site honest about that and avoids paying the +// hook overhead (Rules of Hooks, render-phase identity, etc.) for a +// call that doesn't benefit from being a hook. +// +// Migration: replace useSignTransactions() with getAccountProvider() + +// .signTransactions(). The signature is the same — same input array, +// same return-array shape. + +import { Transaction } from '@multiversx/sdk-core'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; + +// No more hook — the function below is callable from any context, not +// just inside a React component. Useful if you also want to sign from a +// service worker, a CLI, or a server action. +export async function signFlow(txs: Transaction[]): Promise { + const provider = getAccountProvider(); + const signed = await provider.signTransactions(txs); + return signed; +} +``` + +A side benefit: the v5 version is not tied to React. You can call `signFlow(txs)` +from a CLI script, a service worker, or a server action, anywhere +`getAccountProvider()` works. + +## `sendTransactions` to `TransactionManager.send` + `.track` + +v5 splits the v4 monolithic `sendTransactions()` into three steps: sign, send, +track. The split lets you do meaningful things between steps and lets you skip +steps if you only need a subset. + +```tsx +// before (v4): services.sendTransactions +``` + +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed. +// +// migrations/04-send-transactions/before.tsx — v4 sendTransactions. +// +// In v4, sendTransactions() was a single function exported from the +// services module. It sign+send+track in one shot, returning a +// session id you could pass to other hooks. + +import { sendTransactions } from '@multiversx/sdk-dapp/services'; +import { Transaction } from '@multiversx/sdk-core'; + +export async function v4Send(transactions: Transaction[]) { + const { sessionId, error } = await sendTransactions({ + transactions, + transactionsDisplayInfo: { + processingMessage: 'Processing…', + successMessage: 'Done', + errorMessage: 'Failed', + }, + }); + if (error) { + throw new Error(error); + } + return sessionId; +} +``` + +```tsx title="migrations/04-send-transactions/after.tsx" +// migrations/04-send-transactions/after.tsx — v5 sign + TransactionManager. +// +// v5 splits the v4 monolithic sendTransactions() into three steps: +// 1. provider.signTransactions(txs) — opens the wallet UI +// 2. TransactionManager.send(signed) — POSTs to the network +// 3. TransactionManager.track(sent, opts) — starts the WebSocket / polling tracker +// +// The split lets you do meaningful things between steps (e.g., show a +// "preparing to send" UI while the wallet popup is open, or pre-flight +// validate the signed transactions before posting). It also lets you +// skip steps — e.g., if you sign now and send later from a different +// session, you can do that. +// +// The returned sessionId from .track() is the same string the v4 call +// returned. Existing components that consume sessionId via +// useGetPendingTransactions(sessionId) etc. continue to work. + +import { Transaction } from '@multiversx/sdk-core'; +import { TransactionManager } from '@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; + +export async function v5Send(transactions: Transaction[]): Promise { + // 1. Sign on the provider. + const provider = getAccountProvider(); + const signed = await provider.signTransactions(transactions); + + // 2. Send. + const txManager = TransactionManager.getInstance(); + const sent = await txManager.send(signed); + + // 3. Track. The returned sessionId is the lookup key for + // useGetPendingTransactions(sessionId), etc. + const sessionId = await txManager.track(sent, { + transactionsDisplayInfo: { + processingMessage: 'Processing…', + successMessage: 'Done', + errorMessage: 'Failed', + }, + }); + + return sessionId; +} +``` + +The returned `sessionId` from `.track()` is the same string the v4 call returned, +so existing components that consume it keep working. For grouped/batch sends, see +[Migration: useSendTransactions to TransactionManager.send](/sdk-and-tools/sdk-js/cookbook/migration/send-transactions-to-transaction-manager). + +## Per-provider login buttons to `UnlockPanelManager` + +v4 exported a button component per provider. v5 collapses all of them into a single +panel and a single open call. + +```tsx +// before (v4): plus siblings +``` + +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed. +// +// migrations/05-login-buttons/before.tsx — v4 per-provider login buttons. +// +// In v4 you imported a button component per provider and rendered them +// all on your login page. Each one was tightly bound to a single +// provider; choosing which to show meant rendering or not rendering +// each component. + +import { + ExtensionLoginButton, + WalletConnectLoginButton, + LedgerLoginButton, + WebWalletLoginButton, +} from '@multiversx/sdk-dapp/UI'; + +export function LoginPage() { + const callbackRoute = '/dashboard'; + + return ( +
+

Login

+ + DeFi extension + + + xPortal + + Ledger + + Web Wallet + +
+ ); +} +``` + +```tsx title="migrations/05-login-buttons/after.tsx" +// migrations/05-login-buttons/after.tsx — v5 UnlockPanelManager. +// +// v5 collapses all per-provider buttons into a single UnlockPanelManager +// that renders a panel listing every available provider. You configure +// which providers to show via `allowedProviders`. The actual panel is a +// web component from @multiversx/sdk-dapp-ui; you don't render it +// yourself. +// +// Migration: replace N per-provider button components with one button +// that calls UnlockPanelManager.openUnlockPanel(). The init() call — +// configuring loginHandler / onClose — happens once at app startup, +// inside the same wrapper as initApp(). +// +// CRITICAL: loginHandler and onClose return Promise. The docs +// example uses sync callbacks; that fails strict-mode compile. Use +// async or () => Promise.resolve(). + +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; + +// At app startup (inside the same Providers wrapper that calls initApp): +export function setupUnlockPanel(): void { + UnlockPanelManager.init({ + loginHandler: async (): Promise => { + // Replace with your post-login navigation, e.g. router.push('/dashboard'). + }, + onClose: async (): Promise => { + // Optional: handle the user dismissing the panel without logging in. + }, + // Restrict which providers appear in the panel. Omit this to show all. + allowedProviders: ['extension', 'walletConnect', 'ledger', 'crossWindow'], + }); +} + +// Anywhere you'd previously have rendered a login button: +export function LoginPage(): JSX.Element { + const handleConnect = (): void => { + // openUnlockPanel() returns Promise; this handler is a sync + // onClick, so discard it explicitly with `void` (eslint + // no-floating-promises). + void UnlockPanelManager.getInstance().openUnlockPanel(); + }; + + return ( +
+

Login

+ +
+ ); +} +``` + +Filter which providers appear in the panel via `allowedProviders` on `init()`. +Omit it to show all. + +## `useGetNotification` to `NotificationsFeedManager` + +v5 ships a pre-built notifications feed UI as a web component. You no longer render +the list yourself. + +```tsx +// before (v4): hook plus hand-rolled list +``` + +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed. +// +// migrations/06-notifications/before.tsx — v4 useGetNotification. +// +// In v4, you'd render notifications by hand using the data the hook +// returned. The hook gave you the array; you styled and positioned the +// list yourself. + +import { useGetNotification } from '@multiversx/sdk-dapp/hooks'; + +export function NotificationsList() { + const notifications = useGetNotification(); + + return ( +
    + {notifications.map((n) => ( +
  • {n.message}
  • + ))} +
+ ); +} +``` + +```tsx title="migrations/06-notifications/after.tsx" +// migrations/06-notifications/after.tsx — v5 NotificationsFeedManager. +// +// v5 ships a pre-built notifications feed UI as a web component. You no +// longer render the list yourself; the manager + the +// `` element handle it. You only need to call +// .getInstance() to get a reference, and call .open() / .close() / etc. +// to drive the UI. +// +// If you need fine-grained custom rendering (e.g. branding the +// notification card differently), drop down to the store directly via +// useSelector — the data is still there. The manager is the convenience +// layer; bypass it when convenience isn't enough. + +import { NotificationsFeedManager } from '@multiversx/sdk-dapp/out/managers/NotificationsFeedManager/NotificationsFeedManager'; + +export function NotificationsButton(): JSX.Element { + // The manager wires itself to the store automatically once initApp() + // has resolved; no per-component setup is required. If you want to + // register a custom on-notification handler at mount-time, do it in a + // useEffect here. + + const open = (): void => { + // openNotificationsFeed() returns Promise (see + // node_modules/@multiversx/sdk-dapp/out/managers/NotificationsFeedManager/NotificationsFeedManager.d.ts) — + // same floating-promise gate as UnlockPanelManager.openUnlockPanel(). + void NotificationsFeedManager.getInstance().openNotificationsFeed(); + }; + + return ( + + ); +} +``` + +If you need bespoke notification rendering, drop down to the store directly via +`useSelector`; the data is still there. The manager is the convenience layer. + +## The import-path renames + +Most v4 to v5 changes are pure path renames: + +| v4 path | v5 path | Semantic change? | +| --- | --- | --- | +| `/hooks` | `/out/react//...` | Path only | +| `/services` | `/out/methods/...` and `/out/managers/...` | Path only, split | +| `/UI` | `/out/managers/...` (web components) | **UI replaced** | +| `/wrappers` | (gone, see `initApp()`) | **Replaced** | +| `/utils` | `/out/utils/...` | Path only | +| `/types` | `/out/types/...` | Path only | +| `/providers` | `/out/providers/...` | Path only | + +Two semantic gotchas worth flagging loudly: + +1. **`getAccount` is a store read in both versions**, instant return, no network + call. There is **also** a new function called `getAccountFromApi` in v5 that + hits the API. Do not conflate them. +2. **`useGetAccountInfo` is still exported** in v5 for back-compat, but it is a + thinner shim. The recommended v5 path is the three-way split shown above. + +## The smallest viable patch + +The shortest possible diff to make a v4 codebase compile under v5: + +1. Replace `` with the `Providers` wrapper from the Next.js / Vite + minimal recipes. Move the environment plus walletConnect projectId props into + `dappConfig.dAppConfig`. +2. Bulk find-and-replace import paths (`/hooks` to `/out/react//...`, + `/services` to `/out/methods/...` or `/managers/...`, `/types` to + `/out/types/...`, delete `/wrappers` references). +3. Replace `` et al. with one button that calls + `UnlockPanelManager.openUnlockPanel()`. Initialize the manager once at startup. +4. Replace `useSignTransactions` and `sendTransactions` with + `provider.signTransactions(txs)` plus `TransactionManager.send(signed)` plus + `TransactionManager.track(sent, opts)`. +5. Run `tsc --noEmit`. Fix what breaks. Most v4-vs-v5 bugs are caught at + compile-time once you are on v5 deps. + +## Pitfalls + +:::warning[Pitfall 1: getAccount vs getAccountFromApi] +Same prefix, very different semantics. `getAccount()` is a store read (free, +instant); `getAccountFromApi()` makes a network round-trip every call. Search your +codebase for `getAccountFromApi`. If it is used inside a render loop or a +frequently-fired event handler, you have added an unintended network call. +::: + +:::warning[Pitfall 2: Redux DevTools is gone] +v4 used Redux; v5 uses Zustand. Your existing Redux DevTools setup will not surface +state changes. Zustand has its own devtools middleware, but sdk-dapp does not +enable it by default. +::: + +:::warning[Pitfall 3: sessionStorage layout changed] +v4 sessions are not re-readable post-upgrade. Document this in your release notes, +users will be silently logged out on first load after the upgrade. +::: + +:::warning[Pitfall 4: OnCloseUnlockPanelType requires Promise<void>] +The v5 docs example for `UnlockPanelManager.init` uses sync callbacks; that fails +strict-mode compile. Use `async () => {}`. +::: + +:::warning[Pitfall 5: 'use client' boundaries in Next.js] +v4 was loose about SSR; v5 is strict. Any component that calls a sdk-dapp hook (or +`getAccountProvider`, or `getAccount` from the methods directory) must be inside a +`"use client"` tree. +::: + +## See also + +- [Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) + is the target shape for Next.js codebases. +- [Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) + is the target shape for Vite codebases. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the new sign, send, track flow used by every transaction in v5. +- [Migration: DappProvider to initApp](/sdk-and-tools/sdk-js/cookbook/migration/dapp-provider-to-initapp) + is the full-app version of the first change above. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal.mdx b/docs/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal.mdx new file mode 100644 index 000000000..62f7d3376 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal.mdx @@ -0,0 +1,588 @@ +--- +title: Minimal sdk-dapp v5 app in Vite + React +description: The smallest working sdk-dapp v5 setup in a fresh Vite + React + TypeScript project. Compiles strict on the first try, with HTTPS dev built in. +difficulty: beginner +est_minutes: 10 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" + sdk-dapp: "^5.6.0" +tags: + - sdk-dapp + - vite + - react + - typescript + - wallet + - devnet +--- + +This recipe gives you the smallest working sdk-dapp v5 setup in a fresh Vite + +React + TypeScript app. Use this if you are scaffolding a new dApp on Vite: clone +the `recipes/vite-react-minimal` directory and rename it. + +This recipe is **not** the right starting point if you are on Next.js (use +[Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) +instead, the App Router has SSR concerns this recipe deliberately avoids) or if +you have an existing v4 codebase to upgrade (use +[Migrate v4 to v5](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration)). + +The Vite path is simpler than the Next.js path because there is no SSR. +`initApp()` could run at module scope; we keep it in a `useEffect` only so the +React Strict Mode double-mount does not trigger redundant work in dev. + +## Prerequisites + +- Node.js >= 20.13.1 (sdk-dapp's stated minimum). +- pnpm or npm. +- A devnet wallet, either the DeFi browser extension or xPortal (mobile via + WalletConnect). + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/vite-react-minimal +npm install +npm run dev +# open https://localhost:5173 (note the s, see Pitfall 2) +``` + +## package.json: pin the SDK and peer deps + +`bignumber.js` and `protobufjs` are peer dependencies of sdk-core and must be +installed explicitly. The `@vitejs/plugin-basic-ssl` plugin is the simplest way +to get HTTPS on the Vite dev server. + +```json +{ + "name": "cookbook-recipe-vite-react-minimal", + "version": "0.1.0", + "private": true, + "description": "Cookbook recipe — minimal sdk-dapp v5 in Vite + React + TypeScript. Compiles strict.", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit --strict", + "lint": "eslint . --max-warnings=0" + }, + "dependencies": { + "@multiversx/sdk-core": "^15.4.0", + "@multiversx/sdk-dapp": "^5.6.0", + "bignumber.js": "^9.1.2", + "protobufjs": "^7.4.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-basic-ssl": "^1.2.0", + "@vitejs/plugin-react": "^4.3.3", + "eslint": "^8.57.1", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.14", + "typescript": "^5.6.3", + "typescript-eslint": "^8.13.0", + "vite": "^5.4.10" + }, + "engines": { + "node": ">=20.13.1" + } +} +``` + +## vite.config.ts: HTTPS and CommonJS interop + +Two things matter: `basicSsl()` for HTTPS dev, and `optimizeDeps` for sdk-dapp's +CommonJS transitive packages. This exact config was arrived at by actually +running `npm run dev` and `npm run build` and fixing two real failures, not by +reasoning from the docs alone. See Pitfalls 5 and 6 before you trim this file +down for your own project. + +```ts +// vite.config.ts — sdk-dapp v5 + Vite + React. +// +// Two things matter for sdk-dapp to work cleanly under Vite: +// +// 1. HTTPS for the dev server. Most wallet providers (DeFi extension, +// Ledger, Passkey) refuse to talk to http://localhost. We use +// @vitejs/plugin-basic-ssl as a zero-config solution; if you want a +// fully trusted cert (no browser warning every session), see the +// "Local HTTPS for dApp dev" recipe for the mkcert path. +// +// 2. The CommonJS interop plumbing for sdk-dapp's transitive WalletConnect +// and Ledger deps. Unlike Next.js, Vite's default resolver is +// ESM-clean for sdk-dapp 5.x and we don't need the `externals` array +// that recipes/nextjs-minimal/next.config.js carries. If you hit +// "Unexpected token 'export'" or "Cannot use import statement outside +// a module" errors at runtime, narrow the offending package and add +// it to optimizeDeps.include — but see the comment on optimizeDeps +// below before adding '@multiversx/sdk-dapp' itself; that one breaks +// the dev server rather than fixing it. + +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import basicSsl from '@vitejs/plugin-basic-ssl'; + +export default defineConfig({ + plugins: [react(), basicSsl()], + server: { + // Do NOT set `https` here yourself. basicSsl() already injects the + // generated cert into server.https via its own Vite `config` hook — see + // node_modules/@vitejs/plugin-basic-ssl/README.md. It's also not a + // boolean option: Vite's type is `https.ServerOptions | undefined` + // (node_modules/vite/dist/node/index.d.ts), so a literal `https: true` + // fails `tsc --strict` ("no overload matches this call") the moment + // anything actually typechecks this file. See the "Local HTTPS for + // dApp dev" recipe's basic-ssl snippet for the same fix in context. + port: 5173, + }, + // sdk-dapp pulls in a small number of CJS deps that Vite needs to + // pre-bundle. The list is conservative — add to it only if you see + // module-format errors in the browser console. + // + // Do NOT list '@multiversx/sdk-dapp' (the bare package) here. Verified + // against the installed @multiversx/sdk-dapp v5: its package.json has + // no "main", "module", or "exports" field at all — by design, every import + // is a deep path like '@multiversx/sdk-dapp/out/react/account/useGetAccount' + // Telling Vite to eagerly pre-bundle the bare specifier makes it try to + // resolve a package entry point that doesn't exist, and the dev server + // fails to start outright: + // "Error: Failed to resolve entry for package '@multiversx/sdk-dapp'. + // The package may have incorrect main/module/exports specified in its + // package.json." (reproduced with `npm run dev` before this fix). + // '@multiversx/sdk-core' is a different case — it does ship a real + // `main: "out/index.js"` barrel, so including it here is safe and correct. + optimizeDeps: { + include: ['@multiversx/sdk-core', 'bignumber.js', 'protobufjs'], + // These three deep paths are unresolvable in the actually-installed tree + // (npm install on 2026-07-07 pulled @multiversx/sdk-dapp v5): three + // of sdk-dapp's Ledger transport strategies + // (hw-transport-webusb, hw-transport-webhid, hw-transport-web-ble) each + // bundle their OWN nested copy of @ledgerhq/devices@8.16.0, and that + // nested copy is missing the hid-framing.js / ble/sendAPDU.js / + // ble/receiveAPDU.js files its sibling packages import — genuinely + // absent from that package version on disk, not just an exports-map + // resolution quirk (the top-level, separately-hoisted + // @ledgerhq/devices@8.0.3 does have them). This is an upstream Ledger + // packaging bug, not a Vite or sdk-dapp config issue; the fix here only + // silences the *bundler* error the same way `vite build`'s own + // suggestion does, not the underlying missing files. + exclude: [ + '@ledgerhq/devices/hid-framing', + '@ledgerhq/devices/ble/sendAPDU', + '@ledgerhq/devices/ble/receiveAPDU', + ], + }, + build: { + rollupOptions: { + // Same root cause as optimizeDeps.exclude above, mirrored for the + // production build (`vite build` uses Rollup, not esbuild, and hits + // the identical unresolvable-import error there independently). + external: [ + '@ledgerhq/devices/hid-framing', + '@ledgerhq/devices/ble/sendAPDU', + '@ledgerhq/devices/ble/receiveAPDU', + ], + }, + }, +}); +``` + +## index.html: the Vite entry + +Single root div plus an ESM script tag. Vite injects the bundle. + +```html + + + + + + Cookbook recipe — Minimal sdk-dapp v5 in Vite + + +
+ + + +``` + +## src/main.tsx: bootstrap + +Classic React 18 `createRoot` plus `StrictMode`. `` wraps the app, the +same pattern as Next.js, just without the layout/page split. + +```tsx title="src/main.tsx" +// src/main.tsx — Vite entry point. +// +// The classic React 18 createRoot + StrictMode setup. wraps +// the entire app — same pattern as Next.js, just without the layout/page +// split that the App Router imposes. +// +// Why StrictMode? It double-mounts components in dev to flush out unsafe +// side effects. The `initStarted` module-scope flag in providers.tsx +// handles the double-mount cleanly; if you remove that flag, you'll see +// initApp() invoked twice in dev. + +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; +import { Providers } from './providers'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error('No #root element found in index.html'); +} + +createRoot(rootElement).render( + + + + + , +); +``` + +## src/providers.tsx: initApp wrapper + +The body is identical to the Next.js recipe's `app/providers.tsx`, minus the +`"use client"` directive. That is the whole story: same code, no SSR concern. + +```tsx title="src/providers.tsx" +// src/providers.tsx — sdk-dapp v5 init wrapper for Vite. +// +// Same conceptual job as recipes/nextjs-minimal/app/providers.tsx, simpler +// implementation: +// +// - No "use client" needed. Vite has no SSR; the entire app is a CSR +// bundle that hydrates in the browser. sessionStorage is always +// defined by the time React mounts. +// - We can call initApp() at module scope if we want, but we keep it in +// a useEffect so the React-Strict-Mode double-mount in dev doesn't +// throw a warning. The module-scope `initStarted` flag matches the +// Next.js recipe exactly. +// +// IMPORTANT: the OnCloseUnlockPanelType requires Promise. The docs +// example uses sync `() => navigate(...)` +// which fails strict-mode compile. We use async callbacks. + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { dappConfig } from './lib/multiversx'; + +// Module-scope flag prevents double-init under React Strict Mode (which +// deliberately mounts useEffect twice in dev to surface unsafe side +// effects). initApp is idempotent in v5 but skipping the second call +// avoids redundant network round-trips. +let initStarted = false; + +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; + + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + + // UnlockPanelManager.init must be called AFTER initApp resolves. + // Both callbacks return Promise — see Pitfall 1. + UnlockPanelManager.init({ + loginHandler: async (): Promise => { + // Replace with your post-login navigation (e.g. a router push). + // For this minimal recipe we just leave the user on the home + // page — useGetIsLoggedIn() will flip and the UI re-renders. + // eslint-disable-next-line no-console + console.log('Logged in.'); + }, + onClose: async (): Promise => { + // eslint-disable-next-line no-console + console.log('Unlock panel closed.'); + }, + }); + + setReady(true); + }); + + return () => { + cancelled = true; + }; + }, []); + + if (!ready) { + return ( +
+ Initializing wallet SDK… +
+ ); + } + + return <>{children}; +} +``` + +## src/App.tsx: login and account display + +`useGetIsLoggedIn()`, `useGetAccount()`, and `useGetNetworkConfig()` are +sdk-dapp's React hooks. They read from the Zustand store that `initApp()` populates. + +```tsx title="src/App.tsx" +// src/App.tsx — landing component. +// +// Two states: +// 1. Logged out → "Connect wallet" button (opens UnlockPanelManager). +// 2. Logged in → bech32 address + EGLD balance. +// +// All blockchain state comes from sdk-dapp React hooks. These read from the Zustand store that +// initApp populates, so they MUST run inside — they return +// empty defaults until init has resolved. + +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import { formatAmount } from '@multiversx/sdk-dapp/out/lib/sdkDappUtils'; + +export function App(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const account = useGetAccount(); + const { network } = useGetNetworkConfig(); + + const handleConnect = (): void => { + // openUnlockPanel() returns Promise (see + // node_modules/@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager.d.ts). + // This handler is a sync onClick, so we explicitly discard the promise — + // matching the eslint no-floating-promises gate. + void UnlockPanelManager.getInstance().openUnlockPanel(); + }; + + const handleLogout = async (): Promise => { + const provider = getAccountProvider(); + await provider.logout(); + }; + + return ( +
+

Minimal sdk-dapp v5 in Vite + React

+

+ Network: {network.chainId} · API:{' '} + {network.apiAddress} +

+ + {!isLoggedIn ? ( + + ) : ( +
+

Connected

+

+ Address: {account.address} +

+

+ Balance:{' '} + + {formatAmount({ + input: account.balance, + decimals: 18, + digits: 4, + showLastNonZeroDecimal: true, + })}{' '} + {network.egldLabel} + +

+ +
+ )} +
+ ); +} +``` + +## src/lib/multiversx.ts: environment config + +Environment variables under Vite come from `import.meta.env`, not `process.env`. +The `VITE_` prefix is required to expose a variable to the client bundle (see +Pitfall 4). + +```ts title="src/lib/multiversx.ts" +// src/lib/multiversx.ts — environment configuration for sdk-dapp's initApp. +// +// In Vite, environment variables come from import.meta.env (NOT +// process.env). The VITE_ prefix exposes them to the client bundle; vars +// without the prefix are server-only and never leak. +// +// The full dAppConfig shape accepts more keys than this. +// We only set the keys we actually use; sdk-dapp fills the rest with +// defaults. + +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +// Read from import.meta.env (Vite's runtime environment object). +// Fallback is the demo project ID — works in dev but rate-limited; +// register your own at https://cloud.walletconnect.com. +const WALLET_CONNECT_PROJECT_ID = + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; + +export const dappConfig = { + storage: { + // Vite always runs in the browser at dev time and in the static-export + // bundle at runtime — sessionStorage is always defined. We don't need + // the SSR-safety tap-dance the Next.js recipe does. + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + nativeAuth: true, + // See node_modules/@multiversx/sdk-dapp/out/methods/initApp/initApp.types.d.ts — + // `theme` takes one of the enum's string values; the short `'dark'` fails, so use `ThemesEnum.dark`. + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + }, +}; + +export { EnvironmentsEnum }; +``` + +## Run it + +```bash +npm run dev +``` + +Then open `https://localhost:5173`. You should see a "Connect wallet" button plus +the active chain ID and API URL; after clicking, the sdk-dapp unlock panel; after +connecting, the bech32 address and EGLD balance. + +## How it works + +This recipe is the Vite parallel to the +[Next.js minimal recipe](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal). +Three observations on the differences: + +**No `"use client"` needed.** Vite has no SSR; the entire app is CSR. +`sessionStorage` is always defined by the time React mounts, so we do not have to +defer `initApp()` to `useEffect` for SSR-safety reasons. We *do* still keep it in +`useEffect`, but only so the module-scope `initStarted` guard can protect against +React Strict Mode's double-mount in dev. + +**HTTPS via plugin, not flag.** Next.js 14 has `next dev --experimental-https`. +Vite uses a plugin (`@vitejs/plugin-basic-ssl`). Both produce the same outcome, a +self-signed cert with the browser warning the first time. For a fully trusted +cert with no warning, see +[Local HTTPS for dApp dev](/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup). + +**Same hooks, same store.** All of sdk-dapp's React hooks work identically across +Vite and Next.js. The hook layer is framework-agnostic; only the bootstrap differs. + +## Pitfalls + +:::warning[Pitfall 1: UnlockPanelManager.init's onClose must be async] +The docs example uses sync callbacks. That fails under TypeScript strict mode +with `error TS2322: Type '() => void' is not assignable to type 'OnCloseUnlockPanelType'`. +The `OnCloseUnlockPanelType` requires `Promise`. Use `async () => { ... }` +or `() => Promise.resolve(...)`. The sample code in `src/providers.tsx` does this +correctly. +::: + +:::warning[Pitfall 2: wallet providers require HTTPS, even in dev] +`@vitejs/plugin-basic-ssl` flips the dev server to HTTPS automatically; you should +see `https://localhost:5173` in the dev-server output. The DeFi extension and +Ledger refuse to connect over HTTP. WalletConnect (xPortal) works over HTTP +because the auth happens off-origin. +::: + +:::warning[Pitfall 3: HMR can re-run initApp] +Vite's hot-module-replacement re-runs modules without a full page reload. Without +the `initStarted` module-scope flag in `src/providers.tsx`, every save triggers +another `initApp()` invocation. The flag in this recipe matches the Next.js one, +keep it. +::: + +:::warning[Pitfall 4: environment variables are import.meta.env, NOT process.env] +Vite injects env vars via `import.meta.env`. The `VITE_` prefix is required for +client-bundled values; `process.env` does not exist in the browser bundle. The +recipe reads `import.meta.env.VITE_WALLETCONNECT_PROJECT_ID`. If you copy the +Next.js recipe's `process.env.NEXT_PUBLIC_*` reads verbatim, you get `undefined` +everywhere. +::: + +:::danger[Pitfall 5: never add '@multiversx/sdk-dapp' itself to optimizeDeps.include] +This breaks the dev server outright. Confirmed by actually running `npm run dev` +against the installed `@multiversx/sdk-dapp v5`: its `package.json` declares +no `main`, `module`, or `exports` field at all. Every public import is a deep path +like `@multiversx/sdk-dapp/out/react/account/useGetAccount`. Asking Vite to +eagerly pre-bundle the bare package name makes it try to resolve an entry point +that does not exist (`Failed to resolve entry for package "@multiversx/sdk-dapp"`). +`@multiversx/sdk-core` is safe to include, it ships a real `main: "out/index.js"` +barrel. +::: + +:::warning[Pitfall 6: a nested @ledgerhq/devices copy is missing files (upstream bug)] +Also found by actually running `npm run dev` / `npm run build`: three of +sdk-dapp's Ledger transport strategies each bundle their own nested +`@ledgerhq/devices@8.16.0`, and that copy is missing `hid-framing.js` and two +`ble/*` files on disk (the separately hoisted top-level `@ledgerhq/devices@8.0.3` +has them). Left alone, this breaks `npm run dev` and hard-fails `npm run build`. +`vite.config.ts` works around it by externalizing the three specific deep paths in +both `optimizeDeps.exclude` and `build.rollupOptions.external`. This is an upstream +Ledger packaging bug, not a mistake in this config. The DeFi extension, +xPortal/WalletConnect, and web-wallet login paths are unaffected. +::: + +## See also + +- [Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) + is the App Router parallel; choose this if you need SSR. +- [Local HTTPS for dApp dev](/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup) + is the mkcert plus Vite plugin walkthrough for fully trusted certs. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the next thing you will do once login works. +- [Migrate v4 to v5](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration) + if you have an existing v4 Vite codebase. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/tokens/fetch-account-token-balances.mdx b/docs/sdk-and-tools/sdk-js/cookbook/tokens/fetch-account-token-balances.mdx new file mode 100644 index 000000000..ab53ef75c --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/tokens/fetch-account-token-balances.mdx @@ -0,0 +1,244 @@ +--- +title: Fetch an account's token balances +description: Read the tokens an account holds, all fungible ESDTs, all NFTs and SFTs, and one specific token balance, via a network provider. Verified live. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - network-provider + - esdt + - nft + - typescript +--- + +Read the tokens an account holds, EGLD's ESDT siblings. Three reads, all on +`INetworkProvider`: + +- `getFungibleTokensOfAccount(address, pagination?)` returns every fungible ESDT. +- `getNonFungibleTokensOfAccount(address, pagination?)` returns every NFT / SFT / + meta-ESDT. +- `getTokenOfAccount(address, token)` returns one specific token's balance. + +All read-only. This recipe reads a liquidity-pool contract (which reliably holds +several fungibles) and a known NFT-holding address, so every call returns real, +non-empty data. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/fetch-account-token-balances +npm install +npm run build +npm start +``` + +## Reading the balances + +```ts title="src/tokenBalances.ts" +// src/tokenBalances.ts — the subject of this recipe: reading the tokens an +// account holds. Three reads, all on INetworkProvider: +// +// - getFungibleTokensOfAccount(address, pagination?) → every fungible ESDT +// - getNonFungibleTokensOfAccount(address, pagination?) → every NFT / SFT / meta-ESDT +// - getTokenOfAccount(address, token) → one token's balance +// +// getTokenOfAccount has a sharp edge for NFTs — see fetchNftBalance below. + +import { Token, TokenComputer } from '@multiversx/sdk-core'; +import type { + INetworkProvider, + TokenAmountOnNetwork, + Address, + IPagination, +} from '@multiversx/sdk-core'; + +/** All fungible ESDTs the account holds. Pagination is honored by the Api + * provider; the Proxy provider ignores it and returns everything. */ +export async function fetchFungibleTokens( + provider: INetworkProvider, + address: Address, + pagination?: IPagination, +): Promise { + return pagination === undefined + ? provider.getFungibleTokensOfAccount(address) + : provider.getFungibleTokensOfAccount(address, pagination); +} + +/** All non-fungible tokens (NFT / SFT / meta-ESDT) the account holds. */ +export async function fetchNonFungibleTokens( + provider: INetworkProvider, + address: Address, + pagination?: IPagination, +): Promise { + return pagination === undefined + ? provider.getNonFungibleTokensOfAccount(address) + : provider.getNonFungibleTokensOfAccount(address, pagination); +} + +/** One fungible token's balance for the account. */ +export async function fetchFungibleBalance( + provider: INetworkProvider, + address: Address, + identifier: string, +): Promise { + return provider.getTokenOfAccount(address, new Token({ identifier })); +} + +/** + * One NFT's balance. getTokenOfAccount for an NFT wants the BASE collection + * identifier plus the nonce — it appends the nonce hex itself. Passing the + * already-extended identifier (e.g. "XPASS-423274-04") double-appends the nonce + * and the API rejects it as an invalid NFT identifier. Since + * getNonFungibleTokensOfAccount returns the EXTENDED identifier, strip it back + * to the base with TokenComputer first. + */ +export async function fetchNftBalance( + provider: INetworkProvider, + address: Address, + extendedIdentifier: string, + nonce: bigint, +): Promise { + const baseIdentifier = new TokenComputer().extractIdentifierFromExtendedIdentifier(extendedIdentifier); + return provider.getTokenOfAccount(address, new Token({ identifier: baseIdentifier, nonce })); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — read the fungible tokens of a liquidity-pool contract (which +// reliably holds several) and the NFTs of a known NFT-holding address. No +// wallet, no gas — these are public balances. +// +// Usage: +// npm run build && npm start + +import { ApiNetworkProvider, Address } from '@multiversx/sdk-core'; +import { + fetchFungibleTokens, + fetchNonFungibleTokens, + fetchFungibleBalance, + fetchNftBalance, +} from './tokenBalances'; + +const MAINNET_API = 'https://api.multiversx.com'; +// An xExchange WEGLD/USDC pair contract — reliably holds fungible ESDTs. +const FUNGIBLE_HOLDER = 'erd1qqqqqqqqqqqqqpgqeel2kumf0r8ffyhth7pqdujjat9nx0862jpsg2pqaq'; +// A known address that holds an NFT. +const NFT_HOLDER = 'erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th'; + +async function main(): Promise { + const provider = new ApiNetworkProvider(MAINNET_API, { clientName: 'mvx-cookbook' }); + + // Fungible tokens (paginated). + const fungibleHolder = Address.newFromBech32(FUNGIBLE_HOLDER); + const fungibles = await fetchFungibleTokens(provider, fungibleHolder, { from: 0, size: 5 }); + console.log(`Fungible tokens of ${FUNGIBLE_HOLDER} (up to 5):`); + for (const t of fungibles) { + console.log(` ${t.token.identifier}: ${t.amount}`); + } + + // One specific fungible balance. + const wegld = await fetchFungibleBalance(provider, fungibleHolder, 'WEGLD-bd4d79'); + console.log(`getTokenOfAccount('WEGLD-bd4d79'): ${wegld.amount}`); + + // Non-fungible tokens. + const nftHolder = Address.newFromBech32(NFT_HOLDER); + const nfts = await fetchNonFungibleTokens(provider, nftHolder, { from: 0, size: 5 }); + console.log(`\nNon-fungible tokens of ${NFT_HOLDER} (up to 5):`); + for (const t of nfts) { + console.log(` ${t.token.identifier} (nonce ${t.token.nonce}): amount ${t.amount}`); + } + + // One specific NFT balance — showing the base-identifier + nonce requirement. + const [firstNft] = nfts; + if (firstNft !== undefined) { + const back = await fetchNftBalance(provider, nftHolder, firstNft.token.identifier, firstNft.token.nonce); + console.log(`getTokenOfAccount(base id + nonce) for ${firstNft.token.identifier}: amount ${back.amount}`); + } else { + console.log('(no NFTs on this address right now — pass another address to see the NFT path)'); + } +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start +``` + +Expected output (balances are live and will differ): + +```text +Fungible tokens of erd1qqqqqqqqqqqqqpgqeel2kumf0r8ffyhth7pqdujjat9nx0862jpsg2pqaq (up to 5): + USDC-c76f1f: 733827376705 + EGLDUSDC-594e5e: 1000 + WEGLD-bd4d79: 223980465825062238009362 +getTokenOfAccount('WEGLD-bd4d79'): 223980465825062238009362 + +Non-fungible tokens of erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th (up to 5): + XPASS-423274-04 (nonce 4): amount 1 +getTokenOfAccount(base id + nonce) for XPASS-423274-04: amount 1 +``` + +## How it works + +**Fungible and non-fungible are separate calls.** A wallet's USDC and its NFTs +come from two different endpoints. Both return `TokenAmountOnNetwork[]`, each +element a `token` (identifier + nonce) plus an `amount` (bigint, in the token's +own smallest denomination). The single-token `getTokenOfAccount('WEGLD-bd4d79')` +matches the amount from the list, one balance, two ways to reach it. + +**Amounts are raw bigints.** `733827376705` for USDC is `733,827.376705` after +USDC's 6 decimals; `223980465825062238009362` for WEGLD is ~223,980 after 18 +decimals. Divide by `10 ** decimals` (from +[Fetch token metadata](/sdk-and-tools/sdk-js/cookbook/tokens/fetch-token-metadata)) +to display, never hard-code 18. + +## Pitfalls + +:::danger[Pitfall 1: getTokenOfAccount for an NFT wants the BASE identifier + nonce] +`getTokenOfAccount` appends the nonce hex to the identifier itself. +`getNonFungibleTokensOfAccount` returns the already-EXTENDED identifier +(`XPASS-423274-04`), so passing that back in double-appends the nonce (`...-04-04`) +and the API rejects it with "Invalid NFT identifier". Strip it to the base first +with `TokenComputer.extractIdentifierFromExtendedIdentifier()`, then pass +`{ identifier: base, nonce }`, reproduced and fixed against the live API. +::: + +:::warning[Pitfall 2: pagination is Api-only] +`{ from, size }` is honored by `ApiNetworkProvider` and silently ignored by +`ProxyNetworkProvider`, whose `getFungibleTokensOfAccount(address)` signature has +no pagination parameter at all. On the proxy you get everything back regardless, +page in application code if the account holds a lot. +::: + +:::note[Pitfall 3: an empty list is normal, not an error] +An account with no ESDTs returns `[]`, and `getTokenOfAccount` for a token the +account does not hold throws "Token for given account not found" rather than +returning zero. Treat "not held" as a caught error or an empty array, not an +exception you forgot to handle. +::: + +## See also + +- [Fetch token metadata](/sdk-and-tools/sdk-js/cookbook/tokens/fetch-token-metadata) + supplies the `decimals` you need to turn these raw amounts into human numbers. +- [Fetch an account's on-chain state](/sdk-and-tools/sdk-js/cookbook/accounts/fetch-account-state) + is the EGLD balance and nonce side of the same account. +- [Send an ESDT](/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt) + moves one of the tokens you just read. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/tokens/fetch-token-metadata.mdx b/docs/sdk-and-tools/sdk-js/cookbook/tokens/fetch-token-metadata.mdx new file mode 100644 index 000000000..77ea737e9 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/tokens/fetch-token-metadata.mdx @@ -0,0 +1,202 @@ +--- +title: Fetch token metadata +description: Read a token's definition, name, ticker, decimals, owner and property flags, for a fungible token and for an NFT or SFT collection. Verified live. +difficulty: intermediate +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - network-provider + - esdt + - nft + - typescript +--- + +Read a token's **definition**, its metadata and property flags, as opposed to a +balance. Two reads, both on `INetworkProvider`: + +- `getDefinitionOfFungibleToken(identifier)` returns a fungible token's `name`, + `ticker`, `decimals`, `owner`, `supply`, and the `can*` property flags. +- `getDefinitionOfTokenCollection(collection)` returns an NFT / SFT / meta-ESDT + collection's `type`, `name`, `decimals`, `owner`, and property flags. + +The `decimals` this returns is exactly what you need to turn a raw balance from +[Fetch an account's token balances](/sdk-and-tools/sdk-js/cookbook/tokens/fetch-account-token-balances) +into a human number. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/fetch-token-metadata +npm install +npm run build +npm start +``` + +## Reading the definitions + +```ts title="src/tokenMetadata.ts" +// src/tokenMetadata.ts — the subject of this recipe: reading a token's +// DEFINITION (its metadata and property flags), as opposed to a balance. +// +// - getDefinitionOfFungibleToken(identifier) → a fungible token's definition: +// name, ticker, decimals, owner, supply, and the can* property flags. +// - getDefinitionOfTokenCollection(collection) → an NFT / SFT / meta-ESDT +// collection's definition: type, name, decimals, owner, property flags. +// +// Both are on INetworkProvider. Note: pass the COLLECTION identifier +// (e.g. "MEDAL-ae074f") to the collection call, not a single NFT's extended id. + +import type { + INetworkProvider, + DefinitionOfFungibleTokenOnNetwork, + DefinitionOfTokenCollectionOnNetwork, +} from '@multiversx/sdk-core'; + +/** A fungible token's definition (metadata + property flags). */ +export async function fetchFungibleDefinition( + provider: INetworkProvider, + identifier: string, +): Promise { + return provider.getDefinitionOfFungibleToken(identifier); +} + +/** An NFT / SFT / meta-ESDT collection's definition. */ +export async function fetchCollectionDefinition( + provider: INetworkProvider, + collection: string, +): Promise { + return provider.getDefinitionOfTokenCollection(collection); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — read the definition of a well-known fungible token (WEGLD) and +// a well-known NFT collection (MEDAL). No wallet, no gas. +// +// Usage: +// npm run build && npm start [fungibleId] [collectionId] + +import { ApiNetworkProvider } from '@multiversx/sdk-core'; +import { fetchFungibleDefinition, fetchCollectionDefinition } from './tokenMetadata'; + +const MAINNET_API = 'https://api.multiversx.com'; +const DEFAULT_FUNGIBLE = 'WEGLD-bd4d79'; +const DEFAULT_COLLECTION = 'MEDAL-ae074f'; + +async function main(): Promise { + const fungibleId = process.argv[2] ?? DEFAULT_FUNGIBLE; + const collectionId = process.argv[3] ?? DEFAULT_COLLECTION; + const provider = new ApiNetworkProvider(MAINNET_API, { clientName: 'mvx-cookbook' }); + + const fungible = await fetchFungibleDefinition(provider, fungibleId); + console.log(`Fungible token ${fungible.identifier}`); + console.log(` name: ${fungible.name}`); + console.log(` ticker: ${fungible.ticker}`); + console.log(` decimals: ${fungible.decimals}`); + console.log(` owner: ${fungible.owner.toBech32()}`); + console.log(` isPaused: ${fungible.isPaused}`); + console.log(` canFreeze: ${fungible.canFreeze}, canWipe: ${fungible.canWipe}, canUpgrade: ${fungible.canUpgrade}`); + + const collection = await fetchCollectionDefinition(provider, collectionId); + console.log(`\nCollection ${collection.collection}`); + console.log(` type: ${collection.type}`); + console.log(` name: ${collection.name}`); + console.log(` decimals: ${collection.decimals}`); + console.log(` owner: ${collection.owner.toBech32()}`); + console.log(` canFreeze: ${collection.canFreeze}, canWipe: ${collection.canWipe}, canTransferNftCreateRole: ${collection.canTransferNftCreateRole}`); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start # WEGLD-bd4d79 + MEDAL-ae074f +npm start # any token / collection +``` + +Expected output: + +```text +Fungible token WEGLD-bd4d79 + name: WrappedEGLD + ticker: WEGLD + decimals: 18 + owner: erd1ss6u80ruas2phpmr82r42xnkd6rxy40g9jl69frppl4qez9w2jpsqj8x97 + isPaused: false + canFreeze: true, canWipe: true, canUpgrade: true + +Collection MEDAL-ae074f + type: NonFungibleESDT + name: GLUMedals + decimals: 0 + owner: erd126y66ear20cdskrdky0kpzr9agjul7pcut7ktlr6p0eu8syxhvrq0gsqdj + canFreeze: false, canWipe: false, canTransferNftCreateRole: false +``` + +## How it works + +**Definition, not balance.** These endpoints describe the token itself, who owns +it, how many decimals, whether it can be frozen or paused, independent of any +holder. Pair `decimals` with the raw amounts from +[Fetch an account's token balances](/sdk-and-tools/sdk-js/cookbook/tokens/fetch-account-token-balances) +to display real numbers. + +**Fungible vs collection are different calls.** A fungible identifier +(`WEGLD-bd4d79`) goes to `getDefinitionOfFungibleToken`; a collection identifier +(`MEDAL-ae074f`) goes to `getDefinitionOfTokenCollection`. The collection's `type` +field (`NonFungibleESDT`, `SemiFungibleESDT`, `MetaESDT`) tells you which kind it +is. + +**The `can*` flags mirror what you set at issuance.** `canFreeze`, `canWipe`, +`canPause`, `canUpgrade`, `canAddSpecialRoles` are the same properties +[Issue a fungible token](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token) +and [Issue an NFT collection](/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection) +configure, this is how you read them back. + +## Pitfalls + +:::warning[Pitfall 1: the collection call takes the collection id, not an NFT id] +`getDefinitionOfTokenCollection` expects `MEDAL-ae074f`, the collection +identifier, not `MEDAL-ae074f-01`, a single NFT's extended id. If you only have an +NFT's extended identifier, strip the nonce suffix with +`TokenComputer.extractIdentifierFromExtendedIdentifier()` first. +::: + +:::note[Pitfall 2: the property flag is canTransferNftCreateRole (lowercase Nft)] +On `DefinitionOfTokenCollectionOnNetwork` the flag is spelled +`canTransferNftCreateRole`, lowercase "Nft". The issuance factory uses +`canTransferNFTCreateRole` (all-caps "NFT") for the same property. Read-side and +write-side disagree on casing; match whichever class you are actually touching +(confirmed against the installed types). +::: + +:::note[Pitfall 3: supply is a BigNumber, decimals is a number] +On the fungible definition, `supply` is a bignumber.js `BigNumber` (call +`.toString()`), while `decimals` is a plain `number`. Do not assume both are the +same numeric type. +::: + +## See also + +- [Fetch an account's token balances](/sdk-and-tools/sdk-js/cookbook/tokens/fetch-account-token-balances) + holds the raw amounts whose `decimals` this recipe supplies. +- [Issue a fungible token](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token) + is the write side; this recipe reads back what it configures. +- [Issue an NFT collection](/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection) + is the collection whose definition this recipe reads. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token.mdx b/docs/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token.mdx new file mode 100644 index 000000000..882ef6215 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token.mdx @@ -0,0 +1,216 @@ +--- +title: Issue a fungible ESDT +description: Issue a fungible ESDT token with sdk-core's TokenManagementController against DevnetEntrypoint, payload hand-verified against real devnet. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - esdt + - transaction + - gas + - devnet + - typescript +--- + +Issue a new fungible ESDT token using sdk-core's `TokenManagementController` +directly against `DevnetEntrypoint`. + +If instead you are building a browser dApp where the end user's own wallet should +sign the issuance, wire the same `TokenManagementTransactionsFactory` methods +through a connected-wallet flow instead of a PEM-holding script. This recipe is +for when **your own code holds the private key**. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites for how to generate a throwaway one. **No devnet EGLD required** + to verify the payload, see "How it works" below. (Issuing for real costs a fixed + 0.05 EGLD plus gas.) + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/issue-fungible-token +npm install +npm run build +npm start -- ./wallet.pem CookbookToken COOK 1000000000000 6 +``` + +## Issuing + +```ts title="src/issueToken.ts" +// src/issueToken.ts — the actual subject of this recipe: issuing a +// fungible ESDT token via sdk-core's TokenManagementController. +// +// Three confirmed, real discrepancies between a commonly-copied issuance +// snippet and the actually-installed SDK, found by reading +// node_modules/@multiversx/sdk-core/out/tokenManagement/resources.d.ts +// directly (all worth being explicit about, the same way other recipes +// flag the ESDT-casing and Account.signTransaction() gaps): +// +// 1. A commonly-copied snippet includes +// `canTransferNftCreateRole: false`. The real type, +// `IssueFungibleInput = IssueInput & { initialSupply: bigint; numDecimals: bigint }`, +// has NO such field anywhere in `IssueInput` — it doesn't exist for +// fungible tokens at all (this makes sense: an NFT-create role is +// meaningless on a token with no NFT nonce). Including it is a +// `tsc --strict` "object literal may only specify known properties" +// error, not just a style nit. This recipe's options object omits it +// entirely, matching the real type and the real +// mx-sdk-js-core cookbook/tokens.ts example. +// 2. (Relevant to the sibling "Issue an NFT collection" recipe, not this +// one — the field DOES exist for NFT/SFT issuance, spelled +// `canTransferNFTCreateRole`, all-caps "NFT".) +// 3. (Also relevant to the sibling NFT recipe's mint step, not this +// one — `royalties` is a plain `number`, not `bigint`.) +// +// Issuing a token costs a fixed 0.05 EGLD PLUS gas — this recipe's own +// devnet verification (an intentionally unfunded wallet) confirms the +// network rejects for insufficient funds at that combined cost, proving +// the issuance payload itself is well-formed without needing to actually +// spend real EGLD. + +import type { Account, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export interface IssueTokenInput { + tokenName: string; + tokenTicker: string; + /** Total supply in the token's own smallest denomination (see numDecimals). */ + initialSupply: bigint; + /** This token's own decimal count — chosen by the issuer, not fixed like EGLD's 18. */ + numDecimals: bigint; +} + +export interface IssueTokenOutput { + txHash: string; +} + +/** + * Issues a new fungible ESDT token. `sender.nonce` must already reflect + * the network's current nonce — see src/account.ts's loadDevnetAccount. + */ +export async function issueFungibleToken( + entrypoint: DevnetEntrypoint, + sender: Account, + input: IssueTokenInput, +): Promise { + const controller = entrypoint.createTokenManagementController(); + + const transaction = await controller.createTransactionForIssuingFungible( + sender, + sender.getNonceThenIncrement(), + { + tokenName: input.tokenName, + tokenTicker: input.tokenTicker, + initialSupply: input.initialSupply, + numDecimals: input.numDecimals, + canFreeze: false, + canWipe: true, + canPause: true, + canChangeOwner: true, + canUpgrade: true, + canAddSpecialRoles: false, + // No canTransferNftCreateRole field here — see the file header: + // it does not exist on IssueFungibleInput at all. + }, + ); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash }; +} +``` + +## Run it + +```bash +npm start -- +``` + +Expected output: + +```text +Sender: erd1... (nonce 0) +Token name: CookbookToken +Token ticker: COOK +Initial supply: 1000000000000 (6 decimals) +Issue cost: 0.05 EGLD + gas + +========== +IMPORTANT! +========== +You are about to issue (register) a new token. This will set the role "ESDTRoleBurnForAll" (globally). +Once the token is registered, you can unset this role by calling "unsetBurnRoleGlobally" (in a separate transaction). +``` + +(An unfunded wallet gets a clean "insufficient funds" rejection after the notice, +instead of a hash, see "How it works.") + +## How it works + +**Three confirmed, real discrepancies between a commonly-copied "Issue Fungible +Token" snippet and the actually-installed SDK**, found by reading +`node_modules/@multiversx/sdk-core/out/tokenManagement/resources.d.ts` directly: + +1. The naive snippet includes `canTransferNftCreateRole: false`. The real type, + `IssueFungibleInput = IssueInput & { initialSupply: bigint; numDecimals: bigint }`, + has **no such field anywhere** in `IssueInput` for fungible tokens, an + NFT-create role is meaningless on a token with no NFT nonce. Including it is a + `tsc --strict` excess-property error. Confirmed by hand-decoding the real, + logged issuance payload, the wire format has no such field either. +2. The field DOES exist for NFT/SFT issuance, spelled `canTransferNFTCreateRole` + (all-caps "NFT"), see + [Issue an NFT collection + mint an NFT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection). +3. `royalties` (used only in the NFT mint step) is a plain `number`, not a + `bigint`. + +**Issuance costs a fixed 0.05 EGLD, sent to the ESDT system contract, plus gas for +the data payload.** Verified by hand-decoding a real logged issuance request: +`value: "50000000000000000"` (exactly 0.05 EGLD), +`receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u` (the +ESDT system contract address), and `gasLimit: 60426500`, matching +`50,000 + 1,500 x 251 (data bytes) + 60,000,000 (gasLimitIssue)` to the unit. The +decoded `data` field (`issue@436f6f6b626f6f6b546f6b656e@434f4f4b@e8d4a51000@06@...`) +confirms every flag encoded correctly, with no `canTransferNftCreateRole` anywhere +in the wire payload. + +**The SDK itself prints an unconditional warning on every issuance call**, traced +to `notifyAboutUnsettingBurnRoleGlobally()`, called at the start of all 5 issuance +methods (fungible, semi-fungible, non-fungible, meta-ESDT, register-and-set-roles), +not specific to this recipe's flag combination. +`createTransactionForUnsettingBurnRoleGlobally(...)` is the escape hatch it +mentions. + +## Pitfalls + +:::danger[Pitfall 1: canTransferNftCreateRole does not exist for fungible tokens] +Copying a naive fungible-issuance snippet that includes it fails `tsc --strict`, +see "How it works" above. +::: + +:::note[Pitfall 2: the burn-role notice is informational only] +It prints client-side and does not indicate anything went wrong, it appears even +when this recipe's unfunded wallet then gets rejected for insufficient funds. +Worth reading once, since unsetting the role later needs its own transaction. +::: + +:::warning[Pitfall 3: numDecimals is chosen once, at issuance, and cannot change later] +Unlike EGLD's fixed 18, an ESDT's decimal count is whatever you pass here, see +[Send an ESDT](/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt)'s Pitfall 1 +for why getting this wrong later is a real footgun. +::: + +## See also + +- [Issue an NFT collection + mint an NFT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection) + is the non-fungible sibling, including the `canTransferNFTCreateRole` casing this + recipe omits. +- [Send an ESDT](/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt) + spends a token once it is issued. +- [Multi-token transfer in one tx](/sdk-and-tools/sdk-js/cookbook/transactions/multi-token-transfer) + sends several tokens together. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection.mdx b/docs/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection.mdx new file mode 100644 index 000000000..7eb2259c9 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection.mdx @@ -0,0 +1,260 @@ +--- +title: Issue an NFT collection + mint an NFT +description: Issue an NFT collection and mint an NFT into it with sdk-core's TokenManagementController, every field hand-verified against real devnet. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - nft + - esdt + - transaction + - gas + - devnet + - typescript +--- + +Two steps, both via sdk-core's `TokenManagementController`: issue a new NFT +collection, then create (mint) one NFT into it. Per `mx-sdk-js-core`'s own +`cookbook/tokens.ts`, no separate "set special role" transaction is needed in +between, issuing your own collection automatically grants you the NFT-create role +on it. + +If instead you are building a browser dApp where the end user's own wallet should +sign, wire the same `TokenManagementTransactionsFactory` methods through a +connected-wallet flow. This recipe is for when **your own code holds the private +key**. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites for how to generate a throwaway one. **No devnet EGLD required** to + verify both payloads, see "How it works" below. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/issue-nft-collection +npm install +npm run build +npm start -- ./wallet.pem CookbookNFT CNFT +``` + +## Issuing and minting + +```ts title="src/issueNftCollection.ts" +// src/issueNftCollection.ts — the actual subject of this recipe: issuing +// an NFT collection, then creating (minting) one NFT into it. +// +// Two confirmed, real corrections to commonly-copied snippets, found by +// reading node_modules/@multiversx/sdk-core/out/tokenManagement/resources.d.ts +// and .../tokenManagementController.d.ts / tokenManagementTransactionsFactory.d.ts +// directly: +// +// 1. The field is `canTransferNFTCreateRole` — all-caps "NFT" — not +// `canTransferNftCreateRole` (a common miscasing). Confirmed: +// `IssueNonFungibleInput = IssueInput & { canTransferNFTCreateRole: boolean }`. +// (Contrast with fungible issuance, which has no such field at all — +// see the sibling "Issue a fungible ESDT" recipe.) +// 2. `royalties` is a plain `number`, not a `bigint` — a commonly-copied +// `royalties: 500n` (with the `n` suffix) does not match +// `MintInput.royalties: number`. +// +// A THIRD confirmed casing trap, the same shape as the already-documented +// `createTransactionForEsdtTokenTransfer` (Controller, lowercase "sdt") vs +// `createTransactionForESDTTokenTransfer` (Factory, uppercase "ESDT") one +// the `send-esdt` recipe flags — this is not a one-off: +// +// TokenManagementController.createTransactionForCreatingNft (lowercase "Nft") +// TokenManagementTransactionsFactory.createTransactionForCreatingNFT (uppercase "NFT") +// +// Confirmed from both .d.ts files side by side. This recipe uses the +// Controller throughout, so `createTransactionForCreatingNft` (lowercase) +// is correct here — swapping in the Factory's casing is a real +// `tsc --strict` error, not a lint nit. +// +// Per mx-sdk-js-core's own cookbook/tokens.ts: issuing an NFT collection +// and then immediately minting into it, with no separate "set special +// role" transaction in between, is the documented pattern — the issuer +// automatically receives the NFT-create role on their own freshly issued +// collection. + +import type { Account, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export interface IssueNftCollectionInput { + tokenName: string; + tokenTicker: string; +} + +export interface IssueNftCollectionOutput { + txHash: string; +} + +/** + * Issues a new NFT collection. `sender.nonce` must already reflect the + * network's current nonce — see src/account.ts's loadDevnetAccount. Once + * this transaction completes, `awaitCompletedIssueNonFungible(txHash)` + * (not called by this recipe directly against a real balance, but shown + * in mx-sdk-js-core's cookbook) parses out the real collection identifier + * — this recipe's mint step uses a placeholder instead. + */ +export async function issueNftCollection( + entrypoint: DevnetEntrypoint, + sender: Account, + input: IssueNftCollectionInput, +): Promise { + const controller = entrypoint.createTokenManagementController(); + + const transaction = await controller.createTransactionForIssuingNonFungible( + sender, + sender.getNonceThenIncrement(), + { + tokenName: input.tokenName, + tokenTicker: input.tokenTicker, + canFreeze: false, + canWipe: true, + canPause: false, + canTransferNFTCreateRole: true, + canChangeOwner: true, + canUpgrade: true, + canAddSpecialRoles: true, + }, + ); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash }; +} + +export interface MintNftInput { + /** + * The collection identifier to mint into — e.g. "COOKNFT-abcdef". In + * real usage this comes from parsing the issuance transaction's + * outcome (`awaitCompletedIssueNonFungible`), not a hand-typed value. + */ + tokenIdentifier: string; + name: string; + /** Basis points, e.g. 500 = 5%. A plain number — see the file header. */ + royalties: number; + uris: string[]; +} + +export interface MintNftOutput { + txHash: string; +} + +/** + * Creates (mints) one NFT into an already-issued collection. Note the + * Controller method name: `createTransactionForCreatingNft` (lowercase + * "Nft") — see the file header for why the Factory's + * `createTransactionForCreatingNFT` (uppercase) would be wrong here. + */ +export async function mintNft( + entrypoint: DevnetEntrypoint, + sender: Account, + input: MintNftInput, +): Promise { + const controller = entrypoint.createTokenManagementController(); + + const transaction = await controller.createTransactionForCreatingNft( + sender, + sender.getNonceThenIncrement(), + { + tokenIdentifier: input.tokenIdentifier, + initialQuantity: 1n, + name: input.name, + royalties: input.royalties, + hash: '', + attributes: Buffer.from(''), + uris: input.uris, + }, + ); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash }; +} +``` + +## Two independently-verified steps, not a real end-to-end mint + +Because an unfunded wallet's issuance transaction never actually completes +on-chain, this recipe cannot obtain a real collection identifier to mint into, +`src/index.ts` uses a **placeholder** identifier (`COOKBOOK-000000`) for the mint +step. In real usage: send the issuance transaction, parse the real identifier from +its outcome (`awaitCompletedIssueNonFungible(txHash)` returns `[{ tokenIdentifier }]`), +then pass that into `mintNft()`. Both steps are verified independently below. + +## How it works + +**Confirmed casing: `canTransferNFTCreateRole`, all-caps "NFT"**, not +`canTransferNftCreateRole` (a common miscasing). Confirmed from +`IssueNonFungibleInput = IssueInput & { canTransferNFTCreateRole: boolean }`. +Hand-decoding the real issuance payload confirms the wire format uses this exact +spelling too, the flag's literal name on the wire matches the TypeScript property +name character for character. + +**A third instance of the Controller/Factory casing trap** (already documented for +ESDT transfers in `send-esdt`'s Pitfall 2): + +```text +TokenManagementController.createTransactionForCreatingNft (lowercase "Nft") +TokenManagementTransactionsFactory.createTransactionForCreatingNFT (uppercase "NFT") +``` + +This recipe uses the Controller, so `createTransactionForCreatingNft` (lowercase) +is correct here. + +**Issuance payload, hand-decoded and gas-formula-verified.** A real logged request: +`value: "50000000000000000"` (0.05 EGLD), `gasLimit: 60503000`, matching +`50,000 + 1,500 x 302 (data bytes) + 60,000,000` to the unit. The decoded `data` +field confirms every flag encoded correctly, including +`canTransferNFTCreateRole: true`. + +**Mint payload, hand-decoded.** A real logged mint request has `receiver` set to +the **sender's own address**, not a contract. This is expected: `ESDTNFTCreate` is +a builtin function executed in the context of the token-holder/creator's own +account, not a separate contract call. The decoded `data` field, +`ESDTNFTCreate@434f4f4b424f4f4b2d303030303030@01@436f6f6b626f6f6b2054657374204e4654202331@01f4@@@...`, +confirms the collection identifier, quantity (`01` = 1), name, and `01f4` (hex for +decimal `500`), confirming `royalties: 500` (a plain **number**, not `500n`) +encodes correctly. A commonly-copied `royalties: 500n` snippet does not match +`MintInput.royalties: number` and fails `tsc --strict`. + +## Pitfalls + +:::danger[Pitfall 1: canTransferNftCreateRole (the miscasing) fails tsc --strict] +The real field is `canTransferNFTCreateRole` (all-caps "NFT"). Contrast with +fungible issuance, where the field does not exist at all, see +[Issue a fungible ESDT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token). +::: + +:::warning[Pitfall 2: createTransactionForCreatingNft (Controller) vs createTransactionForCreatingNFT (Factory)] +Mixing up the casing for the API level you are using is a real `tsc --strict` +error, not a lint nit, a third confirmed instance of this SDK's recurring +Controller/Factory casing-mismatch pattern. +::: + +:::danger[Pitfall 3: royalties is a plain number, not a bigint] +`500n` fails `tsc --strict` against `MintInput.royalties: number`. +::: + +:::note[Pitfall 4: this recipe's mint step uses a placeholder token identifier] +It cannot mint into a collection that was never actually issued (the unfunded +wallet's issuance transaction is rejected before it reaches the network). See "Two +independently-verified steps" above for the real-usage sequence. +::: + +## See also + +- [Issue a fungible ESDT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token) + is the fungible sibling, including why `canTransferNftCreateRole` does not apply + there at all. +- [Call a contract endpoint with native JS args (NativeSerializer)](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint) + applies the general Controller-vs-Factory casing pattern to smart contract + calls. +- [Multi-token transfer in one tx](/sdk-and-tools/sdk-js/cookbook/transactions/multi-token-transfer) + sends an NFT (once minted) alongside other tokens in a single transaction. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/tokens/issue-sft-collection.mdx b/docs/sdk-and-tools/sdk-js/cookbook/tokens/issue-sft-collection.mdx new file mode 100644 index 000000000..43953b9d6 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/tokens/issue-sft-collection.mdx @@ -0,0 +1,270 @@ +--- +title: Issue an SFT collection + create, add, and burn quantity +description: Issue a semi-fungible (SFT) collection, create an SFT batch, then add and burn quantity with sdk-core's TokenManagementController on devnet. +difficulty: intermediate +est_minutes: 9 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - sft + - esdt + - nft + - transaction + - gas + - devnet + - typescript +--- + +A semi-fungible token (SFT) is a fungible **quantity** living at an NFT-style +nonce inside a collection: 100 identical festival tickets, 50 copies of an in-game +item. Here is the full lifecycle with sdk-core's +`TokenManagementController`: + +1. **Issue** the collection (`issueSemiFungible`, costs 0.05 EGLD). +2. **Create** the first batch (`ESDTNFTCreate` with quantity > 1). +3. **Add quantity** to that batch (`ESDTNFTAddQuantity`). +4. **Burn quantity** from it (`ESDTNFTBurn`). + +An SFT collection is issued with the *same* input shape as an NFT collection (both +carry `canTransferNFTCreateRole`), and an SFT is created with the *same* +`ESDTNFTCreate` builtin as an NFT, only the quantity differs. Contrast the +single-unit case in +[Issue an NFT collection](/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection). + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites. **No devnet EGLD required** to verify the payloads, see "How it + works". + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/issue-sft-collection +npm install +npm run build +npm start -- ./wallet.pem CookbookSFT CSFT +``` + +## The code + +```ts title="src/sft.ts" +// src/sft.ts — the subject of this recipe: the full semi-fungible (SFT) +// lifecycle via sdk-core's TokenManagementController. +// +// 1. issueSemiFungibleCollection — register the collection (costs 0.05 EGLD) +// 2. createSft — mint the first batch (quantity > 1) +// 3. addSftQuantity — increase an existing batch's supply +// 4. burnSftQuantity — destroy some of a batch's supply +// +// An SFT is a fungible quantity that lives at a specific NFT-style nonce +// inside a collection. Its issuance has the SAME shape as an NFT collection +// (IssueSemiFungibleInput === IssueNonFungibleInput — both carry +// `canTransferNFTCreateRole`, all-caps NFT), and creation reuses the exact +// same `ESDTNFTCreate` builtin as an NFT, only with an initialQuantity above +// 1. See issue-nft-collection for the quantity-1 case. +// +// Two casing facts baked in (see the recipe page's Pitfalls): +// - Controller: createTransactionForCreatingNft (lowercase "Nft"). +// Factory: createTransactionForCreatingNFT (all-caps "NFT"). +// - royalties is a plain number, not a bigint. + +import type { Account, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +export interface IssueSftInput { + tokenName: string; + tokenTicker: string; +} + +export interface CreateSftInput { + tokenIdentifier: string; + /** How many units of this new batch to mint — the point of an SFT (> 1). */ + initialQuantity: bigint; + name: string; + /** Basis points, e.g. 500 = 5%. A plain number, NOT a bigint. */ + royalties: number; + uris: string[]; +} + +export interface QuantityInput { + tokenIdentifier: string; + /** The nonce of the batch to change (from the create step's outcome). */ + tokenNonce: bigint; + quantity: bigint; +} + +/** Step 1 — register the SFT collection. Costs a fixed 0.05 EGLD + gas. */ +export async function issueSemiFungibleCollection( + entrypoint: DevnetEntrypoint, + sender: Account, + input: IssueSftInput, +): Promise { + const controller = entrypoint.createTokenManagementController(); + return controller.createTransactionForIssuingSemiFungible(sender, sender.getNonceThenIncrement(), { + tokenName: input.tokenName, + tokenTicker: input.tokenTicker, + canFreeze: true, + canWipe: true, + canPause: true, + // All-caps "NFT" — the field exists for SFT/NFT issuance, unlike + // fungible issuance where it does not exist at all. + canTransferNFTCreateRole: true, + canChangeOwner: true, + canUpgrade: true, + canAddSpecialRoles: true, + }); +} + +/** Step 2 — create (mint) the first batch. Needs the ESDTRoleNFTCreate role. */ +export async function createSft( + entrypoint: DevnetEntrypoint, + sender: Account, + input: CreateSftInput, +): Promise { + const controller = entrypoint.createTokenManagementController(); + // createTransactionForCreatingNft — lowercase "Nft" on the controller. + return controller.createTransactionForCreatingNft(sender, sender.getNonceThenIncrement(), { + tokenIdentifier: input.tokenIdentifier, + initialQuantity: input.initialQuantity, + name: input.name, + royalties: input.royalties, // plain number + hash: '', + attributes: new Uint8Array(), + uris: input.uris, + }); +} + +/** Step 3 — add supply to an existing batch. Needs the ESDTRoleNFTAddQuantity role. */ +export async function addSftQuantity( + entrypoint: DevnetEntrypoint, + sender: Account, + input: QuantityInput, +): Promise { + const controller = entrypoint.createTokenManagementController(); + return controller.createTransactionForAddingQuantity(sender, sender.getNonceThenIncrement(), { + tokenIdentifier: input.tokenIdentifier, + tokenNonce: input.tokenNonce, + quantity: input.quantity, + }); +} + +/** Step 4 — destroy supply from a batch. Needs the ESDTRoleNFTBurn role. */ +export async function burnSftQuantity( + entrypoint: DevnetEntrypoint, + sender: Account, + input: QuantityInput, +): Promise { + const controller = entrypoint.createTokenManagementController(); + return controller.createTransactionForBurningQuantity(sender, sender.getNonceThenIncrement(), { + tokenIdentifier: input.tokenIdentifier, + tokenNonce: input.tokenNonce, + quantity: input.quantity, + }); +} + +/** Decode a transaction's `data` field into `` + hex ``. */ +export function describePayload(transaction: Transaction): { + function: string; + receiver: string; + value: string; + args: string[]; +} { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + function: parts[0] ?? '', + receiver: transaction.receiver.toBech32(), + value: transaction.value.toString(), + args: parts.slice(1), + }; +} +``` + +## Run it + +```bash +npm start -- +``` + +A real captured run (unfunded wallet), abbreviated: + +```text +ISSUE semi-fungible collection: + function: issueSemiFungible + receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + value: 50000000000000000 + +CREATE SFT (CSFT-000000, quantity 100): + function: ESDTNFTCreate + receiver: erd18l2cww3g3g6uc35wy60efd87ss85xdqj9gh9qn6ga6halz3jscaqd4dl3k + args: [435346542d303030303030, 64, 4261746368202331, 01f4, , , 6874747073...] + +ADD quantity (nonce 1, +50): ESDTNFTAddQuantity ... [CSFT..., 01, 32] +BURN quantity (nonce 1, -10): ESDTNFTBurn ... [CSFT..., 01, 0a] + +Broadcasting the issuance... +Rejected (expected for an unfunded wallet): ... insufficient funds for address erd18l2c... +``` + +## Two independently-verified steps, not a real end-to-end mint + +An unfunded wallet's issuance never lands on-chain, so there is no real collection +identifier to mint into, the create/add/burn steps use a **placeholder** +(`CSFT-000000`). In real usage: send the issuance, parse the identifier from its +outcome (`awaitCompletedIssueSemiFungible(txHash)` returns `[{ tokenIdentifier }]`), +then feed that into the create step. + +## How it works + +**Issuance** is addressed to the ESDT system contract with +`value: 50000000000000000` (exactly 0.05 EGLD), and its flags include +`canTransferNFTCreateRole` (all-caps "NFT", same as NFT issuance). + +**Create/add/burn** are builtin functions executed in the creator's own account, +so their receiver is the **sender**, not a contract, hand-verified against the +decoded payloads above: + +- `ESDTNFTCreate@@@@@@@`. + The quantity `64` is hex for 100, the whole point of an SFT. Royalties `01f4` is + hex for 500 (a plain **number**, not a bigint, `500n` fails `tsc --strict` + against `MintInput.royalties: number`). +- `ESDTNFTAddQuantity@@@` and + `ESDTNFTBurn@@@` both take the batch's **nonce** (from + the create outcome) plus an amount. + +## Pitfalls + +:::warning[Pitfall 1: createTransactionForCreatingNft (Controller) vs ...CreatingNFT (Factory)] +The controller spells it lowercase `Nft`; the factory spells it all-caps `NFT`. +Mixing them up is a `tsc --strict` error. This recipe uses the controller. See +[Issue an NFT collection](/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection)'s +Pitfall 2. +::: + +:::danger[Pitfall 2: royalties is a plain number, not a bigint] +`royalties: 500n` fails `tsc --strict` against `MintInput.royalties: number`. Use +`500`. +::: + +:::note[Pitfall 3: add/burn quantity need the matching roles] +`ESDTNFTAddQuantity` needs `ESDTRoleNFTAddQuantity` and `ESDTNFTBurn` needs +`ESDTRoleNFTBurn` on the creator address. Grant them like any special role, see +[Set and unset special roles](/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles) +(the SFT role setter is +`createTransactionForSettingSpecialRoleOnSemiFungibleToken`). +::: + +## See also + +- [Issue an NFT collection + mint an NFT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection) + is the single-unit sibling; same issuance and `ESDTNFTCreate` shape with quantity + 1. +- [Issue a fungible ESDT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token) + is the fully fungible case, and why `canTransferNFTCreateRole` does not apply + there. +- [Set and unset special roles](/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles) + grants the create / add-quantity / burn roles this recipe relies on. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/tokens/local-mint-burn-supply.mdx b/docs/sdk-and-tools/sdk-js/cookbook/tokens/local-mint-burn-supply.mdx new file mode 100644 index 000000000..92e0aa30d --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/tokens/local-mint-burn-supply.mdx @@ -0,0 +1,221 @@ +--- +title: Local mint and burn (change a fungible token's supply) +description: Increase and decrease a fungible ESDT's circulating supply with ESDTLocalMint and ESDTLocalBurn via sdk-core, plus a real SDK method-name typo. +difficulty: intermediate +est_minutes: 7 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - esdt + - transaction + - gas + - devnet + - typescript +--- + +Once a fungible ESDT is issued, its circulating supply is not fixed. An address +holding the right role can mint more (`ESDTLocalMint`, needs `ESDTRoleLocalMint`) +or burn its own (`ESDTLocalBurn`, needs `ESDTRoleLocalBurn`). Both are **builtin +functions** executed in the caller's own account, so the transaction's receiver is +the sender itself, not the ESDT system contract. + +This recipe builds both, decodes the payloads, and broadcasts the mint. It also +walks straight into a real sdk-core method-name trap. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites. **No devnet EGLD required**, see "How it works". + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/local-mint-burn-supply +npm install +npm run build +npm start -- ./wallet.pem COOK-123456 1000 +``` + +## The code + +```ts title="src/supply.ts" +// src/supply.ts — the subject of this recipe: changing a fungible ESDT's +// circulating supply after issuance, using the ESDTLocalMint / ESDTLocalBurn +// builtin functions. +// +// - localMint — requires the ESDTRoleLocalMint role (see set-special-roles) +// - localBurn — requires the ESDTRoleLocalBurn role +// +// Both are builtin functions executed in the caller's OWN account context, +// so the transaction's receiver is the sender itself (not the ESDT system +// contract). This is the correct, on-chain-verified shape for these two +// operations — contrast the freeze/pause/wipe manager operations in the +// token-lifecycle-operations recipe, which must instead target the ESDT +// system contract. +// +// TWO method-name traps, both confirmed against the installed v15.4.1 (see +// the recipe page's Pitfalls): +// - MINT, controller: createTransactionForLocaMinting (note the typo: +// "Loca", missing an "l" — it is NOT createTransactionForLocalMinting). +// - MINT, factory: createTransactionForLocalMint (correct spelling, +// but no "-ing" suffix). +// - BURN is createTransactionForLocalBurning on BOTH — the only consistent +// one of the pair. + +import type { Account, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +export interface SupplyInput { + tokenIdentifier: string; + /** Amount in the token's smallest denomination (respect its numDecimals). */ + amount: bigint; +} + +/** Mint additional supply of a fungible token you hold the local-mint role on. */ +export async function localMint( + entrypoint: DevnetEntrypoint, + sender: Account, + input: SupplyInput, + useFactory: boolean, +): Promise { + if (useFactory) { + const factory = entrypoint.createTokenManagementTransactionsFactory(); + // Factory: createTransactionForLocalMint — no "-ing", correct spelling. + const transaction = await factory.createTransactionForLocalMint(sender.address, { + tokenIdentifier: input.tokenIdentifier, + supplyToMint: input.amount, + }); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; + } + + const controller = entrypoint.createTokenManagementController(); + // Controller: createTransactionForLocaMinting — "Loca", a real SDK typo. + return controller.createTransactionForLocaMinting(sender, sender.getNonceThenIncrement(), { + tokenIdentifier: input.tokenIdentifier, + supplyToMint: input.amount, + }); +} + +/** Burn supply of a fungible token you hold the local-burn role on. */ +export async function localBurn( + entrypoint: DevnetEntrypoint, + sender: Account, + input: SupplyInput, + useFactory: boolean, +): Promise { + if (useFactory) { + const factory = entrypoint.createTokenManagementTransactionsFactory(); + const transaction = await factory.createTransactionForLocalBurning(sender.address, { + tokenIdentifier: input.tokenIdentifier, + supplyToBurn: input.amount, + }); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; + } + + const controller = entrypoint.createTokenManagementController(); + return controller.createTransactionForLocalBurning(sender, sender.getNonceThenIncrement(), { + tokenIdentifier: input.tokenIdentifier, + supplyToBurn: input.amount, + }); +} + +/** Decode a transaction's `data` field into `@@`. */ +export function describeSupplyPayload(transaction: Transaction): { + function: string; + receiver: string; + tokenIdentifier: string; + amountHex: string; +} { + const parts = Buffer.from(transaction.data).toString().split('@'); + const tokenHex = parts[1] ?? ''; + return { + function: parts[0] ?? '', + receiver: transaction.receiver.toBech32(), + tokenIdentifier: Buffer.from(tokenHex, 'hex').toString(), + amountHex: parts[2] ?? '', + }; +} +``` + +## Run it + +```bash +npm start -- [--factory] +``` + +A real captured run (unfunded wallet): + +```text +LOCAL MINT (ESDTLocalMint): + function: ESDTLocalMint + receiver: erd18l2cww3g3g6uc35wy60efd87ss85xdqj9gh9qn6ga6halz3jscaqd4dl3k + tokenIdentifier: COOK-123456 + amount (hex): 03e8 + +LOCAL BURN (ESDTLocalBurn): + function: ESDTLocalBurn + amount (hex): 03e8 + +Broadcasting the mint... +Rejected (expected for an unfunded wallet): ... insufficient funds for address erd18l2c... +``` + +## How it works + +Both payloads decode to `@@` addressed to the +**sender**. `03e8` is hex for 1000, remember this is in the token's smallest +denomination, so respect its `numDecimals` (a 6-decimal token needs `1000000` for +one whole unit). The unfunded broadcast is rejected with a clean `insufficient +funds` keyed to the sender, proving the payload is well-formed; a real mint +additionally needs the token to exist and the sender to hold the matching local +role (see [Set and unset special roles](/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles)). + +**The method names are a minefield, this is the recipe's real value.** Against the +installed v15.4.1: + +- **Mint, controller:** `createTransactionForLocaMinting`, note the typo, **"Loca"** + with a missing "l". It is *not* `createTransactionForLocalMinting`. +- **Mint, factory:** `createTransactionForLocalMint`, correct spelling, but **no + "-ing"** suffix. +- **Burn:** `createTransactionForLocalBurning` on *both*, the only consistent name + of the pair. + +## Pitfalls + +:::danger[Pitfall 1: the controller mint method is misspelled createTransactionForLocaMinting] +"Loca", not "Local". Autocomplete for `createTransactionForLocal…` will not surface +it on the controller. The factory instead uses `createTransactionForLocalMint` (no +"-ing"). Copy the exact name from the code above. +::: + +:::warning[Pitfall 2: amounts are in the smallest denomination] +`ESDTLocalMint@…@03e8` mints 1000 base units. For a token issued with 6 decimals +that is 0.001 of a whole unit. See +[Send an ESDT](/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt)'s decimals +pitfall. +::: + +:::note[Pitfall 3: local mint/burn needs the local role, not ownership] +Being the token's owner is not enough; the sender needs `ESDTRoleLocalMint` / +`ESDTRoleLocalBurn` explicitly granted. Grant it with +[Set and unset special roles](/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles). +::: + +## See also + +- [Set and unset special roles](/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles) + grants the `ESDTRoleLocalMint` / `ESDTRoleLocalBurn` roles these operations + require. +- [Issue a fungible ESDT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token) + issues the token whose supply you then change. +- [Token lifecycle operations](/sdk-and-tools/sdk-js/cookbook/tokens/token-lifecycle-operations) + covers freeze, pause, and wipe (the manager-side operations, which target the + ESDT contract instead of the sender). diff --git a/docs/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles.mdx b/docs/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles.mdx new file mode 100644 index 000000000..f062aa998 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles.mdx @@ -0,0 +1,292 @@ +--- +title: Set and unset special roles on a fungible ESDT +description: Set and unset the local-mint, local-burn, and ESDT-transfer roles on a fungible ESDT with sdk-core, including a confirmed v15.4.1 unset-role bug. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - esdt + - transaction + - gas + - devnet + - typescript +--- + +A freshly issued fungible ESDT can carry three special roles, each granted to a +specific address via the ESDT system contract: + +- **`ESDTRoleLocalMint`**, the holder may mint more supply (see + [Local mint and burn](/sdk-and-tools/sdk-js/cookbook/tokens/local-mint-burn-supply)). +- **`ESDTRoleLocalBurn`**, the holder may burn its own supply. +- **`ESDTTransferRole`**, transfers of the token must route through the holder. + +This recipe sets and unsets those roles with sdk-core's +`TokenManagementController` (and the matching `TokenManagementTransactionsFactory`). +The method name is identical on both API levels, the only difference is the usual +one: the controller sets the nonce and signs, the factory only builds. + +If instead you are building a browser dApp where the token owner's own wallet +should sign, wire the factory method through a connected-wallet flow instead of a +PEM-holding script. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites for a throwaway one. **No devnet EGLD required** to verify the + payload, see "How it works". + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/set-special-roles +npm install +npm run build +npm start -- ./wallet.pem COOK-123456 +``` + +## The code + +```ts title="src/roles.ts" +// src/roles.ts — the subject of this recipe: setting and unsetting the +// three special roles a fungible ESDT can carry, both ways (controller and +// factory). +// +// - ESDTRoleLocalMint — the holder may mint more supply locally +// - ESDTRoleLocalBurn — the holder may burn its own supply locally +// - ESDTTransferRole — transfers of the token must go through the holder +// +// Both `createTransactionForSettingSpecialRoleOnFungibleToken` and its +// unset sibling live on BOTH the controller and the factory under the SAME +// name (unlike Nft/NFT and Loca/Local elsewhere in this class — see the +// local-mint-burn-supply and token-lifecycle-operations recipes). The only +// difference is the usual one: the controller sets the nonce and signs; the +// factory only builds. +// +// A CONFIRMED sdk-core v15.4.1 BUG lives in the unset builder — see +// unsetFungibleRoles below and the recipe page's Pitfalls. It is verified +// here by decoding the real wire payload, not asserted from reading types. + +import { Address } from '@multiversx/sdk-core'; +import type { Account, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +/** Which of the three fungible roles to add. */ +export interface SetRolesInput { + tokenIdentifier: string; + /** The address to grant the roles to (often the contract or an operator). */ + user: Address; + localMint: boolean; + localBurn: boolean; + esdtTransfer: boolean; +} + +/** Which of the three fungible roles to remove. */ +export interface UnsetRolesInput { + tokenIdentifier: string; + user: Address; + localMint: boolean; + localBurn: boolean; + esdtTransfer: boolean; +} + +/** + * Build a "set special role" transaction. + * + * Controller path: `createTransactionForSettingSpecialRoleOnFungibleToken` + * sets the nonce and signs (it takes the whole Account). Factory path: it + * only builds — the caller owns nonce + signature. Same method name on both. + */ +export async function setFungibleRoles( + entrypoint: DevnetEntrypoint, + sender: Account, + input: SetRolesInput, + useFactory: boolean, +): Promise { + const options = { + user: input.user, + tokenIdentifier: input.tokenIdentifier, + addRoleLocalMint: input.localMint, + addRoleLocalBurn: input.localBurn, + addRoleESDTTransferRole: input.esdtTransfer, + }; + + if (useFactory) { + const factory = entrypoint.createTokenManagementTransactionsFactory(); + const transaction = await factory.createTransactionForSettingSpecialRoleOnFungibleToken( + sender.address, + options, + ); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; + } + + const controller = entrypoint.createTokenManagementController(); + return controller.createTransactionForSettingSpecialRoleOnFungibleToken( + sender, + sender.getNonceThenIncrement(), + options, + ); +} + +/** + * Build an "unset special role" transaction. + * + * WARNING — sdk-core v15.4.1 bug (confirmed in the installed build and in + * the v15.4.1 source tag): the unset builder reads the wrong flags. + * `removeRoleLocalBurn` is NEVER consulted, and `removeRoleESDTTransferRole` + * gates BOTH the `ESDTRoleLocalBurn` and the `ESDTTransferRole` wire parts. + * See describeRolesPayload's output in the recipe page — this function + * passes the input through faithfully; the SDK mis-wires it downstream. + */ +export async function unsetFungibleRoles( + entrypoint: DevnetEntrypoint, + sender: Account, + input: UnsetRolesInput, + useFactory: boolean, +): Promise { + const options = { + user: input.user, + tokenIdentifier: input.tokenIdentifier, + removeRoleLocalMint: input.localMint, + removeRoleLocalBurn: input.localBurn, + removeRoleESDTTransferRole: input.esdtTransfer, + }; + + if (useFactory) { + const factory = entrypoint.createTokenManagementTransactionsFactory(); + const transaction = await factory.createTransactionForUnsettingSpecialRoleOnFungibleToken( + sender.address, + options, + ); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; + } + + const controller = entrypoint.createTokenManagementController(); + return controller.createTransactionForUnsettingSpecialRoleOnFungibleToken( + sender, + sender.getNonceThenIncrement(), + options, + ); +} + +/** A fully decoded role transaction, straight from its `data` field. */ +export interface DecodedRolesPayload { + function: string; + receiver: string; + tokenIdentifier: string; + user: string; + roles: string[]; +} + +/** + * Decode a set/unset-role transaction's `data` field into its parts: + * `@@@@...`. Every part + * after the function name is hex; the first two are an ASCII token + * identifier and a 32-byte address, the rest are ASCII role names. + */ +export function describeRolesPayload(transaction: Transaction): DecodedRolesPayload { + const parts = Buffer.from(transaction.data).toString().split('@'); + const tokenHex = parts[1] ?? ''; + const userHex = parts[2] ?? ''; + const roleHexes = parts.slice(3); + + return { + function: parts[0] ?? '', + receiver: transaction.receiver.toBech32(), + tokenIdentifier: Buffer.from(tokenHex, 'hex').toString(), + user: userHex ? Address.newFromHex(userHex).toBech32() : '(none)', + roles: roleHexes.map((hex) => Buffer.from(hex, 'hex').toString()), + }; +} +``` + +## Run it + +```bash +npm start -- [--user ] [--factory] +``` + +A real captured run (unfunded wallet): + +```text +SET (localMint + localBurn + esdtTransfer): + function: setSpecialRole + receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + tokenIdentifier: COOK-123456 + roles on wire: [ESDTRoleLocalMint, ESDTRoleLocalBurn, ESDTTransferRole] + +UNSET requested { localBurn: true } ONLY (expect ESDTRoleLocalBurn): + function: unSetSpecialRole + roles on wire: [] + +UNSET requested { esdtTransfer: true } ONLY (expect ESDTTransferRole): + function: unSetSpecialRole + roles on wire: [ESDTRoleLocalBurn, ESDTTransferRole] + +Broadcasting the SET transaction... +Rejected (expected for an unfunded wallet): ... insufficient funds for address erd18l2cww3g3g6uc35wy60efd87ss85xdqj9gh9qn6ga6halz3jscaqd4dl3k +``` + +## How it works + +**The set path is correct and hand-verified.** The SET transaction is addressed to +the ESDT system contract (`erd1qqqq…zllls8a5w6u`, the documented ESDT system +contract address), with data `setSpecialRole@@@…`. +The role names travel as ASCII on the wire +(`45534454526f6c654c6f63616c4d696e74` = `ESDTRoleLocalMint`). Gas for a three-role +set on `COOK-123456` was `60357500`, matching +`50,000 + 1,500 x 205 (data bytes) + 60,000,000` to the unit. The unfunded +broadcast is rejected with a clean `insufficient funds` keyed to the exact sender, +the payload is well-formed; only the balance is missing. + +**The unset path has a confirmed sdk-core v15.4.1 bug.** The two UNSET lines above +are the proof, decoded from real transactions this recipe built: + +- Requesting **`localBurn` only** produces an **empty** role list, the local-burn + role is silently *not* removed. +- Requesting **`esdtTransfer` only** removes **both** `ESDTRoleLocalBurn` *and* + `ESDTTransferRole`. + +The cause is in `createTransactionForUnsettingSpecialRoleOnFungibleToken`: the +`ESDTRoleLocalBurn` wire part is gated on `removeRoleESDTTransferRole` instead of +`removeRoleLocalBurn`, and `removeRoleLocalBurn` is never read. Confirmed in both +the installed build and the tagged v15.4.1 source. (The **set** builder is fine, +this is specific to unsetting fungible roles.) + +## Pitfalls + +:::danger[Pitfall 1: unsetting a fungible role is broken in v15.4.1] +`removeRoleLocalBurn` is ignored, and `removeRoleESDTTransferRole` removes the +local-burn role too. Until it is fixed upstream, verify the decoded +`unSetSpecialRole` payload before broadcasting, or build the +`unSetSpecialRole@@@ESDTRoleLocalBurn` data by hand for a burn-only +removal. +::: + +:::note[Pitfall 2: roles are granted per address, not globally] +Each set/unset targets one `user` address. Granting `ESDTRoleLocalMint` to a +contract does not grant it to you, pass the right `--user`. +::: + +:::warning[Pitfall 3: the token must exist and canAddSpecialRoles must be true] +Roles are set through the ESDT system contract against an already-issued token +whose `canAddSpecialRoles` flag was set at issuance. See +[Issue a fungible ESDT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token). +::: + +## See also + +- [Issue a fungible ESDT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token) + issues the token you then grant roles on (set `canAddSpecialRoles`). +- [Local mint and burn](/sdk-and-tools/sdk-js/cookbook/tokens/local-mint-burn-supply) + uses the `ESDTRoleLocalMint` / `ESDTRoleLocalBurn` roles this recipe grants. +- [Token lifecycle operations](/sdk-and-tools/sdk-js/cookbook/tokens/token-lifecycle-operations) + covers freeze, pause, and wipe, the manager-side counterpart to roles. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/tokens/token-lifecycle-operations.mdx b/docs/sdk-and-tools/sdk-js/cookbook/tokens/token-lifecycle-operations.mdx new file mode 100644 index 000000000..30298676d --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/tokens/token-lifecycle-operations.mdx @@ -0,0 +1,245 @@ +--- +title: Token lifecycle — freeze, unfreeze, pause, unpause, wipe +description: Freeze/unfreeze an account's token, pause/unpause globally, and wipe with sdk-core, includes the confirmed v15.4.1 wrong-receiver bug and its fix. +difficulty: advanced +est_minutes: 10 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - esdt + - transaction + - gas + - devnet + - typescript +--- + +A token manager can control a token after issuance through the ESDT system +contract: + +- **pause / unpause**, globally halt or resume *all* transfers of the token. +- **freeze / unfreeze**, freeze or unfreeze *one* account's holding. +- **wipe**, remove a frozen account's holding entirely. + +These are privileged manager calls, distinct from the builtin `ESDTLocalMint` / +`ESDTNFTCreate` self-calls in +[Local mint and burn](/sdk-and-tools/sdk-js/cookbook/tokens/local-mint-burn-supply). +And in sdk-core v15.4.1 they ship with a confirmed bug that this recipe both +demonstrates and works around. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites. **No devnet EGLD required**, see "How it works". + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/token-lifecycle-operations +npm install +npm run build +npm start -- ./wallet.pem COOK-123456 +``` + +## The code + +```ts title="src/lifecycle.ts" +// src/lifecycle.ts — the subject of this recipe: the manager-only lifecycle +// operations a token owner can perform through the ESDT system contract. +// +// - pause / unpause — globally halt / resume all transfers of a token +// - freeze / unfreeze — freeze / unfreeze ONE account's holding of a token +// - wipe — remove a frozen account's holding entirely +// +// These are distinct from ESDTLocalMint/Burn and ESDTNFTCreate (builtin +// functions on the caller's own account — see local-mint-burn-supply). A +// pause/freeze/wipe is a privileged call the token MANAGER makes TO the ESDT +// system contract, which enforces it. +// +// ============================================================================ +// CONFIRMED sdk-core v15.4.1 BUG — wrong receiver. +// ---------------------------------------------------------------------------- +// createTransactionForPausing / Unpausing / Freezing / Unfreezing / Wiping +// all build the transaction with `receiver: sender` (the caller's own +// address). Every real on-chain pause/freeze/wipe is instead addressed to the +// ESDT system contract erd1qqqq...zllls8a5w6u (verified via the mainnet API), +// which is the SAME receiver the SDK correctly uses for issue and +// setSpecialRole. A transaction sent to the caller's own address will NOT +// perform the operation. This recipe builds via the factory and overrides the +// receiver before signing — see withCorrectReceiver below. +// +// Confirmed in both the installed build and the v15.4.1 source tag. Contrast: +// issue/setSpecialRole correctly target the ESDT contract; ESDTLocalMint/Burn +// and ESDTNFTCreate correctly target the sender. Only these five are wrong. +// ============================================================================ + +import { Address } from '@multiversx/sdk-core'; +import type { Account, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +/** + * The ESDT system contract — the correct receiver for pause/freeze/wipe. + * The documented ESDT system contract address; verified on-chain as the + * receiver of every real freeze/pause transaction. + */ +export const ESDT_SYSTEM_CONTRACT = Address.newFromBech32( + 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u', +); + +/** Build a pause (or unpause) transaction. Factory path, unsigned. */ +export async function buildPause( + entrypoint: DevnetEntrypoint, + sender: Account, + tokenIdentifier: string, + unpause: boolean, +): Promise { + const factory = entrypoint.createTokenManagementTransactionsFactory(); + return unpause + ? factory.createTransactionForUnpausing(sender.address, { tokenIdentifier }) + : factory.createTransactionForPausing(sender.address, { tokenIdentifier }); +} + +/** Build a freeze (or unfreeze) transaction for one user's holding. */ +export async function buildFreeze( + entrypoint: DevnetEntrypoint, + sender: Account, + tokenIdentifier: string, + user: Address, + unfreeze: boolean, +): Promise { + const factory = entrypoint.createTokenManagementTransactionsFactory(); + // Factory: createTransactionForUnfreezing (lowercase "f"). The controller + // spells the same method createTransactionForUnFreezing (capital "F"). + return unfreeze + ? factory.createTransactionForUnfreezing(sender.address, { user, tokenIdentifier }) + : factory.createTransactionForFreezing(sender.address, { user, tokenIdentifier }); +} + +/** Build a wipe transaction — removes a frozen account's holding. */ +export async function buildWipe( + entrypoint: DevnetEntrypoint, + sender: Account, + tokenIdentifier: string, + user: Address, +): Promise { + const factory = entrypoint.createTokenManagementTransactionsFactory(); + return factory.createTransactionForWiping(sender.address, { user, tokenIdentifier }); +} + +/** + * The v15.4.1 workaround: if the SDK addressed this lifecycle transaction to + * the caller instead of the ESDT system contract, fix it. Returns true if a + * correction was applied. Call this BEFORE setting the nonce and signing, so + * the signature covers the corrected receiver. + */ +export function withCorrectReceiver(transaction: Transaction): boolean { + if (transaction.receiver.toBech32() === ESDT_SYSTEM_CONTRACT.toBech32()) { + return false; + } + transaction.receiver = ESDT_SYSTEM_CONTRACT; + return true; +} + +/** Decode a lifecycle transaction's `data` field. */ +export function describeLifecyclePayload(transaction: Transaction): { + function: string; + receiver: string; + args: string[]; +} { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + function: parts[0] ?? '', + receiver: transaction.receiver.toBech32(), + args: parts.slice(1), + }; +} +``` + +## Run it + +```bash +npm start -- [--user ] +``` + +A real captured run (unfunded wallet): + +```text +As the SDK builds them in v15.4.1 (note the receiver): +PAUSE: function: pause receiver: erd18l2c...z3jscaqd4dl3k <- SENDER (bug) +FREEZE: function: freeze receiver: erd18l2c...z3jscaqd4dl3k <- SENDER (bug) +WIPE: function: wipe receiver: erd18l2c...z3jscaqd4dl3k <- SENDER (bug) + +Corrected freeze receiver -> ESDT system contract (patched: true): +FREEZE (corrected): + function: freeze + receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u <- ESDT contract + +Broadcasting the corrected freeze... +Rejected (expected for an unfunded wallet): ... insufficient funds for address erd18l2c... +``` + +## How it works + +**The v15.4.1 bug: wrong receiver.** `createTransactionForPausing`, `Unpausing`, +`Freezing`, `Unfreezing`, and `Wiping` all build the transaction with +`receiver: sender`, the caller's own address. But every real on-chain +pause/freeze/wipe is addressed to the **ESDT system contract** +(`erd1qqqq…zllls8a5w6u`), the same receiver the SDK correctly uses for `issue` and +`setSpecialRole`. This was verified two ways: the decoded run above shows +`receiver = SENDER`, and a lookup of real mainnet `pause` and `freeze` +transactions shows `receiver = the ESDT contract`. A lifecycle transaction sent to +the caller's own address will not perform the operation. Confirmed in both the +installed build and the tagged v15.4.1 source. + +**The fix.** `withCorrectReceiver()` overrides `transaction.receiver` to the ESDT +system contract. Because this recipe builds via the **factory** (which does not +sign), the override happens *before* the nonce is set and the transaction is +signed, so the signature covers the corrected receiver. The corrected `freeze` +above is a genuine, well-formed transaction: it is rejected only for insufficient +funds, keyed to the sender. + +**Argument shapes.** `pause`/`unPause` take only ``. +`freeze`/`unFreeze`/`wipe` take `@` (the account +whose holding is affected). On the wire the unfreeze function is spelled +`UnFreeze` (capital "F"). + +## Pitfalls + +:::danger[Pitfall 1: v15.4.1 addresses pause/freeze/wipe to the sender, not the ESDT contract] +As shipped, these transactions will not take effect. Override +`transaction.receiver` to +`erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u` before signing +(build via the factory so the signature covers the fix), exactly as +`withCorrectReceiver()` does. +::: + +:::warning[Pitfall 2: createTransactionForUnFreezing (Controller) vs ...Unfreezing (Factory)] +The controller capitalizes the "F" (`UnFreezing`); the factory does not +(`Unfreezing`). Another instance of this SDK's Controller/Factory casing drift. +::: + +:::note[Pitfall 3: wipe only works on a frozen account] +You must `freeze` an account's holding before you can `wipe` it. Freeze/wipe also +require the token to have been issued with `canFreeze` / `canWipe`, see +[Issue a fungible ESDT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token). +::: + +:::note[Pitfall 4: pause is global, freeze is per-account] +`pause` halts every transfer of the token for everyone; `freeze` targets one +address. Do not reach for `pause` when you mean to restrict a single account. +::: + +## See also + +- [Set and unset special roles](/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles) + is the other manager-side surface; grants operational roles rather than + restricting holders. +- [Issue a fungible ESDT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token) + sets `canFreeze` / `canWipe` / `canPause` at issuance so these operations are + permitted. +- [Local mint and burn](/sdk-and-tools/sdk-js/cookbook/tokens/local-mint-burn-supply) + covers the builtin self-calls whose `receiver: sender` is correct, for contrast + with this recipe's bug. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/transactions/multi-token-transfer.mdx b/docs/sdk-and-tools/sdk-js/cookbook/transactions/multi-token-transfer.mdx new file mode 100644 index 000000000..8d8fb796e --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/transactions/multi-token-transfer.mdx @@ -0,0 +1,233 @@ +--- +title: Multi-token transfer in one tx +description: Send EGLD plus two or more ESDT/NFT/SFT tokens in one transaction with sdk-core's TransfersController, every field decoded against real devnet. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - transaction + - esdt + - nft + - gas + - devnet + - typescript +--- + +Send native EGLD plus two (or more) ESDT/NFT/SFT tokens in a **single** +transaction, via sdk-core's `TransfersController.createTransactionForTransfer`. + +Sibling recipe to +[Send an ESDT](/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt) (one +fungible token) and +[Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld) +(native only), this one combines native EGLD with 2+ token transfers in the exact +same call. + +If instead you are building a browser dApp where the end user's own wallet should +sign, see +[Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send). +This recipe is for when **your own code holds the private key**. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet with some devnet EGLD (for gas), see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites for how to generate a throwaway one. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/multi-token-transfer +npm install +npm run build +npm start -- ./wallet.pem erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th +``` + +`TEST-abcdef` and `TESTNFT-abcdef` are illustrative placeholder identifiers, same +convention as +[Send an ESDT](/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt)'s +`TEST-abcdef` example. + +## Sending + +```ts title="src/multiTransfer.ts" +// src/multiTransfer.ts — the actual subject of this recipe: sending +// native EGLD plus two (or more) ESDT/NFT/SFT tokens in a SINGLE +// transaction, via `TransfersController.createTransactionForTransfer`. +// +// Confirmed from node_modules/@multiversx/sdk-core/out/transfers/resources.d.ts: +// +// export declare type CreateTransferTransactionInput = { +// receiver: Address; +// nativeAmount?: bigint; +// tokenTransfers?: TokenTransfer[]; +// data?: Uint8Array; +// }; +// +// Unlike the ESDT-transfer casing trap this Cookbook's `send-esdt` recipe +// flags (`createTransactionForEsdtTokenTransfer` vs +// `createTransactionForESDTTokenTransfer`), `createTransactionForTransfer` +// is spelled identically on both `TransfersController` and +// `TransferTransactionsFactory` — no casing trap here, confirmed by +// reading both .d.ts files. +// +// Sibling recipe to `send-esdt` (a single fungible token) and `send-egld` +// (native only) — this one combines native EGLD with 2+ token transfers +// in the exact same call. + +import { Address, Token, TokenTransfer } from '@multiversx/sdk-core'; +import type { Account, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export interface TokenTransferInput { + /** Token identifier, e.g. "TICKER-abcdef" (fungible) or "TICKER-abcdef-0a" broken apart into identifier + nonce. */ + identifier: string; + /** 0 for a fungible token; > 0 for an NFT/SFT. */ + nonce: bigint; + /** Amount in the token's own smallest denomination. */ + amount: bigint; +} + +export interface MultiTransferInput { + receiver: string; + /** Optional native EGLD to send alongside the token transfers, in smallest denomination. */ + nativeAmountInSmallestDenomination?: bigint; + tokenTransfers: TokenTransferInput[]; +} + +export interface MultiTransferOutput { + txHash: string; +} + +/** + * Sends native EGLD (optional) plus 2+ token transfers in a single + * transaction. `sender.nonce` must already reflect the network's current + * nonce — see src/account.ts's loadDevnetAccount. + */ +export async function sendMultiTokenTransfer( + entrypoint: DevnetEntrypoint, + sender: Account, + input: MultiTransferInput, +): Promise { + const controller = entrypoint.createTransfersController(); + + const tokenTransfers = input.tokenTransfers.map( + (t) => + new TokenTransfer({ + token: new Token({ identifier: t.identifier, nonce: t.nonce }), + amount: t.amount, + }), + ); + + const transaction = await controller.createTransactionForTransfer( + sender, + sender.getNonceThenIncrement(), + { + receiver: Address.newFromBech32(input.receiver), + ...(input.nativeAmountInSmallestDenomination !== undefined + ? { nativeAmount: input.nativeAmountInSmallestDenomination } + : {}), + tokenTransfers, + }, + ); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash }; +} +``` + +## Run it + +```bash +npm start -- +``` + +Expected output: + +```text +Sender: erd1... (nonce 0) +Receiver: erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th +Sending in ONE transaction: + - 0.001 EGLD (native) + - 2.5 TEST-abcdef (fungible, 6 decimals) + - 1 TESTNFT-abcdef-01 (NFT, nonce 1) + +Sent. Transaction hash: <64-char hex hash> +``` + +## How it works + +**`createTransactionForTransfer` is spelled identically on both API levels**, +confirmed from `resources.d.ts` and both +`TransfersController`/`TransferTransactionsFactory` `.d.ts` files. Unlike the +ESDT-transfer casing trap that `send-esdt` documents, there is no +Controller-vs-Factory naming mismatch here. + +**Two genuinely surprising, hand-verified facts about the transaction this +produces:** + +1. **The transaction's outer `receiver` is the SENDER'S OWN address, not the real + destination.** A request built for receiver `erd1qyu5...` logged + `"receiver":"erd1ayggn..."`, the sender's own address. The real destination is + instead the **first argument** inside `data`, hex-encoded, decoding it back to + bech32 gives exactly the intended receiver. If you inspect a built + transaction's `.receiver` or `.value` directly expecting the real recipient or + EGLD amount, you will be looking at the wrong fields for this transaction type. +2. **Combining `nativeAmount` with `tokenTransfers` folds the EGLD into a THIRD + synthetic token-transfer entry**, using the special identifier `EGLD-000000`, + not carried in `value` (which is `"0"` here). 2 real token transfers plus 1 + native amount produced a decoded data field starting + `MultiESDTNFTTransfer@@03@...`, count `03`, not `02`. + +**The full decoded payload, field by field** (from a real run of this recipe): + +```text +MultiESDTNFTTransfer +@0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1 (destination address) +@03 (3 transfers: 2 real tokens + 1 synthetic EGLD) +@544553542d616263646566 ("TEST-abcdef") +@ (nonce 0 — fungible, encoded as an EMPTY argument) +@2625a0 (2,500,000 = 2.5 units at 6 decimals) +@544553544e46542d616263646566 ("TESTNFT-abcdef") +@01 (nonce 1 — NFT) +@01 (amount 1) +@45474c442d303030303030 ("EGLD-000000" — the synthetic native-EGLD entry) +@ (nonce 0, empty again) +@038d7ea4c68000 (1,000,000,000,000,000 = 0.001 EGLD) +``` + +`2625a0` = 2,500,000 matches the same hex `send-esdt` independently verified for +"2.5 units of a 6-decimal token", confirming consistent encoding across both +recipes. + +## Pitfalls + +:::danger[Pitfall 1: do not read tx.receiver or tx.value expecting the real destination/amount] +Both point at the sender's own address / zero, by protocol design, for this +transaction shape. The real information is encoded in `tx.data`. +::: + +:::warning[Pitfall 2: a zero nonce encodes as an empty argument, not a literal 00 byte] +This recipe's own decoded output shows an empty string between the two `@` +separators for both zero-nonce entries, worth knowing if you are hand-parsing a +`MultiESDTNFTTransfer` payload instead of trusting the SDK's decoder. +::: + +:::note[Pitfall 3: this recipe deliberately never succeeds against a real balance or token holdings] +It proves the payload is well-formed via a clean "insufficient funds" rejection +(gas is always paid in EGLD, independent of which tokens move), not a confirmed +on-chain transfer. +::: + +## See also + +- [Send an ESDT](/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt) + is the single-token case this recipe generalizes. +- [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld) + is the native-only case. +- [Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) + covers the fetch-then-increment pattern this recipe relies on. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/transactions/send-egld.mdx b/docs/sdk-and-tools/sdk-js/cookbook/transactions/send-egld.mdx new file mode 100644 index 000000000..cf4db5faf --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/transactions/send-egld.mdx @@ -0,0 +1,338 @@ +--- +title: Send EGLD to an address +description: Send a native EGLD transfer with sdk-core's TransfersController against DevnetEntrypoint, the backend/script pattern for when your own code holds the key. +difficulty: beginner +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - transaction + - typescript + - devnet +--- + +Send a native EGLD transfer using sdk-core's `TransfersController` directly +against `DevnetEntrypoint`. No browser, no connected wallet, no sdk-dapp. This is +the pattern for when **your own code holds the private key**: a script, a bot, a +payout job, a backend service. + +If instead you are building a browser dApp where the end user's own wallet should +sign, you want the sdk-dapp flow. Start with +[Read the connected account](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account). +This recipe and that flow build the same kind of transaction two structurally +different ways, for two different trust models. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet with some devnet EGLD. If you do not have one, generate a + disposable wallet with only sdk-core: + +```ts +import { Mnemonic, Account } from '@multiversx/sdk-core'; + +const mnemonic = Mnemonic.generate(); +const account = Account.newFromMnemonic(mnemonic.toString()); +account.saveToPem('./wallet.pem'); +console.log('Address:', account.address.toBech32()); +``` + +Then fund the printed address at the [devnet faucet](https://r3d4.fr/faucet) or +the [devnet web wallet](https://devnet-wallet.multiversx.com). `wallet.pem` is +git-ignored, devnet-only, and throwaway. Never reuse a plain PEM like this for +mainnet funds. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/send-egld +npm install +npm run build +npm start -- ./wallet.pem erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th 0.01 +``` + +## Loading the account + +```ts title="src/account.ts" +// src/account.ts — load a devnet Account from a PEM file and prime its +// local nonce from the network. Framework-agnostic sdk-core, no sdk-dapp +// involved — this is the "your own code holds the keys" pattern (a script, +// a bot, a backend service), not a browser wallet-connected flow. +// +// The sdk-core nonce rule: fetch the account nonce from the network before +// sending transactions, then increment it locally with +// getNonceThenIncrement(). This helper does exactly the fetch half; the +// increment half matters once you send more than one transaction per run. + +import { Account } from '@multiversx/sdk-core'; +import type { DevnetEntrypoint } from '@multiversx/sdk-core'; + +/** + * Loads an Account from a PEM file and sets its local `nonce` from the + * network's current value — the "fetch" half of the fetch-then-increment + * pattern. + * + * `entrypoint.recallAccountNonce(address)` is a thin convenience wrapper + * around `networkProvider.getAccount(address).nonce`, used here instead of + * constructing a network provider by hand. + */ +export async function loadDevnetAccount( + entrypoint: DevnetEntrypoint, + pemPath: string, +): Promise { + const account = await Account.newFromPem(pemPath); + account.nonce = await entrypoint.recallAccountNonce(account.address); + return account; +} +``` + +## Converting the amount + +```ts title="src/amount.ts" +// src/amount.ts — decimal EGLD string <-> smallest-denomination bigint. +// +// EGLD's denomination is 10^18 (1 EGLD = 1000000000000000000). Getting this +// conversion wrong, or doing it with plain JS `Number` math, is a common +// source of bugs. Use BigNumber (already an sdk-core peer dependency) rather +// than floating point, and reject non-integer results outright instead of +// silently truncating them. + +import BigNumber from 'bignumber.js'; + +export const EGLD_DECIMALS = 18; + +/** + * Converts a decimal EGLD amount (e.g. "0.5") to the smallest-denomination + * bigint the network expects (e.g. 500000000000000000n). + * + * Throws if the input has more precision than 18 decimals — silently + * rounding would mean sending a different amount than what was typed. + */ +export function toSmallestDenomination(amountInEgld: string): bigint { + const value = new BigNumber(amountInEgld).multipliedBy( + new BigNumber(10).pow(EGLD_DECIMALS), + ); + if (!value.isFinite() || value.isNegative()) { + throw new Error(`"${amountInEgld}" is not a valid non-negative EGLD amount.`); + } + if (!value.isInteger()) { + throw new Error( + `"${amountInEgld}" EGLD has more precision than the network supports (${EGLD_DECIMALS} decimals).`, + ); + } + return BigInt(value.toFixed(0)); +} +``` + +## Sending + +```ts title="src/sendEgld.ts" +// src/sendEgld.ts — the actual subject of this recipe: sending EGLD via +// sdk-core's TransfersController, not a hand-built Transaction. +// +// Use this pattern when YOUR code holds the private key directly (a script, +// a bot, a backend payout job): sdk-core's purpose-built transfer +// factory/controller does the gas-limit and payload construction for you. +// When the END USER's own wallet should sign in a browser instead, that is +// the sdk-dapp flow, not this one. +// +// How the pieces fit, verified against the installed @multiversx/sdk-core: +// +// entrypoint.createTransfersController() +// -> TransfersController, a thin wrapper: it calls +// TransferTransactionsFactory.createTransactionForNativeTokenTransfer() +// to build the transaction (which computes a correct gasLimit +// itself — minGasLimit + gasLimitPerByte * data.length, per +// TransactionsFactoryConfig — you never need to hardcode 50000 +// yourself), then BaseController.setupAndSignTransaction() sets the +// nonce and SIGNS it. +// controller.createTransactionForNativeTokenTransfer(sender, nonce, options) +// -> returns an ALREADY-SIGNED Transaction, ready for +// entrypoint.sendTransaction(tx). This is the Controller pattern; +// contrast with the Factory pattern, which returns an UNSIGNED +// transaction and leaves nonce/signing to the caller. +// +// One subtlety worth being explicit about: `Account.signTransaction(tx)` +// returns the raw signature bytes (Promise); it does NOT mutate +// `tx` in place. TransfersController assigns `tx.signature` internally, so it +// is a non-issue here — but if you ever build and sign a transfer by hand +// (for example with the Factory pattern), you must assign +// `tx.signature = await account.signTransaction(tx)` yourself. + +import { Address } from '@multiversx/sdk-core'; +import type { Account, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export interface SendEgldInput { + /** Bech32 address of the recipient. */ + receiver: string; + /** Amount in the smallest denomination (10^18 = 1 EGLD) — see src/amount.ts. */ + amountInSmallestDenomination: bigint; +} + +export interface SendEgldOutput { + txHash: string; +} + +/** + * Builds, signs (via TransfersController), and sends a native EGLD + * transfer. `sender.nonce` must already reflect the network's current + * nonce — see src/account.ts's loadDevnetAccount. + */ +export async function sendEgld( + entrypoint: DevnetEntrypoint, + sender: Account, + input: SendEgldInput, +): Promise { + const controller = entrypoint.createTransfersController(); + + const transaction = await controller.createTransactionForNativeTokenTransfer( + sender, + sender.getNonceThenIncrement(), + { + receiver: Address.newFromBech32(input.receiver), + nativeAmount: input.amountInSmallestDenomination, + }, + ); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash }; +} +``` + +## Wiring it together + +```ts title="src/index.ts" +// src/index.ts — CLI entry point. Wires together account.ts, amount.ts, +// and sendEgld.ts into a single runnable command. +// +// Usage: +// npm run build && npm start -- [amountInEgld] +// +// Example: +// npm start -- ./wallet.pem erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th 0.01 + +import { DevnetEntrypoint } from '@multiversx/sdk-core'; +import { loadDevnetAccount } from './account'; +import { toSmallestDenomination } from './amount'; +import { sendEgld } from './sendEgld'; + +async function main(): Promise { + const [pemPath, receiver, amountArg] = process.argv.slice(2); + + if (!pemPath || !receiver) { + console.error('Usage: npm start -- [amountInEgld]'); + process.exitCode = 1; + return; + } + + const amountInEgld = amountArg ?? '0.001'; + + // DevnetEntrypoint() with no options defaults to + // https://devnet-api.multiversx.com, chain ID "D". + const entrypoint = new DevnetEntrypoint(); + + const sender = await loadDevnetAccount(entrypoint, pemPath); + console.log(`Sender: ${sender.address.toBech32()} (nonce ${sender.nonce})`); + console.log(`Receiver: ${receiver}`); + console.log(`Amount: ${amountInEgld} EGLD`); + + const { txHash } = await sendEgld(entrypoint, sender, { + receiver, + amountInSmallestDenomination: toSmallestDenomination(amountInEgld), + }); + + console.log(`\nSent. Transaction hash: ${txHash}`); + console.log(`Explorer: https://devnet-explorer.multiversx.com/transactions/${txHash}`); + + // TransactionStatus wraps the raw string status plus boolean helpers. + const outcome = await entrypoint.awaitCompletedTransaction(txHash); + console.log(`Status: ${outcome.status.status} (successful: ${outcome.status.isSuccessful()})`); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start -- ./wallet.pem 0.01 +``` + +Expected output: + +```text +Sender: erd1... (nonce 42) +Receiver: erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th +Amount: 0.01 EGLD + +Sent. Transaction hash: <64-char hex hash> +Explorer: https://devnet-explorer.multiversx.com/transactions/ +Status: success (successful: true) +``` + +## How it works + +**`TransfersController` computes gas for you.** Unlike hand-building a raw +`Transaction`, where you duplicate the network's own gas formula by hand, +`entrypoint.createTransfersController().createTransactionForNativeTokenTransfer(...)` +sets `gasLimit` to `minGasLimit + gasLimitPerByte * data.length` (50,000 plus +1,500 per byte) automatically, via `TransferTransactionsFactory` internally. That +was confirmed against the installed sdk-core source and against a real devnet +request logged while authoring this recipe (`"gasLimit":50000` for a plain, +data-less transfer). + +**The Controller pattern returns an already-signed transaction.** `sender` (an +`Account`) is passed straight into +`createTransactionForNativeTokenTransfer(sender, nonce, options)`. Internally, +`BaseController.setupAndSignTransaction()` sets the nonce and does +`transaction.signature = await sender.signTransaction(transaction)` for you. That +is what makes the transaction returned by the controller ready to send as-is, +unlike the Factory pattern below. + +## Pitfalls + +:::danger[Pitfall 1: signTransaction returns bytes, it does not mutate the tx] +`Account.signTransaction(transaction)` only computes and *returns* the raw +signature bytes (`Promise`); it does not touch +`transaction.signature` itself. The Controller pattern used in this recipe +assigns it internally, so this does not bite here. But if you ever sign a +transaction by hand, you need `tx.signature = await account.signTransaction(tx);` +explicitly. +::: + +:::warning[Pitfall 2: the Factory pattern returns an unsigned transaction] +If you use +`TransferTransactionsFactory.createTransactionForNativeTokenTransfer(senderAddress, options)` +directly instead of `TransfersController`, you get back a transaction with +`nonce: 0n` and no signature. You must set the nonce and sign it yourself before +sending. +::: + +:::warning[Pitfall 3: the nonce trap applies here too] +This recipe's `loadDevnetAccount()` fetches the nonce once per process run, which +is correct for a single send. Running two instances of this CLI back-to-back +without waiting for the first to confirm produces a stale, duplicate nonce for +the second. Use the fetch-once, increment-locally pattern across multiple sends. +::: + +:::note[Pitfall 4: amounts beyond 18 decimals throw, on purpose] +`toSmallestDenomination()` rejects (rather than silently rounds) an input with +more precision than EGLD's 18 decimals can represent. Silently truncating would +mean sending a different amount than what was typed. +::: + +## See also + +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the browser + connected-wallet equivalent of this recipe. +- [Send an ESDT](/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt) + is the token-transfer sibling to this native-transfer recipe. +- [Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) + is the fetch-then-increment pattern in depth, for sending more than one + transaction per process run. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt.mdx b/docs/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt.mdx new file mode 100644 index 000000000..856258584 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt.mdx @@ -0,0 +1,221 @@ +--- +title: Send an ESDT +description: Send a fungible ESDT token using sdk-core's TransfersController and the Token/TokenTransfer classes against DevnetEntrypoint, no browser required. +difficulty: beginner +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-core: "^15.4.0" +tags: + - sdk-core + - transaction + - esdt + - typescript + - devnet +--- + +Send a fungible ESDT token using sdk-core's `TransfersController` and the `Token` / +`TokenTransfer` classes, directly against `DevnetEntrypoint`. Sibling recipe to +[Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld); +same Controller pattern, this time for a custom token instead of the native one. + +If instead you are building a browser dApp where the end user's own wallet should +sign, see +[Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send). +This recipe is for when **your own code holds the private key**. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet with some devnet EGLD (for gas), see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites for how to generate and fund a throwaway one. +- A devnet ESDT token identifier you (or your test wallet) already hold a balance + of, plus **its exact decimal count**, see Pitfall 1. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/send-esdt +npm install +npm run build +npm start -- ./wallet.pem erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th TEST-abcdef 2.5 6 +``` + +(2.5 units of a 6-decimal token, adjust the last argument to match the token you +are actually sending.) + +## Converting the amount + +```ts title="src/amount.ts" +// src/amount.ts — decimal token-amount string <-> smallest-denomination +// bigint, for a token with an arbitrary (not fixed-18) number of decimals. +// +// Unlike EGLD (always 18 decimals), an ESDT's decimal count is per-token — +// set at issuance (the `numDecimals` argument) and readable from the token's +// properties on-chain. This recipe takes it as an explicit parameter rather +// than assuming 18, since assuming EGLD's decimals for an arbitrary ESDT +// would silently send the wrong amount by orders of magnitude. + +import BigNumber from 'bignumber.js'; + +/** + * Converts a decimal token amount (e.g. "2.5") to the smallest-denomination + * bigint the network expects, given the token's own decimal count. + * + * Throws if the input has more precision than `numDecimals` supports. + */ +export function toSmallestDenomination(amount: string, numDecimals: number): bigint { + const value = new BigNumber(amount).multipliedBy(new BigNumber(10).pow(numDecimals)); + if (!value.isFinite() || value.isNegative()) { + throw new Error(`"${amount}" is not a valid non-negative token amount.`); + } + if (!value.isInteger()) { + throw new Error( + `"${amount}" has more precision than this token supports (${numDecimals} decimals).`, + ); + } + return BigInt(value.toFixed(0)); +} +``` + +## Sending + +```ts title="src/sendEsdt.ts" +// src/sendEsdt.ts — the actual subject of this recipe: sending a fungible +// ESDT via sdk-core's TransfersController and the Token/TokenTransfer +// classes. +// +// Sibling recipe to "Send EGLD to an address" — same Controller pattern, +// same "your own code holds the keys" scope (see that recipe for the +// contrast with the browser + connected-wallet flow). The only structural +// difference is which factory method builds the transaction and what +// `resources` input shape it takes. +// +// One easy-to-miss, source-verified naming gotcha this recipe deliberately +// gets right: the CONTROLLER method is +// `createTransactionForEsdtTokenTransfer` (lowercase "sdt"), while the +// underlying FACTORY method it wraps is +// `createTransactionForESDTTokenTransfer` (uppercase "ESDT") — confirmed +// from node_modules/@multiversx/sdk-core/out/transfers/transfersControllers.d.ts +// vs .../transferTransactionsFactory.d.ts. Copying the Factory's casing +// onto a Controller call (or vice versa) is a real `tsc --strict` error, +// not a style nit — TypeScript's property-name matching is exact. + +import { Address, Token, TokenTransfer } from '@multiversx/sdk-core'; +import type { Account, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export interface SendEsdtInput { + /** Bech32 address of the recipient. */ + receiver: string; + /** Token identifier, e.g. "TICKER-abcdef" (fungible — no nonce). */ + tokenIdentifier: string; + /** Amount in the token's own smallest denomination — see src/amount.ts. */ + amountInSmallestDenomination: bigint; +} + +export interface SendEsdtOutput { + txHash: string; +} + +/** + * Builds, signs (via TransfersController), and sends a single fungible ESDT + * transfer. `sender.nonce` must already reflect the network's current + * nonce — see src/account.ts's loadDevnetAccount. + */ +export async function sendEsdt( + entrypoint: DevnetEntrypoint, + sender: Account, + input: SendEsdtInput, +): Promise { + const controller = entrypoint.createTransfersController(); + + const token = new Token({ identifier: input.tokenIdentifier }); + const transfer = new TokenTransfer({ token, amount: input.amountInSmallestDenomination }); + + const transaction = await controller.createTransactionForEsdtTokenTransfer( + sender, + sender.getNonceThenIncrement(), + { + receiver: Address.newFromBech32(input.receiver), + tokenTransfers: [transfer], + }, + ); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash }; +} +``` + +## Run it + +```bash +npm start -- ./wallet.pem +``` + +Expected output: + +```text +Sender: erd1... (nonce 42) +Receiver: erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th +Token: TEST-abcdef +Amount: 2.5 (6 decimals) + +Sent. Transaction hash: <64-char hex hash> +Explorer: https://devnet-explorer.multiversx.com/transactions/ +Status: success (successful: true) +``` + +## How it works + +**`Token` + `TokenTransfer` describe what to send; `TransfersController` builds +and signs the transaction.** `new Token({ identifier })` for a fungible token +needs no `nonce` field (that is only for NFTs/SFTs). +`controller.createTransactionForEsdtTokenTransfer(sender, nonce, { receiver, tokenTransfers: [transfer] })` +builds the ESDT payload, computes gas, sets the nonce, and signs, the same +Controller pattern as +[Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld). + +**Gas for an ESDT transfer is higher than a plain EGLD one, computed for you.** +The factory underneath adds `gasLimitESDTTransfer` (200,000) plus a fixed 100,000 +on top of the base data-movement gas. Verified against a real devnet request: +sending 2.5 units of a 6-decimal token produced a payload +`ESDTTransfer@544553542d616263646566@2625a0` (decodes to `ESDTTransfer` + +hex("TEST-abcdef") + hex(2,500,000)) with `gasLimit: 413000`, exactly +`(50,000 + 1,500 x 42 bytes) + 200,000 + 100,000`. + +## Pitfalls + +:::danger[Pitfall 1: guessing a token's decimal count is a real footgun] +EGLD is always 18 decimals; an ESDT's decimal count is chosen by whoever issued +it and varies token to token, 6, 8, and 18 are all common. Sending +`amount * 10^18` to a 6-decimal token would be off by 10^12. This recipe requires +`numDecimals` as an explicit argument rather than defaulting it, look the real +value up before running this against a token you do not already know the decimals +for. +::: + +:::warning[Pitfall 2: the Controller method's casing does not match the Factory it wraps] +It is `createTransactionForEsdtTokenTransfer` (lowercase "sdt") on +`TransfersController`, but `createTransactionForESDTTokenTransfer` (uppercase +"ESDT") on the underlying `TransferTransactionsFactory`, confirmed from both +`.d.ts` files directly. Using the wrong casing for the pattern you are using is a +`tsc --strict` error, not a lint nit. +::: + +:::note[Pitfall 3: this sends a single fungible token in one transaction] +For NFTs or SFTs, `Token` needs a non-zero `nonce`; for more than one token +transfer in the same transaction, pass multiple entries in `tokenTransfers` (see +[Multi-token transfer in one tx](/sdk-and-tools/sdk-js/cookbook/transactions/multi-token-transfer)). +::: + +## See also + +- [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld) + is the native-transfer sibling to this recipe; same Controller pattern. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the browser, connected-wallet equivalent of this recipe. +- [Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) + covers the fetch-then-increment pattern in depth, for sending more than one + transaction per process run. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/transactions/track-transaction-status.mdx b/docs/sdk-and-tools/sdk-js/cookbook/transactions/track-transaction-status.mdx new file mode 100644 index 000000000..7ab2898f8 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/transactions/track-transaction-status.mdx @@ -0,0 +1,444 @@ +--- +title: Track a transaction (WebSocket + polling fallback) +description: Track a transaction's live status via sdk-dapp's WebSocket-with-polling-fallback tracker and the pending/successful/failed hooks. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-dapp: "^5.6.0" + sdk-core: "^15.4.0" +tags: + - sdk-dapp + - transaction + - websocket + - vite + - react + - typescript + - devnet +--- + +Track a transaction's live status after sending it. sdk-dapp's +`trackTransactions()` monitors status via WebSocket with a polling fallback, +configured during `initApp` via `transactionTracking` callbacks. It has two +halves: the `transactionTracking` config in `initApp`, and the flat +React hooks (`useGetPendingTransactions`, `useGetSuccessfulTransactions`, +`useGetFailedTransactions`) that re-render automatically as status changes, no +polling code of your own anywhere in this recipe. + +## Prerequisites + +- Node.js >= 20.13.1. +- A MultiversX wallet with devnet access, to click through the demo. **Not + required to verify this recipe.** + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/track-transaction-status +npm install +npm run dev +``` + +Open the HTTPS URL Vite prints, accept the local dev certificate warning, connect +a wallet, then send the demo transaction. + +## Configuring transactionTracking + +```ts title="src/lib/multiversx.ts" +// src/lib/multiversx.ts — environment configuration for sdk-dapp's initApp, +// PLUS the `transactionTracking` config block this recipe is actually +// about. +// +// A confirmed, real discrepancy some docs get wrong: a snippet showing +// `onFail: (sessionId, error) => { /* callback */ }` implies two +// parameters. The real type, read directly from +// node_modules/@multiversx/sdk-dapp/out/methods/initApp/initApp.types.d.ts: +// +// export type TransactionTrackingConfigType = { +// successfulToastLifetime?: number; +// onSuccess?: (sessionId: string) => Promise; +// onFail?: (sessionId: string) => Promise; +// }; +// +// takes only `sessionId` — there is no `error` parameter. Confirmed again +// at the actual call site +// (out/methods/trackTransactions/helpers/checkTransactionStatus/helpers/checkBatch/helpers/runSessionCallbacks.cjs): +// `onSuccess?.(sessionId)` / `onFail?.(sessionId)`, always exactly one +// argument. Declaring `onFail: (sessionId, error) => {...}` as a two +// -parameter function literal here is a real `tsc --strict` error (the +// declared function requires 2 arguments; the type only ever supplies 1) +// — not a style nit. This file uses the confirmed-correct one-parameter +// signature. + +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +const WALLET_CONNECT_PROJECT_ID = + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; + +// A tiny, page-scoped log the demo UI reads from — see TransactionTracker.tsx. +// Not part of the sdk-dapp API surface; just how this recipe surfaces the +// two callbacks' firing on screen instead of only in the console. +export const trackingEvents: string[] = []; + +export const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + nativeAuth: true, + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + // The actual subject of this recipe's config side. `onSuccess`/`onFail` + // fire once per tracked SESSION (a group of one or more transactions + // sent together via the same `.track()` call), not once per + // transaction — confirmed from runSessionCallbacks.cjs, which also + // shows `onFail` fires for FOUR terminal states, not just a generic + // "fail": TransactionBatchStatusesEnum.fail, .cancelled, .timedOut, + // and .invalid all route to the same onFail callback. + transactionTracking: { + successfulToastLifetime: 5000, + onSuccess: async (sessionId: string): Promise => { + trackingEvents.push(`onSuccess(sessionId=${sessionId}) at ${new Date().toLocaleTimeString()}`); + }, + onFail: async (sessionId: string): Promise => { + trackingEvents.push(`onFail(sessionId=${sessionId}) at ${new Date().toLocaleTimeString()}`); + }, + }, + }, +}; + +export { EnvironmentsEnum }; +``` + +## Sending and handing off to the tracker + +```ts title="src/transactions.ts" +// src/transactions.ts — the canonical sign / send / track flow (same +// verified pattern as the sign-and-send recipe's lib/transactions.ts), +// sending a trivial self-transfer of 0 EGLD purely as a vehicle to observe +// tracked status change — this recipe is about what happens AFTER +// `.track()` is called, not about the transaction itself. + +import { Address, Transaction } from '@multiversx/sdk-core'; +import { GAS_PRICE, GAS_LIMIT } from '@multiversx/sdk-dapp/out/constants/mvx.constants'; +import { TransactionManager } from '@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager'; +import { getAccount } from '@multiversx/sdk-dapp/out/methods/account/getAccount'; +import { getNetworkConfig } from '@multiversx/sdk-dapp/out/methods/network/getNetworkConfig'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import type { SignedTransactionType } from '@multiversx/sdk-dapp/out/types/transactions.types'; + +export interface SendTrackedTransactionOutput { + sessionId: string; + transactionHash: string; +} + +/** + * Builds a zero-value self-transfer (sender === receiver), signs it via + * the connected provider, sends it, and starts tracking it — + * `TransactionManager.track()` is the call that hands the sent + * transaction over to sdk-dapp's WebSocket-with-polling-fallback tracker. + * Returns the sessionId the pending/successful/failed hooks key off of. + */ +export async function sendTrackedTransaction(): Promise { + const account = getAccount(); + const { network } = getNetworkConfig(); + + if (!account.address) { + throw new Error('No account connected. Call after a successful login.'); + } + + const selfAddress = Address.newFromBech32(account.address); + + const tx = new Transaction({ + sender: selfAddress, + receiver: selfAddress, + value: 0n, + gasLimit: BigInt(GAS_LIMIT), + gasPrice: BigInt(GAS_PRICE), + chainID: network.chainId, + nonce: BigInt(account.nonce), + version: 1, + }); + + const provider = getAccountProvider(); + const [signed] = await provider.signTransactions([tx]); + if (!signed) { + throw new Error('User cancelled signing.'); + } + + const txManager = TransactionManager.getInstance(); + const sentTransactions = await txManager.send([signed]); + + if (!isFlatSentTransactions(sentTransactions)) { + throw new Error('Unexpected response shape from TransactionManager.send().'); + } + const [sent] = sentTransactions; + if (!sent) { + throw new Error('Send failed: TransactionManager returned no transactions.'); + } + + // THE actual subject of this recipe: handing the sent transaction to the + // tracker. Everything that happens after this line — the WebSocket + // subscription, the polling-interval fallback, the pending/successful/ + // failed hook updates, and the onSuccess/onFail callbacks configured in + // src/lib/multiversx.ts — runs on its own, with no further code needed + // here. + const sessionId = await txManager.track(sentTransactions, { + transactionsDisplayInfo: { + processingMessage: 'Tracking self-transfer…', + successMessage: 'Tracked transaction confirmed.', + errorMessage: 'Tracked transaction failed.', + }, + }); + + return { sessionId, transactionHash: sent.hash }; +} + +function isFlatSentTransactions( + value: SignedTransactionType[] | SignedTransactionType[][], +): value is SignedTransactionType[] { + return value.length === 0 || !Array.isArray(value[0]); +} +``` + +## Reading live status + +```tsx title="src/TransactionTracker.tsx" +// src/TransactionTracker.tsx — the actual subject of this recipe: reading +// LIVE tracked-transaction status via sdk-dapp's flat React hooks, while a +// transaction moves from "pending" to "successful" or "failed" — updated +// by the WebSocket-with-polling-fallback tracker `sendTrackedTransaction()` +// (src/transactions.ts) hands the transaction to, with no manual refresh +// needed anywhere in this component. +// +// `useGetPendingTransactions()` / `useGetSuccessfulTransactions()` / +// `useGetFailedTransactions()` ALL return a flat `SignedTransactionType[]` +// — confirmed from their .d.ts files +// (node_modules/@multiversx/sdk-dapp/out/react/transactions/*.d.ts), the +// same shape the sign-and-send recipe already confirmed for the pending +// case alone. This recipe confirms the successful/failed hooks share that +// exact shape too. Each array reflects the WHOLE store's current state, not +// scoped to one sessionId — fine here since this demo only ever tracks one +// session at a time; for multiple concurrent sessions, the *Sessions +// variants (useGetPendingTransactionsSessions(), returning +// Record) are the ones keyed by +// sessionId — see this recipe's Pitfall 3. + +import { useState } from 'react'; +import { useGetPendingTransactions } from '@multiversx/sdk-dapp/out/react/transactions/useGetPendingTransactions'; +import { useGetSuccessfulTransactions } from '@multiversx/sdk-dapp/out/react/transactions/useGetSuccessfulTransactions'; +import { useGetFailedTransactions } from '@multiversx/sdk-dapp/out/react/transactions/useGetFailedTransactions'; +import { sendTrackedTransaction } from './transactions'; +import { trackingEvents } from './lib/multiversx'; + +export function TransactionTracker(): JSX.Element { + const [status, setStatus] = useState<'idle' | 'signing' | 'error'>('idle'); + const [error, setError] = useState(null); + const [lastSessionId, setLastSessionId] = useState(null); + + // Flat arrays, re-rendered automatically as the store updates — no + // polling or WebSocket code anywhere in THIS component. That machinery + // lives entirely inside sdk-dapp, started the moment + // sendTrackedTransaction() calls TransactionManager.track(). + const pending = useGetPendingTransactions(); + const successful = useGetSuccessfulTransactions(); + const failed = useGetFailedTransactions(); + + const handleSend = (): void => { + setStatus('signing'); + setError(null); + sendTrackedTransaction() + .then(({ sessionId }) => { + setLastSessionId(sessionId); + setStatus('idle'); + }) + .catch((err: unknown) => { + setError(err instanceof Error ? err.message : String(err)); + setStatus('error'); + }); + }; + + return ( +
+

Track a transaction

+ + + {lastSessionId && ( +

+ Last sessionId: {lastSessionId} +

+ )} + {error &&

Error: {error}

} + +
+
+

useGetPendingTransactions()

+

{pending.length} pending

+
    + {pending.map((tx) => ( +
  • + {tx.hash.slice(0, 12)}… — {tx.status ?? 'unknown'} +
  • + ))} +
+
+
+

useGetSuccessfulTransactions()

+

{successful.length} successful

+
    + {successful.map((tx) => ( +
  • + {tx.hash.slice(0, 12)}… — {tx.status ?? 'unknown'} +
  • + ))} +
+
+
+

useGetFailedTransactions()

+

{failed.length} failed

+
    + {failed.map((tx) => ( +
  • + {tx.hash.slice(0, 12)}… — {tx.status ?? 'unknown'} +
  • + ))} +
+
+
+ +

transactionTracking.onSuccess / onFail events (src/lib/multiversx.ts)

+ {trackingEvents.length === 0 ? ( +

(none fired yet)

+ ) : ( +
    + {trackingEvents.map((event) => ( +
  • + {event} +
  • + ))} +
+ )} +
+ ); +} + +const cardStyle: React.CSSProperties = { + border: '1px solid #444', + borderRadius: '8px', + padding: '1.25rem', + marginTop: '1rem', +}; + +const buttonStyle: React.CSSProperties = { + padding: '0.75rem 1.25rem', + fontSize: '1rem', + cursor: 'pointer', +}; + +const columnsStyle: React.CSSProperties = { + display: 'grid', + gridTemplateColumns: '1fr 1fr 1fr', + gap: '1rem', + marginTop: '1rem', +}; + +const errorStyle: React.CSSProperties = { + color: '#e05d44', +}; + +const rawStyle: React.CSSProperties = { + color: '#888', + fontSize: '0.85em', +}; +``` + +## How it works + +**All three status hooks return a flat array, not a sessionId-keyed object.** +Confirmed from the installed `.d.ts` files: `useGetPendingTransactions()`, +`useGetSuccessfulTransactions()`, and `useGetFailedTransactions()` all return +`SignedTransactionType[]`. This extends what +[Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) +already confirmed for the pending case alone to the successful/failed hooks too. +Each reflects the whole store's current tracked state, not scoped to one +sessionId, the `*Sessions` variants are what you would use for multiple concurrent +sessions (see Pitfall 3). + +**The actual WebSocket-vs-polling mechanism, read directly from the compiled +source** (`trackTransactions.cjs`), not guessed from a one-line description: + +- `trackTransactions()` subscribes to the store's `websocketStatus` field. +- While the WebSocket is `PENDING` or not yet initialized, it runs + `setInterval(checkTransactionStatus, pollingInterval)`, the polling fallback. +- Once status flips to `COMPLETED`, it clears that interval and switches to + checking status only when a NEW `websocketEvent` arrives in the store, no + interval running while the socket is live. +- `pollingInterval` is `max(1000, roundDuration / 2)` when the network's block + round duration is known, falling back to a fixed `90000` (90 seconds, confirmed + from `constants/transactions.constants.cjs`) only when it is not. This is a + different constant, and a more adaptive mechanism, than sdk-core's own + `TransactionWatcher` default (every 6000ms, timeout after 90000ms), that class + is a separate, backend-oriented poller used by + `entrypoint.awaitCompletedTransaction()`, not what powers these browser hooks. + +**A confirmed, real bug some docs get wrong in their `transactionTracking` +snippet.** A snippet showing `onFail: (sessionId, error) => { }` implies two +parameters. The real type: + +```ts +export type TransactionTrackingConfigType = { + successfulToastLifetime?: number; + onSuccess?: (sessionId: string) => Promise; + onFail?: (sessionId: string) => Promise; +}; +``` + +Only `sessionId`, confirmed again at the actual call site +(`onSuccess?.(sessionId)` / `onFail?.(sessionId)`, always one argument). Tested +directly: declaring a two-parameter `onFail` fails `tsc --strict` with +`Target signature provides too few arguments. Expected 2 or more, but got 1.` + +**`onSuccess`/`onFail` fire once per tracked SESSION, and `onFail` covers four +terminal states**, `fail`, `cancelled`, `timedOut`, and `invalid` all route to the +same callback, confirmed from source. + +## Pitfalls + +:::danger[Pitfall 1: a two-parameter onFail fails tsc --strict] +Use a single-parameter `(sessionId: string) => Promise`, see "How it works" +above for the exact error message this produces. +::: + +:::warning[Pitfall 2: do not assume a fixed polling interval] +It adapts to the network's actual round duration when known; the 90-second +constant is a fallback, not the steady-state behavior on a healthy connection to a +live network. +::: + +:::note[Pitfall 3: the flat hooks do not scope by sessionId] +If your app tracks multiple independent sessions concurrently and needs each one's +status separately, use the `*Sessions` variants +(`useGetPendingTransactionsSessions()` and friends), keyed by sessionId, instead +of filtering the flat arrays yourself. +::: + +## See also + +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + covers the build, sign, send steps this recipe's `transactions.ts` shares, + without the tracking focus. +- [Read the connected account with useGetAccount](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + applies the same "flat hooks reflect live store state, no manual refresh" + pattern to account data. +- [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld) + is the sdk-core-only equivalent for backend/script contexts, where a different + poller (`TransactionWatcher`) plays the analogous role. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login.mdx b/docs/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login.mdx new file mode 100644 index 000000000..20bc00b2f --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login.mdx @@ -0,0 +1,423 @@ +--- +title: Log in with the DeFi Wallet browser extension +description: A dedicated single-provider login button using ProviderFactory directly, with real extension detection and no generic multi-provider picker UI. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-dapp: "^5.6.0" +tags: + - sdk-dapp + - wallet + - wallet-extension + - react + - typescript + - devnet +--- + +[Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button) +covers the common case: one button, `UnlockPanelManager` picks the provider. +This recipe is for the other case. You want a single, dedicated button for +exactly one provider, the DeFi Wallet browser extension, with no picker in +between. That means talking to `ProviderFactory` directly instead of +`UnlockPanelManager`. + +Use this when your dApp only supports one wallet, or when you want +provider-specific buttons side by side (a "Connect DeFi Wallet" next to a +"Connect xPortal", see +[Login via xPortal / WalletConnect](/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login) +for that sibling). + +## Prerequisites + +- A working sdk-dapp v5 setup (Next.js or Vite, see the start-here recipes). +- The DeFi Wallet browser extension, to test the connected path. The recipe + handles its absence gracefully (see below) so you can also see that path + without installing anything. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/defi-extension-login +npm install +npm run dev +# open https://localhost:5173 +``` + +## The button + +```tsx title="src/ExtensionLoginButton.tsx" +// src/ExtensionLoginButton.tsx — a dedicated, single-provider login button. +// +// Unlike "Adding a wallet login button" (which opens a picker listing every +// registered provider via UnlockPanelManager), this component talks to +// ProviderFactory directly for exactly one provider: the DeFi Wallet +// browser extension. Use this pattern when your dApp only supports one +// wallet, or when you want a dedicated "Connect with DeFi Wallet" button +// alongside (not inside) the generic picker. +// +// The full sequence — verified against the actual sdk-dapp source, not +// just its .d.ts files, because the .d.ts alone doesn't say whether extra +// steps are needed after provider.login(): +// +// ProviderFactory.create({ type: ProviderTypeEnum.extension }) +// → internally calls setAccountProvider() for you (see +// node_modules/@multiversx/sdk-dapp/out/providers/ProviderFactory.cjs) +// provider.login() +// → internally dispatches BOTH the login-info store action and the +// account store action (see +// node_modules/@multiversx/sdk-dapp/out/providers/DappProvider/helpers/login/helpers/accountLogin.cjs) +// +// So after `await provider.login()` resolves, useGetIsLoggedIn() and +// useGetAccount() already reflect the new session — no manual store +// wiring needed beyond calling these two functions in order. + +import { useState } from 'react'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { ProviderFactory } from '@multiversx/sdk-dapp/out/providers/ProviderFactory'; +import { ProviderTypeEnum } from '@multiversx/sdk-dapp/out/providers/types/providerFactory.types'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; + +// The DeFi Wallet extension injects `window.multiversxWallet` once it's +// installed and active (`window.elrondWallet` is the deprecated predecessor +// — the SDK still checks both). This is the SAME check +// ExtensionProviderStrategy performs internally +// (node_modules/@multiversx/sdk-extension-provider/out/extensionProvider.js); +// checking it here up-front lets us show an install link instead of a +// cryptic error after the user clicks. The ambient `Window.multiversxWallet` +// type comes from @multiversx/sdk-extension-provider's own `declare global` +// block (its extensionProvider.d.ts) — a transitive dependency of sdk-dapp. +function isExtensionInstalled(): boolean { + return ( + typeof window !== 'undefined' && + Boolean(window.multiversxWallet || window.elrondWallet) + ); +} + +export function ExtensionLoginButton(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const [status, setStatus] = useState<'idle' | 'connecting' | 'error'>('idle'); + const [error, setError] = useState(null); + + const handleConnect = async (): Promise => { + setStatus('connecting'); + setError(null); + try { + const provider = await ProviderFactory.create({ + type: ProviderTypeEnum.extension, + }); + // nativeAuth is enabled in this recipe's dappConfig, so login() + // generates its own native-auth token internally — no `token` arg + // needed here. Pass one only if you're supplying a custom token. + await provider.login(); + setStatus('idle'); + } catch (err) { + // ExtensionProviderStrategy throws "Extension provider is not + // initialised, call init() first" if the extension wasn't detected at + // provider-creation time (see + // node_modules/@multiversx/sdk-extension-provider/out/extensionProvider.js). + // The isExtensionInstalled() pre-check above should catch this case + // before we ever get here, but the catch stays as defense in depth — + // e.g. the user disables the extension between the check and the click. + const message = err instanceof Error ? err.message : String(err); + setError(message); + setStatus('error'); + } + }; + + const handleDisconnect = async (): Promise => { + const provider = getAccountProvider(); + await provider.logout(); + }; + + if (isLoggedIn) { + return ( + + ); + } + + if (!isExtensionInstalled()) { + return ( + + Install DeFi Wallet extension + + ); + } + + return ( + <> + + {status === 'error' && error && ( +

+ Connection failed: {error} +

+ )} + + ); +} + +const buttonStyle: React.CSSProperties = { + padding: '0.75rem 1.25rem', + fontSize: '1rem', + cursor: 'pointer', +}; +``` + +## The Window type declaration + +The extension injects `window.multiversxWallet`, but that global's type does not +arrive through the deep `@multiversx/sdk-dapp/out/...` import paths this recipe +uses. Declare it locally, or `tsc --strict` reports +`Property 'multiversxWallet' does not exist on type 'Window'`: + +```ts title="src/vite-env.d.ts" +// src/vite-env.d.ts — in a real Vite project this file also carries the line +// `/// ` for import.meta.env typing. The +// augmentation below is the part this recipe specifically needs. +// +// @multiversx/sdk-extension-provider declares this same augmentation in its +// own extensionProvider.d.ts (a `declare global { interface Window { ... } }` +// block), but that file only enters a project's compilation if something +// imports a TYPE from that package directly. This project only imports deep +// @multiversx/sdk-dapp/out/... paths, so the augmentation never gets pulled +// in transitively — redeclared here with the identical shape so +// `window.multiversxWallet` / `window.elrondWallet` type-check. Verified +// against node_modules/@multiversx/sdk-extension-provider/out/extensionProvider.d.ts +// on the actually-installed @multiversx/sdk-dapp v5's dependency tree. +interface Window { + /** @deprecated Use `multiversxWallet` instead. */ + elrondWallet?: { + extensionId: string; + }; + multiversxWallet?: { + extensionId: string; + }; +} +``` + +## The demo page + +```tsx title="src/App.tsx" +// src/App.tsx — demo page for ExtensionLoginButton. + +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { ExtensionLoginButton } from './ExtensionLoginButton'; + +export function App(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const account = useGetAccount(); + const { network } = useGetNetworkConfig(); + + return ( +
+

Login via the DeFi Wallet extension

+

+ Network: {network.chainId} +

+ + + + {isLoggedIn && ( +

+ Connected as {account.address}. +

+ )} +
+ ); +} +``` + +## Provider bootstrap + +`providers.tsx` calls `initApp()` once and gates rendering until the store is +ready; `lib/multiversx.ts` holds the environment config it passes in. This +recipe does not call `UnlockPanelManager.init()` in the bootstrap, because it +never uses the panel. + +```tsx title="src/providers.tsx" +// src/providers.tsx — sdk-dapp v5 init wrapper. +// +// Deliberately does NOT configure UnlockPanelManager. This recipe bypasses +// the generic multi-provider picker entirely and talks to ProviderFactory +// directly for one specific provider — see src/ExtensionLoginButton.tsx. +// If you want the picker instead, see "Adding a wallet login button." + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { dappConfig } from './lib/multiversx'; + +let initStarted = false; + +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; + + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + setReady(true); + }); + + return () => { + cancelled = true; + }; + }, []); + + if (!ready) { + return ( +
+ Initializing wallet SDK… +
+ ); + } + + return <>{children}; +} +``` + +```ts title="src/lib/multiversx.ts" +// src/lib/multiversx.ts — environment configuration for sdk-dapp's initApp. +// Same shape as every other Vite recipe in this Cookbook. + +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +const WALLET_CONNECT_PROJECT_ID = + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; + +export const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + nativeAuth: true, + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + }, +}; + +export { EnvironmentsEnum }; +``` + +## How it works + +**`ProviderFactory.create()` already registers the provider.** Verified against +the actual sdk-dapp source +(`node_modules/@multiversx/sdk-dapp/out/providers/ProviderFactory.cjs`), not +inferred from the `.d.ts` alone: `create()`'s last step before returning is +calling `setAccountProvider()` internally. You never call that yourself. + +**`provider.login()` already populates the whole store.** Also verified against +source +(`node_modules/@multiversx/sdk-dapp/out/providers/DappProvider/helpers/login/helpers/accountLogin.cjs`): +it dispatches the login-info store action, fetches and dispatches the account, +registers the websocket listener, and starts transaction tracking, all before +the returned promise resolves. So this really is the complete sequence: + +```ts +const provider = await ProviderFactory.create({ type: ProviderTypeEnum.extension }); +await provider.login(); +// useGetIsLoggedIn() / useGetAccount() already reflect the new session here. +``` + +**Detecting the extension before the click, not after.** +`ExtensionProviderStrategy` checks `window.multiversxWallet || window.elrondWallet` +internally +(`node_modules/@multiversx/sdk-extension-provider/out/extensionProvider.js`). If +neither is present it silently sets an internal flag rather than throwing, and +the throw only happens later, on `login()`. This recipe runs the identical check +in the UI layer first, so a user without the extension sees an install link +instead of a button that is guaranteed to fail. + +## Pitfalls + +:::warning[Pitfall 1: the not-installed error throws from login(), not create()] +`ProviderFactory.create({ type: ProviderTypeEnum.extension })` calls the +strategy's `init()` internally, which just sets a flag to `false` if the +extension is not found. It does not throw, and `create()` still returns a +provider object successfully. The actual throw +(`"Extension provider is not initialised, call init() first"`) only happens on +the next call, e.g. `login()`. Pre-check with `isExtensionInstalled()` (as this +recipe does) rather than relying on `create()` to fail fast. +::: + +:::warning[Pitfall 2: window.multiversxWallet needs a local type declaration] +`@multiversx/sdk-extension-provider` declares this global itself, but that +`.d.ts` only enters your compilation if something imports a *type* from that +package directly. This recipe only imports deep `@multiversx/sdk-dapp/out/...` +paths, so the augmentation never arrives transitively. `src/vite-env.d.ts` +redeclares the identical shape locally. +::: + +:::warning[Pitfall 3: this button only ever shows the extension option] +It is provider-specific by design, but that means a mobile user, or a desktop +user without the extension, hits a dead end unless you also offer another path. +Pair it with +[Login via xPortal / WalletConnect](/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login), +or use +[Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button)'s +picker instead if you would rather the SDK decide which providers to surface. +::: + +:::warning[Pitfall 4: HTTPS required] +The DeFi extension refuses `http://localhost`. See +[Local HTTPS for dApp dev](/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup) +for a fully trusted mkcert alternative to this recipe's self-signed dev certificate. +::: + +## See also + +- [Login via xPortal / WalletConnect](/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login) + is the same `ProviderFactory` pattern, for the mobile-QR provider. +- [Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button) + is the multi-provider picker alternative to this dedicated single-provider button. +- [Read the connected account](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + is what to do once `provider.login()` resolves. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/wallets/ledger-login.mdx b/docs/sdk-and-tools/sdk-js/cookbook/wallets/ledger-login.mdx new file mode 100644 index 000000000..265683670 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/wallets/ledger-login.mdx @@ -0,0 +1,420 @@ +--- +title: Log in with a Ledger hardware wallet +description: A dedicated Ledger login button using ProviderFactory directly, including the anchor element the SDK renders its own device-connect and account-picker UI into. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-dapp: "^5.6.0" +tags: + - sdk-dapp + - wallet + - ledger + - react + - typescript + - devnet +--- + +A dedicated, single-provider login button for a Ledger hardware wallet. It is the +same `ProviderFactory` pattern as +[Login via the DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login) +and +[Login via xPortal / WalletConnect](/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login), +this time for `ProviderTypeEnum.ledger`. Ledger needs the same real DOM anchor +WalletConnect does, but for a different reason: instead of a QR code, the SDK +renders its own device-connect screen, a paginated account picker, and a confirm +screen into it. + +Use this when you want a dedicated "Connect Ledger" button, and when your users +are expected to bring physical hardware wallets. This recipe cannot be exercised +end to end without one, see Pitfall 1. + +## Prerequisites + +- A working sdk-dapp v5 setup (Next.js or Vite, see the start-here recipes). +- A physical Ledger device with the MultiversX app installed and open on it. +- A Chromium-based browser. WebHID/WebUSB, what `@multiversx/sdk-hw-provider` + uses to talk to the device, are not implemented in Firefox or Safari. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/ledger-login +npm install +npm run dev +# open https://localhost:5173 +``` + +## The button + +```tsx title="src/LedgerLoginButton.tsx" +// src/LedgerLoginButton.tsx — a dedicated Ledger hardware-wallet login +// button with an anchor element for the SDK's own device/account UI. +// +// Same overall pattern as "Login via the DeFi extension" and "Login via +// xPortal / WalletConnect" — ProviderFactory.create() + provider.login() — +// with the anchor requirement specific to Ledger: +// +// ProviderFactory.create({ type: ProviderTypeEnum.ledger, anchor }) needs +// a real HTMLElement. The SDK renders a `` web +// component (from the optional @multiversx/sdk-dapp-ui package — it +// installs automatically as an npm optionalDependency of sdk-dapp, see +// this recipe's README) into that anchor: first a "connecting" screen, +// then a paginated list of the accounts derived from the device (10 per +// page), then a confirm screen while you approve the login on the +// physical device itself. +// +// Everything below is verified against the actually-installed +// @multiversx/sdk-dapp source (not just its .d.ts files), the same standard +// this Cookbook's WalletConnect/DeFi-extension recipes were held to: +// +// ProviderFactory.create({ type: ProviderTypeEnum.ledger, anchor }) +// → constructs `new LedgerProviderStrategy({ anchor })`, then calls +// `LedgerIdleStateManager.getInstance().init()` +// (node_modules/@multiversx/sdk-dapp/out/providers/ProviderFactory.cjs) +// → internally calls setAccountProvider() for you, same as every other +// provider type — no separate call needed. +// strategy.init() +// → connects to the physical device via @multiversx/sdk-hw-provider's +// HWProvider (WebUSB/WebHID under the hood). Because no account is +// logged in yet, this happens eagerly, as part of create() itself — +// not deferred to login() — confirmed from +// .../LedgerProviderStrategy/helpers/getLedgerProvider/getLedgerProvider.cjs: +// `shouldInitProvider = options?.shouldInitProvider || !isLoggedIn`. +// Practically: clicking "Connect Ledger" immediately triggers the +// browser's native device-permission prompt, before any app UI shows. +// provider.login() +// → renders the account list + confirm screen into the anchor via +// LedgerConnectStateManager (confirmed from +// .../LedgerProviderStrategy/helpers/authenticateLedgerAccount/authenticateLedgerAccount.cjs). +// You do NOT pass an addressIndex yourself — the SDK's own UI collects +// it and feeds it back internally. Once the user approves on-device, +// login() resolves the same way it does for every other provider: +// useGetIsLoggedIn() / useGetAccount() are correct immediately after. + +import { useRef, useState } from 'react'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { ProviderFactory } from '@multiversx/sdk-dapp/out/providers/ProviderFactory'; +import { ProviderTypeEnum } from '@multiversx/sdk-dapp/out/providers/types/providerFactory.types'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; + +export function LedgerLoginButton(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const [status, setStatus] = useState<'idle' | 'connecting' | 'error'>('idle'); + const [error, setError] = useState(null); + // The device-connection status, account list, and confirm screen all + // render into this div via a web component the SDK defines at runtime. + // It must exist in the DOM before create() runs — rendered unconditionally + // below, so by the time a click handler fires, anchorRef.current is set. + const anchorRef = useRef(null); + + const handleConnect = async (): Promise => { + if (!anchorRef.current) { + // Should not happen — the anchor div always renders — but keeps the + // types honest under strict null checks rather than asserting `!`. + setError('Ledger anchor not mounted yet.'); + setStatus('error'); + return; + } + + setStatus('connecting'); + setError(null); + try { + // This call itself triggers the browser's WebHID/WebUSB device picker + // — it happens here, not inside login() — see the file header. + const provider = await ProviderFactory.create({ + type: ProviderTypeEnum.ledger, + anchor: anchorRef.current, + }); + // nativeAuth is enabled in this recipe's dappConfig, so login() + // generates its own native-auth token internally, same as every + // other provider. + await provider.login(); + setStatus('idle'); + } catch (err) { + // Two distinct failure classes reach this catch, and this recipe + // cannot tell them apart without a physical device to reproduce + // against — flagged honestly rather than guessed: + // 1. The device/transport layer failing (no device plugged in, + // wrong app open, browser without WebHID/WebUSB support — + // Firefox and Safari do not implement either API). + // 2. The user cancelling from the account list or confirm screen + // rendered inside the anchor. + // sdk-dapp's own internal recovery path (rebuildProvider, used before + // signing) shows a toast reading "Unlock your device & open the + // MultiversX App" for case 1 — a reasonable model for what to tell + // the user here too, but this recipe surfaces the raw message rather + // than pattern-matching on it. + const message = err instanceof Error ? err.message : String(err); + setError(message); + setStatus('error'); + } + }; + + const handleDisconnect = async (): Promise => { + const provider = getAccountProvider(); + await provider.logout(); + }; + + if (isLoggedIn) { + return ( + + ); + } + + return ( +
+ + + {/* The SDK renders its device-connect / account-picker / confirm UI + into this element once ProviderFactory.create() runs. Give it real + dimensions — an empty 0x0 div means that UI has nowhere visible to + draw, the same requirement as the WalletConnect recipe's QR anchor. */} +
+ + {status === 'error' && error && ( +

+ Connection failed: {error} +

+ )} +
+ ); +} + +const buttonStyle: React.CSSProperties = { + padding: '0.75rem 1.25rem', + fontSize: '1rem', + cursor: 'pointer', +}; + +const anchorStyle: React.CSSProperties = { + marginTop: '1rem', + minHeight: '280px', + minWidth: '280px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}; +``` + +## The demo page + +```tsx title="src/App.tsx" +// src/App.tsx — demo page for LedgerLoginButton. + +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { LedgerLoginButton } from './LedgerLoginButton'; + +export function App(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const account = useGetAccount(); + const { network } = useGetNetworkConfig(); + + return ( +
+

Login via Ledger

+

+ Network: {network.chainId} +

+

+ Requires a physical Ledger device with the MultiversX app installed + and open, plus a Chromium-based browser (Chrome, Edge) — WebHID/WebUSB + are not implemented in Firefox or Safari. +

+ + + + {isLoggedIn && ( +

+ Connected as {account.address}. +

+ )} +
+ ); +} +``` + +## Provider bootstrap + +`providers.tsx` calls `initApp()` once and gates rendering until the store is +ready; `lib/multiversx.ts` holds the environment config it passes in. + +```tsx title="src/providers.tsx" +// src/providers.tsx — sdk-dapp v5 init wrapper. +// +// Deliberately does NOT configure UnlockPanelManager — this recipe bypasses +// the generic multi-provider picker and talks to ProviderFactory directly +// for one specific provider. See src/LedgerLoginButton.tsx. + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { dappConfig } from './lib/multiversx'; + +let initStarted = false; + +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; + + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + setReady(true); + }); + + return () => { + cancelled = true; + }; + }, []); + + if (!ready) { + return ( +
+ Initializing wallet SDK… +
+ ); + } + + return <>{children}; +} +``` + +```ts title="src/lib/multiversx.ts" +// src/lib/multiversx.ts — environment configuration for sdk-dapp's initApp. +// Same shape as every other Vite recipe in this Cookbook — see +// vite-react-minimal's README for why the fields are set this way. +// walletConnectV2ProjectId isn't exercised by this recipe (LedgerLoginButton +// talks to ProviderFactory with ProviderTypeEnum.ledger only) but is kept +// here for copy-paste consistency with the rest of the corpus. + +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +const WALLET_CONNECT_PROJECT_ID = + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; + +export const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + nativeAuth: true, + // `theme` takes one of the enum's string values (e.g. 'mvx:dark-theme'); the + // short `'dark'` fails — see node_modules/@multiversx/sdk-dapp/out/methods/initApp/initApp.types.d.ts. + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + }, +}; + +export { EnvironmentsEnum }; +``` + +## How it works + +**The anchor is not optional in practice, the same requirement as the +WalletConnect QR code.** `ProviderFactory.create({ type: ProviderTypeEnum.ledger, anchor })` +passes the anchor straight into `new LedgerProviderStrategy({ anchor })` +(`node_modules/@multiversx/sdk-dapp/out/providers/ProviderFactory.cjs`). The SDK +renders a `` web component into it, from the optional +`@multiversx/sdk-dapp-ui` package, which installs automatically as an +`optionalDependencies` entry of `@multiversx/sdk-dapp` itself; nothing extra to +add to `package.json`. + +**Connecting to the device happens inside `create()`, before `login()` is ever +called.** Because no account is logged in yet on a fresh page load, +`getLedgerProvider()` computes +`shouldInitProvider = options?.shouldInitProvider || !isLoggedIn`, which +evaluates to `true`. That is what triggers the browser's native WebHID/WebUSB +device-permission prompt, as soon as you click "Connect Ledger," before any +account list appears. + +**No `addressIndex` is passed from this component.** The SDK's own UI (rendered +into the anchor) lists the accounts derivable from the device and calls back into +the login flow with the chosen index internally. This component only calls +`provider.login()` with no arguments, the same call shape as every other provider +in this Cookbook. + +## Pitfalls + +:::warning[Pitfall 1: this recipe cannot be verified end-to-end without physical hardware] +Everything above the "physical device" line, the `ProviderFactory` dispatch, the +anchor requirement, the `shouldInitProvider` timing, the account-list hand-off, +is confirmed against the actually-installed `@multiversx/sdk-dapp` source. What a +real device-pairing session looks like in detail (exact error messages on cancel, +exact WebHID permission-prompt wording) is not independently reproduced here. +Treat error messages as unstructured strings to display. +::: + +:::danger[Pitfall 2: Firefox and Safari cannot run this recipe at all] +WebHID and WebUSB are Chromium-only browser APIs. This is a platform limitation, +not something the SDK or this recipe can work around. +::: + +:::warning[Pitfall 3: the Ledger deep-import fix is load-bearing here, not dormant] +The other wallet recipes in this Cookbook carry the same `vite.config.ts` +`optimizeDeps.exclude` / `rollupOptions.external` fix for three `@ledgerhq/devices` +deep-import paths, but only because `ProviderFactory` transitively references the +Ledger strategy. This recipe actually calls into that code path, so dropping the +fix here breaks both `npm run dev` and `npm run build`, not just a path you never +exercise. +::: + +:::note[Pitfall 4: the MultiversX app must be open on the device] +If it is not, or the device goes idle mid-session, sdk-dapp's own recovery path +(used before signing) shows a toast reading "Unlock your device & open the +MultiversX App", confirmed from the compiled source's `rebuildProvider` catch block. +::: + +## See also + +- [Login via the DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login) + is the same `ProviderFactory` pattern for the browser-extension provider. +- [Login via xPortal / WalletConnect](/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login) + is the same pattern, QR-code anchor instead of a device UI. +- [Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button) + is the multi-provider picker alternative to this dedicated button. +- [Read the connected account](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + is what to do once `provider.login()` resolves. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/wallets/native-auth.mdx b/docs/sdk-and-tools/sdk-js/cookbook/wallets/native-auth.mdx new file mode 100644 index 000000000..5862b8901 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/wallets/native-auth.mdx @@ -0,0 +1,470 @@ +--- +title: Native auth, token issuance, expiry, auto-logout +description: How nativeAuth issues a bearer token on login, why loginExpiresAt is not the token's real expiry, and how LogoutManager schedules the warning toast and logout. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-dapp: "^5.6.0" +tags: + - sdk-dapp + - wallet + - native-auth + - react + - typescript + - devnet +--- + +What `nativeAuth` actually gives you once configured: a bearer token issued +automatically on login, an automatic warning toast before it expires, and an +automatic logout when it does, all scheduled by `LogoutManager`, none of it wired +up by hand anywhere in this recipe. + +Use this recipe once you already have login working (see +[Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button)) +and need to understand what to do with the resulting token, or need to control +the auto-logout behavior. Do not use it if you have not enabled `nativeAuth` at +all; the config itself is a one-line `initApp()` option, covered in "Configuring +native auth" below. + +## Prerequisites + +- A working sdk-dapp v5 setup (Next.js or Vite, see the start-here recipes). +- Any wallet provider to log in with. This recipe uses the generic + `UnlockPanelManager` picker. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/native-auth +npm install +npm run dev +# open https://localhost:5173 +``` + +## Configuring native auth + +```ts title="src/lib/multiversx.ts" +// src/lib/multiversx.ts — environment configuration for sdk-dapp's initApp, +// with an explicit (not just `nativeAuth: true`) native auth config so every +// field is visible and documented in one place. +// +// NativeAuthConfigType (node_modules/@multiversx/sdk-dapp/out/services/nativeAuth/nativeAuth.types.d.ts): +// every field is optional; omitted fields fall back to +// getDefaultNativeAuthConfig() (.../services/nativeAuth/methods/getDefaultNativeAuthConfig.cjs): +// origin -> window.location.origin +// apiAddress -> the configured network's API address +// expirySeconds -> 86400 (24h) +// tokenExpirationToastWarningSeconds -> 300 (5 min) +// +// This recipe deliberately overrides both time values to a couple of +// minutes so the auto-logout warning toast and the actual auto-logout are +// both observable within one `npm run dev` session, instead of requiring a +// 24-hour wait. A real dApp should use the defaults (or its own security +// policy's value) — do not ship NATIVE_AUTH_EXPIRY_SECONDS this short. +export const NATIVE_AUTH_EXPIRY_SECONDS = 120; +export const NATIVE_AUTH_WARNING_SECONDS = 30; + +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +// This recipe uses UnlockPanelManager's generic picker (see providers.tsx), +// which offers every registered provider including WalletConnect — so the +// project ID is still needed here even though the recipe's own point is +// native auth, not any one specific provider. Same demo ID used across the +// rest of the corpus; register your own at https://cloud.walletconnect.com. +const WALLET_CONNECT_PROJECT_ID = + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; + +export const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + nativeAuth: { + expirySeconds: NATIVE_AUTH_EXPIRY_SECONDS, + tokenExpirationToastWarningSeconds: NATIVE_AUTH_WARNING_SECONDS, + }, + // `theme` takes one of the enum's string values (e.g. 'mvx:dark-theme'); the + // short `'dark'` fails — see node_modules/@multiversx/sdk-dapp/out/methods/initApp/initApp.types.d.ts. + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + }, +}; + +export { EnvironmentsEnum }; +``` + +## Reading token and expiry state + +```tsx title="src/NativeAuthPanel.tsx" +// src/NativeAuthPanel.tsx — the actual subject of this recipe: reading +// native-auth token state, and the automatic expiry-warning/auto-logout +// behavior that comes with `nativeAuth` for free. +// +// Three things this component demonstrates, all verified against the +// actually-installed @multiversx/sdk-dapp source (not just its .d.ts files): +// +// 1. TOKEN ISSUANCE. Once nativeAuth is configured (src/lib/multiversx.ts) +// and the user logs in, `useGetLoginInfo().tokenLogin.nativeAuthToken` +// is populated automatically — no extra call needed. `TokenLoginType` +// (node_modules/@multiversx/sdk-dapp/out/types/login.types.d.ts) shows +// the field name; it's a bearer token, safe to attach to `Authorization: +// Bearer ` headers on your own backend's API calls. +// +// 2. EXPIRY IS AUTOMATIC, NOT SOMETHING YOU SCHEDULE YOURSELF. +// `DappProvider.login()` calls `LogoutManager.getInstance().init()` as +// its last step (.../providers/DappProvider/DappProvider.cjs). That +// schedules, purely from the values you passed as `nativeAuth` config: +// - a warning toast (`toastId: 'native-auth-expired'`) at +// `tokenExpirationToastWarningSeconds` before real expiry +// - a "Logging out" toast (`toastId: 'native-auth-logout'`) 3 seconds +// before the actual logout +// - the actual logout — `getAccountProvider().logout()` — at +// `expirySeconds` after login +// (.../managers/LogoutManager/LogoutManager.cjs). None of this is wired +// up in this component; it happens because nativeAuth was configured. +// +// 3. `LogoutManager.getInstance().stop()` is the ONLY public knob. It +// clears all three scheduled timers. There's no public "extend the +// session" call — re-arming means calling `.init()` again, which +// re-reads the CURRENT token's real expiry from the store and +// reschedules from there (it does not mint a new token or change +// `expirySeconds` itself). + +import { useGetLoginInfo } from '@multiversx/sdk-dapp/out/react/loginInfo/useGetLoginInfo'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { LogoutManager } from '@multiversx/sdk-dapp/out/managers/LogoutManager/LogoutManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import { NATIVE_AUTH_EXPIRY_SECONDS, NATIVE_AUTH_WARNING_SECONDS } from './lib/multiversx'; + +export function NativeAuthPanel(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const { tokenLogin, providerType, loginExpiresAt } = useGetLoginInfo(); + + if (!isLoggedIn) { + return

Log in to see the issued native-auth token and its expiry state.

; + } + + const token = tokenLogin?.nativeAuthToken; + + const handleLogoutNow = async (): Promise => { + await getAccountProvider().logout(); + }; + + const handleStopAutoLogout = (): void => { + LogoutManager.getInstance().stop(); + }; + + const handleRearmAutoLogout = (): void => { + void LogoutManager.getInstance().init(); + }; + + return ( +
+

Native auth state

+
+
providerType
+
+ {providerType ?? 'none'} +
+ +
nativeAuthToken
+
{token ? {truncate(token)} : not issued}
+ +
configured expirySeconds
+
+ {NATIVE_AUTH_EXPIRY_SECONDS} — real per-token TTL, + baked into the token before the wallet signs it + (`services/nativeAuth/nativeAuth.cjs`'s `initialize()` embeds it as + the token string's 3rd dot-separated segment). +
+ +
configured tokenExpirationToastWarningSeconds
+
+ {NATIVE_AUTH_WARNING_SECONDS} — how long before real + expiry the automatic warning toast fires. +
+ +
loginExpiresAt (from useGetLoginInfo)
+
+ {loginExpiresAt ?? 'null'} + {loginExpiresAt !== null && ( + <> + {' '} + ({new Date(loginExpiresAt).toISOString()}) + + )} +
+ Not the same thing as the token's real expiry above.{' '} + This is a separate, generic rolling session ceiling (defaults to + "24 hours from now", already in milliseconds — no ×1000 needed) + maintained by an internal store middleware, unrelated to the + `expirySeconds` value configured for nativeAuth. Confirmed from + `node_modules/@multiversx/sdk-dapp/out/store/middleware/logoutMiddleware.cjs`. + Don't use this field to display "your session expires at X" — it + will disagree with the LogoutManager-driven toasts above whenever + `expirySeconds` isn't ~24h. +
+
+ +
+ + + +
+
+ ); +} + +function truncate(value: string): string { + return value.length > 24 ? `${value.slice(0, 12)}…${value.slice(-8)}` : value; +} + +const buttonStyle: React.CSSProperties = { + padding: '0.5rem 1rem', + fontSize: '0.9rem', + cursor: 'pointer', +}; + +const dlStyle: React.CSSProperties = { + display: 'grid', + gridTemplateColumns: 'minmax(140px, max-content) 1fr', + columnGap: '1rem', + rowGap: '0.75rem', + fontSize: '0.9rem', +}; +``` + +## The demo page + +```tsx title="src/App.tsx" +// src/App.tsx — demo page. Login UI here is the generic picker (same as +// "Adding a wallet login button") — the point of this recipe is what +// NativeAuthPanel shows once you're in, not how you got there. + +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import { NativeAuthPanel } from './NativeAuthPanel'; + +export function App(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const account = useGetAccount(); + const { network } = useGetNetworkConfig(); + + const handleConnect = (): void => { + void UnlockPanelManager.getInstance().openUnlockPanel(); + }; + + const handleDisconnect = async (): Promise => { + await getAccountProvider().logout(); + }; + + return ( +
+

Native auth: token issuance, expiry, auto-logout

+

+ Network: {network.chainId} +

+ + {isLoggedIn ? ( + <> +

+ Connected as {account.address}. +

+ + + ) : ( + + )} + + +
+ ); +} +``` + +## Provider bootstrap + +`providers.tsx` calls `initApp()` (with the native-auth config above) once and +gates rendering until the store is ready. + +```tsx title="src/providers.tsx" +// src/providers.tsx — sdk-dapp v5 init wrapper + UnlockPanelManager setup. +// +// Same shape as the "Adding a wallet login button" recipe — this recipe is +// about what happens AFTER login (the native-auth token and its automatic +// expiry handling), not about which login UI you use, so it reuses the +// generic picker rather than a dedicated single-provider button. +// +// initApp(dappConfig) is what actually turns on native auth — see +// src/lib/multiversx.ts for the `nativeAuth` config object. Nothing in this +// file configures LogoutManager directly: DappProvider.login() calls +// `LogoutManager.getInstance().init()` for you as its last step +// (node_modules/@multiversx/sdk-dapp/out/providers/DappProvider/DappProvider.cjs) +// — confirmed from source, not assumed. See src/NativeAuthPanel.tsx for the +// consumer-facing half of this recipe. + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { dappConfig } from './lib/multiversx'; + +let initStarted = false; + +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; + + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + + UnlockPanelManager.init({ + loginHandler: async (): Promise => { + // eslint-disable-next-line no-console + console.log('Logged in — native auth token issued.'); + }, + onClose: async (): Promise => { + // eslint-disable-next-line no-console + console.log('Unlock panel closed without logging in.'); + }, + }); + + setReady(true); + }); + + return () => { + cancelled = true; + }; + }, []); + + if (!ready) { + return ( +
+ Initializing wallet SDK… +
+ ); + } + + return <>{children}; +} +``` + +## How it works + +**`nativeAuth` accepts `true` (all defaults) or a full config object.** This +recipe passes an explicit object so both timing fields are visible: +`expirySeconds` (default 86400 / 24h) and `tokenExpirationToastWarningSeconds` +(default 300 / 5 min), both confirmed from +`node_modules/@multiversx/sdk-dapp/out/services/nativeAuth/methods/getDefaultNativeAuthConfig.cjs`. +This recipe deliberately overrides both to a couple of minutes so the whole +lifecycle is observable in one dev session. Ship the defaults, or your own +policy's values, not these. + +**`expirySeconds` is a real, per-token TTL, signed by the wallet, not a +client-only timer.** The login-token string sdk-dapp builds before the wallet +signs it is `${origin}.${blockHash}.${expirySeconds}.${extraInfo}` +(`node_modules/@multiversx/sdk-dapp/out/services/nativeAuth/nativeAuth.cjs`'s +`initialize()`). The API validates the signed token against that same embedded +value, so a short `expirySeconds` really does produce a short-lived token. + +**Auto-logout is wired up inside `provider.login()` itself — you never call it.** +`DappProvider.login()`'s last step is `LogoutManager.getInstance().init()`. +`LogoutManager.init()` reads the real token's remaining TTL and schedules a +warning toast, a "Logging out" toast 3 seconds before expiry, and the actual +`getAccountProvider().logout()` call. The only public methods this recipe calls +directly are `.stop()` (cancel all three timers) and `.init()` again (re-arm from +the token's current remaining TTL). + +## Pitfalls + +:::warning[Pitfall 1: the 120-second expirySeconds in this recipe is for demonstration only] +It exists purely so the warning-toast then logout-toast then real-logout sequence +is observable in one sitting. Ship a real value, the 86400-second default, or +whatever your own security policy requires. +::: + +:::danger[Pitfall 2: loginExpiresAt is NOT your native-auth token's expiry] +`useGetLoginInfo().loginExpiresAt` is a separate, generic rolling ~24-hour +session ceiling (already in milliseconds) maintained by an unrelated store +middleware, not the `expirySeconds` you configure for nativeAuth. Displaying it +as "your session expires at X" would be wrong whenever `expirySeconds` is not +~24h. There is currently no public field that returns the real per-token TTL +directly; this recipe shows the *configured* value instead of decoding the token +client-side. +::: + +:::warning[Pitfall 3: there is no public "extend my session" call] +`LogoutManager` only exposes `.stop()` and `.init()`. Calling `.init()` again +reschedules from the *current* token's remaining TTL. It does not mint a new token +or push the expiry further out. Extending a session for real means logging in again. +::: + +:::note[Pitfall 4: .stop() only cancels the client-side timers] +The token still expires server-side at its real TTL. Any authenticated API call +made after that point fails regardless of whether `LogoutManager` is running. +`.stop()` just means the SDK will not proactively force a client-side logout for you. +::: + +## See also + +- [Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button) + is the generic picker this recipe's login UI reuses. +- [Read the connected account](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + is the account-level counterpart to this recipe's login-info focus. +- [Login via the DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login) + is a single-provider login flow that also issues a native-auth token the same way. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account.mdx b/docs/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account.mdx new file mode 100644 index 000000000..b7559b5ad --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account.mdx @@ -0,0 +1,409 @@ +--- +title: Read the connected account with useGetAccount +description: Every AccountType field explained, which ones are optional, how to format the balance correctly, and when to reach for useGetAccountInfo instead. +difficulty: beginner +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-dapp: "^5.6.0" +tags: + - sdk-dapp + - wallet + - react + - typescript + - devnet +--- + +Once a user is logged in, `useGetAccount()` is how you read their address, +balance, and nonce back out of the sdk-dapp store. This recipe goes field by +field through `AccountType`, shows the two sibling hooks (`useGetAccountInfo()`, +`useGetLatestNonce()`) you will reach for less often, and gets the +balance-formatting pitfall out of the way early. + +## Prerequisites + +- A working sdk-dapp v5 setup (Next.js or Vite). +- A logged-in account on devnet. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/read-connected-account +npm install +npm run dev +# open https://localhost:5173 +``` + +## The account card + +```tsx title="src/AccountCard.tsx" +// src/AccountCard.tsx — the useGetAccount() field-by-field breakdown. +// +// This is the piece this recipe is actually about. useGetAccount() returns +// AccountType (node_modules/@multiversx/sdk-dapp/out/types/account.types.d.ts) +// — a plain object read straight from the Zustand store sdk-dapp populates +// on login. No network call happens here; the values are whatever the store +// currently holds (refreshed automatically after transactions settle). + +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetAccountInfo } from '@multiversx/sdk-dapp/out/react/account/useGetAccountInfo'; +import { useGetLatestNonce } from '@multiversx/sdk-dapp/out/react/account/useGetLatestNonce'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { formatAmount } from '@multiversx/sdk-dapp/out/lib/sdkDappUtils'; + +export function AccountCard(): JSX.Element { + // The hook you want almost all of the time. Returns AccountType directly — + // no destructuring through a bigger "everything" object. + const account = useGetAccount(); + + // The latest nonce, alone — a thin convenience wrapper around the same + // store value as account.nonce. Handy when a component only cares about + // the nonce and you don't want to pull in the whole AccountType. + const latestNonce = useGetLatestNonce(); + + // The network config, so we can label the balance with the right symbol + // (EGLD on mainnet, xEGLD on some custom networks) instead of hard-coding it. + const { network } = useGetNetworkConfig(); + + // The lower-level, back-compat-shaped hook. Useful when you also need + // provider-adjacent fields (publicKey, ledgerAccount, walletConnectAccount, + // websocket event snapshots) that AREN'T part of AccountType. Note this is + // NOT the same shape as sdk-dapp v4's useGetAccountInfo() — v5 dropped + // isLoggedIn and tokenLogin from it (those moved to useGetLoginInfo() / + // useGetIsLoggedIn()). + const accountInfo = useGetAccountInfo(); + + return ( +
+

useGetAccount()

+
+
address
+
+ {account.address} +
+ +
balance
+
+ + {formatAmount({ + input: account.balance, + decimals: 18, + digits: 4, + showLastNonZeroDecimal: true, + })}{' '} + {network.egldLabel} + {' '} + (raw: {account.balance}) +
+ +
nonce
+
+ {account.nonce}{' '} + + (useGetLatestNonce() agrees: {latestNonce}) + +
+ +
shard
+
+ {account.shard ?? 'unknown — optional field'} +
+ +
username
+
+ {account.username || '(none set — optional field)'} +
+ +
txCount / scrCount
+
+ + {account.txCount} / {account.scrCount} + +
+ +
isGuarded
+
+ {String(account.isGuarded)} +
+
+ +

useGetAccountInfo() — the fields AccountType doesn't have

+
+
publicKey
+
+ {accountInfo.publicKey} +
+
ledgerAccount
+
+ {accountInfo.ledgerAccount ? 'connected via Ledger' : 'null'} +
+
walletConnectAccount
+
+ {accountInfo.walletConnectAccount ?? 'null'} +
+
+
+ ); +} + +const cardStyle: React.CSSProperties = { + border: '1px solid #444', + borderRadius: '8px', + padding: '1.25rem', + marginTop: '1rem', +}; + +const dlStyle: React.CSSProperties = { + display: 'grid', + gridTemplateColumns: 'max-content 1fr', + columnGap: '1rem', + rowGap: '0.4rem', + margin: 0, +}; + +const rawStyle: React.CSSProperties = { + color: '#888', + fontSize: '0.85em', +}; +``` + +## The demo page + +```tsx title="src/App.tsx" +// src/App.tsx — demo page: connect, then show every account field. + +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import { AccountCard } from './AccountCard'; + +export function App(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + + const handleConnect = (): void => { + void UnlockPanelManager.getInstance().openUnlockPanel(); + }; + + const handleDisconnect = async (): Promise => { + const provider = getAccountProvider(); + await provider.logout(); + }; + + return ( +
+

Reading the connected account

+ + {!isLoggedIn ? ( + + ) : ( + <> + + + + )} +
+ ); +} + +const buttonStyle: React.CSSProperties = { + padding: '0.75rem 1.25rem', + fontSize: '1rem', + cursor: 'pointer', +}; +``` + +## Provider bootstrap + +Every sdk-dapp recipe shares the same init wrapper. `providers.tsx` calls +`initApp()` once and gates rendering until the store is ready; `lib/multiversx.ts` +holds the environment config it passes in. They are shown here so the recipe +compiles as a complete unit, but the subject of this recipe is `AccountCard.tsx` +above. + +```tsx title="src/providers.tsx" +// src/providers.tsx — sdk-dapp v5 init wrapper. +// +// This recipe's subject is AccountCard.tsx, not this file — this is the +// same login bootstrap every recipe in this Cookbook uses. + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { dappConfig } from './lib/multiversx'; + +let initStarted = false; + +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; + + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + + UnlockPanelManager.init({ + loginHandler: async (): Promise => { + // eslint-disable-next-line no-console + console.log('Logged in.'); + }, + onClose: async (): Promise => { + // eslint-disable-next-line no-console + console.log('Unlock panel closed.'); + }, + }); + + setReady(true); + }); + + return () => { + cancelled = true; + }; + }, []); + + if (!ready) { + return ( +
+ Initializing wallet SDK… +
+ ); + } + + return <>{children}; +} +``` + +```ts title="src/lib/multiversx.ts" +// src/lib/multiversx.ts — environment configuration for sdk-dapp's initApp. +// Same shape as every other Vite recipe in this Cookbook. + +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +const WALLET_CONNECT_PROJECT_ID = + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; + +export const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + nativeAuth: true, + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + }, +}; + +export { EnvironmentsEnum }; +``` + +## How it works + +**`useGetAccount()` is a synchronous store read, not a network call.** It returns +`AccountType`, verified field by field against +`node_modules/@multiversx/sdk-dapp/out/types/account.types.d.ts` on the +actually-installed `@multiversx/sdk-dapp v5`: + +```ts +interface AccountType { + address: string; + balance: string; // smallest denomination, decimal string + nonce: number; + txCount: number; + scrCount: number; + claimableRewards: string; + isGuarded: boolean; + // optional: username, shard, code, ownerAddress, developerReward, + // deployedAt, scamInfo, isUpgradeable, isReadable, isPayable, + // isPayableBySmartContract, assets, and the active/pending guardian fields +} +``` + +The store refreshes this automatically, on initial login and after each tracked +transaction settles. Calling the hook again on every render is fine and expected; +it does not refetch anything by itself. + +**Format the balance, always.** `account.balance` is the raw smallest-unit +string. `formatAmount({ input: account.balance, decimals: 18, digits: 4, showLastNonZeroDecimal: true })` +gives you the human-readable decimal EGLD amount with the precision behavior you +want. + +**`useGetAccountInfo()` still exists in v5, with a narrower shape than v4.** It +dropped `isLoggedIn` and `tokenLogin` (now `useGetIsLoggedIn()` / +`useGetLoginInfo()`), but adds fields `AccountType` does not have: `publicKey`, +`ledgerAccount`, `walletConnectAccount`, `websocketEvent`, `websocketBatchEvent`. +Reach for it only when you need one of those; `useGetAccount()` is the right +default for everything else. + +## Pitfalls + +:::danger[Pitfall 1: never render account.balance directly] +It is the raw smallest-unit string (`"1500000000000000000"`, not `"1.5"`). Always +pass it through `formatAmount()` first. Rendering it raw is the single most common +"why does my balance say a huge number" support question. +::: + +:::warning[Pitfall 2: shard and username are optional] +A freshly created account, or one the API has not fully indexed, may have +`shard: undefined`. Guard with `??` or a conditional. Do not assume every optional +`AccountType` field is always present, as `AccountCard.tsx` does for both. +::: + +:::warning[Pitfall 3: the hook doesn't refetch, refreshAccount() does] +`useGetAccount()` returns whatever the store currently holds. It updates +automatically after login and after tracked transactions settle. If you need to +force a fresh read outside those triggers, call `refreshAccount()` +(`@multiversx/sdk-dapp/out/utils/account/refreshAccount`) instead of expecting the +hook itself to hit the network. +::: + +:::warning[Pitfall 4: getAccount vs getAccountFromApi] +`useGetAccount()` (and its non-React twin `getAccount()`) are free, instant store +reads. `getAccountFromApi` is a *different* function that makes a real network +round-trip on every call. Do not reach for it inside a render loop thinking it is +just a renamed getter. +::: + +## See also + +- [Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button) + gets a user connected before you have an account to read. +- [Log in with the DeFi Wallet browser extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login) + is a dedicated single-provider login path. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the next thing you do once you can read the connected account. +- [docs.multiversx.com, sdk-dapp](https://docs.multiversx.com/sdk-and-tools/sdk-dapp) + is the conceptual reference. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/wallets/typescript-strict-mode-checklist.mdx b/docs/sdk-and-tools/sdk-js/cookbook/wallets/typescript-strict-mode-checklist.mdx new file mode 100644 index 000000000..8a66bd142 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/wallets/typescript-strict-mode-checklist.mdx @@ -0,0 +1,277 @@ +--- +title: TypeScript strict-mode checklist for sdk-dapp consumers +description: Four real bugs found while verifying cookbook recipes against installed sdk-dapp, plus the tsconfig flags and ESLint rules that catch them. +difficulty: intermediate +est_minutes: 7 +last_validated: "2026-07-16" +sdk_versions: + sdk-dapp: "^5.6.0" +tags: + - sdk-dapp + - typescript + - react +--- + +Every item below is a real bug this Cookbook's own build found while verifying its +recipes against actually-installed `@multiversx/sdk-dapp`, not a hypothetical list. +Each is the concrete, runnable version of a class of failure: docs examples that +do not survive `tsc --strict`. + +## Prerequisites + +- A TypeScript project consuming `@multiversx/sdk-dapp` v5. +- `strict: true` in `tsconfig.json`. + +## The tsconfig + +| Flag | Why it matters here | +| --- | --- | +| `strict: true` | Umbrella flag; without it, most of the bugs below compile silently. | +| `strictNullChecks` | Catches `account.balance` used before confirming `account` is not `null`/`undefined`, common when reading store state before `initApp()` resolves. | +| `noUncheckedIndexedAccess` | Array/object index access returns `T \| undefined` instead of `T`. Catches assuming `sentTransactions[0]` exists after a `.send()` call, see Item 4. | +| `exactOptionalPropertyTypes` | Distinguishes "property omitted" from "property explicitly `undefined`" in config objects like `dAppConfig`. | +| `noImplicitAny` | Without it, an unresolved sdk-dapp import path silently degrades to `any`, and every bug on this page stops being a compile error. | +| `noUnusedLocals` / `noUnusedParameters` | Catches leftover v4 imports after a migration pass. | + +## The checklist + +### 1. theme is a ThemesEnum member, not a string literal + +```ts title="src/theme.ts" +// src/theme.ts — Checklist item 1: `theme` is an enum member, not a string. +// +// The naive port of sdk-dapp's own prose docs (which show `theme: 'dark'` +// without importing anything) fails strict-mode compile. Confirmed +// against the real, installed +// node_modules/@multiversx/sdk-dapp/out/methods/initApp/initApp.types.d.ts +// while verifying this Cookbook's start-here recipes (nextjs-minimal, +// sign-and-send, vite-react-minimal all had this bug). + +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +// WRONG — fails tsc --strict: +// +// const dappConfig = { +// dAppConfig: { +// environment: EnvironmentsEnum.devnet, +// theme: 'dark' as const, +// }, +// }; +// +// error TS2322: Type '"dark"' is not assignable to type +// '`${ThemesEnum}`'. + +// RIGHT: +export const dappConfig = { + dAppConfig: { + environment: EnvironmentsEnum.devnet, + theme: ThemesEnum.dark, + }, +}; +``` + +Found in three of this Cookbook's own recipes during verification (`nextjs-minimal`, +`sign-and-send`, `vite-react-minimal`) before being fixed. + +### 2. UnlockPanelManager callbacks must return Promise<void> + +```ts title="src/unlockPanel.ts" +// src/unlockPanel.ts — Checklist item 2: UnlockPanelManager callbacks must +// return Promise, not void. +// +// A real bug this Cookbook hit. The docs' own example for +// UnlockPanelManager.init uses plain synchronous callbacks; that shape +// does not satisfy the real installed type. Confirmed from +// node_modules/@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager.types.d.ts. + +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; + +// WRONG — fails tsc --strict: +// +// UnlockPanelManager.init({ +// loginHandler: () => { +// console.log('logged in'); +// }, +// }); +// +// error TS2322: Type '() => void' is not assignable to type +// 'OnProviderLoginType'. +// Type 'void' is not assignable to type 'Promise'. + +// RIGHT — async, even if the body has nothing to await: +export function initUnlockPanel(): void { + UnlockPanelManager.init({ + loginHandler: async (): Promise => { + // eslint-disable-next-line no-console + console.log('logged in'); + }, + onClose: async (): Promise => { + // eslint-disable-next-line no-console + console.log('panel closed'); + }, + }); +} +``` + +This is the reason `OnCloseUnlockPanelType` shows up by name in this Cookbook's +migration recipes. + +### 3. @typescript-eslint/no-floating-promises must actually be wired, and actually run + +```ts title="src/floatingPromise.ts" +// src/floatingPromise.ts — Checklist item 3: the ESLint rule that catches +// what tsc alone does not, PLUS the meta-bug of the rule silently not +// running at all. +// +// UnlockPanelManager.getInstance().openUnlockPanel() returns Promise +// (confirmed from the real .d.ts). Calling it from a synchronous onClick +// handler without `void` or `await` is a floating promise: if it rejects +// (e.g. the user's environment has no injected wallet), the rejection is +// silently swallowed — no error boundary, no console warning by default. +// tsc --strict does NOT flag this on its own; a floating Promise is +// still assignable to a `void`-returning callback signature. This is +// exactly why @typescript-eslint/no-floating-promises exists as a +// SEPARATE gate from tsc. +// +// Validation performed while authoring this recipe: temporarily removed +// the `void` operator below and re-ran `npm run lint` against this +// exact .eslintrc.cjs. It failed with: +// error Promises must be awaited, end with a call to .catch, end with +// a call to .then with a rejection handler or be explicitly marked as +// ignored with the `void` operator @typescript-eslint/no-floating-promises +// — confirming this recipe's own lint config does NOT have the +// silent-no-op bug it warns about (see .eslintrc.cjs's header comment). +// Restored the `void` operator afterward; this file lints clean. + +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; + +export function handleConnectClick(): void { + // WRONG — a real, reproducible eslint error under this exact config: + // + // UnlockPanelManager.getInstance().openUnlockPanel(); + // + // RIGHT — explicitly discard the promise (there's nothing to await from + // a sync onClick handler here): + void UnlockPanelManager.getInstance().openUnlockPanel(); +} +``` + +Two distinct failure modes, both real: the rule silently does nothing if +`parserOptions.project` is not set (this Cookbook's own `nextjs-minimal` recipe +shipped exactly this bug once), and, once wired, it catches real bugs `tsc` alone +does not, since a floating `Promise` is still assignable everywhere a +`void`-returning callback is expected. + +### 4. Don't assume a .send() result is the class instance you signed + +```ts title="src/transactionHash.ts" +// src/transactionHash.ts — Checklist item 4: don't assume a helper class +// accepts the object shape a hand-signed value happens to look like, AND +// don't assume a union return type narrows just because your own input +// didn't. +// +// TransactionManager.getInstance().send() returns SignedTransactionType[] +// (a plain object shape: `{ ...Transaction fields, hash, status }`), +// NOT sdk-core Transaction class instances. Passing that return value +// into TransactionComputer.computeTransactionHash() — which expects a +// real Transaction instance with its class methods — is a real, +// reproducible tsc --strict failure, confirmed while authoring this +// Cookbook's sign-and-send recipe. The fix isn't a cast: SignedTransactionType already +// carries `hash: string` directly, computed server-side, so there's +// nothing to recompute client-side at all. +// +// A second, related trap this file's own first draft hit while being +// verified for this Cookbook: send()'s declared return type is +// `SignedTransactionType[] | SignedTransactionType[][]` — a union, +// UNCONDITIONALLY, even when the input you passed was a plain flat +// `Transaction[]` (see the +// send-transactions-to-transaction-manager recipe for why the return +// type is a union at all: send() also accepts a nested batch input). +// There is no overload that narrows the return based on which arm of the +// input union you passed, so `const [first] = sent; first.hash` fails — +// `first`'s type includes `SignedTransactionType[]` (the nested-batch +// member), which has no `.hash`. Narrow explicitly at runtime instead of +// casting blindly. + +import { TransactionManager } from '@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager'; +import type { SignedTransactionType } from '@multiversx/sdk-dapp/out/types/transactions.types'; +import { TransactionComputer, type Transaction } from '@multiversx/sdk-core'; + +/** + * Narrows send()'s always-a-union return type down to the flat shape, + * using the same Array.isArray check the SDK's own internal + * isBatchTransaction() helper uses — not a blind `as` cast. + */ +function assertFlatResult( + sent: SignedTransactionType[] | SignedTransactionType[][], +): SignedTransactionType[] { + if (sent.length > 0 && Array.isArray(sent[0])) { + throw new Error( + 'Expected a flat transaction result; got a nested batch result instead.', + ); + } + return sent as SignedTransactionType[]; +} + +export async function sendAndGetHash(signed: Transaction[]): Promise { + const txManager = TransactionManager.getInstance(); + + // WRONG — fails tsc --strict, even though `signed` above is flat: + // + // const sent = await txManager.send(signed); + // const [first] = sent; + // return first.hash; + // + // error TS2339: Property 'hash' does not exist on type + // 'SignedTransactionType | SignedTransactionType[]'. + + // RIGHT — narrow explicitly, then read the hash the network already + // computed; no TransactionComputer call needed: + const flatSent = assertFlatResult(await txManager.send(signed)); + const [first] = flatSent; + if (!first) { + throw new Error('No transaction was sent.'); + } + return first.hash; +} + +// TransactionComputer.computeTransactionHash IS the right tool — just for +// a different input: an sdk-core Transaction instance you haven't sent +// yet (e.g. to display a hash before broadcasting). Kept here to show +// the type it actually wants, not to suggest it's unused: +export function hashBeforeSending(tx: Transaction): string { + const computer = new TransactionComputer(); + return computer.computeTransactionHash(tx); +} +``` + +Two stacked traps: `TransactionManager.getInstance().send()` returns +`SignedTransactionType[]` (plain objects with `hash` already on them), not +`Transaction` class instances, so `TransactionComputer.computeTransactionHash()` +rejects it. And `send()`'s declared return type is a union +(`SignedTransactionType[] | SignedTransactionType[][]`) **even when your input was +flat** (see +[Migration: grouped/batch sends](/sdk-and-tools/sdk-js/cookbook/migration/send-transactions-to-transaction-manager)), +so destructuring the first element needs an explicit `Array.isArray` narrow, not a +blind cast. + +## How to use this checklist on an existing codebase + +1. Turn on every flag in the table above, one at a time if the codebase is large. +2. Fix in the order `tsc --noEmit --strict` reports them; resist the urge to + blanket-suppress with `// @ts-ignore`. +3. Add the two `@typescript-eslint` promise rules last, once the codebase compiles. +4. Confirm the ESLint rules are actually running (Item 3) before trusting a clean + `eslint` pass. + +## See also + +- [Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) + and + [Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) + are where Items 1 to 3 were first found and fixed. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is where Item 4 was first found and fixed. +- [Migrate v4 to v5: hook-by-hook diffs](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration) + references Item 2 by name (`OnCloseUnlockPanelType`). diff --git a/docs/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button.mdx b/docs/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button.mdx new file mode 100644 index 000000000..abbb658ff --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button.mdx @@ -0,0 +1,396 @@ +--- +title: Add a wallet login button +description: Configure UnlockPanelManager once at startup, then add a reusable connect and disconnect button that works with every registered wallet provider. +difficulty: beginner +est_minutes: 6 +last_validated: "2026-07-16" +sdk_versions: + sdk-dapp: "^5.6.0" +tags: + - sdk-dapp + - wallet + - vite + - react + - typescript + - devnet +--- + +Every dApp needs a "Connect wallet" button. In sdk-dapp v5 that button does not +render a picker itself. It calls `UnlockPanelManager.getInstance().openUnlockPanel()`, +and a pre-built web component, configured once at app startup, does the rest. +Here is the full pattern: configure the panel, build one reusable +button component, and see what each `UnlockPanelManager.init()` option does. + +This recipe assumes you already have a working sdk-dapp v5 + Vite setup. If not, +start with +[Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) +first. If you want a login button for one *specific* provider (skip the picker +entirely, e.g. a "Connect DeFi Wallet" button with no other options shown), see +[Login via the DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login) +or +[Login via xPortal / WalletConnect](/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login) +instead, which use the lower-level `ProviderFactory` directly. + +## Prerequisites + +- A working sdk-dapp v5 setup (Next.js or Vite, see the start-here recipes). +- A devnet wallet to test with (DeFi extension or xPortal). + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/wallet-login-button +npm install +npm run dev +# open https://localhost:5173 +``` + +## The reusable button + +Zero props. Every instance reads the same sdk-dapp store, so dropping +`` in five different places in your tree keeps all five in sync +automatically: + +```tsx title="src/LoginButton.tsx" +// src/LoginButton.tsx — the reusable connect/disconnect button. +// +// This is the piece you copy into your own dApp: a single component that +// renders "Connect wallet" when logged out and "Disconnect" when logged in. +// It has zero props because it reads everything it needs from the sdk-dapp +// store (via hooks) and the UnlockPanelManager / provider singletons — drop +// it anywhere in the tree, as many times as you want, and every instance +// stays in sync automatically. + +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; + +export function LoginButton(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + + const handleConnect = (): void => { + // The panel singleton was configured once in (src/providers.tsx). + // openUnlockPanel() returns Promise; this is a sync onClick, so we + // discard it explicitly with `void` (eslint no-floating-promises). + void UnlockPanelManager.getInstance().openUnlockPanel(); + }; + + const handleDisconnect = async (): Promise => { + const provider = getAccountProvider(); + await provider.logout(); + }; + + if (isLoggedIn) { + return ( + + ); + } + + return ( + + ); +} + +const buttonStyle: React.CSSProperties = { + padding: '0.75rem 1.25rem', + fontSize: '1rem', + cursor: 'pointer', +}; +``` + +## The demo page + +```tsx title="src/App.tsx" +// src/App.tsx — demo page for the LoginButton component. +// +// Deliberately thin: this recipe's job is the button + the UnlockPanelManager +// setup behind it, not the full account display (see the "Reading the +// connected account" recipe for the useGetAccount() deep dive). + +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { LoginButton } from './LoginButton'; + +export function App(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const account = useGetAccount(); + const { network } = useGetNetworkConfig(); + + return ( +
+

Wallet login button

+

+ Network: {network.chainId} +

+ + + + {isLoggedIn && ( +

+ Connected as {account.address}. See{' '} + + Reading the connected account + {' '} + for the full field breakdown. +

+ )} + +

+ Drop <LoginButton /> anywhere in your component + tree — it needs no props and stays in sync with every other instance + automatically, because all of them read the same sdk-dapp store. +

+
+ ); +} +``` + +## The panel setup + +`UnlockPanelManager.init()` runs exactly once, at app startup, in the same +wrapper that calls `initApp()`. `loginHandler` and `onClose` both return +`Promise`, not `void`. The SDK's own type declarations require it (see +Pitfall 1), even though some example code you will find floating around uses a +sync arrow function. + +```tsx title="src/providers.tsx" +// src/providers.tsx — sdk-dapp v5 init wrapper + UnlockPanelManager setup. +// +// This file is the actual subject of this recipe. Two things happen here, +// in order: +// +// 1. initApp(dappConfig) — boots the SDK (store, network config, native +// auth, provider factory). Must resolve before anything else touches +// the store. +// 2. UnlockPanelManager.init({...}) — configures the (singleton) unlock +// panel: which callback runs after login, which runs if the user +// closes the panel without logging in, and which providers to show. +// This call happens ONCE, here, at app startup — not per-button. Every +// "Connect wallet" button anywhere in the app just calls +// UnlockPanelManager.getInstance().openUnlockPanel(). + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { dappConfig } from './lib/multiversx'; + +// Module-scope flag prevents double-init under React Strict Mode (which +// deliberately mounts effects twice in dev). Matches every other recipe in +// this Cookbook — see Pitfall 3. +let initStarted = false; + +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; + + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + + UnlockPanelManager.init({ + // loginHandler accepts TWO different shapes (both documented in + // node_modules/@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager.types.d.ts): + // + // 1) A zero-arg callback — the SDK has already run the full + // provider-create + login sequence for you; you just react to + // "login succeeded" (e.g. navigate away). This is what almost + // every dApp wants, and what this recipe uses. + // + // 2) A `({ type, anchor }) => Promise` function — you take + // over the ENTIRE login sequence yourself, including calling + // ProviderFactory.create({ type, anchor }) and provider.login() + // by hand. Use this only if you need to intercept or customize + // that sequence (custom loading UI per provider, analytics + // hooks, etc.) — see the "Login via the DeFi extension" and + // "Login via xPortal / WalletConnect" recipes for the + // lower-level ProviderFactory pattern this shape wraps. + // + // IMPORTANT: both shapes must return Promise, not void — the + // docs' own sync example fails strict-mode compile. See Pitfall 1. + loginHandler: async (): Promise => { + // Replace with your post-login navigation (e.g. router.push). + // eslint-disable-next-line no-console + console.log('Logged in.'); + }, + // Called if the user dismisses the panel without completing login. + onClose: async (): Promise => { + // eslint-disable-next-line no-console + console.log('Unlock panel closed without logging in.'); + }, + // Optional: restrict + reorder which providers the panel shows. + // Omit this key entirely to show every registered provider (the + // default, and what this recipe does). Example from the SDK's own + // JSDoc: `allowedProviders: [ProviderTypeEnum.walletConnect, 'inMemoryProvider']`. + // allowedProviders: [ProviderTypeEnum.extension, ProviderTypeEnum.walletConnect], + }); + + setReady(true); + }); + + return () => { + cancelled = true; + }; + }, []); + + if (!ready) { + return ( +
+ Initializing wallet SDK… +
+ ); + } + + return <>{children}; +} +``` + +`providers.tsx` imports the shared environment config that every recipe in this +Cookbook passes to `initApp()`: + +```ts title="src/lib/multiversx.ts" +// src/lib/multiversx.ts — environment configuration for sdk-dapp's initApp. +// +// The full dAppConfig shape accepts more keys than this; we only set the +// keys this recipe actually uses. + +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +// Read from import.meta.env (Vite's runtime environment object) — NOT +// process.env, which does not exist in the browser bundle. Fallback is the +// demo project ID; register your own at https://cloud.walletconnect.com. +const WALLET_CONNECT_PROJECT_ID = + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; + +export const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + nativeAuth: true, + // `theme` takes one of the enum's string values (e.g. 'mvx:dark-theme'); the + // short `'dark'` fails — see node_modules/@multiversx/sdk-dapp/out/methods/initApp/initApp.types.d.ts. + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + }, +}; + +export { EnvironmentsEnum }; +``` + +## How it works + +**Configure once, use everywhere.** `UnlockPanelManager` is a singleton. +`.init({...})` sets its `loginHandler`, `onClose`, and (optionally) +`allowedProviders`. Call it once, at startup. Every button anywhere in the app +calls `.getInstance().openUnlockPanel()`, which just raises the +already-configured panel. + +**`loginHandler` has two shapes.** Per +`node_modules/@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager.types.d.ts`: + +```ts +// 1) Zero-arg callback: the SDK already ran the full login sequence. +loginHandler: () => { navigate('/dashboard'); }; + +// 2) Full control: you run the login sequence yourself. +loginHandler: async ({ type, anchor }) => { + const provider = await ProviderFactory.create({ type, anchor }); + await provider?.login(); + navigate('/dashboard'); +}; +``` + +This recipe uses shape 1, the common case. Shape 2 is the same `ProviderFactory` +pattern the dedicated single-provider recipes +([DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login), +[xPortal/WalletConnect](/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login)) +use directly, without going through the panel at all. + +**`allowedProviders` restricts and reorders the picker.** Omit it to show every +registered provider (this recipe's default). Pass +`[ProviderTypeEnum.extension, ProviderTypeEnum.walletConnect]` to show only those +two, in that order. Its type also accepts custom provider-name strings, for apps +that registered a custom `IProvider`. + +## Pitfalls + +:::warning[Pitfall 1: loginHandler and onClose must return Promise<void>] +A sync callback fails under TypeScript strict mode: + +```text +error TS2322: Type '() => void' is not assignable to type 'OnCloseUnlockPanelType'. + Type 'void' is not assignable to type 'Promise'. +``` + +Use `async () => { ... }` for both callbacks. `src/providers.tsx` does this +correctly. +::: + +:::warning[Pitfall 2: UnlockPanelManager.init() runs once, not per button] +Calling `.init()` a second time from another component overwrites the first +configuration (last call wins). It does not merge or stack. Configure it exactly +once, in the same wrapper that calls `initApp()`. Every button elsewhere only +ever calls `.getInstance().openUnlockPanel()`. +::: + +:::warning[Pitfall 3: don't render login buttons before init resolves] +`UnlockPanelManager.getInstance()` before `.init()` has run returns a panel with +no configured `loginHandler`. This recipe's `ready` gate in `src/providers.tsx` +prevents children, and therefore any ``, from rendering until +`initApp()` and `UnlockPanelManager.init()` have both completed. Don't remove +that gate. +::: + +:::warning[Pitfall 4: HTTPS required for most wallets] +The DeFi extension and Ledger refuse `http://localhost`. This recipe's dev server +uses `@vitejs/plugin-basic-ssl`. See +[Local HTTPS for dApp dev](/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup) +for a fully trusted mkcert alternative. +::: + +## See also + +- [Login via the DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login) + is a dedicated single-provider button using `ProviderFactory` directly, no picker. +- [Login via xPortal / WalletConnect](/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login) + is the same lower-level pattern, for the mobile-QR provider. +- [Read the connected account](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + is what to do with `useGetAccount()` once a user is connected. +- [Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) + is the base setup this recipe builds on. diff --git a/docs/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login.mdx b/docs/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login.mdx new file mode 100644 index 000000000..8b9691b04 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login.mdx @@ -0,0 +1,392 @@ +--- +title: Log in with xPortal via WalletConnect +description: A dedicated xPortal login button using ProviderFactory and WalletConnect v2 directly, including the QR-code anchor element and project ID setup. +difficulty: intermediate +est_minutes: 8 +last_validated: "2026-07-16" +sdk_versions: + sdk-dapp: "^5.6.0" +tags: + - sdk-dapp + - wallet + - walletconnect + - react + - typescript + - devnet +--- + +The sibling to +[Login via the DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login): +a dedicated, single-provider button, this time for xPortal via WalletConnect v2. +Same `ProviderFactory` pattern, with two things specific to WalletConnect: a +`walletConnectV2ProjectId` configured up front, and a real DOM anchor for the QR +code to render into. + +Use this when you want a dedicated "Connect xPortal" button next to (or instead +of) +[Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button)'s +generic picker. + +## Prerequisites + +- A working sdk-dapp v5 setup (Next.js or Vite, see the start-here recipes). +- A WalletConnect v2 project ID from + [cloud.walletconnect.com](https://cloud.walletconnect.com). The recipe ships + with a shared, rate-limited demo ID so it runs immediately, but replace it + before shipping. +- The xPortal mobile app, to actually scan the QR code and test the connected path. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/walletconnect-login +npm install +npm run dev +# open https://localhost:5173 +``` + +## Configuring the project ID + +```ts title="src/lib/multiversx.ts" +// src/lib/multiversx.ts — environment configuration for sdk-dapp's initApp. +// +// walletConnectV2ProjectId here is not optional in practice: ProviderFactory +// throws `WalletConnectV2Error.invalidConfig` ("Invalid WalletConnect +// setup") if you try to create a walletConnect provider without one +// configured (verified in +// node_modules/@multiversx/sdk-dapp/out/providers/ProviderFactory.cjs). +// Register a real project ID at https://cloud.walletconnect.com before +// shipping — the demo one here is shared and rate-limited. + +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +const WALLET_CONNECT_PROJECT_ID = + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; + +export const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + nativeAuth: true, + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + }, +}; + +export { EnvironmentsEnum }; +``` + +## The button + +```tsx title="src/WalletConnectLoginButton.tsx" +// src/WalletConnectLoginButton.tsx — a dedicated xPortal / WalletConnect +// login button with a QR-code anchor. +// +// Same overall pattern as the "Login via the DeFi extension" recipe — +// ProviderFactory.create() + provider.login() — with two differences +// specific to WalletConnect: +// +// 1. It needs an `anchor`: an HTMLElement the SDK renders the QR code +// (desktop) / deep-link prompt (mobile) into. Without one, there's +// nowhere for the user to actually see the code to scan. +// 2. It needs `walletConnectV2ProjectId` configured in initApp()'s +// dAppConfig — see src/lib/multiversx.ts. Omitting it makes +// ProviderFactory.create() throw immediately (Pitfall 1). +// +// The rest of the sequence is identical to the DeFi extension recipe and +// is verified the same way — against the actual sdk-dapp source, not just +// its .d.ts files: +// +// ProviderFactory.create({ type, anchor }) +// → internally calls setAccountProvider() for you (see +// node_modules/@multiversx/sdk-dapp/out/providers/ProviderFactory.cjs) +// provider.login() +// → internally dispatches both the login-info and account store +// actions (see +// node_modules/@multiversx/sdk-dapp/out/providers/DappProvider/helpers/login/helpers/accountLogin.cjs) + +import { useRef, useState } from 'react'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { ProviderFactory } from '@multiversx/sdk-dapp/out/providers/ProviderFactory'; +import { ProviderTypeEnum } from '@multiversx/sdk-dapp/out/providers/types/providerFactory.types'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; + +export function WalletConnectLoginButton(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const [status, setStatus] = useState<'idle' | 'connecting' | 'error'>('idle'); + const [error, setError] = useState(null); + // The QR code / deep-link UI renders into this div. It must exist in the + // DOM before ProviderFactory.create() runs — it's rendered unconditionally + // below, so by the time a click handler fires, anchorRef.current is set. + const anchorRef = useRef(null); + + const handleConnect = async (): Promise => { + if (!anchorRef.current) { + // Should not happen — the anchor div always renders — but keeps the + // types honest under strict null checks rather than asserting `!`. + setError('QR anchor not mounted yet.'); + setStatus('error'); + return; + } + + setStatus('connecting'); + setError(null); + try { + const provider = await ProviderFactory.create({ + type: ProviderTypeEnum.walletConnect, + anchor: anchorRef.current, + }); + // nativeAuth is enabled in this recipe's dappConfig, so login() + // generates its own native-auth token internally. + await provider.login(); + setStatus('idle'); + } catch (err) { + // If walletConnectV2ProjectId isn't configured, this throws + // "Invalid WalletConnect setup" (WalletConnectV2Error.invalidConfig, + // node_modules/@multiversx/sdk-dapp/out/providers/strategies/WalletConnectProviderStrategy/types/walletConnect.types.d.ts) + // — before the QR code ever renders. That specific case is confirmed + // from source. What happens if the user closes the QR modal without + // scanning is NOT independently confirmed here — the strategy's + // compiled source logs a `WalletConnectV2Error.userRejected` label in + // a catch block around the approval flow, but tracing exactly what + // (if anything) it re-throws to this call site would need a live + // WalletConnect session to observe, not just reading the bundle. + // Don't assume the caught message always matches one of the + // WalletConnectV2Error enum strings — display it, but treat it as + // unstructured for now. + const message = err instanceof Error ? err.message : String(err); + setError(message); + setStatus('error'); + } + }; + + const handleDisconnect = async (): Promise => { + const provider = getAccountProvider(); + await provider.logout(); + }; + + if (isLoggedIn) { + return ( + + ); + } + + return ( +
+ + + {/* The SDK renders the QR code / deep-link prompt into this element + once ProviderFactory.create() runs. Give it real dimensions — + an empty 0x0 div means the QR code has nowhere visible to draw. */} +
+ + {status === 'error' && error && ( +

+ Connection failed: {error} +

+ )} +
+ ); +} + +const buttonStyle: React.CSSProperties = { + padding: '0.75rem 1.25rem', + fontSize: '1rem', + cursor: 'pointer', +}; + +const anchorStyle: React.CSSProperties = { + marginTop: '1rem', + minHeight: '280px', + minWidth: '280px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}; +``` + +## The demo page + +```tsx title="src/App.tsx" +// src/App.tsx — demo page for WalletConnectLoginButton. + +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { WalletConnectLoginButton } from './WalletConnectLoginButton'; + +export function App(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const account = useGetAccount(); + const { network } = useGetNetworkConfig(); + + return ( +
+

Login via xPortal / WalletConnect

+

+ Network: {network.chainId} +

+ + + + {isLoggedIn && ( +

+ Connected as {account.address}. +

+ )} +
+ ); +} +``` + +## Provider bootstrap + +`providers.tsx` calls `initApp()` once and gates rendering until the store is ready. + +```tsx title="src/providers.tsx" +// src/providers.tsx — sdk-dapp v5 init wrapper. +// +// Deliberately does NOT configure UnlockPanelManager — this recipe bypasses +// the generic multi-provider picker and talks to ProviderFactory directly +// for one specific provider. See src/WalletConnectLoginButton.tsx. + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { dappConfig } from './lib/multiversx'; + +let initStarted = false; + +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; + + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + setReady(true); + }); + + return () => { + cancelled = true; + }; + }, []); + + if (!ready) { + return ( +
+ Initializing wallet SDK… +
+ ); + } + + return <>{children}; +} +``` + +## How it works + +**The anchor is not optional in practice.** +`ProviderFactory.create({ type: ProviderTypeEnum.walletConnect, anchor })` needs a +real `HTMLElement` to draw the QR code (desktop) or deep-link prompt (mobile +browser) into, verified from `WalletConnectProviderStrategyConfigType`, which +extends `WalletConnectConfig` with `anchor?: HTMLElement`. This recipe uses a +`useRef` div rendered unconditionally, so it exists before any +click handler runs. + +**The project ID gate happens before the QR code ever renders.** +`ProviderFactory.create()` reads `walletConnectV2ProjectId` from the store (the +same config object passed to `initApp()`) and throws `"Invalid WalletConnect setup"` +immediately if it is missing, confirmed directly from the compiled source +(`node_modules/@multiversx/sdk-dapp/out/providers/ProviderFactory.cjs`): +`if (!config?.walletConnectV2ProjectId) throw new Error(WalletConnectV2Error.invalidConfig)`. + +**Everything after `create()` matches the DeFi extension recipe exactly.** +`ProviderFactory.create()` calls `setAccountProvider()` internally; +`provider.login()` dispatches both the login-info and account store actions +internally. Once `await provider.login()` resolves, `useGetIsLoggedIn()` and +`useGetAccount()` already reflect the session. + +## Pitfalls + +:::danger[Pitfall 1: missing walletConnectV2ProjectId fails immediately] +`ProviderFactory.create({ type: ProviderTypeEnum.walletConnect })` throws +`"Invalid WalletConnect setup"` (`WalletConnectV2Error.invalidConfig`) if +`dAppConfig.providers.walletConnect.walletConnectV2ProjectId` was not set in +`initApp()`. Register a real project ID at +[cloud.walletconnect.com](https://cloud.walletconnect.com) before shipping. The +shared demo ID here is rate-limited and meant for local development only. +::: + +:::warning[Pitfall 2: give the anchor real dimensions] +An anchor `
` with no width or height renders a QR code with nowhere visible +to draw. This recipe's anchor style sets an explicit `min-height`/`min-width`. +Don't drop that when you copy the pattern into your own layout. +::: + +:::warning[Pitfall 3: the closed-modal error path is not independently confirmed here] +The compiled strategy source references a `WalletConnectV2Error.userRejected` +label inside a `catch` block around its approval flow, but confirming exactly what +(if anything) propagates to your own `catch` when the user closes the modal +without scanning would need a live WalletConnect session to observe, not just +reading the minified bundle. Display whatever you get, do not pattern-match on it. +::: + +:::warning[Pitfall 4: this button only ever offers WalletConnect] +Pair it with +[Login via the DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login) +for a desktop-first option, or use +[Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button)'s +picker if you would rather let the SDK decide which providers to surface. +::: + +## See also + +- [Login via the DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login) + is the same `ProviderFactory` pattern for the browser-extension provider. +- [Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button) + is the multi-provider picker alternative to this dedicated button. +- [Read the connected account](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + is what to do once `provider.login()` resolves. diff --git a/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v15.md b/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v15.md deleted file mode 100644 index ef636bfa5..000000000 --- a/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v15.md +++ /dev/null @@ -1,3467 +0,0 @@ ---- -id: sdk-js-cookbook -title: Cookbook (v15) -pagination_prev: sdk-and-tools/sdk-js/sdk-js-cookbook-v14 -pagination_next: null -description: "sdk‑js Cookbook (v15): common tasks, entrypoints, API/proxy usage and migration notes." ---- - -[comment]: # (mx-abstract) - -## Overview - -This guide walks you through handling common tasks using the MultiversX Javascript SDK (v15, latest stable version). - -:::important -This cookbook makes use of `sdk-js v15`. In order to migrate from `sdk-js v14.x` to `sdk-js v15`, please also follow [the migration guide](https://github.com/multiversx/mx-sdk-js-core/issues/648). -::: - -## Creating an Entrypoint - -An Entrypoint represents a network client that simplifies access to the most common operations. -There is a dedicated entrypoint for each network: `MainnetEntrypoint`, `DevnetEntrypoint`, `TestnetEntrypoint`, `LocalnetEntrypoint`. - -For example, to create a Devnet entrypoint you have the following command: - -```js -const entrypoint = new DevnetEntrypoint(); -``` - -#### Using a Custom API -If you'd like to connect to a third-party API, you can specify the url parameter: - -```js -const apiEntrypoint = new DevnetEntrypoint({ url: "https://custom-multiversx-devnet-api.com" }); -``` - -#### Using a Proxy - -By default, the DevnetEntrypoint uses the standard API. However, you can create a custom entrypoint that interacts with a proxy by specifying the kind parameter: - -```js -const customEntrypoint = new DevnetEntrypoint({ url: "https://devnet-gateway.multiversx.com", kind: "proxy" }); -``` - -## Creating Accounts - -You can initialize an account directly from the entrypoint. Keep in mind that the account is network agnostic, meaning it doesn't matter which entrypoint is used. -Accounts are used for signing transactions and messages and managing the account's nonce. They can also be saved to a PEM or keystore file for future use. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const account = entrypoint.createAccount(); -} -``` - -### Other Ways to Instantiate an Account - -#### From a Secret Key -```js -{ - const secretKeyHex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; - const secretKey = new UserSecretKey(Buffer.from(secretKeyHex, "hex")); - - const accountFromSecretKey = new Account(secretKey); -} -``` - -#### From a PEM file -```js -{ - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const accountFromPem = Account.newFromPem(filePath); -} -``` - -#### From a Keystore File -```js -{ - const keystorePath = path.join("../src", "testdata", "testwallets", "alice.json"); - const accountFromKeystore = Account.newFromKeystore(keystorePath, "password"); -} -``` - -#### From a Mnemonic -```js - -const mnemonic = Mnemonic.generate(); -const accountFromMnemonic = Account.newFromMnemonic(mnemonic.toString()); -``` - -#### From a KeyPair - -```js -const keypair = KeyPair.generate(); -const accountFromKeyPairs = Account.newFromKeypair(keypair); -``` - -### Managing the Account Nonce - -An account has a `nonce` property that the user is responsible for managing. -You can fetch the nonce from the network and increment it after each transaction. -Each transaction must have the correct nonce, otherwise it will fail to execute. - -```js -{ - const secretKeyHex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; - const key = new UserSecretKey(Buffer.from(secretKeyHex, "hex")); - - const accountWithNonce = new Account(key); - const entrypoint = new DevnetEntrypoint(); - - // Fetch the current nonce from the network - accountWithNonce.nonce = await entrypoint.recallAccountNonce(accountWithNonce.address); - - // Create and send a transaction here... - - // Increment nonce after each transaction - const nonce = accountWithNonce.getNonceThenIncrement(); -} -``` - -For more details, see the [Creating Transactions](#creating-transactions) section. - -#### Saving the Account to a File - -Accounts can be saved to either a PEM file or a keystore file. -While PEM wallets are less secure for storing secret keys, they are convenient for testing purposes. -Keystore files offer a higher level of security. - -#### Saving the Account to a PEM File -```js -{ - const secretKeyHex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; - const secretKey = new UserSecretKey(Buffer.from(secretKeyHex, "hex")); - - const account = new Account(secretKey); - account.saveToPem(path.resolve("wallet.pem")); -} -``` - -#### Saving the Account to a Keystore File -```js -{ - const secretKeyHex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; - const secretKey = new UserSecretKey(Buffer.from(secretKeyHex, "hex")); - - const account = new Account(secretKey); - account.saveToKeystore(path.resolve("keystoreWallet.json"), "password"); -} - -``` - -### Using a Ledger Device - -You can manage your account with a Ledger device, allowing you to sign both transactions and messages while keeping your keys secure. - -Note: **The multiversx-sdk package does not include Ledger support by default. To enable it, install the package with Ledger dependencies**: -```bash -npm install @multiversx/sdk-hw-provider -``` - -#### Creating a Ledger Account -This can be done using the dedicated library. You can find more information [here](/sdk-and-tools/sdk-js/sdk-js-signing-providers/#the-hardware-wallet-provider). - -When signing transactions or messages, the Ledger device will prompt you to confirm the details before proceeding. - -### Compatibility with IAccount Interface - -The `Account` implements the `IAccount` interface, making it compatible with transaction controllers and any other component that expects this interface. - -## Calling the Faucet - -This functionality is not yet available through the entrypoint, but we recommend using the faucet available within the Web Wallet. For more details about the faucet [see this](/wallet/web-wallet/#testnet-and-devnet-faucet). - -- [Testnet Wallet](https://testnet-wallet.multiversx.com/). -- [Devnet Wallet](https://devnet-wallet.multiversx.com/). - -### Interacting with the network - -The entrypoint exposes a few ways to directly interact with the network, such as: - -- `recallAccountNonce(address: Address): Promise;` -- `sendTransactions(transactions: Transaction[]): Promise<[number, string[]]>;` -- `sendTransaction(transaction: Transaction): Promise;` -- `getTransaction(txHash: string): Promise;` -- `awaitCompletedTransaction(txHash: string): Promise;` - -Some other methods are exposed through a so called **network provider**. - -- **ApiNetworkProvider**: Interacts with the API, which is a layer over the proxy. It fetches data from the network and `Elastic Search`. -- **ProxyNetworkProvider**: Interacts directly with the proxy of an observing squad. - -To get the underlying network provider from our entrypoint, we can do as follows: - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const networkProvider = entrypoint.createNetworkProvider(); -} -``` - -### Creating a network provider -When manually instantiating a network provider, you can provide a configuration to specify the client name and set custom request options. - -```js -{ - // Create a configuration object - const config = { - clientName: "hello-multiversx", - requestsOptions: { - timeout: 1000, // Timeout in milliseconds - auth: { - username: "user", - password: "password", - }, - }, - }; - - // Instantiate the network provider with the config - const api = new ApiNetworkProvider("https://devnet-api.multiversx.com", config); -} -``` - -Here you can find a full list of available methods for [`ApiNetworkProvider`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/ApiNetworkProvider.html). - -Both `ApiNetworkProvider` and `ProxyNetworkProvider` implement a common interface, which can be found [here](https://multiversx.github.io/mx-sdk-js-core/v15/interfaces/INetworkProvider.html). This allows them to be used interchangeably. - -The classes returned by the API expose the most commonly used fields directly for convenience. However, each object also contains a `raw` field that stores the original API response, allowing access to additional fields if needed. - -## Fetching data from the network - -### Fetching the network config - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const networkProvider = entrypoint.createNetworkProvider(); - - const networkConfig = networkProvider.getNetworkConfig(); -} -``` - -### Fetching the network status - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const networkProvider = entrypoint.createNetworkProvider(); - - const metaNetworkStatus = networkProvider.getNetworkStatus(); // fetches status from metachain - const networkStatus = networkProvider.getNetworkStatus(); // fetches status from shard one -} -``` - -### Fetching a Block from the Network -To fetch a block, we first instantiate the required arguments and use its hash. The API only supports fetching blocks by hash, whereas the **PROXY** allows fetching blocks by either hash or nonce. - -When using the **PROXY**, keep in mind that the shard must also be specified in the arguments. - -#### Fetching a block using the **API** -```js -{ - const api = new ApiNetworkProvider("https://devnet-api.multiversx.com"); - const blockHash = "1147e111ce8dd860ae43a0f0d403da193a940bfd30b7d7f600701dd5e02f347a"; - const block = await api.getBlock(blockHash); -} -``` - -Additionally, we can fetch the latest block from the network: - -```js -{ - const api = new ApiNetworkProvider("https://devnet-api.multiversx.com"); - const latestBlock = await api.getLatestBlock(); -} -``` - -#### Fetching a block using the **PROXY** - -When using the proxy, we have to provide the shard, as well. -```js -{ - const proxy = new ProxyNetworkProvider("https://devnet-api.multiversx.com"); - const blockHash = "1147e111ce8dd860ae43a0f0d403da193a940bfd30b7d7f600701dd5e02f347a"; - const block = proxy.getBlock({ blockHash, shard: 1 }); -} -``` - -We can also fetch the latest block from the network. -By default, the shard will be the metachain, but we can specify a different shard if needed. - -```js -{ - const proxy = new ProxyNetworkProvider("https://devnet-api.multiversx.com"); - const latestBlock = proxy.getLatestBlock(); -} -``` - -### Fetching an Account -To fetch an account, we need its address. Once we have the address, we create an `Address` object and pass it as an argument to the method. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const account = await api.getAccount(alice); -} -``` - -### Fetching an Account's Storage -We can also fetch an account's storage, allowing us to retrieve all key-value pairs saved for that account. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const account = await api.getAccountStorage(alice); -} -``` - -If we only want to fetch a specific key, we can do so as follows: - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const account = await api.getAccountStorageEntry(alice, "testKey"); -} -``` - -### Waiting for an Account to Meet a Condition -There are times when we need to wait for a specific condition to be met before proceeding with an action. -For example, let's say we want to send 7 EGLD from Alice to Bob, but this can only happen once Alice's balance reaches at least 7 EGLD. -This approach is useful in scenarios where you're waiting for external funds to be sent to Alice, enabling her to transfer the required amount to another recipient. - -To implement this, we need to define the condition to check each time the account is fetched from the network. We create a function that takes an `AccountOnNetwork` object as an argument and returns a `bool`. -Keep in mind that this method has a default timeout, which can be adjusted using the `AwaitingOptions` class. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - - const condition = (account: any) => { - return account.balance >= 7000000000000000000n; // 7 EGLD - }; - const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const account = await api.awaitAccountOnCondition(alice, condition); -} -``` - -### Sending and Simulating Transactions -To execute transactions, we use the network providers to broadcast them to the network. Keep in mind that for transactions to be processed, they must be signed. - -#### Sending a Transaction - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - - const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const bob = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); - - const transaction = new Transaction({ - sender: alice, - receiver: bob, - gasLimit: 50000n, - chainID: "D", - }); - - // set the correct nonce and sign the transaction ... - - const transactionHash = await api.sendTransaction(transaction); -} -``` - -#### Sending multiple transactions -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - - const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const bob = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); - - const firstTransaction = new Transaction({ - sender: alice, - receiver: bob, - gasLimit: 50000n, - chainID: "D", - nonce: 2n, - }); - - const secondTransaction = new Transaction({ - sender: bob, - receiver: alice, - gasLimit: 50000n, - chainID: "D", - nonce: 1n, - }); - - const thirdTransaction = new Transaction({ - sender: alice, - receiver: alice, - gasLimit: 60000n, - chainID: "D", - nonce: 3n, - data: new Uint8Array(Buffer.from("hello")), - }); - - // set the correct nonce and sign the transaction ... - - const [numOfSentTxs, hashes] = await api.sendTransactions([ - firstTransaction, - secondTransaction, - thirdTransaction, - ]); -} -``` - -#### Simulating transactions -A transaction can be simulated before being sent for processing by the network. This is primarily used for smart contract calls, allowing you to preview the results produced by the smart contract. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - - const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqccmyzj9sade2495w78h42erfrw7qmqxpd8sss6gmgn"); - - const transaction = new Transaction({ - sender: alice, - receiver: contract, - gasLimit: 5000000n, - chainID: "D", - data: new Uint8Array(Buffer.from("add@07")), - }); - - const transactionOnNetwork = await api.simulateTransaction(transaction); -} -``` - -#### Estimating the gas cost of a transaction -Before sending a transaction to the network for processing, you can retrieve the estimated gas limit required for the transaction to be executed. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - - const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqccmyzj9sade2495w78h42erfrw7qmqxpd8sss6gmgn"); - - const nonce = await entrypoint.recallAccountNonce(alice); - - const transaction = new Transaction({ - sender: alice, - receiver: contract, - gasLimit: 5000000n, - chainID: "D", - data: new Uint8Array(Buffer.from("add@07")), - nonce: nonce, - }); - - const transactionCostResponse = await api.estimateTransactionCost(transaction); -} -``` - -### Waiting for transaction completion -After sending a transaction, you may want to wait until it is processed before proceeding with another action. Keep in mind that this method has a default timeout, which can be adjusted using the `AwaitingOptions` class. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - - const txHash = "exampletransactionhash"; - const transactionOnNetwork = await api.awaitTransactionCompleted(txHash); -} -``` - -### Waiting for a Transaction to Satisfy a Condition -Similar to accounts, we can wait until a transaction meets a specific condition. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - - const condition = (txOnNetwork: any) => !txOnNetwork.status.isSuccessful(); - - const txHash = "exampletransactionhash"; - const transactionOnNetwork = await api.awaitTransactionOnCondition(txHash, condition); -} -``` - -### Waiting for transaction completion -After sending a transaction, you may want to wait until it is processed before proceeding with another action. Keep in mind that this method has a default timeout, which can be adjusted using the `AwaitingOptions` class. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - - const txHash = "exampletransactionhash"; - const transactionOnNetwork = await api.awaitTransactionCompleted(txHash); -} -``` - -### Fetching Transactions from the Network -After sending a transaction, we can fetch it from the network using the transaction hash, which we receive after broadcasting the transaction. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - - const txHash = "exampletransactionhash"; - const transactionOnNetwork = await api.getTransaction(txHash); -} -``` - -### Fetching a token from an account -We can fetch a specific token (ESDT, MetaESDT, SFT, NFT) from an account by providing the account's address and the token identifier. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - - const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - let token = new Token({ identifier: "TEST-ff155e" }); // ESDT - let tokenOnNetwork = await api.getTokenOfAccount(alice, token); - - token = new Token({ identifier: "NFT-987654", nonce: 11n }); // NFT - tokenOnNetwork = await api.getTokenOfAccount(alice, token); -} -``` - -### Fetching all fungible tokens of an account -Fetches all fungible tokens held by an account. Note that this method does not handle pagination, but it can be achieved using `doGetGeneric`. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - - const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const fungibleTokens = await api.getFungibleTokensOfAccount(alice); -} -``` - -### Fetching all non-fungible tokens of an account -Fetches all non-fungible tokens held by an account. Note that this method does not handle pagination, but it can be achieved using `doGetGeneric`. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - - const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const nfts = await api.getNonFungibleTokensOfAccount(alice); -} -``` - -### Fetching token metadata -If we want to fetch the metadata of a token (e.g., owner, decimals, etc.), we can use the following methods: - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - - // used for ESDT - const fungibleTokenDefinition = await api.getDefinitionOfFungibleToken("TEST-ff155e"); - - // used for METAESDT, SFT, NFT - const nonFungibleTokenDefinition = await api.getDefinitionOfTokenCollection("NFTEST-ec88b8"); -} -``` - -### Querying Smart Contracts -Smart contract queries, or view functions, are endpoints that only read data from the contract. To send a query to the observer nodes, we can proceed as follows: - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - - const query = new SmartContractQuery({ - contract: Address.newFromBech32("erd1qqqqqqqqqqqqqpgqqy34h7he2ya6qcagqre7ur7cc65vt0mxrc8qnudkr4"), - function: "getSum", - arguments: [], - }); - const response = await api.queryContract(query); -} -``` - -### Custom Api/Proxy calls -The methods exposed by the `ApiNetworkProvider` or `ProxyNetworkProvider` are the most common and widely used. However, there may be times when custom API calls are needed. For these cases, we’ve created generic methods for both GET and POST requests. -Let’s assume we want to retrieve all the transactions sent by Alice in which the `delegate` function was called. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const api = entrypoint.createNetworkProvider(); - - const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const url = `transactions/${alice.toBech32()}?function=delegate`; - - const response = await api.doGetGeneric(url); -} -``` - -## Creating transactions - -In this section, we’ll explore how to create different types of transactions. To create transactions, we can use either controllers or factories. -Controllers are ideal for quick scripts or network interactions, while factories provide a more granular and lower-level approach, typically required for DApps. - -Controllers typically use the same parameters as factories, but they also require an Account object and the sender’s nonce. -Controllers also include extra functionality, such as waiting for transaction completion and parsing transactions. -The same functionality can be achieved for transactions built using factories, and we’ll see how in the sections below. In the next section, we’ll learn how to create transactions using both methods. - -### Instantiating Controllers and Factories -There are two ways to create controllers and factories: -1. Get them from the entrypoint. -2. Manually instantiate them. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - - // getting the controller and the factory from the entrypoint - const transfersController = entrypoint.createTransfersController(); - const transfersFactory = entrypoint.createTransfersTransactionsFactory(); - - // manually instantiating the controller and the factory - const controller = new TransfersController({ chainID: "D" }); - - const config = new TransactionsFactoryConfig({ chainID: "D" }); - const factory = new TransferTransactionsFactory({ config }); -} -``` - -### Estimating the Gas Limit for Transactions -Additionally, when creating transaction factories or controllers, we can pass an additional argument, a **gas limit estimator**. -This gas estimator simulates the transaction before being sent and computes the `gasLimit` that it will require. -The `GasLimitEstimator` can be initialized with a multiplier, so that the estimated value will be multiplied by the specified value. -The gas limit estimator can be provided to any factory or controller available. Let's see how we can create a `GasLimitEstimator` and use it. - -```js -{ - const api = new ApiNetworkProvider("https://devnet-api.multiversx.com"); - let gasEstimator = new GasLimitEstimator({ networkProvider: api }); // create a gas limit estimator with default multiplier of 1.0 - let gasEstimatorWithMultiplier = new GasLimitEstimator({ networkProvider: api, gasMultiplier: 1.5 }); // create a gas limit estimator with a multiplier of 1.5 - - const config = new TransactionsFactoryConfig({ chainID: "D" }); - const transfersFactory = new TransferTransactionsFactory({ - config: config, - gasLimitEstimator: gasEstimatorWithMultiplier, // or `gasEstimator` - }); -} -``` - -### Token transfers -We can send both native tokens (EGLD) and ESDT tokens using either the controller or the factory. -#### Native Token Transfers Using the Controller -When using the controller, the transaction will be signed because we’ll be working with an Account. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - const bob = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); - - // the developer is responsible for managing the nonce - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const transfersController = entrypoint.createTransfersController(); - const transaction = await transfersController.createTransactionForTransfer( - alice, - alice.getNonceThenIncrement(), - { - receiver: bob, - nativeAmount: 1n, - }, - ); - - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -If you know you’ll only be sending native tokens, you can create the transaction using the `createTransactionForNativeTokenTransfer` method. - -#### Native Token Transfers Using the Factory -When using the factory, only the sender's address is required. As a result, the transaction won’t be signed, and the nonce field won’t be set correctly. -You will need to handle these aspects after the transaction is created. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createTransfersTransactionsFactory(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // the developer is responsible for managing the nonce - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const bob = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); - - const transaction = await factory.createTransactionForTransfer(alice.address, { - receiver: bob, - nativeAmount: 1000000000000000000n, - }); - - // set the sender's nonce - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction using the sender's account - transaction.signature = await alice.signTransaction(transaction); - - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -If you know you’ll only be sending native tokens, you can create the transaction using the `createTransactionForNativeTokenTransfer` method. - -#### Custom token transfers using the controller - -```js -{ - const entrypoint = new DevnetEntrypoint(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - const bob = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); - - // the developer is responsible for managing the nonce - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const esdt = new Token({ identifier: "TEST-123456" }); - const firstTransfer = new TokenTransfer({ token: esdt, amount: 1000000000n }); - - const nft = new Token({ identifier: "NFT-987654", nonce: 10n }); - const secondTransfer = new TokenTransfer({ token: nft, amount: 1n }); - - const sft = new Token({ identifier: "SFT-987654", nonce: 10n }); - const thirdTransfer = new TokenTransfer({ token: sft, amount: 7n }); - - const transfersController = entrypoint.createTransfersController(); - const transaction = await transfersController.createTransactionForTransfer( - alice, - alice.getNonceThenIncrement(), - { - receiver: bob, - tokenTransfers: [firstTransfer, secondTransfer, thirdTransfer], - }, - ); - - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -If you know you'll only send ESDT tokens, the same transaction can be created using createTransactionForEsdtTokenTransfer. - -#### Custom token transfers using the factory -When using the factory, only the sender's address is required. As a result, the transaction won’t be signed, and the nonce field won’t be set correctly. These aspects should be handled after the transaction is created. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createTransfersTransactionsFactory(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - const bob = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); - - // the developer is responsible for managing the nonce - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const esdt = new Token({ identifier: "TEST-123456" }); // fungible tokens don't have a nonce - const firstTransfer = new TokenTransfer({ token: esdt, amount: 1000000000n }); // we set the desired amount we want to send - - const nft = new Token({ identifier: "NFT-987654", nonce: 10n }); - const secondTransfer = new TokenTransfer({ token: nft, amount: 1n }); // for NFTs we set the amount to `1` - - const sft = new Token({ identifier: "SFT-987654", nonce: 10n }); - const thirdTransfer = new TokenTransfer({ token: sft, amount: 7n }); // for SFTs we set the desired amount we want to send - - const transaction = await factory.createTransactionForTransfer(alice.address, { - receiver: bob, - tokenTransfers: [firstTransfer, secondTransfer, thirdTransfer], - }); - - // set the sender's nonce - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction using the sender's account - transaction.signature = await alice.signTransaction(transaction); - - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -If you know you'll only send ESDT tokens, the same transaction can be created using createTransactionForEsdtTokenTransfer. - -#### Sending native and custom tokens -Both native and custom tokens can now be sent. If a `nativeAmount` is provided along with `tokenTransfers`, the native token will be included in the `MultiESDTNFTTransfer` built-in function call. -We can send both types of tokens using either the `controller` or the `factory`, but for simplicity, we’ll use the controller in this example. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - const bob = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); - - // the developer is responsible for managing the nonce - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const esdt = new Token({ identifier: "TEST-123456" }); - const firstTransfer = new TokenTransfer({ token: esdt, amount: 1000000000n }); - - const nft = new Token({ identifier: "NFT-987654", nonce: 10n }); - const secondTransfer = new TokenTransfer({ token: nft, amount: 1n }); - - const transfersController = entrypoint.createTransfersController(); - const transaction = await transfersController.createTransactionForTransfer( - alice, - alice.getNonceThenIncrement(), - { - receiver: bob, - nativeAmount: 1000000000000000000n, - tokenTransfers: [firstTransfer, secondTransfer], - }, - ); - - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -### Smart Contracts - -#### Contract ABIs - -A contract's ABI (Application Binary Interface) describes the endpoints, data structures, and events that the contract exposes. -While interactions with the contract are possible without the ABI, they are much easier to implement when the definitions are available. - -#### Loading the ABI from a file -```js -{ - let abiJson = await promises.readFile("../src/testData/adder.abi.json", { encoding: "utf8" }); - let abiObj = JSON.parse(abiJson); - let abi = Abi.create(abiObj); -} -``` - -#### Loading the ABI from an URL - -```js -{ - const response = await axios.get( - "https://github.com/multiversx/mx-sdk-js-core/raw/main/src/testdata/adder.abi.json", - ); - let abi = Abi.create(response.data); -} -``` - -#### Manually construct the ABI - -If an ABI file isn’t available, but you know the contract’s endpoints and data types, you can manually construct the ABI. - -```js -{ - let abi = Abi.create({ - endpoints: [ - { - name: "add", - inputs: [], - outputs: [], - }, - ], - }); -} -``` - -```js -{ - let abi = Abi.create({ - endpoints: [ - { - name: "foo", - inputs: [{ type: "BigUint" }, { type: "u32" }, { type: "Address" }], - outputs: [{ type: "u32" }], - }, - { - name: "bar", - inputs: [{ type: "counted-variadic" }, { type: "variadic" }], - outputs: [], - }, - ], - }); -} -``` - -### Smart Contract deployments -For creating smart contract deployment transactions, we have two options: a controller and a factory. Both function similarly to the ones used for token transfers. -When creating transactions that interact with smart contracts, it's recommended to provide the ABI file to the controller or factory if possible. -This allows arguments to be passed as native Javascript values. If the ABI is not available, but we know the expected data types, we can pass arguments as typed values (e.g., `BigUIntValue`, `ListValue`, `StructValue`, etc.) or as raw bytes. - -#### Deploying a Smart Contract Using the Controller - -```js -{ - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const sender = await Account.newFromPem(filePath); - const entrypoint = new DevnetEntrypoint(); - - // the developer is responsible for managing the nonce - sender.nonce = await entrypoint.recallAccountNonce(sender.address); - - // load the contract bytecode - const bytecode = await promises.readFile("../src/testData/adder.wasm"); - // load the abi file - const abi = await loadAbiRegistry("../src/testdata/adder.abi.json"); - - const controller = entrypoint.createSmartContractController(abi); - - // For deploy arguments, use "TypedValue" objects if you haven't provided an ABI to the factory: - let args: any[] = [new U32Value(42)]; - // Or use simple, plain JavaScript values and objects if you have provided an ABI to the factory: - args = [42]; - - const deployTransaction = await controller.createTransactionForDeploy(sender, sender.getNonceThenIncrement(), { - bytecode: bytecode, - gasLimit: 6000000n, - arguments: args, - }); - - // broadcasting the transaction - const txHash = await entrypoint.sendTransaction(deployTransaction); -} -``` - -:::tip -When creating transactions using [`SmartContractController`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/SmartContractController.html) or [`SmartContractTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/SmartContractTransactionsFactory.html), even if the ABI is available and provided, -you can still use [`TypedValue`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/TypedValue.html) objects as arguments for deployments and interactions. - -Even further, you can use a mix of [`TypedValue`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/TypedValue.html) objects and plain JavaScript values and objects. For example: - -```js -let args = [new U32Value(42), "hello", { foo: "bar" }, new TokenIdentifierValue("TEST-abcdef")]; -``` -::: - -#### Parsing contract deployment transactions - -```js -{ - // We use the transaction hash we got when broadcasting the transaction - - const abi = await loadAbiRegistry("../src/testdata/adder.abi.json"); - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createSmartContractController(abi); - const outcome = await controller.awaitCompletedDeploy("txHash"); // waits for transaction completion and parses the result - const contractAddress = outcome.contracts[0].address; -} -``` - -If we want to wait for transaction completion and parse the result in two different steps, we can do as follows: - -```js -{ - // We use the transaction hash we got when broadcasting the transaction - // If we want to wait for transaction completion and parse the result in two different steps, we can do as follows: - - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createSmartContractController(); - const networkProvider = entrypoint.createNetworkProvider(); - const transactionOnNetwork = await networkProvider.awaitTransactionCompleted("txHash"); - - // parsing the transaction - const outcome = await controller.parseDeploy(transactionOnNetwork); -} -``` - -#### Computing the contract address - -Even before broadcasting, at the moment you know the sender's address and the nonce for your deployment transaction, you can (deterministically) compute the (upcoming) address of the smart contract: - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createSmartContractTransactionsFactory(); - const bytecode = await promises.readFile("../contracts/adder.wasm"); - - // For deploy arguments, use "TypedValue" objects if you haven't provided an ABI to the factory: - let args: any[] = [new BigUIntValue(42)]; - // Or use simple, plain JavaScript values and objects if you have provided an ABI to the factory: - args = [42]; - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - const deployTransaction = await factory.createTransactionForDeploy(alice.address, { - bytecode: bytecode, - gasLimit: 6000000n, - arguments: args, - }); - const addressComputer = new AddressComputer(); - const contractAddress = addressComputer.computeContractAddress( - deployTransaction.sender, - deployTransaction.nonce, - ); - - console.log("Contract address:", contractAddress.toBech32()); -} -``` - -#### Deploying a Smart Contract using the factory -After the transaction is created the nonce needs to be properly set and the transaction should be signed before broadcasting it. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createSmartContractTransactionsFactory(); - - // load the contract bytecode - const bytecode = await promises.readFile("../src/testData/adder.wasm"); - - // For deploy arguments, use "TypedValue" objects if you haven't provided an ABI to the factory: - let args: any[] = [new BigUIntValue(42)]; - // Or use simple, plain JavaScript values and objects if you have provided an ABI to the factory: - args = [42]; - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const deployTransaction = await factory.createTransactionForDeploy(alice.address, { - bytecode: bytecode, - gasLimit: 6000000n, - arguments: args, - }); - - // the developer is responsible for managing the nonce - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - // set the nonce - deployTransaction.nonce = alice.nonce; - - // sign the transaction - deployTransaction.signature = await alice.signTransaction(deployTransaction); - - // broadcasting the transaction - const txHash = await entrypoint.sendTransaction(deployTransaction); - - // waiting for transaction to complete - const transactionOnNetwork = await entrypoint.awaitCompletedTransaction(txHash); - - // parsing transaction - const parser = new SmartContractTransactionsOutcomeParser(); - const parsedOutcome = parser.parseDeploy({ transactionOnNetwork }); - const contractAddress = parsedOutcome.contracts[0].address; - - console.log(contractAddress.toBech32()); -} -``` - -### Smart Contract calls - -In this section we'll see how we can call an endpoint of our previously deployed smart contract using both approaches with the `controller` and the `factory`. - -#### Calling a smart contract using the controller - -```js -{ - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const sender = await Account.newFromPem(filePath); - const entrypoint = new DevnetEntrypoint(); - - // the developer is responsible for managing the nonce - sender.nonce = await entrypoint.recallAccountNonce(sender.address); - - // load the abi file - const abi = await loadAbiRegistry("../src/testdata/adder.abi.json"); - const controller = entrypoint.createSmartContractController(abi); - - const contractAddress = Address.newFromBech32("erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug"); - - // For deploy arguments, use "TypedValue" objects if you haven't provided an ABI to the factory: - let args: any[] = [new U32Value(42)]; - // Or use simple, plain JavaScript values and objects if you have provided an ABI to the factory: - args = [42]; - - const transaction = await controller.createTransactionForExecute(sender, sender.getNonceThenIncrement(), { - contract: contractAddress, - gasLimit: 5000000n, - function: "add", - arguments: args, - }); - - // broadcasting the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - console.log(txHash); -} -``` - -#### Parsing smart contract call transactions -In our case, calling the add endpoint does not return anything, but similar to the example above, we could parse this transaction to get the output values of a smart contract call. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createSmartContractController(); - const txHash = "b3ae88ad05c464a74db73f4013de05abcfcb4fb6647c67a262a6cfdf330ef4a9"; - // waits for transaction completion and parses the result - const parsedOutcome = await controller.awaitCompletedExecute(txHash); - const values = parsedOutcome.values; -} -``` - -#### Calling a smart contract and sending tokens (transfer & execute) -Additionally, if an endpoint requires a payment when called, we can send tokens to the contract while creating a smart contract call transaction. -Both EGLD and ESDT tokens or a combination of both can be sent. This functionality is supported by both the controller and the factory. - -```js -{ - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const sender = await Account.newFromPem(filePath); - const entrypoint = new DevnetEntrypoint(); - - // the developer is responsible for managing the nonce - sender.nonce = await entrypoint.recallAccountNonce(sender.address); - - // load the abi file - const abi = await loadAbiRegistry("../src/testdata/adder.abi.json"); - - // get the smart contracts controller - const controller = entrypoint.createSmartContractController(abi); - - const contractAddress = Address.newFromBech32("erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug"); - - // For deploy arguments, use "TypedValue" objects if you haven't provided an ABI to the factory: - let args: any[] = [new U32Value(42)]; - // Or use simple, plain JavaScript values and objects if you have provided an ABI to the factory: - args = [42]; - - // creating the transfers - const firstToken = new Token({ identifier: "TEST-38f249", nonce: 10n }); - const firstTransfer = new TokenTransfer({ token: firstToken, amount: 1n }); - - const secondToken = new Token({ identifier: "BAR-c80d29" }); - const secondTransfer = new TokenTransfer({ token: secondToken, amount: 10000000000000000000n }); - - const transaction = await controller.createTransactionForExecute(sender, sender.getNonceThenIncrement(), { - contract: contractAddress, - gasLimit: 5000000n, - function: "add", - arguments: args, - nativeTransferAmount: 1000000000000000000n, - tokenTransfers: [firstTransfer, secondTransfer], - }); - - // broadcasting the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - console.log(txHash); -} -``` - -#### Calling a smart contract using the factory -Let's create the same smart contract call transaction, but using the `factory`. - -```js -{ - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - const entrypoint = new DevnetEntrypoint(); - - // the developer is responsible for managing the nonce - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - // get the smart contracts controller - const controller = entrypoint.createSmartContractTransactionsFactory(); - - const contractAddress = Address.newFromBech32("erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug"); - - // For deploy arguments, use "TypedValue" objects if you haven't provided an ABI to the factory: - let args: any[] = [new U32Value(42)]; - // Or use simple, plain JavaScript values and objects if you have provided an ABI to the factory: - args = [42]; - - // creating the transfers - const firstToken = new Token({ identifier: "TEST-38f249", nonce: 10n }); - const firstTransfer = new TokenTransfer({ token: firstToken, amount: 1n }); - - const secondToken = new Token({ identifier: "BAR-c80d29" }); - const secondTransfer = new TokenTransfer({ token: secondToken, amount: 10000000000000000000n }); - - const transaction = await controller.createTransactionForExecute(alice.address, { - contract: contractAddress, - gasLimit: 5000000n, - function: "add", - arguments: args, - nativeTransferAmount: 1000000000000000000n, - tokenTransfers: [firstTransfer, secondTransfer], - }); - - transaction.nonce = alice.getNonceThenIncrement(); - transaction.signature = await alice.signTransaction(transaction); - - // broadcasting the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - console.log(txHash); -} -``` - -#### Parsing transaction outcome -As said before, the `add` endpoint we called does not return anything, but we could parse the outcome of smart contract call transactions, as follows: - -```js -{ - // load the abi file - const entrypoint = new DevnetEntrypoint(); - const abi = await loadAbiRegistry("../src/testdata/adder.abi.json"); - const parser = new SmartContractTransactionsOutcomeParser({ abi }); - const txHash = "b3ae88ad05c464a74db73f4013de05abcfcb4fb6647c67a262a6cfdf330ef4a9"; - const transactionOnNetwork = await entrypoint.getTransaction(txHash); - const outcome = parser.parseExecute({ transactionOnNetwork }); -} -``` - -#### Decoding transaction events -You might be interested into decoding events emitted by a contract. You can do so by using the `TransactionEventsParser`. - -Suppose we'd like to decode a `startPerformAction` event emitted by the [multisig](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/multisig) contract. - -First, we load the abi file, then we fetch the transaction, we extract the event from the transaction and then we parse it. - -```js -{ - // load the abi files - const entrypoint = new DevnetEntrypoint(); - const abi = await loadAbiRegistry("../src/testdata/adder.abi.json"); - const parser = new TransactionEventsParser({ abi }); - const txHash = "b3ae88ad05c464a74db73f4013de05abcfcb4fb6647c67a262a6cfdf330ef4a9"; - const transactionOnNetwork = await entrypoint.getTransaction(txHash); - const events = gatherAllEvents(transactionOnNetwork); - const outcome = parser.parseEvents({ events }); -} -``` - -#### Encoding / decoding custom types -Whenever needed, the contract ABI can be used for manually encoding or decoding custom types. - -Let's encode a struct called EsdtTokenPayment (of [multisig](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/multisig) contract) into binary data. -```js -{ - const abi = await loadAbiRegistry("../src/testdata/multisig-full.abi.json"); - const paymentType = abi.getStruct("EsdtTokenPayment"); - const codec = new BinaryCodec(); - - const paymentStruct = new Struct(paymentType, [ - new Field(new TokenIdentifierValue("TEST-8b028f"), "token_identifier"), - new Field(new U64Value(0n), "token_nonce"), - new Field(new BigUIntValue(10000n), "amount"), - ]); - - const encoded = codec.encodeNested(paymentStruct); - - console.log(encoded.toString("hex")); -} -``` - -Now let's decode a struct using the ABI. -```js -{ - const abi = await loadAbiRegistry("../src/testdata/multisig-full.abi.json"); - const actionStructType = abi.getEnum("Action"); - const data = Buffer.from( - "0500000000000000000500d006f73c4221216fa679bc559005584c4f1160e569e1000000012a0000000003616464000000010000000107", - "hex", - ); - - const codec = new BinaryCodec(); - const [decoded] = codec.decodeNested(data, actionStructType); - const decodedValue = decoded.valueOf(); - console.log(JSON.stringify(decodedValue, null, 4)); -} -``` - -### Smart Contract queries -When querying a smart contract, a **view function** is called. A view function does not modify the state of the contract, so we do not need to send a transaction. -To perform this query, we use the **SmartContractController**. While we can use the contract's ABI file to encode the query arguments, we can also use it to parse the result. -In this example, we will query the **adder smart contract** by calling its `getSum` endpoint. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const contractAddress = Address.newFromBech32("erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug"); - const abi = await loadAbiRegistry("../src/testdata/adder.abi.json"); - - // create the controller - const controller = entrypoint.createSmartContractController(abi); - - // creates the query, runs the query, parses the result - const response = await controller.query({ contract: contractAddress, function: "getSum", arguments: [] }); -} -``` - -If we need more granular control, we can split the process into three steps: **create the query, run the query, and parse the query response**. -This approach achieves the same result as the previous example. - -```js -{ - const entrypoint = new DevnetEntrypoint(); - - // load the abi - const abi = await loadAbiRegistry("../src/testdata/adder.abi.json"); - - // the contract address we'll query - const contractAddress = Address.newFromBech32("erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug"); - - // create the controller - const controller = entrypoint.createSmartContractController(abi); - - // create the query - const query = await controller.createQuery({ contract: contractAddress, function: "getSum", arguments: [] }); - // runs the query - const response = await controller.runQuery(query); - - // parse the result - const parsedResponse = controller.parseQueryResponse(response); -} -``` - -### Upgrading a smart contract -Contract upgrade transactions are similar to deployment transactions (see above) because they also require contract bytecode. -However, in this case, the contract address is already known. Like deploying a smart contract, we can upgrade a smart contract using either the **controller** or the **factory**. - -#### Uprgrading a smart contract using the controller -```js -{ - // prepare the account - const entrypoint = new DevnetEntrypoint(); - const keystorePath = path.join("../src", "testdata", "testwallets", "alice.json"); - const sender = Account.newFromKeystore(keystorePath, "password"); - // the developer is responsible for managing the nonce - sender.nonce = await entrypoint.recallAccountNonce(sender.address); - - // load the abi - const abi = await loadAbiRegistry("../src/testdata/adder.abi.json"); - - // create the controller - const controller = entrypoint.createSmartContractController(abi); - - // load the contract bytecode; this is the new contract code, the one we want to upgrade to - const bytecode = await promises.readFile("../src/testData/adder.wasm"); - - // For deploy arguments, use "TypedValue" objects if you haven't provided an ABI to the factory: - let args: any[] = [new U32Value(42)]; - // Or use simple, plain JavaScript values and objects if you have provided an ABI to the factory: - args = [42]; - - const contractAddress = Address.newFromBech32("erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug"); - - const upgradeTransaction = await controller.createTransactionForUpgrade( - sender, - sender.getNonceThenIncrement(), - { - contract: contractAddress, - bytecode: bytecode, - gasLimit: 6000000n, - arguments: args, - }, - ); - - // broadcasting the transaction - const txHash = await entrypoint.sendTransaction(upgradeTransaction); - - console.log({ txHash }); -} -``` - -### Token management - -In this section, we're going to create transactions to issue fungible tokens, issue semi-fungible tokens, create NFTs, set token roles, but also parse these transactions to extract their outcome (e.g. get the token identifier of the newly issued token). - -These methods are available through the `TokenManagementController` and the `TokenManagementTransactionsFactory`. -The controller also includes built-in methods for awaiting the completion of transactions and parsing their outcomes. -For the factory, the same functionality can be achieved using the `TokenManagementTransactionsOutcomeParser`. - -For scripts or quick network interactions, we recommend using the controller. However, for a more granular approach (e.g., DApps), the factory is the better choice. - -#### Issuing fungible tokens using the controller -```js -{ - // create the entrypoint and the token management controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createTokenManagementController(); - - // create the issuer of the token - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const transaction = await controller.createTransactionForIssuingFungible(alice, alice.getNonceThenIncrement(), { - tokenName: "NEWFNG", - tokenTicker: "FNG", - initialSupply: 1_000_000_000000n, - numDecimals: 6n, - canFreeze: false, - canWipe: true, - canPause: false, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: false, - }); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - // wait for transaction to execute, extract the token identifier - const outcome = await controller.awaitCompletedIssueFungible(txHash); - - const tokenIdentifier = outcome[0].tokenIdentifier; -} -``` - -#### Issuing fungible tokens using the factory -```js -{ - // create the entrypoint and the token management transactions factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createTokenManagementTransactionsFactory(); - - // create the issuer of the token - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const transaction = await factory.createTransactionForIssuingFungible(alice.address, { - tokenName: "NEWFNG", - tokenTicker: "FNG", - initialSupply: 1_000_000_000000n, - numDecimals: 6n, - canFreeze: false, - canWipe: true, - canPause: false, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: false, - }); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - // wait for transaction to execute, extract the token identifier - // if we know that the transaction is completed, we can simply call `entrypoint.get_transaction(tx_hash)` - const transactionOnNetwork = await entrypoint.awaitCompletedTransaction(txHash); - - // extract the token identifier - const parser = new TokenManagementTransactionsOutcomeParser(); - const outcome = parser.parseIssueFungible(transactionOnNetwork); - const tokenIdentifier = outcome[0].tokenIdentifier; -} -``` - -#### Setting special roles for fungible tokens using the controller -```js -{ - // create the entrypoint and the token management controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createTokenManagementController(); - - // create the issuer of the token - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const bob = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); - - const transaction = await controller.createTransactionForSettingSpecialRoleOnFungibleToken( - alice, - alice.getNonceThenIncrement(), - { - user: bob, - tokenIdentifier: "TEST-123456", - addRoleLocalMint: true, - addRoleLocalBurn: true, - addRoleESDTTransferRole: true, - }, - ); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - // wait for transaction to execute, extract the token identifier - const outcome = await controller.awaitCompletedSetSpecialRoleOnFungibleToken(txHash); - - const roles = outcome[0].roles; - const user = outcome[0].userAddress; -} -``` - -#### Setting special roles for fungible tokens using the factory -```js -{ - // create the entrypoint and the token management controller - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createTokenManagementTransactionsFactory(); - - // create the issuer of the token - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const transaction = await factory.createTransactionForIssuingFungible(alice.address, { - tokenName: "TEST", - tokenTicker: "TEST", - initialSupply: 100n, - numDecimals: 0n, - canFreeze: true, - canWipe: true, - canPause: true, - canChangeOwner: true, - canUpgrade: false, - canAddSpecialRoles: false, - }); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - // wait for transaction to execute, extract the token identifier - // if we know that the transaction is completed, we can simply call `entrypoint.get_transaction(tx_hash)` - const transactionOnNetwork = await entrypoint.awaitCompletedTransaction(txHash); - - const parser = new TokenManagementTransactionsOutcomeParser(); - const outcome = parser.parseSetSpecialRole(transactionOnNetwork); - - const roles = outcome[0].roles; - const user = outcome[0].userAddress; -} -``` - -#### Issuing semi-fungible tokens using the controller -```js -{ - // create the entrypoint and the token management controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createTokenManagementController(); - - // create the issuer of the token - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const transaction = await controller.createTransactionForIssuingSemiFungible( - alice, - alice.getNonceThenIncrement(), - { - tokenName: "NEWSEMI", - tokenTicker: "SEMI", - canFreeze: false, - canWipe: true, - canPause: false, - canTransferNFTCreateRole: true, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: true, - }, - ); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - // wait for transaction to execute, extract the token identifier - const outcome = await controller.awaitCompletedIssueSemiFungible(txHash); - - const tokenIdentifier = outcome[0].tokenIdentifier; -} -``` - -#### Issuing semi-fungible tokens using the factory -```js -{ - // create the entrypoint and the token management controller - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createTokenManagementTransactionsFactory(); - - // create the issuer of the token - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const transaction = await factory.createTransactionForIssuingSemiFungible(alice.address, { - tokenName: "NEWSEMI", - tokenTicker: "SEMI", - canFreeze: false, - canWipe: true, - canPause: false, - canTransferNFTCreateRole: true, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: true, - }); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - // wait for transaction to execute, extract the token identifier - const transactionOnNetwork = await entrypoint.awaitCompletedTransaction(txHash); - - // extract the token identifier - const parser = new TokenManagementTransactionsOutcomeParser(); - const outcome = parser.parseIssueSemiFungible(transactionOnNetwork); - - const tokenIdentifier = outcome[0].tokenIdentifier; -} -``` - -#### Issuing NFT collection & creating NFTs using the controller - -```js -{ - // create the entrypoint and the token management controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createTokenManagementController(); - - // create the issuer of the token - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - let transaction = await controller.createTransactionForIssuingNonFungible( - alice, - alice.getNonceThenIncrement(), - { - tokenName: "NEWNFT", - tokenTicker: "NFT", - canFreeze: false, - canWipe: true, - canPause: false, - canTransferNFTCreateRole: true, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: true, - }, - ); - - // sending the transaction - let txHash = await entrypoint.sendTransaction(transaction); - - // wait for transaction to execute, extract the token identifier - const outcome = await controller.awaitCompletedIssueNonFungible(txHash); - - const collectionIdentifier = outcome[0].tokenIdentifier; - - // create an NFT - transaction = await controller.createTransactionForCreatingNft(alice, alice.getNonceThenIncrement(), { - tokenIdentifier: "FRANK-aa9e8d", - initialQuantity: 1n, - name: "test", - royalties: 1000, - hash: "abba", - attributes: Buffer.from("test"), - uris: ["a", "b"], - }); - - // sending the transaction - txHash = await entrypoint.sendTransaction(transaction); - - // wait for transaction to execute, extract the token identifier - const outcomeNft = await controller.awaitCompletedCreateNft(txHash); - - const identifier = outcomeNft[0].tokenIdentifier; - const nonce = outcomeNft[0].nonce; - const initialQuantity = outcomeNft[0].initialQuantity; -} -``` - -#### Issuing NFT collection & creating NFTs using the factory -```js -{ - // create the entrypoint and the token management transdactions factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createTokenManagementTransactionsFactory(); - - // create the issuer of the token - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - let transaction = await factory.createTransactionForIssuingNonFungible(alice.address, { - tokenName: "NEWNFT", - tokenTicker: "NFT", - canFreeze: false, - canWipe: true, - canPause: false, - canTransferNFTCreateRole: true, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: true, - }); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - let txHash = await entrypoint.sendTransaction(transaction); - - // wait for transaction to execute, extract the token identifier - let transactionOnNetwork = await entrypoint.awaitCompletedTransaction(txHash); - - // extract the token identifier - let parser = new TokenManagementTransactionsOutcomeParser(); - let outcome = parser.parseIssueNonFungible(transactionOnNetwork); - - const collectionIdentifier = outcome[0].tokenIdentifier; - - transaction = await factory.createTransactionForCreatingNFT(alice.address, { - tokenIdentifier: "FRANK-aa9e8d", - initialQuantity: 1n, - name: "test", - royalties: 1000, - hash: "abba", - attributes: Buffer.from("test"), - uris: ["a", "b"], - }); - - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - txHash = await entrypoint.sendTransaction(transaction); - - // ### wait for transaction to execute, extract the token identifier - transactionOnNetwork = await entrypoint.awaitCompletedTransaction(txHash); - - outcome = parser.parseIssueNonFungible(transactionOnNetwork); - - const identifier = outcome[0].tokenIdentifier; -} -``` - -These are just a few examples of what you can do using the token management controller or factory. For a complete list of supported methods, please refer to the autogenerated documentation: - -- [`TokenManagementController`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/TokenManagementController.html) -- [`TokenManagementTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/TokenManagementTransactionsFactory.html) - -### Account management - -The account management controller and factory allow us to create transactions for managing accounts, such as: -- Guarding and unguarding accounts -- Saving key-value pairs in the account storage, on the blockchain. - -To learn more about Guardians, please refer to the [official documentation](/developers/built-in-functions/#setguardian). -A guardian can also be set using the WebWallet, which leverages our hosted `Trusted Co-Signer Service`. Follow [this guide](/wallet/web-wallet/#guardian) for step-by-step instructions on guarding an account using the wallet. - -#### Guarding an account using the controller -```js -{ - // create the entrypoint and the account controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createAccountController(); - - // create the account to guard - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - // we can use a trusted service that provides a guardian, or simply set another address we own or trust - const guardian = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); - - const transaction = await controller.createTransactionForSettingGuardian(alice, alice.getNonceThenIncrement(), { - guardianAddress: guardian, - serviceID: "SelfOwnedAddress", // this is just an example - }); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Guarding an account using the factory -```js -{ - // create the entrypoint and the account management factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createAccountTransactionsFactory(); - - // create the account to guard - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // we can use a trusted service that provides a guardian, or simply set another address we own or trust - const guardian = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); - - const transaction = await factory.createTransactionForSettingGuardian(alice.address, { - guardianAddress: guardian, - serviceID: "SelfOwnedAddress", // this is just an example - }); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - // set the nonce - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -Once a guardian is set, we must wait **20 epochs** before it can be activated. After activation, all transactions sent from the account must also be signed by the guardian. - -#### Activating the guardian using the controller -```js -{ - // create the entrypoint and the account controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createAccountController(); - - // create the account to guard - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const transaction = await controller.createTransactionForGuardingAccount( - alice, - alice.getNonceThenIncrement(), - {}, - ); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Activating the guardian using the factory -```js -{ - // create the entrypoint and the account factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createAccountTransactionsFactory(); - - // create the account to guard - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const transaction = await factory.createTransactionForGuardingAccount(alice.address); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - // set the nonce - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Unguarding the account using the controller -```js -{ - // create the entrypoint and the account controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createAccountController(); - - // create the account to unguard - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const transaction = await controller.createTransactionForUnguardingAccount( - alice, - alice.getNonceThenIncrement(), - {}, - ); - - // the transaction should also be signed by the guardian before being sent otherwise it won't be executed - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Unguarding the guardian using the factory -```js -{ - // create the entrypoint and the account factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createAccountTransactionsFactory(); - - // create the account to guard - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const transaction = await factory.createTransactionForUnguardingAccount(alice.address, {}); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - // set the nonce - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Saving a key-value pair to an account using the controller -You can find more information [here](/developers/account-storage) regarding the account storage. - -```js -{ - // create the entrypoint and the account controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createAccountController(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // creating the key-value pairs we want to save - const keyValuePairs = new Map([[Buffer.from("key0"), Buffer.from("value0")]]); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const transaction = await controller.createTransactionForSavingKeyValue(alice, alice.getNonceThenIncrement(), { - keyValuePairs: keyValuePairs, - }); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Saving a key-value pair to an account using the factory -```js -{ - // create the entrypoint and the account factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createAccountTransactionsFactory(); - - // create the account to guard - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // creating the key-value pairs we want to save - const keyValuePairs = new Map([[Buffer.from("key0"), Buffer.from("value0")]]); - - const transaction = await factory.createTransactionForSavingKeyValue(alice.address, { - keyValuePairs: keyValuePairs, - }); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - // set the nonce - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -### Delegation management - -To learn more about staking providers and delegation, please refer to the official [documentation](/validators/delegation-manager/#introducing-staking-providers). -In this section, we'll cover how to: -- Create a new delegation contract -- Retrieve the contract address -- Delegate funds to the contract -- Redelegate rewards -- Claim rewards -- Undelegate and withdraw funds - -These operations can be performed using both the controller and the **factory**. For a complete list of supported methods, please refer to the autogenerated documentation: -- [`DelegationController`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/DelegationController.html) -- [`DelegationTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/DelegationTransactionsFactory.html) - -#### Creating a New Delegation Contract Using the Controller -```js -{ - // create the entrypoint and the delegation controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createDelegationController(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const transaction = await controller.createTransactionForNewDelegationContract( - alice, - alice.getNonceThenIncrement(), - { - totalDelegationCap: 0n, - serviceFee: 10n, - amount: 1250000000000000000000n, - }, - ); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - // wait for transaction completion, extract delegation contract's address - const outcome = await controller.awaitCompletedCreateNewDelegationContract(txHash); - - const contractAddress = outcome[0].contractAddress; -} -``` - -#### Creating a new delegation contract using the factory -```js -{ - // create the entrypoint and the delegation factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createDelegationTransactionsFactory(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const transaction = await factory.createTransactionForNewDelegationContract(alice.address, { - totalDelegationCap: 0n, - serviceFee: 10n, - amount: 1250000000000000000000n, - }); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - // set the nonce - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - // waits until the transaction is processed and fetches it from the network - const transactionOnNetwork = await entrypoint.awaitCompletedTransaction(txHash); - - // extract the token identifier - const parser = new TokenManagementTransactionsOutcomeParser(); - const outcome = parser.parseIssueFungible(transactionOnNetwork); - const tokenIdentifier = outcome[0].tokenIdentifier; -} -``` - -#### Delegating funds to the contract using the Controller -We can send funds to a delegation contract to earn rewards. - -```js -{ - // create the entrypoint and the delegation controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createDelegationController(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva"); - - const transaction = await controller.createTransactionForDelegating(alice, alice.getNonceThenIncrement(), { - delegationContract: contract, - amount: 5000000000000000000000n, - }); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Delegating funds to the contract using the factory -```js -{ - // create the entrypoint and the delegation factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createDelegationTransactionsFactory(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva"); - - const transaction = await factory.createTransactionForDelegating(alice.address, { - delegationContract: contract, - amount: 5000000000000000000000n, - }); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - // set the nonce - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Redelegating rewards using the Controller -Over time, as rewards accumulate, we may choose to redelegate them to the contract to maximize earnings. - -```js -{ - // create the entrypoint and the delegation controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createDelegationController(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva"); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const transaction = await controller.createTransactionForRedelegatingRewards( - alice, - alice.getNonceThenIncrement(), - { - delegationContract: contract, - }, - ); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Redelegating rewards using the factory -```js -{ - // create the entrypoint and the delegation factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createDelegationTransactionsFactory(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva"); - - const transaction = await factory.createTransactionForRedelegatingRewards(alice.address, { - delegationContract: contract, - }); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - // set the nonce - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Claiming rewards using the Controller -We can also claim our rewards when needed. - -```js -{ - // create the entrypoint and the delegation controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createDelegationController(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva"); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const transaction = await controller.createTransactionForClaimingRewards(alice, alice.getNonceThenIncrement(), { - delegationContract: contract, - }); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Claiming rewards using the factory -```js -{ - // create the entrypoint and the delegation factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createDelegationTransactionsFactory(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva"); - - const transaction = await factory.createTransactionForClaimingRewards(alice.address, { - delegationContract: contract, - }); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - // set the nonce - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Undelegating funds using the Controller -By **undelegating**, we signal the contract that we want to retrieve our staked funds. This process requires a **10-epoch unbonding period** before the funds become available. - -```js -{ - // create the entrypoint and the delegation controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createDelegationController(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva"); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const transaction = await controller.createTransactionForUndelegating(alice, alice.getNonceThenIncrement(), { - delegationContract: contract, - amount: 1000000000000000000000n, // 1000 EGLD - }); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Undelegating funds using the factory -```js -{ - // create the entrypoint and the delegation factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createDelegationTransactionsFactory(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva"); - - const transaction = await factory.createTransactionForUndelegating(alice.address, { - delegationContract: contract, - amount: 1000000000000000000000n, // 1000 EGLD - }); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - // set the nonce - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Withdrawing funds using the Controller -After the `10-epoch unbonding period` is complete, we can proceed with withdrawing our staked funds using the controller. This final step allows us to regain access to the previously delegated funds. - -```js -{ - // create the entrypoint and the delegation controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createDelegationController(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva"); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const transaction = await controller.createTransactionForWithdrawing(alice, alice.getNonceThenIncrement(), { - delegationContract: contract, - }); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Withdrawing funds using the factory -```js -{ - // create the entrypoint and the delegation factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createDelegationTransactionsFactory(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva"); - - const transaction = await factory.createTransactionForWithdrawing(alice.address, { - delegationContract: contract, - }); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - // set the nonce - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -### Relayed transactions -We are currently on the third iteration (V3) of relayed transactions. V1 and V2 will be deactivated soon, so we'll focus on V3. - -For V3, two new fields have been added on transactions: `relayer` and `relayerSignature`. - -Note that: -1. the sender and the relayer can sign the transaction in any order. -2. before any of the sender or relayer can sign the transaction, the `relayer` field must be set. -3. relayed transactions require an additional `50,000` of gas. -4. the sender and the relayer must be in the same network shard. - -Let’s see how to create a relayed transaction: - -```js -{ - const entrypoint = new DevnetEntrypoint(); - const walletsPath = path.join("../src", "testdata", "testwallets"); - const bob = await Account.newFromPem(path.join(walletsPath, "bob.pem")); - const grace = Address.newFromBech32("erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede"); - const mike = await Account.newFromPem(path.join(walletsPath, "mike.pem")); - - // fetch the nonce of the network - bob.nonce = await entrypoint.recallAccountNonce(bob.address); - - const transaction = new Transaction({ - chainID: "D", - sender: bob.address, - receiver: grace, - relayer: mike.address, - gasLimit: 110_000n, - data: Buffer.from("hello"), - nonce: bob.getNonceThenIncrement(), - }); - - // sender signs the transaction - transaction.signature = await bob.signTransaction(transaction); - - // relayer signs the transaction - transaction.relayerSignature = await mike.signTransaction(transaction); - - // broadcast the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Creating relayed transactions using controllers -We can create relayed transactions using any of the available controllers. -Each controller includes a relayer argument, which must be set if we want to create a relayed transaction. - -Let’s issue a fungible token using a relayed transaction: - -```js -{ - // create the entrypoint and the token management controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createTokenManagementController(); - - // create the issuer of the token - const walletsPath = path.join("../src", "testdata", "testwallets"); - const alice = await Account.newFromPem(path.join(walletsPath, "alice.pem")); - - // Carol will be our relayer, that means she is paying the gas for the transaction - const frank = await Account.newFromPem(path.join(walletsPath, "frank.pem")); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const transaction = await controller.createTransactionForIssuingFungible(alice, alice.getNonceThenIncrement(), { - tokenName: "NEWFNG", - tokenTicker: "FNG", - initialSupply: 1_000_000_000000n, - numDecimals: 6n, - canFreeze: false, - canWipe: true, - canPause: false, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: false, - relayer: frank.address, - }); - - // relayer also signs the transaction - transaction.relayerSignature = await frank.signTransaction(transaction); - - // broadcast the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Creating relayed transactions using factories -Unlike controllers, `transaction factories` do not have a `relayer` argument. Instead, the **relayer must be set after creating the transaction**. -This approach is beneficial because the **transaction is not signed by the sender at the time of creation**, allowing flexibility in setting the relayer before signing. - -Let’s issue a fungible token using the `TokenManagementTransactionsFactory`: - -```js -{ - // create the entrypoint and the token management factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createTokenManagementTransactionsFactory(); - - // create the issuer of the token - const walletsPath = path.join("../src", "testdata", "testwallets"); - const alice = await Account.newFromPem(path.join(walletsPath, "alice.pem")); - - // carol will be our relayer, that means she is paying the gas for the transaction - const frank = await Account.newFromPem(path.join(walletsPath, "frank.pem")); - - const transaction = await factory.createTransactionForIssuingFungible(alice.address, { - tokenName: "NEWFNG", - tokenTicker: "FNG", - initialSupply: 1_000_000_000000n, - numDecimals: 6n, - canFreeze: false, - canWipe: true, - canPause: false, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: false, - }); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - transaction.nonce = alice.getNonceThenIncrement(); - - // set the relayer - transaction.relayer = frank.address; - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // relayer also signs the transaction - transaction.relayerSignature = await frank.signTransaction(transaction); - - // broadcast the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -### Guarded transactions -Similar to relayers, transactions also have two additional fields: - -- guardian -- guardianSignature - -Each controller includes an argument for the guardian. The transaction can either: -1. Be sent to a service that signs it using the guardian’s account, or -2. Be signed by another account acting as a guardian. - -Let’s issue a token using a guarded account: - -#### Creating guarded transactions using controllers -We can create guarded transactions using any of the available controllers. - -Each controller method includes a guardian argument, which must be set if we want to create a guarded transaction. -Let’s issue a fungible token using a relayed transaction: - -```js -{ - // create the entrypoint and the token management controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createTokenManagementController(); - - // create the issuer of the token - const walletsPath = path.join("../src", "testdata", "testwallets"); - const alice = await Account.newFromPem(path.join(walletsPath, "alice.pem")); - - // carol will be our guardian - const carol = await Account.newFromPem(path.join(walletsPath, "carol.pem")); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const transaction = await controller.createTransactionForIssuingFungible(alice, alice.getNonceThenIncrement(), { - tokenName: "NEWFNG", - tokenTicker: "FNG", - initialSupply: 1_000_000_000000n, - numDecimals: 6n, - canFreeze: false, - canWipe: true, - canPause: false, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: false, - guardian: carol.address, - }); - - // guardian also signs the transaction - transaction.guardianSignature = await carol.signTransaction(transaction); - - // broadcast the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Creating guarded transactions using factories -Unlike controllers, `transaction factories` do not have a `guardian` argument. Instead, the **guardian must be set after creating the transaction**. -This approach is beneficial because the transaction is **not signed by the sender at the time of creation**, allowing flexibility in setting the guardian before signing. - -Let’s issue a fungible token using the `TokenManagementTransactionsFactory`: - -```js -{ - // create the entrypoint and the token management factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createTokenManagementTransactionsFactory(); - - // create the issuer of the token - const walletsPath = path.join("../src", "testdata", "testwallets"); - const alice = await Account.newFromPem(path.join(walletsPath, "alice.pem")); - - // carol will be our guardian - const carol = await Account.newFromPem(path.join(walletsPath, "carol.pem")); - - const transaction = await factory.createTransactionForIssuingFungible(alice.address, { - tokenName: "NEWFNG", - tokenTicker: "FNG", - initialSupply: 1_000_000_000000n, - numDecimals: 6n, - canFreeze: false, - canWipe: true, - canPause: false, - canChangeOwner: true, - canUpgrade: true, - canAddSpecialRoles: false, - }); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - transaction.nonce = alice.getNonceThenIncrement(); - - // set the guardian - transaction.guardian = carol.address; - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // guardian also signs the transaction - transaction.guardianSignature = await carol.signTransaction(transaction); - - // broadcast the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -We can create guarded relayed transactions just like we did before. However, keep in mind: - -Only the sender can be guarded, the relayer cannot be guarded. - -Flow for Creating Guarded Relayed Transactions: -- Using Controllers: -1. Set both guardian and relayer fields. -2. The transaction must be signed by both the guardian and the relayer. -- Using Factories: - -1. Create the transaction. -2. Set both guardian and relayer fields. -3. First, the sender signs the transaction. -4. Then, the guardian signs. -5. Finally, the relayer signs before broadcasting. - -### Multisig - -The sdk contains components to interact with the [Multisig Contract](https://github.com/multiversx/mx-contracts-rs/releases/tag/v0.45.5). -We can deploy a multisig smart contract, add members, propose and execute actions and query the contract. -The same as the other components, to interact with a multisig smart contract we can use either the MultisigController or the MultisigTransactionsFactory. - -These operations can be performed using both the **controller** and the **factory**. For a complete list of supported methods, please refer to the autogenerated documentation: -- [`MultisigController`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/MultisigController.html) -- [`MultisigTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/MultisigTransactionsFactory.html) - -#### Deploying a Multisig Smart Contract using the controller -```js -{ - const abi = await loadAbiRegistry("src/testdata/multisig-full.abi.json"); - const bytecode = await loadContractCode("src/testdata/multisig-full.wasm"); - - // create the entrypoint and the multisig controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createMultisigController(abi); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const transaction = await controller.createTransactionForDeploy(alice, alice.getNonceThenIncrement(), { - quorum: 2, - board: [ - alice.address, - Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), - ], - bytecode: bytecode.valueOf(), - gasLimit: 100000000n, - }); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - // wait for transaction completion, extract multisig contract's address - const outcome = await controller.awaitCompletedDeploy(txHash); - - const contractAddress = outcome[0].contractAddress; -} -``` - -#### Deploying a Multisig Smart Contract using the factory -```js -{ - const abi = await loadAbiRegistry("src/testdata/multisig-full.abi.json"); - const bytecode = await loadContractCode("src/testdata/multisig-full.wasm"); - - // create the entrypoint and the multisig factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createMultisigTransactionsFactory(abi); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const transaction = await factory.createTransactionForDeploy(alice.address, { - quorum: 2, - board: [ - alice.address, - Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), - ], - bytecode: bytecode.valueOf(), - gasLimit: 100000000n, - }); - - transaction.nonce = alice.getNonceThenIncrement(); - transaction.signature = await alice.signTransaction(transaction); - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); -} -``` - -#### Propose an action using the controller -We'll propose an action to send some EGLD to Carol. After we sent the proposal, we'll also parse the outcome of the transaction to get the `proposal id`. -The id can be used later for signing and performing the proposal. - -```js -{ - // create the entrypoint and the multisig controller - const entrypoint = new DevnetEntrypoint(); - const abi = await loadAbiRegistry("src/testdata/multisig-full.abi.json"); - const controller = entrypoint.createMultisigController(abi); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva"); - - const transaction = await controller.createTransactionForProposeTransferExecute( - alice, - alice.getNonceThenIncrement(), - { - multisigContract: contract, - to: Address.newFromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"), - gasLimit: 10000000n, - nativeTokenAmount: 1000000000000000000n, - }, - ); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - // parse the outcome and get the proposal id - const actionId = await controller.awaitCompletedPerformAction(txHash); -} -``` - -#### Propose an action using the factory -Proposing an action for a multisig contract using the MultisigFactory is very similar to using the controller, but in order to get the proposal id, we need to use MultisigTransactionsOutcomeParser. - -```js -{ - const abi = await loadAbiRegistry("src/testdata/multisig-full.abi.json"); - - // create the entrypoint and the multisig factory - const entrypoint = new DevnetEntrypoint(); - const provider = entrypoint.createNetworkProvider(); - const factory = entrypoint.createMultisigTransactionsFactory(abi); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva"); - - const transaction = await factory.createTransactionForProposeTransferExecute(alice.address, { - multisigContract: contract, - to: Address.newFromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"), - gasLimit: 10000000n, - nativeTokenAmount: 1000000000000000000n, - }); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - // set the nonce - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - // wait for the transaction to execute - const transactionAwaiter = new TransactionWatcher(provider); - const transactionOnNetwork = await transactionAwaiter.awaitCompleted(txHash); - - // parse the outcome of the transaction - const parser = new MultisigTransactionsOutcomeParser({ abi }); - const actionId = parser.parseProposeAction(transactionOnNetwork); -} -``` - -#### Querying the Multisig Smart Contract -Unlike creating transactions, querying the multisig can be performed only using the controller. -Let's query the contract to get all board members. - -```js -{ - const abi = await loadAbiRegistry("src/testdata/multisig-full.abi.json"); - - // create the entrypoint and the multisig controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createMultisigController(abi); - - const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva"); - - const boardMembers = await controller.getAllBoardMembers({ multisigAddress: contract.toBech32() }); -} -``` - -### Governance - -We can create transactions for creating a new governance proposal, vote for a proposal or query the governance contract. - -These operations can be performed using both the **controller** and the **factory**. For a complete list of supported methods, please refer to the autogenerated documentation: -- [`GovernanceController`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/GovernanceController.html) -- [`GovernanceTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/GovernanceTransactionsFactory.html) - -#### Creating a new proposal using the controller -```js -{ - // create the entrypoint and the governance controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createGovernanceController(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - const commitHash = "1db734c0315f9ec422b88f679ccfe3e0197b9d67"; - - const transaction = await controller.createTransactionForNewProposal(alice, alice.getNonceThenIncrement(), { - commitHash: commitHash, - startVoteEpoch: 10, - endVoteEpoch: 15, - nativeTokenAmount: 500_000000000000000000n, - }); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - // wait for transaction completion, extract proposal's details - const outcome = await controller.awaitCompletedProposeProposal(txHash); - - const proposalNonce = outcome[0].proposalNonce; - const proposalCommitHash = outcome[0].commitHash; - const proposalStartVoteEpoch = outcome[0].startVoteEpoch; - const proposalEndVoteEpoch = outcome[0].endVoteEpoch; -} -``` - -#### Creating a new proposal using the factory -```js -{ - // create the entrypoint and the governance factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createGovernanceTransactionsFactory(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const commitHash = "1db734c0315f9ec422b88f679ccfe3e0197b9d67"; - - const transaction = await factory.createTransactionForNewProposal(alice.address, { - commitHash: commitHash, - startVoteEpoch: 10, - endVoteEpoch: 15, - nativeTokenAmount: 500_000000000000000000n, - }); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - // set the nonce - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - // waits until the transaction is processed and fetches it from the network - const transactionOnNetwork = await entrypoint.awaitCompletedTransaction(txHash); - - const parser = new GovernanceTransactionsOutcomeParser({}); - const outcome = parser.parseNewProposal(transactionOnNetwork); - const proposalNonce = outcome[0].proposalNonce; - const proposalCommitHash = outcome[0].commitHash; - const proposalStartVoteEpoch = outcome[0].startVoteEpoch; - const proposalEndVoteEpoch = outcome[0].endVoteEpoch; -} -``` - -#### Vote for a proposal using the controller - -```js -{ - // create the entrypoint and the governance controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createGovernanceController(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - const transaction = await controller.createTransactionForVoting(alice, alice.getNonceThenIncrement(), { - proposalNonce: 1, - vote: Vote.YES, - }); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - // wait for transaction completion, extract proposal's details - const outcome = await controller.awaitCompletedVote(txHash); - const proposalNonce = outcome[0].proposalNonce; - const vote = outcome[0].vote; - const voteTotalStake = outcome[0].totalStake; - const voteVotingPower = outcome[0].votingPower; -} -``` - -#### Vote for a proposal using the factory -```js -{ - // create the entrypoint and the governance factory - const entrypoint = new DevnetEntrypoint(); - const factory = entrypoint.createGovernanceTransactionsFactory(); - - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const transaction = await factory.createTransactionForVoting(alice.address, { - proposalNonce: 1, - vote: Vote.YES, - }); - // fetch the nonce of the network - alice.nonce = await entrypoint.recallAccountNonce(alice.address); - - // set the nonce - transaction.nonce = alice.getNonceThenIncrement(); - - // sign the transaction - transaction.signature = await alice.signTransaction(transaction); - - // sending the transaction - const txHash = await entrypoint.sendTransaction(transaction); - - // wait for transaction completion, extract proposal's details - const transactionOnNetwork = await entrypoint.awaitCompletedTransaction(txHash); - const parser = new GovernanceTransactionsOutcomeParser({}); - const outcome = parser.parseVote(transactionOnNetwork); - const proposalNonce = outcome[0].proposalNonce; - const vote = outcome[0].vote; - const voteTotalStake = outcome[0].totalStake; - const voteVotingPower = outcome[0].votingPower; -} -``` - -#### Querying the governance contract -Unlike creating transactions, querying the contract is only possible using the controller. Let's query the contract to get more details about a proposal. - -```js -{ - // create the entrypoint and the governance controller - const entrypoint = new DevnetEntrypoint(); - const controller = entrypoint.createGovernanceController(); - - const proposalInfo = await controller.getProposal(1); - console.log({ proposalInfo }); -} -``` - -## Addresses - -Create an `Address` object from a bech32-encoded string: - -``` js -{ - // Create an Address object from a bech32-encoded string - const address = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - - console.log("Address (bech32-encoded):", address.toBech32()); - console.log("Public key (hex-encoded):", address.toHex()); - console.log("Public key (hex-encoded):", Buffer.from(address.getPublicKey()).toString("hex")); -} - -``` - -Here’s how you can create an address from a hex-encoded string using the MultiversX JavaScript SDK: -If the HRP (human-readable part) is not provided, the SDK will use the default one ("erd"). - -``` js -{ - // Create an address from a hex-encoded string with a specified HRP - const address = Address.newFromHex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1", "erd"); - - console.log("Address (bech32-encoded):", address.toBech32()); - console.log("Public key (hex-encoded):", address.toHex()); -} -``` - -#### Create an address from a raw public key - -``` js -{ - const pubkey = Buffer.from("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1", "hex"); - const addressFromPubkey = new Address(pubkey, "erd"); -} -``` - -#### Getting the shard of an address -``` js - -const addressComputer = new AddressComputer(); -const address = Address.newFromHex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1"); -console.log("Shard:", addressComputer.getShardOfAddress(address)); -``` - -Checking if an address is a smart contract -``` js - -const contractAddress = Address.newFromBech32("erd1qqqqqqqqqqqqqpgquzmh78klkqwt0p4rjys0qtp3la07gz4d396qn50nnm"); -console.log("Is contract address:", contractAddress.isSmartContract()); -``` - -### Changing the default hrp -The **LibraryConfig** class manages the default **HRP** (human-readable part) for addresses, which is set to `"erd"` by default. -You can change the HRP when creating an address or modify it globally in **LibraryConfig**, affecting all newly created addresses. -``` js - -console.log(LibraryConfig.DefaultAddressHrp); -const defaultAddress = Address.newFromHex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1"); -console.log(defaultAddress.toBech32()); - -LibraryConfig.DefaultAddressHrp = "test"; -const testAddress = Address.newFromHex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1"); -console.log(testAddress.toBech32()); - -// Reset HRP back to "erd" to avoid affecting other parts of the application. -LibraryConfig.DefaultAddressHrp = "erd"; -``` - -## Wallets - -#### Generating a mnemonic -Mnemonic generation is based on [bip39](https://www.npmjs.com/package/bip39) and can be achieved as follows: - -``` js - -const mnemonic = Mnemonic.generate(); -const words = mnemonic.getWords(); -``` - -#### Saving the mnemonic to a keystore file -The mnemonic can be saved to a keystore file: - -``` js -{ - const mnemonic = Mnemonic.generate(); - - // saves the mnemonic to a keystore file with kind=mnemonic - const wallet = UserWallet.fromMnemonic({ mnemonic: mnemonic.toString(), password: "password" }); - - const filePath = path.join("../src", "testdata", "testwallets", "walletWithMnemonic.json"); - wallet.save(filePath); -} -``` - -#### Deriving secret keys from a mnemonic -Given a mnemonic, we can derive keypairs: - -``` js -{ - const mnemonic = Mnemonic.generate(); - - const secretKey = mnemonic.deriveKey(0); - const publicKey = secretKey.generatePublicKey(); - - console.log("Secret key: ", secretKey.hex()); - console.log("Public key: ", publicKey.hex()); -} -``` - -#### Saving a secret key to a keystore file -The secret key can also be saved to a keystore file: - -``` js -{ - const mnemonic = Mnemonic.generate(); - const secretKey = mnemonic.deriveKey(); - - const wallet = UserWallet.fromSecretKey({ secretKey: secretKey, password: "password" }); - - const filePath = path.join("../src", "testdata", "testwallets", "walletWithSecretKey.json"); - wallet.save(filePath); -} -``` - -#### Saving a secret key to a PEM file -We can save a secret key to a pem file. *This is not recommended as it is not secure, but it's very convenient for testing purposes.* - -``` js -{ - const mnemonic = Mnemonic.generate(); - - // by default, derives using the index = 0 - const secretKey = mnemonic.deriveKey(); - const publicKey = secretKey.generatePublicKey(); - - const label = publicKey.toAddress().toBech32(); - const pem = new UserPem(label, secretKey); - - const filePath = path.join("../src", "testdata", "testwallets", "wallet.pem"); - pem.save(filePath); -} -``` - -#### Generating a KeyPair -A `KeyPair` is a wrapper over a secret key and a public key. We can create a keypair and use it for signing or verifying. - -``` js -{ - const keypair = KeyPair.generate(); - - // by default, derives using the index = 0 - const secretKey = keypair.getSecretKey(); - const publicKey = keypair.getPublicKey(); -} -``` - -#### Loading a wallet from keystore mnemonic file -Load a keystore that holds an encrypted mnemonic (and perform wallet derivation at the same time): - -``` js -{ - const filePath = path.join("../src", "testdata", "testwallets", "walletWithMnemonic.json"); - - // loads the mnemonic and derives the a secret key; default index = 0 - let secretKey = UserWallet.loadSecretKey(filePath, "password"); - let address = secretKey.generatePublicKey().toAddress("erd"); - - console.log("Secret key: ", secretKey.hex()); - console.log("Address: ", address.toBech32()); - - // derive secret key with index = 7 - secretKey = UserWallet.loadSecretKey(filePath, "password", 7); - address = secretKey.generatePublicKey().toAddress(); - - console.log("Secret key: ", secretKey.hex()); - console.log("Address: ", address.toBech32()); -} -``` - -#### Loading a wallet from a keystore secret key file - -``` js -{ - const filePath = path.join("../src", "testdata", "testwallets", "walletWithSecretKey.json"); - - let secretKey = UserWallet.loadSecretKey(filePath, "password"); - let address = secretKey.generatePublicKey().toAddress("erd"); - - console.log("Secret key: ", secretKey.hex()); - console.log("Address: ", address.toBech32()); -} -``` - -#### Loading a wallet from a PEM file - -``` js -{ - const filePath = path.join("../src", "testdata", "testwallets", "wallet.pem"); - - let pem = UserPem.fromFile(filePath); - - console.log("Secret key: ", pem.secretKey.hex()); - console.log("Public key: ", pem.publicKey.hex()); -} -``` - -## Signing objects - -Signing is done using an account's secret key. To simplify this process, we provide wrappers like [Account](#creating-accounts), which streamline signing operations. -First, we'll explore how to sign using an Account, followed by signing directly with a secret key. - -#### Signing a Transaction using an Account -We are going to assume we have an account at this point. If you don't, feel free to check out the [creating an account](#creating-accounts) section. -```js -{ - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const transaction = new Transaction({ - chainID: "D", - sender: alice.address, - receiver: Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), - gasLimit: 50000n, - nonce: 90n, - }); - - transaction.signature = await alice.signTransaction(transaction); - console.log(transaction.toPlainObject()); -} -``` - -#### Signing a Transaction using a SecretKey -```js -{ - const secretKeyHex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; - const secretKey = UserSecretKey.fromString(secretKeyHex); - const publicKey = secretKey.generatePublicKey(); - - const transaction = new Transaction({ - nonce: 90n, - sender: publicKey.toAddress(), - receiver: Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), - value: 1000000000000000000n, - gasLimit: 50000n, - chainID: "D", - }); - - // serialize the transaction - const transactionComputer = new TransactionComputer(); - const serializedTransaction = transactionComputer.computeBytesForSigning(transaction); - - // apply the signature on the transaction - transaction.signature = await secretKey.sign(serializedTransaction); - - console.log(transaction.toPlainObject()); -} -``` - -#### Signing a Transaction by hash -```js -{ - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const transaction = new Transaction({ - nonce: 90n, - sender: alice.address, - receiver: Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), - value: 1000000000000000000n, - gasLimit: 50000n, - chainID: "D", - }); - - const transactionComputer = new TransactionComputer(); - - // sets the least significant bit of the options field to `1` - transactionComputer.applyOptionsForHashSigning(transaction); - - // compute a keccak256 hash for signing - const hash = transactionComputer.computeHashForSigning(transaction); - - // sign and apply the signature on the transaction - transaction.signature = await alice.signTransaction(transaction); - - console.log(transaction.toPlainObject()); -} -``` - -#### Signing a Message using an Account: -```js -{ - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - - const message = new Message({ - data: new Uint8Array(Buffer.from("hello")), - address: alice.address, - }); - - message.signature = await alice.signMessage(message); -} -``` - -#### Signing a Message using an SecretKey: -```js -{ - const secretKeyHex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; - const secretKey = UserSecretKey.fromString(secretKeyHex); - const publicKey = secretKey.generatePublicKey(); - - const messageComputer = new MessageComputer(); - const message = new Message({ - data: new Uint8Array(Buffer.from("hello")), - address: publicKey.toAddress(), - }); - // serialized the message - const serialized = messageComputer.computeBytesForSigning(message); - - message.signature = await secretKey.sign(serialized); -} -``` - -## Verifying signatures - -Signature verification is performed using an account’s public key. -To simplify this process, we provide wrappers over public keys that make verification easier and more convenient. - -#### Verifying Transaction signature using a UserVerifier -```js -{ - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const account = await Account.newFromPem(filePath); - - const transaction = new Transaction({ - nonce: 90n, - sender: account.address, - receiver: Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), - value: 1000000000000000000n, - gasLimit: 50000n, - chainID: "D", - }); - - // sign and apply the signature on the transaction - transaction.signature = await account.signTransaction(transaction); - - // instantiating a user verifier; basically gets the public key - const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const aliceVerifier = UserVerifier.fromAddress(alice); - - // serialize the transaction for verification - const transactionComputer = new TransactionComputer(); - const serializedTransaction = transactionComputer.computeBytesForVerifying(transaction); - - // verify the signature - const isSignedByAlice = aliceVerifier.verify(serializedTransaction, transaction.signature); - - console.log("Transaction is signed by Alice: ", isSignedByAlice); -} -``` - -#### Verifying Message signature using a UserVerifier - -```ts -{ - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const account = await Account.newFromPem(filePath); - - const message = new Message({ - data: new Uint8Array(Buffer.from("hello")), - address: account.address, - }); - - // sign and apply the signature on the message - message.signature = await account.signMessage(message); - - // instantiating a user verifier; basically gets the public key - const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const aliceVerifier = UserVerifier.fromAddress(alice); - - // serialize the message for verification - const messageComputer = new MessageComputer(); - const serializedMessage = messageComputer.computeBytesForVerifying(message); - - // verify the signature - const isSignedByAlice = await aliceVerifier.verify(serializedMessage, message.signature); - - console.log("Message is signed by Alice: ", isSignedByAlice); -} -``` - -#### Verifying a signature using a public key -```js -{ - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const account = await Account.newFromPem(filePath); - - const transaction = new Transaction({ - nonce: 90n, - sender: account.address, - receiver: Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), - value: 1000000000000000000n, - gasLimit: 50000n, - chainID: "D", - }); - - // sign and apply the signature on the transaction - transaction.signature = await account.signTransaction(transaction); - - // instantiating a public key - const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); - const publicKey = new UserPublicKey(alice.getPublicKey()); - - // serialize the transaction for verification - const transactionComputer = new TransactionComputer(); - const serializedTransaction = transactionComputer.computeBytesForVerifying(transaction); - - // verify the signature - const isSignedByAlice = await publicKey.verify(serializedTransaction, transaction.signature); - console.log("Transaction is signed by Alice: ", isSignedByAlice); -} -``` - -#### Sending messages over boundaries -Signed Message objects are typically sent to a remote party (e.g., a service), which can then verify the signature. -To prepare a message for transmission, you can use the `MessageComputer.packMessage()` utility method. - -```js -{ - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const account = await Account.newFromPem(filePath); - - const message = new Message({ - data: new Uint8Array(Buffer.from("hello")), - address: account.address, - }); - - // sign and apply the signature on the message - message.signature = await account.signMessage(message); - - const messageComputer = new MessageComputer(); - const packedMessage = messageComputer.packMessage(message); - - console.log("Packed message", packedMessage); -} -``` - -Then, on the receiving side, you can use [`MessageComputer.unpackMessage()`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/MessageComputer.html#unpackMessage) to reconstruct the message, prior verification: - -```js -{ - const filePath = path.join("../src", "testdata", "testwallets", "alice.pem"); - const alice = await Account.newFromPem(filePath); - const messageComputer = new MessageComputer(); - const data = Buffer.from("test"); - - const message = new Message({ - data: data, - address: alice.address, - }); - - message.signature = await alice.signMessage(message); - // restore message - - const packedMessage = messageComputer.packMessage(message); - const unpackedMessage = messageComputer.unpackMessage(packedMessage); - - // verify the signature - const isSignedByAlice = await alice.verifyMessageSignature(unpackedMessage, message.signature); - - console.log("Transaction is signed by Alice: ", isSignedByAlice); -} -``` diff --git a/docs/sdk-and-tools/sdk-js/writing-and-running-sdk-js-snippets.md b/docs/sdk-and-tools/sdk-js/writing-and-running-sdk-js-snippets.md index 1fc2cc8db..adaf9088c 100644 --- a/docs/sdk-and-tools/sdk-js/writing-and-running-sdk-js-snippets.md +++ b/docs/sdk-and-tools/sdk-js/writing-and-running-sdk-js-snippets.md @@ -8,6 +8,6 @@ description: "Write and run sdk‑js snippets to test blockchain interactions qu Generally speaking, we recommended to use [sc-meta CLI](/developers/meta/sc-meta-cli) to [generate the boilerplate code for your contract interactions](/developers/meta/sc-meta-cli/#calling-snippets). -Though, for writing contract interaction snippets in **JavaScript** or **TypeScript**, please refer to the [`sdk-js` cookbook](/sdk-and-tools/sdk-js/sdk-js-cookbook). If you'd like these snippets to function as system tests of your contract, a choice would be to structure them as Mocha or Jest tests - take the `*.local.net.spec.ts` tests in [`mx-sdk-js-core`](https://github.com/multiversx/mx-sdk-js-core) as examples. For writing contract interaction snippets in **Python**, please refer to the [`sdk-py` cookbook](/sdk-and-tools/sdk-py) - if desired, you can shape them as simple scripts, as Python unit tests, or as Jupyter notebooks. +Though, for writing contract interaction snippets in **JavaScript** or **TypeScript**, please refer to the [`sdk-js` cookbook](/sdk-and-tools/sdk-js/cookbook). If you'd like these snippets to function as system tests of your contract, a choice would be to structure them as Mocha or Jest tests - take the `*.local.net.spec.ts` tests in [`mx-sdk-js-core`](https://github.com/multiversx/mx-sdk-js-core) as examples. For writing contract interaction snippets in **Python**, please refer to the [`sdk-py` cookbook](/sdk-and-tools/sdk-py) - if desired, you can shape them as simple scripts, as Python unit tests, or as Jupyter notebooks. You might also want to have a look over [**xSuite**](https://xsuite.dev), a toolkit to init, build, test, deploy contracts using JavaScript, made by the [Arda team](https://arda.run). diff --git a/docusaurus.config.js b/docusaurus.config.js index 4e9fe022b..9b45fcd91 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -76,7 +76,7 @@ const config = { }, blog: false, theme: { - customCss: "./src/css/custom.css", + customCss: ["./src/css/custom.css", "./src/css/cookbook.css"], }, gtag: { trackingID: "G-TW3LCJ0LS7", @@ -242,6 +242,10 @@ const config = { "@docusaurus/plugin-client-redirects", { redirects: [ + { + from: "/sdk-and-tools/sdk-js/sdk-js-cookbook", + to: "/sdk-and-tools/sdk-js/cookbook", + }, { from: "/technology/glossary", to: "/welcome/terminology", @@ -422,10 +426,6 @@ const config = { from: "/sdk-and-tools/sdk-py/sdk-py-cookbook-v0", to: "/sdk-and-tools/sdk-py", }, - { - from: "/sdk-and-tools/sdk-js/sdk-js-cookbook", - to: "/sdk-and-tools/sdk-js/sdk-js-cookbook", - }, { from: "/sdk-and-tools/sdk-py/configuring-mxpy", to: "/sdk-and-tools/mxpy/mxpy-cli", diff --git a/package.json b/package.json index 6727a5ffb..5bdd918c6 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "scripts": { "docusaurus": "docusaurus", "start": "docusaurus start", - "prebuild": "node scripts/generate-llms-txt.js && node scripts/generate-md-urls.js", + "prebuild": "node scripts/generate-cookbook-manifest.js && node scripts/generate-llms-txt.js && node scripts/generate-md-urls.js", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", @@ -21,7 +21,8 @@ "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "generate:llms": "node scripts/generate-llms-txt.js", - "generate:md-urls": "node scripts/generate-md-urls.js" + "generate:md-urls": "node scripts/generate-md-urls.js", + "generate:cookbook": "node scripts/generate-cookbook-manifest.js" }, "dependencies": { "@docusaurus/core": "3.8.1", diff --git a/scripts/generate-cookbook-manifest.js b/scripts/generate-cookbook-manifest.js new file mode 100644 index 000000000..f256bee57 --- /dev/null +++ b/scripts/generate-cookbook-manifest.js @@ -0,0 +1,140 @@ +#!/usr/bin/env node +/* + generate-cookbook-manifest.js + + Builds the data the Cookbook landing page renders. Instead of hand-listing 69 + recipes in MDX, this scans each recipe's real frontmatter and emits a JSON + manifest that `docs/sdk-and-tools/sdk-js/cookbook/index.mdx` imports and feeds + to . Same idea as generate-llms-txt.js (frontmatter is the single + source of truth); here the output feeds the human-facing index instead of the + agent surface. + + Section order and the recipe order *within* each section are read from + sidebars.js — the one curated reading order — so the index and the sidebar can + never drift. Recipe card fields are read from each page's YAML frontmatter via + gray-matter, mapped to the exact prop names consumes. + + Output: src/data/cookbook-manifest.json (committed, and regenerated on prebuild + so CI keeps it fresh). + + Usage: node scripts/generate-cookbook-manifest.js +*/ + +const fs = require("fs"); +const path = require("path"); +const matter = require("gray-matter"); + +const ROOT = path.join(__dirname, ".."); +const DOCS_DIR = path.join(ROOT, "docs"); +const OUTPUT_FILE = path.join(ROOT, "src", "data", "cookbook-manifest.json"); + +const COOKBOOK_CATEGORY_LABEL = "Cookbook (recipes)"; +// A real recipe doc id: cookbook/
/. The section index and the +// agent on-ramp live one level up (cookbook/index, cookbook/agents) and are +// intentionally excluded. +const RECIPE_ID_RE = /^sdk-and-tools\/sdk-js\/cookbook\/[^/]+\/[^/]+$/; + +// Recursively locate the sidebar category object with the given label. +function findCategory(node, label) { + if (Array.isArray(node)) { + for (const child of node) { + const found = findCategory(child, label); + if (found) return found; + } + return null; + } + if (node && typeof node === "object") { + if (node.type === "category" && node.label === label) return node; + for (const value of Object.values(node)) { + const found = findCategory(value, label); + if (found) return found; + } + } + return null; +} + +// Resolve a doc id to its .mdx/.md file on disk. +function resolveDocFile(docId) { + for (const ext of [".mdx", ".md"]) { + const candidate = path.join(DOCS_DIR, `${docId}${ext}`); + if (fs.existsSync(candidate)) return candidate; + } + return null; +} + +// Map one recipe's frontmatter to the shape reads. Field names are +// deliberately the card's camelCase props so index.mdx can pass records straight +// through with no transform. +function recipeRecord(docId, data) { + const sdkVersions = + data.sdk_versions && typeof data.sdk_versions === "object" + ? data.sdk_versions + : undefined; + + return { + title: data.title || docId, + description: data.description || "", + href: `/${docId}`, + difficulty: data.difficulty || undefined, + lastValidated: data.last_validated || undefined, + stale: Boolean(data.stale), + tags: Array.isArray(data.tags) ? data.tags : [], + sdkVersions, + }; +} + +function main() { + const sidebars = require(path.join(ROOT, "sidebars.js")); + const category = findCategory(sidebars, COOKBOOK_CATEGORY_LABEL); + if (!category || !Array.isArray(category.items)) { + console.error( + `Could not find the "${COOKBOOK_CATEGORY_LABEL}" category in sidebars.js.` + ); + process.exit(1); + } + + const sections = []; + let total = 0; + + for (const item of category.items) { + // Only the section sub-categories carry recipes; string children (the agent + // on-ramp) and the category link (the index) are skipped by construction. + if (!item || item.type !== "category" || !Array.isArray(item.items)) { + continue; + } + + const recipeIds = item.items.filter( + (it) => typeof it === "string" && RECIPE_ID_RE.test(it) + ); + if (recipeIds.length === 0) continue; + + const recipes = []; + for (const docId of recipeIds) { + const file = resolveDocFile(docId); + if (!file) { + console.error(` ! missing file for recipe id: ${docId}`); + process.exit(1); + } + const { data } = matter(fs.readFileSync(file, "utf8")); + recipes.push(recipeRecord(docId, data)); + } + + // Anchor id for in-page section links: the shared
folder segment + // of sdk-and-tools/sdk-js/cookbook/
/. + const sectionId = recipeIds[0].split("/")[3]; + + sections.push({ id: sectionId, label: item.label, recipes }); + total += recipes.length; + } + + fs.mkdirSync(path.dirname(OUTPUT_FILE), { recursive: true }); + fs.writeFileSync(OUTPUT_FILE, `${JSON.stringify(sections, null, 2)}\n`, "utf8"); + + console.log(`Wrote ${path.relative(ROOT, OUTPUT_FILE)}`); + console.log(` ${sections.length} sections, ${total} recipes:`); + for (const s of sections) { + console.log(` ${String(s.recipes.length).padStart(2)} ${s.label}`); + } +} + +main(); diff --git a/scripts/generate-llms-txt.js b/scripts/generate-llms-txt.js index 91d56629d..c7fad39bc 100644 --- a/scripts/generate-llms-txt.js +++ b/scripts/generate-llms-txt.js @@ -196,11 +196,18 @@ function parseFrontmatter(mdContent) { title: block.match(/^\s*title:\s*(["']?)(.+?)\1\s*$/m), slug: block.match(/^\s*slug:\s*(["']?)(.+?)\1\s*$/m), description: block.match(/^\s*description:\s*(["']?)([\s\S]*?)\1\s*$/m), + // Cookbook recipe frontmatter — surfaced in the agent-ingestible llms.txt + // so a coding agent sees difficulty + CI-verified date alongside each + // recipe. Optional: only cookbook pages declare these fields. + difficulty: block.match(/^\s*difficulty:\s*(["']?)(.+?)\1\s*$/m), + last_validated: block.match(/^\s*last_validated:\s*(["']?)(.+?)\1\s*$/m), }; if (pairs.id) meta.id = pairs.id[2].trim(); if (pairs.title) meta.title = pairs.title[2].trim(); if (pairs.slug) meta.slug = pairs.slug[2].trim(); if (pairs.description) meta.description = pairs.description[2].trim(); + if (pairs.difficulty) meta.difficulty = pairs.difficulty[2].trim(); + if (pairs.last_validated) meta.lastValidated = pairs.last_validated[2].trim(); meta._fmEnd = end + '\n---'.length; return meta; } @@ -302,7 +309,7 @@ async function getDocMeta(docId) { title = `${humanizeSegmentForTitle(parent)} ${title}`; } } - meta = { ...meta, title, description, filePath, source: fromFm ? 'frontmatter' : (description ? 'content' : 'none') }; + meta = { ...meta, title, description, filePath, source: fromFm ? 'frontmatter' : (description ? 'content' : 'none'), difficulty: fm.difficulty, lastValidated: fm.lastValidated }; } catch { // ignore } @@ -393,7 +400,7 @@ async function main() { const meta = await getDocMeta(id); // eslint-disable-next-line no-await-in-loop const url = await computeUrlForDoc(id, siteUrl); - all.push({ id, title: meta.title, description: meta.description, url, source: meta.source, filePath: meta.filePath }); + all.push({ id, title: meta.title, description: meta.description, url, source: meta.source, filePath: meta.filePath, difficulty: meta.difficulty, lastValidated: meta.lastValidated }); } all.sort((a, b) => a.title.localeCompare(b.title)); for (const e of all) { @@ -402,7 +409,17 @@ async function main() { desc = generatedDescriptionFromPath(e.id, e.filePath, brand); } const clipped = desc.length > 400 ? `${desc.slice(0, 397)}...` : desc; - outputLines.push(`- [${e.title}](${e.url})${clipped ? `: ${clipped}` : ''}`); + // Cookbook pages append a compact, machine-parseable metadata tag so the + // agent surface carries difficulty + CI-verified date. Only pages that + // declare `difficulty` frontmatter are affected; all other entries are + // byte-for-byte unchanged. + let recipeTag = ''; + if (e.difficulty) { + const bits = [e.difficulty]; + if (e.lastValidated) bits.push(`verified ${e.lastValidated}`); + recipeTag = ` [${bits.join(', ')}]`; + } + outputLines.push(`- [${e.title}](${e.url})${clipped ? `: ${clipped}` : ''}${recipeTag}`); // Build full content entry if (e.filePath) { diff --git a/sidebars.js b/sidebars.js index 61e0612fe..f759a63ce 100644 --- a/sidebars.js +++ b/sidebars.js @@ -221,16 +221,163 @@ const sidebars = { label: "Javascript SDK", items: [ "sdk-and-tools/sdk-js/sdk-js", + "sdk-and-tools/sdk-js/sdk-js-cookbook-v14", { type: "category", - label: "Cookbook (versioned)", + label: "Cookbook (recipes)", link: { type: "doc", - id: "sdk-and-tools/sdk-js/sdk-js-cookbook" + id: "sdk-and-tools/sdk-js/cookbook/index" }, items: [ - "sdk-and-tools/sdk-js/sdk-js-cookbook-v14", - "sdk-and-tools/sdk-js/sdk-js-cookbook", + "sdk-and-tools/sdk-js/cookbook/agents", + { + type: "category", + label: "Start here", + items: [ + "sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal", + "sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal", + "sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup", + "sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send", + "sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration", + ] + }, + { + type: "category", + label: "Accounts and signing", + items: [ + "sdk-and-tools/sdk-js/cookbook/accounts/generate-mnemonic-derive-keys", + "sdk-and-tools/sdk-js/cookbook/accounts/account-from-keys", + "sdk-and-tools/sdk-js/cookbook/accounts/keystore-save-load", + "sdk-and-tools/sdk-js/cookbook/accounts/pem-save-load", + "sdk-and-tools/sdk-js/cookbook/accounts/address-utilities", + "sdk-and-tools/sdk-js/cookbook/accounts/fetch-account-state", + "sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces", + "sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-message", + "sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-transaction", + "sdk-and-tools/sdk-js/cookbook/accounts/hash-signing-transaction", + "sdk-and-tools/sdk-js/cookbook/accounts/set-guardian", + "sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account", + "sdk-and-tools/sdk-js/cookbook/accounts/apply-guardian-to-transaction", + "sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction", + ] + }, + { + type: "category", + label: "Network providers", + items: [ + "sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider", + "sdk-and-tools/sdk-js/cookbook/network-providers/fetch-network-config-status", + "sdk-and-tools/sdk-js/cookbook/network-providers/fetch-a-block", + "sdk-and-tools/sdk-js/cookbook/network-providers/await-account-on-condition", + "sdk-and-tools/sdk-js/cookbook/network-providers/await-transaction-on-condition", + "sdk-and-tools/sdk-js/cookbook/network-providers/simulate-estimate-transaction", + "sdk-and-tools/sdk-js/cookbook/network-providers/custom-api-request", + ] + }, + { + type: "category", + label: "Transactions", + items: [ + "sdk-and-tools/sdk-js/cookbook/transactions/send-egld", + "sdk-and-tools/sdk-js/cookbook/transactions/send-esdt", + "sdk-and-tools/sdk-js/cookbook/transactions/multi-token-transfer", + "sdk-and-tools/sdk-js/cookbook/transactions/track-transaction-status", + ] + }, + { + type: "category", + label: "Tokens (ESDT / NFT / SFT)", + items: [ + "sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token", + "sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection", + "sdk-and-tools/sdk-js/cookbook/tokens/issue-sft-collection", + "sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles", + "sdk-and-tools/sdk-js/cookbook/tokens/local-mint-burn-supply", + "sdk-and-tools/sdk-js/cookbook/tokens/token-lifecycle-operations", + "sdk-and-tools/sdk-js/cookbook/tokens/fetch-token-metadata", + "sdk-and-tools/sdk-js/cookbook/tokens/fetch-account-token-balances", + ] + }, + { + type: "category", + label: "Wallets", + items: [ + "sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account", + "sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button", + "sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login", + "sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login", + "sdk-and-tools/sdk-js/cookbook/wallets/ledger-login", + "sdk-and-tools/sdk-js/cookbook/wallets/native-auth", + "sdk-and-tools/sdk-js/cookbook/wallets/typescript-strict-mode-checklist", + ] + }, + { + type: "category", + label: "Migration", + items: [ + "sdk-and-tools/sdk-js/cookbook/migration/dapp-provider-to-initapp", + "sdk-and-tools/sdk-js/cookbook/migration/get-account-info-v4-vs-v5", + "sdk-and-tools/sdk-js/cookbook/migration/send-transactions-to-transaction-manager", + ] + }, + { + type: "category", + label: "Governance", + items: [ + "sdk-and-tools/sdk-js/cookbook/governance/create-proposal", + "sdk-and-tools/sdk-js/cookbook/governance/vote-close-proposal", + ] + }, + { + type: "category", + label: "Delegation", + items: [ + "sdk-and-tools/sdk-js/cookbook/delegation/create-delegation-contract", + "sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake", + "sdk-and-tools/sdk-js/cookbook/delegation/claim-and-redelegate-rewards", + "sdk-and-tools/sdk-js/cookbook/delegation/undelegate-and-withdraw", + "sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract", + ] + }, + { + type: "category", + label: "Multisig", + items: [ + "sdk-and-tools/sdk-js/cookbook/multisig/propose-action", + "sdk-and-tools/sdk-js/cookbook/multisig/sign-perform-action", + "sdk-and-tools/sdk-js/cookbook/multisig/read-multisig-state", + ] + }, + { + type: "category", + label: "Smart contracts (call & query)", + items: [ + "sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi", + "sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view", + "sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint", + "sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-payable-endpoint", + "sdk-and-tools/sdk-js/cookbook/smart-contracts-call/decode-return-data", + "sdk-and-tools/sdk-js/cookbook/smart-contracts-call/decode-contract-events", + ] + }, + { + type: "category", + label: "Smart contracts (deploy & upgrade)", + items: [ + "sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/compute-contract-address", + "sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/deploy-contract", + "sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/upgrade-contract", + ] + }, + { + type: "category", + label: "Smart contracts (Rust authoring)", + items: [ + "sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/new-contract-from-template", + "sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/storage-mapper-decision-table", + ] + }, ] }, "sdk-and-tools/sdk-js/extending-sdk-js", diff --git a/src/components/cookbook/CookbookIndex/index.jsx b/src/components/cookbook/CookbookIndex/index.jsx new file mode 100644 index 000000000..4a0575da3 --- /dev/null +++ b/src/components/cookbook/CookbookIndex/index.jsx @@ -0,0 +1,43 @@ +import React from "react"; +import RecipeGrid from "@site/src/components/cookbook/RecipeGrid"; +import styles from "./styles.module.css"; + +// The browsable front door for the cookbook. Given the build-time manifest +// (src/data/cookbook-manifest.json, generated from each recipe's real +// frontmatter), it renders a jump-nav plus one RecipeGrid per section. Nothing +// here is hand-maintained: sections and their order come straight from the +// manifest, which mirrors the curated sidebar order. +export default function CookbookIndex({ sections = [] }) { + const total = sections.reduce((n, s) => n + s.recipes.length, 0); + + return ( +
+ + + {sections.map((s) => ( +
+

+ {s.label} + + {s.recipes.length} {s.recipes.length === 1 ? "recipe" : "recipes"} + +

+ +
+ ))} + +

+ {total} recipes across {sections.length} sections. Every card links to a + page whose code is extracted and type-checked in CI; the teal badge means + that check is currently green. +

+
+ ); +} diff --git a/src/components/cookbook/CookbookIndex/styles.module.css b/src/components/cookbook/CookbookIndex/styles.module.css new file mode 100644 index 000000000..a33926aee --- /dev/null +++ b/src/components/cookbook/CookbookIndex/styles.module.css @@ -0,0 +1,74 @@ +/* Front-door layout for the cookbook index. Structure only — hairline borders + on Infima neutrals so it tracks the light/dark toggle; the teal accent stays + reserved for the verified badge inside each card, never spent on chrome. */ + +.index { + margin-top: 1rem; +} + +/* ---- Jump-to-section nav ---- */ +.jump { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin: 0 0 2rem; + padding-bottom: 1.25rem; + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +.jumpLink { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.28rem 0.6rem; + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 6px; + font-size: 0.82rem; + color: var(--ifm-color-content); + text-decoration: none; + transition: border-color 0.15s ease, color 0.15s ease; +} + +.jumpLink:hover { + border-color: var(--cookbook-accent); + color: var(--ifm-color-content); + text-decoration: none; +} + +.jumpCount { + font-family: var(--cookbook-font-mono); + font-size: 0.7rem; + font-variant-numeric: tabular-nums; + color: var(--ifm-color-emphasis-600); +} + +/* ---- Section heading ---- */ +.section { + margin-bottom: 2.25rem; +} + +.heading { + display: flex; + align-items: baseline; + gap: 0.7rem; + margin-bottom: 0.25rem; + font-size: 1.35rem; + scroll-margin-top: 5rem; +} + +.count { + font-family: var(--cookbook-font-mono); + font-size: 0.75rem; + font-weight: 400; + font-variant-numeric: tabular-nums; + color: var(--ifm-color-emphasis-600); + letter-spacing: 0; +} + +.footNote { + margin-top: 2.5rem; + padding-top: 1.25rem; + border-top: 1px solid var(--ifm-color-emphasis-200); + font-size: 0.85rem; + color: var(--ifm-color-emphasis-700); +} diff --git a/src/components/cookbook/DifficultyDots/index.jsx b/src/components/cookbook/DifficultyDots/index.jsx new file mode 100644 index 000000000..136bff297 --- /dev/null +++ b/src/components/cookbook/DifficultyDots/index.jsx @@ -0,0 +1,29 @@ +import React from "react"; +import styles from "./styles.module.css"; + +// Difficulty is drawn as filled dots (1 = beginner, 3 = advanced). Deliberately +// neutral-toned — structure, not accent — so it never competes with the teal +// "verified" signal. +const LEVELS = { beginner: 1, intermediate: 2, advanced: 3 }; + +export default function DifficultyDots({ difficulty }) { + const level = LEVELS[difficulty] ?? 0; + const label = difficulty + ? difficulty.charAt(0).toUpperCase() + difficulty.slice(1) + : "Unknown"; + + return ( + + + ); +} diff --git a/src/components/cookbook/DifficultyDots/styles.module.css b/src/components/cookbook/DifficultyDots/styles.module.css new file mode 100644 index 000000000..43f124ff4 --- /dev/null +++ b/src/components/cookbook/DifficultyDots/styles.module.css @@ -0,0 +1,32 @@ +.wrap { + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-size: 0.8rem; +} + +.dots { + display: inline-flex; + gap: 3px; +} + +.dotOn, +.dotOff { + width: 6px; + height: 6px; + border-radius: 50%; + display: inline-block; +} + +.dotOn { + background: var(--ifm-color-emphasis-800); +} + +.dotOff { + background: var(--ifm-color-emphasis-300); +} + +.label { + color: var(--ifm-color-emphasis-700); + font-weight: 500; +} diff --git a/src/components/cookbook/MetaChip/index.jsx b/src/components/cookbook/MetaChip/index.jsx new file mode 100644 index 000000000..a86b46b0d --- /dev/null +++ b/src/components/cookbook/MetaChip/index.jsx @@ -0,0 +1,17 @@ +import React from "react"; +import styles from "./styles.module.css"; + +// A hairline pill for one piece of recipe metadata (est-time, an SDK version, +// etc.). `mono` renders the value in JetBrains Mono with tabular figures, the +// house treatment for anything version- or number-like. +export default function MetaChip({ label, value, mono = false, title }) { + return ( + + {label && {label}} + {value} + + ); +} diff --git a/src/components/cookbook/MetaChip/styles.module.css b/src/components/cookbook/MetaChip/styles.module.css new file mode 100644 index 000000000..b375ec1cd --- /dev/null +++ b/src/components/cookbook/MetaChip/styles.module.css @@ -0,0 +1,31 @@ +.chip { + display: inline-flex; + align-items: baseline; + gap: 0.35rem; + font-size: 0.76rem; + line-height: 1.4; + padding: 0.22rem 0.5rem; + border-radius: 6px; + border: 1px solid var(--ifm-color-emphasis-200); + background: var(--ifm-color-emphasis-100); + color: var(--ifm-color-emphasis-800); + white-space: nowrap; +} + +.label { + color: var(--ifm-color-emphasis-600); + text-transform: uppercase; + letter-spacing: 0.03em; + font-size: 0.66rem; + font-weight: 600; +} + +.value { + font-weight: 500; +} + +.valueMono { + font-family: var(--cookbook-font-mono); + font-variant-numeric: tabular-nums; + font-weight: 500; +} diff --git a/src/components/cookbook/RecipeCard/index.jsx b/src/components/cookbook/RecipeCard/index.jsx new file mode 100644 index 000000000..aa8cfb53e --- /dev/null +++ b/src/components/cookbook/RecipeCard/index.jsx @@ -0,0 +1,50 @@ +import React from "react"; +import Link from "@docusaurus/Link"; +import DifficultyDots from "@site/src/components/cookbook/DifficultyDots"; +import VerifiedBadge from "@site/src/components/cookbook/VerifiedBadge"; +import styles from "./styles.module.css"; + +// One recipe as a card in the browsable index. Props map to the same recipe +// frontmatter fields the swizzle reads, so a card and its page always agree. +export default function RecipeCard({ recipe = {} }) { + const { + title, + description, + href = "#", + difficulty, + lastValidated, + stale, + tags = [], + sdkVersions, + } = recipe; + + const primaryVersion = + sdkVersions && typeof sdkVersions === "object" + ? Object.entries(sdkVersions).find(([, range]) => Boolean(range)) + : null; + + return ( + +
+ {difficulty && } + +
+ +
{title}
+ {description &&

{description}

} + +
+ {tags.slice(0, 3).map((t) => ( + + {t} + + ))} + {primaryVersion && ( + + {primaryVersion[0]} {primaryVersion[1]} + + )} +
+ + ); +} diff --git a/src/components/cookbook/RecipeCard/styles.module.css b/src/components/cookbook/RecipeCard/styles.module.css new file mode 100644 index 000000000..26fffc2f2 --- /dev/null +++ b/src/components/cookbook/RecipeCard/styles.module.css @@ -0,0 +1,65 @@ +.card { + display: flex; + flex-direction: column; + gap: 0.6rem; + padding: 1rem 1.1rem; + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 10px; + background: var(--ifm-background-color); + text-decoration: none !important; + color: var(--ifm-color-content); + transition: border-color 0.15s ease, transform 0.15s ease; + height: 100%; +} + +.card:hover { + border-color: var(--cookbook-accent); + transform: translateY(-1px); +} + +.head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; +} + +.title { + font-weight: 600; + font-size: 1.02rem; + line-height: 1.3; + color: var(--ifm-heading-color); +} + +.desc { + margin: 0; + font-size: 0.85rem; + line-height: 1.45; + color: var(--ifm-color-emphasis-700); +} + +.foot { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.35rem; + margin-top: auto; + padding-top: 0.4rem; +} + +.tag { + font-size: 0.68rem; + padding: 0.12rem 0.4rem; + border-radius: 4px; + background: var(--ifm-color-emphasis-100); + color: var(--ifm-color-emphasis-700); + border: 1px solid var(--ifm-color-emphasis-200); +} + +.version { + margin-left: auto; + font-family: var(--cookbook-font-mono); + font-size: 0.7rem; + color: var(--ifm-color-emphasis-600); + font-variant-numeric: tabular-nums; +} diff --git a/src/components/cookbook/RecipeGrid/index.jsx b/src/components/cookbook/RecipeGrid/index.jsx new file mode 100644 index 000000000..3cbf6730e --- /dev/null +++ b/src/components/cookbook/RecipeGrid/index.jsx @@ -0,0 +1,15 @@ +import React from "react"; +import RecipeCard from "@site/src/components/cookbook/RecipeCard"; +import styles from "./styles.module.css"; + +// Responsive grid of RecipeCards. Usable directly in MDX: +// +export default function RecipeGrid({ recipes = [] }) { + return ( +
+ {recipes.map((r) => ( + + ))} +
+ ); +} diff --git a/src/components/cookbook/RecipeGrid/styles.module.css b/src/components/cookbook/RecipeGrid/styles.module.css new file mode 100644 index 000000000..9a73fe267 --- /dev/null +++ b/src/components/cookbook/RecipeGrid/styles.module.css @@ -0,0 +1,6 @@ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 1rem; + margin: 1.5rem 0; +} diff --git a/src/components/cookbook/RecipeMeta/index.jsx b/src/components/cookbook/RecipeMeta/index.jsx new file mode 100644 index 000000000..206a4ecb1 --- /dev/null +++ b/src/components/cookbook/RecipeMeta/index.jsx @@ -0,0 +1,50 @@ +import React from "react"; +import DifficultyDots from "@site/src/components/cookbook/DifficultyDots"; +import VerifiedBadge from "@site/src/components/cookbook/VerifiedBadge"; +import MetaChip from "@site/src/components/cookbook/MetaChip"; +import styles from "./styles.module.css"; + +// The metadata strip rendered above every recipe title by the DocItem/Content +// swizzle. Every item maps to a real frontmatter field — difficulty, +// est_minutes, sdk_versions, last_validated, stale — so the strip reflects the +// product (CI-verified recipes), never decoration. Each field renders only when +// present, so partial frontmatter degrades gracefully. +export default function RecipeMeta({ frontMatter = {} }) { + const { + difficulty, + est_minutes: estMinutes, + last_validated: lastValidated, + stale, + sdk_versions: sdkVersions, + } = frontMatter; + + const versions = + sdkVersions && typeof sdkVersions === "object" + ? Object.entries(sdkVersions).filter(([, range]) => Boolean(range)) + : []; + + return ( +
+ {difficulty && } + + {typeof estMinutes === "number" && ( + + )} + + {versions.map(([name, range]) => ( + + ))} + + {(lastValidated || stale) && ( + + + + )} +
+ ); +} diff --git a/src/components/cookbook/RecipeMeta/styles.module.css b/src/components/cookbook/RecipeMeta/styles.module.css new file mode 100644 index 000000000..076d71a25 --- /dev/null +++ b/src/components/cookbook/RecipeMeta/styles.module.css @@ -0,0 +1,19 @@ +.strip { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.6rem; + margin: 0 0 1.25rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +.trailing { + margin-left: auto; +} + +@media (max-width: 600px) { + .trailing { + margin-left: 0; + } +} diff --git a/src/components/cookbook/VerifiedBadge/index.jsx b/src/components/cookbook/VerifiedBadge/index.jsx new file mode 100644 index 000000000..1bf4d483e --- /dev/null +++ b/src/components/cookbook/VerifiedBadge/index.jsx @@ -0,0 +1,40 @@ +import React from "react"; +import styles from "./styles.module.css"; + +// The single load-bearing teal element in the whole design layer: it signals +// that a recipe compiles green / is CI-verified. When the nightly recheck marks +// a recipe stale, it degrades to a neutral-warning state instead of teal, so the +// teal only ever means "trustworthy right now". +export default function VerifiedBadge({ date, stale = false, compact = false }) { + if (stale) { + return ( + + + {compact ? "Stale" : "Needs recheck"} + + ); + } + + return ( + + + {compact || !date ? "Verified" : `Verified ${date}`} + + ); +} diff --git a/src/components/cookbook/VerifiedBadge/styles.module.css b/src/components/cookbook/VerifiedBadge/styles.module.css new file mode 100644 index 000000000..b0fcffd54 --- /dev/null +++ b/src/components/cookbook/VerifiedBadge/styles.module.css @@ -0,0 +1,43 @@ +.badge { + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-size: 0.78rem; + font-weight: 600; + line-height: 1; + padding: 0.28rem 0.5rem; + border-radius: 999px; + font-family: var(--cookbook-font-mono); + font-variant-numeric: tabular-nums; + border: 1px solid transparent; + white-space: nowrap; +} + +.ok { + color: var(--cookbook-accent); + border-color: color-mix(in srgb, var(--cookbook-accent) 40%, transparent); + background: var(--cookbook-accent-soft); +} + +.stale { + color: #8a6d00; + border-color: color-mix(in srgb, var(--cookbook-yellow) 55%, transparent); + background: color-mix(in srgb, var(--cookbook-yellow) 12%, transparent); +} + +html[data-theme="dark"] .stale { + color: var(--cookbook-yellow); +} + +.icon { + display: inline-flex; +} + +.bang { + display: inline-flex; + align-items: center; + justify-content: center; + width: 12px; + height: 12px; + font-weight: 700; +} diff --git a/src/css/cookbook.css b/src/css/cookbook.css new file mode 100644 index 000000000..39091eeba --- /dev/null +++ b/src/css/cookbook.css @@ -0,0 +1,54 @@ +/** + * cookbook.css — MultiversX Cookbook design layer (scoped, additive). + * + * SCOPE DISCIPLINE: every rule here is namespaced under `.cookbook-recipe` + * (added by the DocItem/Content swizzle only on recipe pages) or consumed by + * the CSS-module components under src/components/cookbook/. Nothing here + * restyles global docs chrome (navbar / sidebar / footer / search). The only + * top-level declarations are CSS custom properties, which are inert until + * referenced, so registering this file globally cannot regress other pages. + * + * Tokens are the canonical MvX values (design-reference/MVX-DESIGN-LANGUAGE.md): + * accent teal #23f7dd, near-black grounds, Roobert + JetBrains Mono. Structure + * is driven by Infima neutral variables so components track the light/dark + * toggle automatically; the teal accent is reserved for the single load-bearing + * idea — "this recipe is verified" — never to tint structure. + */ + +:root { + /* MvX brand tokens. */ + --cookbook-teal: #23f7dd; + --cookbook-green: #4ade80; + --cookbook-yellow: #facc15; + + /* Accent is theme-aware: mint collapses to ~1.2:1 on light grounds and is + unreadable as text, so light mode uses the brand system's own accessible + deep-teal ink (5.3:1 AA), not a straight color-invert. */ + --cookbook-accent: #0b6f63; + --cookbook-accent-soft: rgba(11, 111, 99, 0.1); + + /* Roobert is already loaded site-wide (src/css/custom.css @font-face). + JetBrains Mono is not bundled here, so fall back cleanly to system mono. */ + --cookbook-font-brand: "Roobert", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --cookbook-font-mono: "JetBrains Mono", "SFMono-Regular", ui-monospace, Menlo, Consolas, monospace; +} + +html[data-theme="dark"] { + --cookbook-accent: #23f7dd; + --cookbook-accent-soft: rgba(35, 247, 221, 0.1); +} + +/* ---- Recipe article treatment (scoped: only present on recipe pages) ---- */ +.cookbook-recipe { + font-family: var(--cookbook-font-brand); +} + +.cookbook-recipe code, +.cookbook-recipe kbd, +.cookbook-recipe pre { + font-family: var(--cookbook-font-mono); +} + +.cookbook-recipe :is(h1, h2, h3) { + letter-spacing: -0.01em; +} diff --git a/src/data/cookbook-manifest.json b/src/data/cookbook-manifest.json new file mode 100644 index 000000000..88ae6dbe7 --- /dev/null +++ b/src/data/cookbook-manifest.json @@ -0,0 +1,1318 @@ +[ + { + "id": "start-here", + "label": "Start here", + "recipes": [ + { + "title": "Minimal sdk-dapp v5 app in Vite + React", + "description": "The smallest working sdk-dapp v5 setup in a fresh Vite + React + TypeScript project. Compiles strict on the first try, with HTTPS dev built in.", + "href": "/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-dapp", + "vite", + "react", + "typescript", + "wallet", + "devnet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0", + "sdk-dapp": "^5.6.0" + } + }, + { + "title": "Minimal sdk-dapp v5 app in Next.js (App Router)", + "description": "A working Next.js 14 App Router starter with sdk-dapp v5. Client-only init wrapper, the real next.config.js, login button, account hook, compiles strict.", + "href": "/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-dapp", + "nextjs", + "react", + "typescript", + "wallet", + "devnet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0", + "sdk-dapp": "^5.6.0" + } + }, + { + "title": "Local HTTPS for dApp dev (mkcert + Vite + Next.js)", + "description": "Wallet providers refuse to connect over plain HTTP. Set up trusted HTTPS for localhost using mkcert with Vite, Next.js, or framework-agnostic plain Node.", + "href": "/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "https", + "mkcert", + "vite", + "nextjs", + "wallet" + ], + "sdkVersions": { + "sdk-dapp": "^5.6.0" + } + }, + { + "title": "Sign and send a transaction (the working path)", + "description": "The canonical sdk-dapp v5 sign, send, and track flow. provider.signTransactions then TransactionManager.send and .track, with proper nonce handling.", + "href": "/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-dapp", + "sdk-core", + "transaction", + "typescript", + "devnet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0", + "sdk-dapp": "^5.6.0" + } + }, + { + "title": "Migrate sdk-dapp v4 to v5: hook-by-hook diffs", + "description": "Side-by-side v4 vs v5 diffs for every removed hook, component, and import path. The shortest viable upgrade path for an existing v4 dApp.", + "href": "/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-dapp", + "migration", + "v4-to-v5", + "typescript", + "react" + ], + "sdkVersions": { + "sdk-dapp": "^5.6.0" + } + } + ] + }, + { + "id": "accounts", + "label": "Accounts and signing", + "recipes": [ + { + "title": "Generate a mnemonic + derive keys", + "description": "Generate a BIP39 mnemonic with sdk-core and derive secret keys at several address indices, fully offline, with a determinism check and secrecy notes.", + "href": "/sdk-and-tools/sdk-js/cookbook/accounts/generate-mnemonic-derive-keys", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "typescript", + "wallet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Create an Account from a KeyPair, secret key, or mnemonic", + "description": "Build an sdk-core Account from a KeyPair, a raw secret key, or a mnemonic, fully offline, and learn which named constructors are sync versus async.", + "href": "/sdk-and-tools/sdk-js/cookbook/accounts/account-from-keys", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "typescript", + "wallet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Save and load a keystore (encrypted JSON)", + "description": "Save a wallet to an encrypted keystore and load it back with a password, for both secret-key and mnemonic keystores, with sdk-core. Fully offline.", + "href": "/sdk-and-tools/sdk-js/cookbook/accounts/keystore-save-load", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "typescript", + "wallet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Save and load a PEM (dev only)", + "description": "Save a wallet to a PEM file and load it back via Account and UserPem with sdk-core. PEM is unencrypted and dev-only; fully offline, no network.", + "href": "/sdk-and-tools/sdk-js/cookbook/accounts/pem-save-load", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "typescript", + "wallet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Address utilities", + "description": "The sdk-core Address toolkit, bech32/hex conversion, raw public key, shard computation, isSmartContract, and the HRP. Fully offline, no network.", + "href": "/sdk-and-tools/sdk-js/cookbook/accounts/address-utilities", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "typescript", + "address" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Fetch an account's on-chain state", + "description": "Read any address's nonce, balance, username, and guarded flag, plus its key-value storage, through a network provider. Verified live against mainnet.", + "href": "/sdk-and-tools/sdk-js/cookbook/accounts/fetch-account-state", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "network-provider", + "api", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Manage nonces (fetch-then-increment)", + "description": "Fetch an account's nonce from the network once, then increment it locally for every transaction sent afterward in a batch, plus recovery from a nonce gap.", + "href": "/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "transaction", + "typescript", + "devnet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Sign a message + verify a signature", + "description": "Sign a message with an account or secret key and verify it with a UserVerifier, fully offline, including proof the check fails on a tampered signature.", + "href": "/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-message", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Sign + verify a transaction (offline)", + "description": "Sign a transaction offline with a raw secret key and verify it three ways with sdk-core, including tamper checks. No devnet, no broadcast.", + "href": "/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-transaction", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "typescript", + "transaction" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Hash-signing a transaction", + "description": "Opt a transaction into hash signing with sdk-core, assert the version/options bits, and sign the hash correctly. Includes a real v15.4.1 signing trap.", + "href": "/sdk-and-tools/sdk-js/cookbook/accounts/hash-signing-transaction", + "difficulty": "advanced", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "typescript", + "transaction" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Set a guardian on an account", + "description": "Nominate a guardian for an account with SetGuardian via sdk-core's AccountController and AccountTransactionsFactory, payload verified on devnet.", + "href": "/sdk-and-tools/sdk-js/cookbook/accounts/set-guardian", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "guardian", + "transaction", + "gas", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Guard and unguard an account", + "description": "Activate guardianship with GuardAccount and remove it with UnGuardAccount via sdk-core, including the guardian co-sign requirement, verified on devnet.", + "href": "/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "guardian", + "transaction", + "gas", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Apply a guardian to a transaction and co-sign it", + "description": "Turn any transaction into a guarded one with TransactionComputer.applyGuardian and co-sign it, asserting version, options, and guardian fields on devnet.", + "href": "/sdk-and-tools/sdk-js/cookbook/accounts/apply-guardian-to-transaction", + "difficulty": "advanced", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "guardian", + "transaction", + "gas", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Build a relayed v3 transaction", + "description": "A sender who has no EGLD for gas, and a relayer who pays it, both signing the same transaction, devnet-verified, with V1/V2 deprecation flagged.", + "href": "/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction", + "difficulty": "advanced", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "transaction", + "relayed", + "typescript", + "devnet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + } + ] + }, + { + "id": "network-providers", + "label": "Network providers", + "recipes": [ + { + "title": "Configure a network provider (Api vs Proxy)", + "description": "Construct an ApiNetworkProvider or ProxyNetworkProvider, tune clientName and timeout, or get one from an entrypoint. Both share one INetworkProvider interface.", + "href": "/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "network-provider", + "api", + "proxy", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Fetch network config and status", + "description": "Read the static network config (gas costs, shard count, round duration) and the live per-shard status (block nonce, epoch, round) via a network provider.", + "href": "/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-network-config-status", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "network-provider", + "api", + "gas", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Fetch a block", + "description": "Fetch the latest block and a block by hash via ApiNetworkProvider, and a block by shard and nonce via ProxyNetworkProvider. Verified live against mainnet.", + "href": "/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-a-block", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "network-provider", + "api", + "proxy", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Await an account on a custom condition", + "description": "Block until an account matches a predicate you write with awaitAccountOnCondition, tuning the poll interval and timeout via AwaitingOptions.", + "href": "/sdk-and-tools/sdk-js/cookbook/network-providers/await-account-on-condition", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "network-provider", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Await a transaction on a custom condition", + "description": "Block until a transaction matches a predicate you write with awaitTransactionOnCondition, tuning the poll interval and timeout via AwaitingOptions.", + "href": "/sdk-and-tools/sdk-js/cookbook/network-providers/await-transaction-on-condition", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "network-provider", + "transaction", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Simulate and estimate a transaction", + "description": "Ask the network what a transaction would cost with estimateTransactionCost and what it would do with simulateTransaction, without ever broadcasting it.", + "href": "/sdk-and-tools/sdk-js/cookbook/network-providers/simulate-estimate-transaction", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "network-provider", + "transaction", + "gas", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Custom API/Proxy request", + "description": "Call any API or Proxy endpoint the typed provider methods do not cover, via doGetGeneric and doPostGeneric, for economics, stats, and a raw VM query.", + "href": "/sdk-and-tools/sdk-js/cookbook/network-providers/custom-api-request", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "network-provider", + "api", + "proxy", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + } + ] + }, + { + "id": "transactions", + "label": "Transactions", + "recipes": [ + { + "title": "Send EGLD to an address", + "description": "Send a native EGLD transfer with sdk-core's TransfersController against DevnetEntrypoint, the backend/script pattern for when your own code holds the key.", + "href": "/sdk-and-tools/sdk-js/cookbook/transactions/send-egld", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "transaction", + "typescript", + "devnet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Send an ESDT", + "description": "Send a fungible ESDT token using sdk-core's TransfersController and the Token/TokenTransfer classes against DevnetEntrypoint, no browser required.", + "href": "/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "transaction", + "esdt", + "typescript", + "devnet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Multi-token transfer in one tx", + "description": "Send EGLD plus two or more ESDT/NFT/SFT tokens in one transaction with sdk-core's TransfersController, every field decoded against real devnet.", + "href": "/sdk-and-tools/sdk-js/cookbook/transactions/multi-token-transfer", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "transaction", + "esdt", + "nft", + "gas", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Track a transaction (WebSocket + polling fallback)", + "description": "Track a transaction's live status via sdk-dapp's WebSocket-with-polling-fallback tracker and the pending/successful/failed hooks.", + "href": "/sdk-and-tools/sdk-js/cookbook/transactions/track-transaction-status", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-dapp", + "transaction", + "websocket", + "vite", + "react", + "typescript", + "devnet" + ], + "sdkVersions": { + "sdk-dapp": "^5.6.0", + "sdk-core": "^15.4.0" + } + } + ] + }, + { + "id": "tokens", + "label": "Tokens (ESDT / NFT / SFT)", + "recipes": [ + { + "title": "Issue a fungible ESDT", + "description": "Issue a fungible ESDT token with sdk-core's TokenManagementController against DevnetEntrypoint, payload hand-verified against real devnet.", + "href": "/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "esdt", + "transaction", + "gas", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Issue an NFT collection + mint an NFT", + "description": "Issue an NFT collection and mint an NFT into it with sdk-core's TokenManagementController, every field hand-verified against real devnet.", + "href": "/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "nft", + "esdt", + "transaction", + "gas", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Issue an SFT collection + create, add, and burn quantity", + "description": "Issue a semi-fungible (SFT) collection, create an SFT batch, then add and burn quantity with sdk-core's TokenManagementController on devnet.", + "href": "/sdk-and-tools/sdk-js/cookbook/tokens/issue-sft-collection", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "sft", + "esdt", + "nft", + "transaction", + "gas", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Set and unset special roles on a fungible ESDT", + "description": "Set and unset the local-mint, local-burn, and ESDT-transfer roles on a fungible ESDT with sdk-core, including a confirmed v15.4.1 unset-role bug.", + "href": "/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "esdt", + "transaction", + "gas", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Local mint and burn (change a fungible token's supply)", + "description": "Increase and decrease a fungible ESDT's circulating supply with ESDTLocalMint and ESDTLocalBurn via sdk-core, plus a real SDK method-name typo.", + "href": "/sdk-and-tools/sdk-js/cookbook/tokens/local-mint-burn-supply", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "esdt", + "transaction", + "gas", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Token lifecycle — freeze, unfreeze, pause, unpause, wipe", + "description": "Freeze/unfreeze an account's token, pause/unpause globally, and wipe with sdk-core, includes the confirmed v15.4.1 wrong-receiver bug and its fix.", + "href": "/sdk-and-tools/sdk-js/cookbook/tokens/token-lifecycle-operations", + "difficulty": "advanced", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "esdt", + "transaction", + "gas", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Fetch token metadata", + "description": "Read a token's definition, name, ticker, decimals, owner and property flags, for a fungible token and for an NFT or SFT collection. Verified live.", + "href": "/sdk-and-tools/sdk-js/cookbook/tokens/fetch-token-metadata", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "network-provider", + "esdt", + "nft", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Fetch an account's token balances", + "description": "Read the tokens an account holds, all fungible ESDTs, all NFTs and SFTs, and one specific token balance, via a network provider. Verified live.", + "href": "/sdk-and-tools/sdk-js/cookbook/tokens/fetch-account-token-balances", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "network-provider", + "esdt", + "nft", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + } + ] + }, + { + "id": "wallets", + "label": "Wallets", + "recipes": [ + { + "title": "Read the connected account with useGetAccount", + "description": "Every AccountType field explained, which ones are optional, how to format the balance correctly, and when to reach for useGetAccountInfo instead.", + "href": "/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-dapp", + "wallet", + "react", + "typescript", + "devnet" + ], + "sdkVersions": { + "sdk-dapp": "^5.6.0" + } + }, + { + "title": "Add a wallet login button", + "description": "Configure UnlockPanelManager once at startup, then add a reusable connect and disconnect button that works with every registered wallet provider.", + "href": "/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-dapp", + "wallet", + "vite", + "react", + "typescript", + "devnet" + ], + "sdkVersions": { + "sdk-dapp": "^5.6.0" + } + }, + { + "title": "Log in with the DeFi Wallet browser extension", + "description": "A dedicated single-provider login button using ProviderFactory directly, with real extension detection and no generic multi-provider picker UI.", + "href": "/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-dapp", + "wallet", + "wallet-extension", + "react", + "typescript", + "devnet" + ], + "sdkVersions": { + "sdk-dapp": "^5.6.0" + } + }, + { + "title": "Log in with xPortal via WalletConnect", + "description": "A dedicated xPortal login button using ProviderFactory and WalletConnect v2 directly, including the QR-code anchor element and project ID setup.", + "href": "/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-dapp", + "wallet", + "walletconnect", + "react", + "typescript", + "devnet" + ], + "sdkVersions": { + "sdk-dapp": "^5.6.0" + } + }, + { + "title": "Log in with a Ledger hardware wallet", + "description": "A dedicated Ledger login button using ProviderFactory directly, including the anchor element the SDK renders its own device-connect and account-picker UI into.", + "href": "/sdk-and-tools/sdk-js/cookbook/wallets/ledger-login", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-dapp", + "wallet", + "ledger", + "react", + "typescript", + "devnet" + ], + "sdkVersions": { + "sdk-dapp": "^5.6.0" + } + }, + { + "title": "Native auth, token issuance, expiry, auto-logout", + "description": "How nativeAuth issues a bearer token on login, why loginExpiresAt is not the token's real expiry, and how LogoutManager schedules the warning toast and logout.", + "href": "/sdk-and-tools/sdk-js/cookbook/wallets/native-auth", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-dapp", + "wallet", + "native-auth", + "react", + "typescript", + "devnet" + ], + "sdkVersions": { + "sdk-dapp": "^5.6.0" + } + }, + { + "title": "TypeScript strict-mode checklist for sdk-dapp consumers", + "description": "Four real bugs found while verifying cookbook recipes against installed sdk-dapp, plus the tsconfig flags and ESLint rules that catch them.", + "href": "/sdk-and-tools/sdk-js/cookbook/wallets/typescript-strict-mode-checklist", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-dapp", + "typescript", + "react" + ], + "sdkVersions": { + "sdk-dapp": "^5.6.0" + } + } + ] + }, + { + "id": "migration", + "label": "Migration", + "recipes": [ + { + "title": "Migration: DappProvider to initApp, side-by-side", + "description": "The full-app version of the DappProvider removal, every file that touches it, including the session-restore state v4 quietly handled for you.", + "href": "/sdk-and-tools/sdk-js/cookbook/migration/dapp-provider-to-initapp", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-dapp", + "migration", + "v4-to-v5", + "react", + "typescript" + ], + "sdkVersions": { + "sdk-dapp": "^5.6.0" + } + }, + { + "title": "Migration: useGetAccountInfo v4 vs v5", + "description": "The same hook name still resolves in v5 but returns less data, silently. The easiest migration bug to miss because nothing forces a second look at it.", + "href": "/sdk-and-tools/sdk-js/cookbook/migration/get-account-info-v4-vs-v5", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-dapp", + "migration", + "v4-to-v5", + "react", + "typescript" + ], + "sdkVersions": { + "sdk-dapp": "^5.6.0" + } + }, + { + "title": "Migration: useSendTransactions to TransactionManager.send", + "description": "The v4 hook this migration is named for does not exist in the real package. This recipe covers what does, batch sends and a v5 nested-array trigger.", + "href": "/sdk-and-tools/sdk-js/cookbook/migration/send-transactions-to-transaction-manager", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-dapp", + "migration", + "v4-to-v5", + "transaction", + "typescript" + ], + "sdkVersions": { + "sdk-dapp": "^5.6.0" + } + } + ] + }, + { + "id": "governance", + "label": "Governance", + "recipes": [ + { + "title": "Create a governance proposal", + "description": "Create a MultiversX governance proposal with sdk-core's GovernanceController and factory, reading the live proposal fee from getConfig first.", + "href": "/sdk-and-tools/sdk-js/cookbook/governance/create-proposal", + "difficulty": "advanced", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "governance", + "transaction", + "devnet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Vote and close a proposal", + "description": "Vote on and close a MultiversX governance proposal with sdk-core's GovernanceController and factory, reading a live proposal's tallies first.", + "href": "/sdk-and-tools/sdk-js/cookbook/governance/vote-close-proposal", + "difficulty": "advanced", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "governance", + "transaction", + "devnet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + } + ] + }, + { + "id": "delegation", + "label": "Delegation", + "recipes": [ + { + "title": "Create a delegation contract", + "description": "Create a new MultiversX delegation (staking-provider) contract with sdk-core DelegationController and factory, then parse the outcome for its address.", + "href": "/sdk-and-tools/sdk-js/cookbook/delegation/create-delegation-contract", + "difficulty": "advanced", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "delegation", + "transaction", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Delegate (stake) EGLD", + "description": "Delegate (stake) EGLD to an existing MultiversX delegation contract with sdk-core DelegationController and factory, then parse the staked amount.", + "href": "/sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "delegation", + "transaction", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Claim and re-delegate rewards", + "description": "Claim delegation rewards to your wallet or re-delegate (compound) them into the same MultiversX staking contract, with sdk-core DelegationController.", + "href": "/sdk-and-tools/sdk-js/cookbook/delegation/claim-and-redelegate-rewards", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "delegation", + "transaction", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Undelegate and withdraw", + "description": "Exit a MultiversX delegation: unDelegate an amount, wait out the unbonding period, then withdraw it, using sdk-core DelegationController and factory.", + "href": "/sdk-and-tools/sdk-js/cookbook/delegation/undelegate-and-withdraw", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "delegation", + "transaction", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Read a delegation contract's state", + "description": "Read a MultiversX staking-provider contract state with read-only queries: total stake, service fee, a delegator active stake and claimable rewards.", + "href": "/sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "delegation", + "network-provider", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + } + ] + }, + { + "id": "multisig", + "label": "Multisig", + "recipes": [ + { + "title": "Propose a multisig action", + "description": "Propose a multisig action, a transfer-execute or an add-board-member, with sdk-core's MultisigController and factory, then parse the new action id.", + "href": "/sdk-and-tools/sdk-js/cookbook/multisig/propose-action", + "difficulty": "advanced", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "multisig", + "smart-contract", + "transaction", + "devnet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Sign and perform a multisig action", + "description": "Sign a proposed multisig action to quorum and perform it with sdk-core's MultisigController and factory, reading its live signer state first.", + "href": "/sdk-and-tools/sdk-js/cookbook/multisig/sign-perform-action", + "difficulty": "advanced", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "multisig", + "smart-contract", + "transaction", + "devnet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Read a multisig's state", + "description": "Read a MultiversX multisig's quorum, board members, and pending actions with their signers using sdk-core's MultisigController, no wallet needed.", + "href": "/sdk-and-tools/sdk-js/cookbook/multisig/read-multisig-state", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "multisig", + "smart-contract", + "devnet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + } + ] + }, + { + "id": "smart-contracts-call", + "label": "Smart contracts (call & query)", + "recipes": [ + { + "title": "Load an ABI", + "description": "Load a contract's ABI from a file, a URL, or by hand with sdk-core's Abi/AbiRegistry, then introspect its endpoints and argument types.", + "href": "/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "abi", + "smart-contract", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Query a read-only view", + "description": "Query a smart contract's read-only view with sdk-core's SmartContractController, no wallet, no gas, no transaction, verified against live devnet.", + "href": "/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "smart-contract", + "abi", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Call a contract endpoint with native JS args (NativeSerializer)", + "description": "Call a mutable contract endpoint with plain JS arguments, auto-converted to typed values by sdk-core's NativeSerializer, verified against live devnet.", + "href": "/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "smart-contract", + "abi", + "transaction", + "typescript", + "devnet" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Call a payable endpoint with EGLD", + "description": "Call a payable contract endpoint, attaching EGLD via nativeTransferAmount, against a real live devnet contract with sdk-core's controller pattern.", + "href": "/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-payable-endpoint", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "smart-contract", + "abi", + "transaction", + "gas", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Decode contract return data (ABI codec)", + "description": "Decode raw contract return data and encode or decode custom struct and enum types with sdk-core's BinaryCodec, getStruct, and getEnum, fully offline.", + "href": "/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/decode-return-data", + "difficulty": "advanced", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "smart-contract", + "abi", + "codec", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Decode contract events", + "description": "Decode a smart contract's emitted events into named typed fields with sdk-core's TransactionEventsParser and an ABI, verified against a real event.", + "href": "/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/decode-contract-events", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "smart-contract", + "abi", + "events", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + } + ] + }, + { + "id": "smart-contracts-deploy", + "label": "Smart contracts (deploy & upgrade)", + "recipes": [ + { + "title": "Compute a contract address before deploy", + "description": "Predict a smart contract's address before deploying it from the deployer address and deployment nonce with sdk-core's AddressComputer, fully offline.", + "href": "/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/compute-contract-address", + "difficulty": "beginner", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "smart-contract", + "address", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Deploy a smart contract", + "description": "Deploy a smart contract from WASM bytecode and constructor arguments with sdk-core's controller and factory, then parse the outcome for the new address.", + "href": "/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/deploy-contract", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "smart-contract", + "deploy", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + }, + { + "title": "Upgrade a smart contract", + "description": "Upgrade a deployed smart contract to new WASM bytecode with sdk-core's controller and factory, and read the upgradeContract builtin wire payload.", + "href": "/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/upgrade-contract", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-core", + "smart-contract", + "upgrade", + "devnet", + "typescript" + ], + "sdkVersions": { + "sdk-core": "^15.4.0" + } + } + ] + }, + { + "id": "smart-contracts-rust", + "label": "Smart contracts (Rust authoring)", + "recipes": [ + { + "title": "New contract from sc-meta new --template empty", + "description": "The real, unedited output of sc-meta new --template empty, then the smallest real customization on top, with every command actually run and captured.", + "href": "/sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/new-contract-from-template", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-rs", + "rust", + "smart-contract" + ], + "sdkVersions": { + "sdk-rs": "^0.64.1" + } + }, + { + "title": "Storage mappers: which to pick, when", + "description": "A working, tested contract exercising six storage mappers side by side, with a decision table grounded in the real multiversx-sc crate source.", + "href": "/sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/storage-mapper-decision-table", + "difficulty": "intermediate", + "lastValidated": "2026-07-16", + "stale": false, + "tags": [ + "sdk-rs", + "rust", + "smart-contract" + ], + "sdkVersions": { + "sdk-rs": "^0.64.1" + } + } + ] + } +] diff --git a/src/theme/DocItem/Content/index.js b/src/theme/DocItem/Content/index.js new file mode 100644 index 000000000..35f1c69a3 --- /dev/null +++ b/src/theme/DocItem/Content/index.js @@ -0,0 +1,32 @@ +import React from "react"; +import Content from "@theme-original/DocItem/Content"; +import { useDoc } from "@docusaurus/plugin-content-docs/client"; +import RecipeMeta from "@site/src/components/cookbook/RecipeMeta"; + +/** + * Wraps the original DocItem/Content. + * + * On cookbook recipe pages — identified by the presence of the recipe-specific + * frontmatter fields (`difficulty` + `sdk_versions`) — it renders the metadata + * strip above the title and scopes the recipe design layer via the + * `.cookbook-recipe` class. Every other doc page is returned untouched (same + * element, same props, no wrapper), so this swizzle cannot change the + * appearance of anything outside the cookbook. + */ +export default function ContentWrapper(props) { + const { frontMatter } = useDoc(); + const isRecipe = Boolean( + frontMatter && frontMatter.difficulty && frontMatter.sdk_versions + ); + + if (!isRecipe) { + return ; + } + + return ( +
+ + +
+ ); +} diff --git a/static/llms-full.txt b/static/llms-full.txt index 537252bb6..ea234d0fb 100644 --- a/static/llms-full.txt +++ b/static/llms-full.txt @@ -1611,6 +1611,1254 @@ curl --request GET \ --- +### Add a wallet login button + +Every dApp needs a "Connect wallet" button. In sdk-dapp v5 that button does not +render a picker itself. It calls `UnlockPanelManager.getInstance().openUnlockPanel()`, +and a pre-built web component, configured once at app startup, does the rest. +This recipe shows the full pattern: configure the panel, build one reusable +button component, and understand what each `UnlockPanelManager.init()` option does. + +This recipe assumes you already have a working sdk-dapp v5 + Vite setup. If not, +start with +[Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) +first. If you want a login button for one *specific* provider (skip the picker +entirely, e.g. a "Connect DeFi Wallet" button with no other options shown), see +[Login via the DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login) +or +[Login via xPortal / WalletConnect](/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login) +instead, which use the lower-level `ProviderFactory` directly. + +## Prerequisites + +- A working sdk-dapp v5 setup (Next.js or Vite, see the start-here recipes). +- A devnet wallet to test with (DeFi extension or xPortal). + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/wallet-login-button +npm install +npm run dev +# open https://localhost:5173 +``` + +## The reusable button + +Zero props. Every instance reads the same sdk-dapp store, so dropping +`` in five different places in your tree keeps all five in sync +automatically: + +```tsx title="src/LoginButton.tsx" +// src/LoginButton.tsx — the reusable connect/disconnect button. +// +// This is the piece you copy into your own dApp: a single component that +// renders "Connect wallet" when logged out and "Disconnect" when logged in. +// It has zero props because it reads everything it needs from the sdk-dapp +// store (via hooks) and the UnlockPanelManager / provider singletons — drop +// it anywhere in the tree, as many times as you want, and every instance +// stays in sync automatically. + +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; + +export function LoginButton(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + + const handleConnect = (): void => { + // The panel singleton was configured once in (src/providers.tsx). + // openUnlockPanel() returns Promise; this is a sync onClick, so we + // discard it explicitly with `void` (eslint no-floating-promises). + void UnlockPanelManager.getInstance().openUnlockPanel(); + }; + + const handleDisconnect = async (): Promise => { + const provider = getAccountProvider(); + await provider.logout(); + }; + + if (isLoggedIn) { + return ( + + ); + } + + return ( + + ); +} + +const buttonStyle: React.CSSProperties = { + padding: '0.75rem 1.25rem', + fontSize: '1rem', + cursor: 'pointer', +}; +``` + +## The demo page + +```tsx title="src/App.tsx" +// src/App.tsx — demo page for the LoginButton component. +// +// Deliberately thin: this recipe's job is the button + the UnlockPanelManager +// setup behind it, not the full account display (see the "Reading the +// connected account" recipe for the useGetAccount() deep dive). + +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { LoginButton } from './LoginButton'; + +export function App(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const account = useGetAccount(); + const { network } = useGetNetworkConfig(); + + return ( +
+

Wallet login button

+

+ Network: {network.chainId} +

+ + + + {isLoggedIn && ( +

+ Connected as {account.address}. See{' '} + + Reading the connected account + {' '} + for the full field breakdown. +

+ )} + +

+ Drop <LoginButton /> anywhere in your component + tree — it needs no props and stays in sync with every other instance + automatically, because all of them read the same sdk-dapp store. +

+
+ ); +} +``` + +## The panel setup + +`UnlockPanelManager.init()` runs exactly once, at app startup, in the same +wrapper that calls `initApp()`. `loginHandler` and `onClose` both return +`Promise`, not `void`. The SDK's own type declarations require it (see +Pitfall 1), even though some example code you will find floating around uses a +sync arrow function. + +```tsx title="src/providers.tsx" +// src/providers.tsx — sdk-dapp v5 init wrapper + UnlockPanelManager setup. +// +// This file is the actual subject of this recipe. Two things happen here, +// in order: +// +// 1. initApp(dappConfig) — boots the SDK (store, network config, native +// auth, provider factory). Must resolve before anything else touches +// the store. +// 2. UnlockPanelManager.init({...}) — configures the (singleton) unlock +// panel: which callback runs after login, which runs if the user +// closes the panel without logging in, and which providers to show. +// This call happens ONCE, here, at app startup — not per-button. Every +// "Connect wallet" button anywhere in the app just calls +// UnlockPanelManager.getInstance().openUnlockPanel(). + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { dappConfig } from './lib/multiversx'; + +// Module-scope flag prevents double-init under React Strict Mode (which +// deliberately mounts effects twice in dev). Matches every other recipe in +// this Cookbook — see Pitfall 3. +let initStarted = false; + +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; + + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + + UnlockPanelManager.init({ + // loginHandler accepts TWO different shapes (both documented in + // node_modules/@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager.types.d.ts): + // + // 1) A zero-arg callback — the SDK has already run the full + // provider-create + login sequence for you; you just react to + // "login succeeded" (e.g. navigate away). This is what almost + // every dApp wants, and what this recipe uses. + // + // 2) A `({ type, anchor }) => Promise` function — you take + // over the ENTIRE login sequence yourself, including calling + // ProviderFactory.create({ type, anchor }) and provider.login() + // by hand. Use this only if you need to intercept or customize + // that sequence (custom loading UI per provider, analytics + // hooks, etc.) — see the "Login via the DeFi extension" and + // "Login via xPortal / WalletConnect" recipes for the + // lower-level ProviderFactory pattern this shape wraps. + // + // IMPORTANT: both shapes must return Promise, not void — the + // docs' own sync example fails strict-mode compile. See Pitfall 1. + loginHandler: async (): Promise => { + // Replace with your post-login navigation (e.g. router.push). + // eslint-disable-next-line no-console + console.log('Logged in.'); + }, + // Called if the user dismisses the panel without completing login. + onClose: async (): Promise => { + // eslint-disable-next-line no-console + console.log('Unlock panel closed without logging in.'); + }, + // Optional: restrict + reorder which providers the panel shows. + // Omit this key entirely to show every registered provider (the + // default, and what this recipe does). Example from the SDK's own + // JSDoc: `allowedProviders: [ProviderTypeEnum.walletConnect, 'inMemoryProvider']`. + // allowedProviders: [ProviderTypeEnum.extension, ProviderTypeEnum.walletConnect], + }); + + setReady(true); + }); + + return () => { + cancelled = true; + }; + }, []); + + if (!ready) { + return ( +
+ Initializing wallet SDK… +
+ ); + } + + return <>{children}; +} +``` + +`providers.tsx` imports the shared environment config that every recipe in this +Cookbook passes to `initApp()`: + +```ts title="src/lib/multiversx.ts" +// src/lib/multiversx.ts — environment configuration for sdk-dapp's initApp. +// +// The full dAppConfig shape accepts more keys than this; we only set the +// keys this recipe actually uses. + +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +// Read from import.meta.env (Vite's runtime environment object) — NOT +// process.env, which does not exist in the browser bundle. Fallback is the +// demo project ID; register your own at https://cloud.walletconnect.com. +const WALLET_CONNECT_PROJECT_ID = + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; + +export const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + nativeAuth: true, + // `theme` is a ThemesEnum member (runtime value 'mvx:dark-theme'), not a + // bare string — see node_modules/@multiversx/sdk-dapp/out/methods/initApp/initApp.types.d.ts. + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + }, +}; + +export { EnvironmentsEnum }; +``` + +## How it works + +**Configure once, use everywhere.** `UnlockPanelManager` is a singleton. +`.init({...})` sets its `loginHandler`, `onClose`, and (optionally) +`allowedProviders`. Call it once, at startup. Every button anywhere in the app +calls `.getInstance().openUnlockPanel()`, which just raises the +already-configured panel. + +**`loginHandler` has two shapes.** Per +`node_modules/@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager.types.d.ts`: + +```ts +// 1) Zero-arg callback: the SDK already ran the full login sequence. +loginHandler: () => { navigate('/dashboard'); }; + +// 2) Full control: you run the login sequence yourself. +loginHandler: async ({ type, anchor }) => { + const provider = await ProviderFactory.create({ type, anchor }); + await provider?.login(); + navigate('/dashboard'); +}; +``` + +This recipe uses shape 1, the common case. Shape 2 is the same `ProviderFactory` +pattern the dedicated single-provider recipes +([DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login), +[xPortal/WalletConnect](/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login)) +use directly, without going through the panel at all. + +**`allowedProviders` restricts and reorders the picker.** Omit it to show every +registered provider (this recipe's default). Pass +`[ProviderTypeEnum.extension, ProviderTypeEnum.walletConnect]` to show only those +two, in that order. Its type also accepts custom provider-name strings, for apps +that registered a custom `IProvider`. + +## Pitfalls + +:::warning[Pitfall 1: loginHandler and onClose must return Promise<void>] +A sync callback fails under TypeScript strict mode: + +```text +error TS2322: Type '() => void' is not assignable to type 'OnCloseUnlockPanelType'. + Type 'void' is not assignable to type 'Promise'. +``` + +Use `async () => { ... }` for both callbacks. `src/providers.tsx` does this +correctly. +::: + +:::warning[Pitfall 2: UnlockPanelManager.init() runs once, not per button] +Calling `.init()` a second time from another component overwrites the first +configuration (last call wins). It does not merge or stack. Configure it exactly +once, in the same wrapper that calls `initApp()`. Every button elsewhere only +ever calls `.getInstance().openUnlockPanel()`. +::: + +:::warning[Pitfall 3: don't render login buttons before init resolves] +`UnlockPanelManager.getInstance()` before `.init()` has run returns a panel with +no configured `loginHandler`. This recipe's `ready` gate in `src/providers.tsx` +prevents children, and therefore any ``, from rendering until +`initApp()` and `UnlockPanelManager.init()` have both completed. Don't remove +that gate. +::: + +:::warning[Pitfall 4: HTTPS required for most wallets] +The DeFi extension and Ledger refuse `http://localhost`. This recipe's dev server +uses `@vitejs/plugin-basic-ssl`. See +[Local HTTPS for dApp dev](/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup) +for a fully trusted mkcert alternative. +::: + +## See also + +- [Login via the DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login) + is a dedicated single-provider button using `ProviderFactory` directly, no picker. +- [Login via xPortal / WalletConnect](/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login) + is the same lower-level pattern, for the mobile-QR provider. +- [Read the connected account](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + is what to do with `useGetAccount()` once a user is connected. +- [Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) + is the base setup this recipe builds on. + +--- + +### Address utilities + +The sdk-core `Address` toolkit in one place: bech32 to hex conversion, building +an address from a raw public key, computing an address's shard, checking whether +an address is a smart contract, and the HRP (human-readable part). Fully offline: +an `Address` is a pure value type, with no network call. + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates one throwaway address and uses one fixed, + well-known contract address. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/address-utilities +npm install +npm run build +npm start +``` + +## The code + +```ts title="src/index.ts" +// src/index.ts — the sdk-core Address toolkit: bech32 <-> hex conversion, +// building an address from a raw public key, computing an address's shard, +// checking whether an address is a smart contract, and the HRP concept +// (LibraryConfig.DefaultAddressHrp). +// +// Fully offline. No devnet, no network — an Address is a pure value type. +// One address is generated fresh per run (so its exact value and shard vary); +// the ESDT system contract address is a fixed, well-known value. +// +// Grounded in the installed @multiversx/sdk-core v15.4.1: Address / +// AddressComputer (core/address.d.ts) and LibraryConfig (core/config.d.ts). + +import { Address, AddressComputer, LibraryConfig, Mnemonic } from '@multiversx/sdk-core'; + +function main(): void { + // A throwaway user address to work with. + const userAddress = Mnemonic.generate().deriveKey(0).generatePublicKey().toAddress(); + const bech32 = userAddress.toBech32(); + console.log(`User address (bech32): ${bech32}`); + + // === 1. bech32 <-> hex. === + // toHex() gives the 64-char (32-byte) public key; newFromHex parses it + // back. The round-trip must return the original bech32. + const hex = userAddress.toHex(); + const roundTripped = Address.newFromHex(hex); + console.log(`\n1. hex (${hex.length} chars): ${hex}`); + console.log(` bech32 -> hex -> bech32 round-trips: ${roundTripped.toBech32() === bech32}`); + + // === 2. From a raw public-key buffer. === + // The generic constructor accepts an Address, a bech32/hex string, or the + // raw 32 bytes. getPublicKey() returns those bytes. + const rawPublicKey = userAddress.getPublicKey(); + const fromBytes = new Address(rawPublicKey); + console.log(`2. new Address(rawPublicKeyBytes) matches: ${fromBytes.toBech32() === bech32}`); + + // === 3. Shard of an address. === + // AddressComputer defaults to 3 shards (without metachain); getShardOfAddress + // returns 0, 1, or 2 for a user address. + const computer = new AddressComputer(); + const shard = computer.getShardOfAddress(userAddress); + console.log(`3. Shard of this address: ${shard} (0, 1, or 2)`); + + // === 4. Is it a smart contract? === + // Contract addresses have a fixed all-zero prefix. The ESDT system contract + // is a known contract; a user address is not. + const esdtSystemContract = Address.newFromBech32( + 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u', + ); + console.log(`4. ESDT system contract isSmartContract(): ${esdtSystemContract.isSmartContract()}`); + console.log(` User address isSmartContract(): ${userAddress.isSmartContract()}`); + + // === 5. The HRP (human-readable part). === + // Every bech32 address starts with an HRP — "erd" on MultiversX. It is a + // global default on LibraryConfig, and each Address also carries its own. + console.log(`\n5. LibraryConfig.DefaultAddressHrp: "${LibraryConfig.DefaultAddressHrp}"`); + console.log(` This address's own HRP: "${userAddress.getHrp()}"`); + + // A per-call override builds an address under a different HRP WITHOUT + // touching global state — the safe way to handle a non-"erd" prefix. + const testHrpAddress = Address.newFromHex(hex, 'test'); + console.log(` Same key under HRP "test": ${testHrpAddress.toBech32()}`); + + // The two bech32 parse paths differ in strictness. The named constructor + // Address.newFromBech32 accepts ANY hrp (allowCustomHrp: true): + const lenient = Address.newFromBech32(testHrpAddress.toBech32()); + console.log(` Address.newFromBech32 accepts a "test" address (parsed hrp="${lenient.getHrp()}").`); + + // ...but the generic constructor new Address(bech32) validates the hrp + // against LibraryConfig.DefaultAddressHrp and rejects a mismatch. + let strictRejected = false; + try { + const parsed = new Address(testHrpAddress.toBech32()); + console.log(` new Address unexpectedly accepted hrp="${parsed.getHrp()}".`); + } catch { + strictRejected = true; + } + console.log(` new Address(...) rejects the "test" address while default is "erd": ${strictRejected}`); + + console.log('\nExpected: round-trip true, from-bytes true, a shard 0-2, isSmartContract true then false, HRP "erd", newFromBech32 lenient, and new Address(...) strict-rejects.'); +} + +main(); +``` + +## Run it + +One address is generated fresh per run, so its value and shard vary; the ESDT +system contract address is fixed: + +```text +User address (bech32): erd1ggdn9z7aunpuqrk7z3ywpngh5vcsaz0y6hslqvj92tudsahwghkqvj67m2 + +1. hex (64 chars): 421b328bdde4c3c00ede1448e0cd17a3310e89e4d5e1f0324552f8d876ee45ec + bech32 -> hex -> bech32 round-trips: true +2. new Address(rawPublicKeyBytes) matches: true +3. Shard of this address: 0 (0, 1, or 2) +4. ESDT system contract isSmartContract(): true + User address isSmartContract(): false + +5. LibraryConfig.DefaultAddressHrp: "erd" + This address's own HRP: "erd" + Same key under HRP "test": test1ggdn9z7aunpuqrk7z3ywpngh5vcsaz0y6hslqvj92tudsahwghkquaenm7 + Address.newFromBech32 accepts a "test" address (parsed hrp="test"). + new Address(...) rejects the "test" address while default is "erd": true +``` + +## How it works + +Grounded in the installed `@multiversx/sdk-core` v15.4.1 `Address` / +`AddressComputer` (`core/address.d.ts`) and `LibraryConfig` (`core/config.d.ts`): + +1. **bech32 to hex.** `toHex()` gives the 64-char (32-byte) public key; + `Address.newFromHex(hex)` parses it back. +2. **From raw bytes.** The generic constructor `new Address(bytes)` accepts the + 32-byte public key that `getPublicKey()` returns. +3. **Shard.** `new AddressComputer().getShardOfAddress(address)` returns 0, 1, or + 2 (the computer defaults to 3 shards without the metachain). +4. **Smart-contract check.** `isSmartContract()` is `true` for the ESDT system + contract and `false` for a user address, contract addresses have a fixed + all-zero prefix. +5. **HRP.** `LibraryConfig.DefaultAddressHrp` is the global default (`"erd"`); + each `Address` also carries its own via `getHrp()`. A per-call override like + `Address.newFromHex(hex, "test")` builds an address under a different HRP + without touching global state. + +## Pitfalls + +:::warning[Pitfall 1: new Address(bech32) and Address.newFromBech32(bech32) differ on HRP strictness] +The generic constructor validates the HRP against +`LibraryConfig.DefaultAddressHrp` (`allowCustomHrp: false`) and **throws** on a +mismatch, a `"test1..."` string is rejected while the default is `"erd"`. The +named `newFromBech32` uses `allowCustomHrp: true` and **accepts any HRP**. Use +`newFromBech32` to parse a non-`erd` address; use the strict constructor to +reject foreign ones. A real, verified divergence in v15.4.1. +::: + +:::note[Pitfall 2: an Address's HRP is fixed at construction] +Mutating `LibraryConfig.DefaultAddressHrp` afterward changes only *newly built* +addresses; existing objects keep the HRP they were made with. The config's own +doc comment warns never to alter it inside a library, prefer the per-call `hrp` +argument over changing the global. +::: + +:::tip[Pitfall 3: the hex form is the public key, not a transaction hash] +`toHex()` returns the 32-byte account public key (64 hex chars). Do not confuse +it with a 32-byte transaction hash, which is also 64 hex chars but a different +thing entirely. +::: + +## See also + +- [Create an Account from a KeyPair, secret key, or mnemonic](/sdk-and-tools/sdk-js/cookbook/accounts/account-from-keys) + is where the public key behind an address comes from. +- [Sign a message + verify a signature](/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-message) + is another pure-value, offline sdk-core recipe. +- `AddressComputer.computeContractAddress` is the deploy-side counterpart to + `getShardOfAddress`, covered in the smart-contract deploy recipes. + +--- + +### Agent or agentic engineer? Start here + +The cookbook is built to be read two ways: by a developer copying a recipe into +their project, and by a coding agent reading the docs programmatically. Both get +the same CI-verified recipes. Pick your lane. + +## If you are a developer + +Browse the full set on the [cookbook overview](./index.mdx), or jump straight to +a common starting point: + +- [Minimal sdk-dapp v5 app in Vite + React](./start-here/vite-react-minimal.mdx): the smallest working dApp setup. +- [Sign and send a transaction](./start-here/sign-and-send.mdx): the canonical sign, send, and track flow. +- [Send EGLD to an address](./transactions/send-egld.mdx): the backend and script transfer pattern. +- [Call a contract endpoint](./smart-contracts-call/call-contract-endpoint.mdx): invoke a contract with typed arguments. +- [Issue a fungible token](./tokens/issue-fungible-token.mdx): mint your own ESDT. + +Each recipe shows every file it needs and states the SDK versions it was verified +against. The teal badge on a page means its code compiled green in CI on the date +shown. + +## If you are a coding agent (or wiring one up) + +Point your coding agent at this documentation and let it read the recipes +directly. Because every recipe is a closed, type-checked unit, the code an agent +lifts from a page compiles as shown, against the SDK versions the page declares. + +### The machine-readable surface + +MultiversX publishes the whole documentation set in the +[`llms.txt`](https://llmstxt.org) format, so a model can load it as context: + +- [`https://docs.multiversx.com/llms.txt`](https://docs.multiversx.com/llms.txt): the index. Every page as a titled, described link; cookbook recipes carry an inline `[difficulty, verified DATE]` tag, so an agent sees each recipe's difficulty and its last green CI date without opening the page. +- [`https://docs.multiversx.com/llms-full.txt`](https://docs.multiversx.com/llms-full.txt): the same set with full page content inlined, for one-shot ingestion. + +### The wider MultiversX agent stack + +For the MCP server, the skills collection, and agent identity tooling, see +[AI agents](../../../learn/ai-agents.md). That page also links the +[`mx-ai-skills`](https://github.com/multiversx/mx-ai-skills) repository, a +structured collection of MultiversX-specific skills, roles, and documentation for +equipping agents to build, audit, and optimize on MultiversX. + +--- + +### Apply a guardian to a transaction and co-sign it + +Once an account is guarded, *every* transaction it sends needs a guardian +co-signature. This recipe takes a plain EGLD transfer and makes it a guarded +transaction with `TransactionComputer.applyGuardian`, then co-signs it with both +the sender and the guardian, verifying the version, options, and guardian fields +end up correct. + +Unlike a relayer (which must share the sender's shard, see +[Build a relayed v3 transaction](/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction)), +a guardian has no shard constraint; it is purely a co-signer. + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates two fresh, unfunded accounts and only needs + devnet **read** access before proving the shape via a clean rejection. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/apply-guardian-to-transaction +npm install +npm run build +npm start +``` + +## The code + +First, a fresh sender and guardian (two independent accounts, no shard search +needed): + +```ts title="src/keys.ts" +// src/keys.ts — generate a sender and a guardian as two fresh, independent, +// intentionally-unfunded accounts. +// +// Unlike a relayer (which must share the sender's shard — see the +// relayed-v3-transaction recipe), a guardian has NO shard constraint: it is +// purely a co-signer. So there is no same-shard search here — two independent +// mnemonics are enough, reflecting that the guardian is a separate party (a +// guardian service, a second device) from the account owner. + +import { Account, Mnemonic } from '@multiversx/sdk-core'; + +/** Generate a fresh sender and a fresh guardian (both unfunded). */ +export async function generateSenderAndGuardian(): Promise<{ + sender: Account; + guardian: Account; +}> { + const sender = await Account.newFromMnemonic(Mnemonic.generate().toString()); + const guardian = await Account.newFromMnemonic(Mnemonic.generate().toString()); + return { sender, guardian }; +} +``` + +Then apply the guardian, assert the guarded fields, and co-sign: + +```ts title="src/guarded.ts" +// src/guarded.ts — the subject of this recipe: taking any transaction and +// making it a GUARDED transaction, then co-signing it. +// +// A guarded transaction carries a second signature from the account's +// guardian. `TransactionComputer.applyGuardian(tx, guardianAddress)` does +// three things (confirmed by reading the installed v15.4.1 source): +// 1. raises `version` to at least 2 (the minimum that supports options), +// 2. sets the TX_GUARDED bit in `options`, +// 3. sets `tx.guardian` to the guardian's address. +// It does NOT touch gas — the +50,000 for guarded transactions is the +// caller's job on a hand-built transaction (the controllers add it for you). +// +// Signing ORDER matters: applyGuardian must run BEFORE either party signs, so +// both signatures cover the version/options/guardian fields. + +import { Transaction, TransactionComputer } from '@multiversx/sdk-core'; +import type { Account, Address } from '@multiversx/sdk-core'; + +// sdk-core's internal constant for the extra guarded-transaction gas (guarded +// transactions require an extra 50,000 gas). It is not re-exported from the +// package barrel, so it is restated here. +const EXTRA_GAS_LIMIT_FOR_GUARDED_TRANSACTIONS = 50_000n; + +export interface GuardedBuildResult { + transaction: Transaction; + assertions: { + versionIsAtLeast2: boolean; + guardedBitSet: boolean; + guardianMatches: boolean; + hasSenderSignature: boolean; + hasGuardianSignature: boolean; + }; +} + +/** + * Build a plain EGLD transfer, apply a guardian to it, assert the guarded + * fields, then co-sign it with both the sender and the guardian. + */ +export async function buildGuardedTransfer( + sender: Account, + guardian: Account, + receiver: Address, + chainID: string, + amount: bigint, +): Promise { + const transaction = new Transaction({ + sender: sender.address, + receiver, + gasLimit: 50_000n, // minimum for a plain EGLD transfer, before the guarded extra + chainID, + value: amount, + nonce: sender.getNonceThenIncrement(), + }); + + const computer = new TransactionComputer(); + + // Apply the guardian BEFORE signing, and add the guarded gas ourselves. + computer.applyGuardian(transaction, guardian.address); + transaction.gasLimit += EXTRA_GAS_LIMIT_FOR_GUARDED_TRANSACTIONS; + + // Co-sign: the sender signs `signature`, the guardian signs + // `guardianSignature` — both over the same (now guarded) bytes. + transaction.signature = await sender.signTransaction(transaction); + transaction.guardianSignature = await guardian.signTransaction(transaction); + + return { + transaction, + assertions: { + versionIsAtLeast2: transaction.version >= 2, + guardedBitSet: computer.hasOptionsSetForGuardedTransaction(transaction), + guardianMatches: transaction.guardian.toBech32() === guardian.address.toBech32(), + hasSenderSignature: transaction.signature.length > 0, + hasGuardianSignature: transaction.guardianSignature.length > 0, + }, + }; +} +``` + +## Run it + +A real captured run (addresses differ every run): + +```text +Guarded transaction fields: + version: 2 + options: 2 + guardian: erd16pwwy3l8du89tljqht00tg8rt3tuhqysv85x5kakpda3e0znfmkqpdas4v + gasLimit: 100000 (50000 base + 50000 guarded extra) + signature length: 64 bytes + guardianSig length: 64 bytes + +Assertions: + version >= 2: true + guarded bit set: true + guardian field matches: true + sender signature set: true + guardian signature set: true + +Broadcasting the guarded transfer... +Rejected (expected for an unfunded wallet): ... insufficient funds for address erd12kzk5e... +``` + +## How it works + +`TransactionComputer.applyGuardian(tx, guardianAddress)` does exactly three things +(confirmed by reading the installed v15.4.1 source): + +1. raises `version` to at least `2` (the minimum that supports the options field), +2. sets the `TX_GUARDED` bit in `options` (so `options` becomes `2`), +3. sets `tx.guardian` to the guardian's address. + +It does **not** touch gas, the `50,000` guarded premium is the caller's job on a +hand-built transaction (the controllers add it for you). This recipe adds it +explicitly, so the transfer's `50,000` minimum becomes `100,000`. + +**Order matters.** `applyGuardian` runs *before* either party signs, so both +signatures cover the version/options/guardian fields. The sender signs +`signature`; the guardian signs `guardianSignature`. Both are 64-byte Ed25519 +signatures over the same serialized bytes. The unfunded broadcast is rejected +with a clean `insufficient funds` keyed to the sender, confirming the guarded +shape is well-formed. + +For built-in function and guardian detail, see +[docs.multiversx.com/developers/built-in-functions](https://docs.multiversx.com/developers/built-in-functions/). + +## Pitfalls + +:::danger[Pitfall 1: apply the guardian BEFORE signing] +`applyGuardian` mutates `version`, `options`, and `guardian`. Sign first and both +signatures cover the wrong bytes, so the network rejects them. Apply, then sign. +::: + +:::warning[Pitfall 2: a guarded transaction costs an extra 50,000 gas] +`applyGuardian` does not add it. On a hand-built transaction you must add `50,000` +yourself (the controllers do it automatically). Under-budget and the transaction +is rejected for gas. +::: + +:::note[Pitfall 3: the guarded constants are not exported from the barrel] +`TRANSACTION_OPTIONS_TX_GUARDED` and `EXTRA_GAS_LIMIT_FOR_GUARDED_TRANSACTIONS` +are internal to sdk-core. Read the guarded flag through +`TransactionComputer.hasOptionsSetForGuardedTransaction`, and restate the +`50,000` gas constant locally (as this recipe does). +::: + +:::note[Pitfall 4: the account must actually be guarded on-chain] +This recipe proves the transaction *shape* with unfunded accounts. A real guarded +transfer also requires the sender's account to be guarded +([Guard an account](/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account)) +with a guardian whose key produces the `guardianSignature`. +::: + +## See also + +- [Set a guardian on an account](/sdk-and-tools/sdk-js/cookbook/accounts/set-guardian) + nominates the guardian whose signature co-signs here. +- [Guard and unguard an account](/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account) + activates guardianship so guarded transactions are actually required. +- [Build a relayed v3 transaction](/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction) + is the other multi-signature shape, where a relayer pays gas instead of a + guardian co-signing. + +--- + +### Await a transaction on a custom condition + +You broadcast a transaction. Now you need to block until it reaches a state you +care about, not just "completed", but maybe "completed **and** produced a +smart-contract result", or "reached a specific status". That is +`awaitTransactionOnCondition`. + +The provider gives you two awaiters, both on `INetworkProvider`: + +- `awaitTransactionCompleted(hash, options?)`, the common case, a fixed condition. +- `awaitTransactionOnCondition(hash, predicate, options?)`, **any** predicate you + write against the live `TransactionOnNetwork`. + +Both poll the network on an interval until the predicate holds or the timeout +elapses. You would normally call these right after sending a transaction; this +recipe points them at an already-final historical transaction so the whole thing +runs read-only. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/await-transaction-on-condition +npm install +npm run build +npm start +``` + +## The awaiters + +```ts title="src/awaitTransaction.ts" +// src/awaitTransaction.ts — the subject of this recipe: blocking until a +// transaction reaches a state YOU define, not just "completed". +// +// The provider gives you two awaiters: +// - awaitTransactionCompleted(hash, options?) — the common case +// - awaitTransactionOnCondition(hash, predicate, options?) — any predicate +// +// Both poll the network on an interval until the predicate holds or the timeout +// elapses. Both are on INetworkProvider, so an Api or a Proxy provider works. +// +// You normally call these right after broadcasting a transaction, to block your +// script until the tx reaches a state you care about. This recipe instead +// points them at an already-final historical transaction so the whole thing +// runs read-only — no wallet, no funds, no broadcast. + +import type { INetworkProvider, TransactionOnNetwork, AwaitingOptions } from '@multiversx/sdk-core'; + +/** Block until `condition(tx)` is true, using the SDK's default poll/timeout. */ +export async function awaitTransaction( + provider: INetworkProvider, + txHash: string, + condition: (tx: TransactionOnNetwork) => boolean, +): Promise { + return provider.awaitTransactionOnCondition(txHash, condition); +} + +/** + * The same, but with your own `AwaitingOptions` — poll faster/slower, or fail + * sooner. On timeout the promise REJECTS (it does not resolve with a partial + * result), so wrap it in try/catch if a timeout is expected. + */ +export async function awaitTransactionWithOptions( + provider: INetworkProvider, + txHash: string, + condition: (tx: TransactionOnNetwork) => boolean, + options: AwaitingOptions, +): Promise { + return provider.awaitTransactionOnCondition(txHash, condition, options); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — three awaits against one already-final mainnet transaction: +// 1. a plain "is it completed" condition (returns on the first poll), +// 2. a custom condition ("has smart-contract results"), +// 3. a condition that is never true, with a short timeout, to show the +// reject path. +// No wallet, no gas. +// +// Usage: +// npm run build && npm start [txHash] + +import { ApiNetworkProvider, AwaitingOptions } from '@multiversx/sdk-core'; +import { awaitTransaction, awaitTransactionWithOptions } from './awaitTransaction'; + +const MAINNET_API = 'https://api.multiversx.com'; +// A real, final, permanent mainnet transaction (a successful call with +// smart-contract results). Override with your own hash as argv[2]. +const DEFAULT_TX = 'd537da3f347191cf906602a83bcf3207b0c7d55e6b120649d224b7ee255bfbbb'; + +async function main(): Promise { + const txHash = process.argv[2] ?? DEFAULT_TX; + const provider = new ApiNetworkProvider(MAINNET_API, { clientName: 'mvx-cookbook' }); + + // 1. Default awaiter — block until the tx is completed. + const completed = await awaitTransaction(provider, txHash, (tx) => tx.status.isCompleted()); + console.log('1. awaited status.isCompleted():'); + console.log(` status ${completed.status.toString()}, nonce ${completed.nonce}, scResults ${completed.smartContractResults.length}`); + + // 2. Custom predicate — block until the tx has produced smart-contract results. + const withResults = await awaitTransaction( + provider, + txHash, + (tx) => tx.smartContractResults.length > 0, + ); + console.log('2. awaited a custom predicate (smartContractResults.length > 0):'); + console.log(` resolved with ${withResults.smartContractResults.length} smart-contract results`); + + // 3. A condition that can never be true for this successful tx, with a short + // timeout — demonstrates the reject-on-timeout path and AwaitingOptions. + const options = new AwaitingOptions(); + options.pollingIntervalInMilliseconds = 500; + options.timeoutInMilliseconds = 3000; + const startedAt = Date.now(); + try { + await awaitTransactionWithOptions(provider, txHash, (tx) => tx.status.isInvalid(), options); + console.log('3. unexpected: condition matched'); + } catch { + console.log(`3. never-true condition timed out after ~${Math.round((Date.now() - startedAt) / 1000)}s (rejected, as expected)`); + } +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start # uses a known permanent mainnet tx +npm start # await any transaction you like +``` + +Expected output: + +```text +1. awaited status.isCompleted(): + status success, nonce 20052, scResults 3 +2. awaited a custom predicate (smartContractResults.length > 0): + resolved with 3 smart-contract results +3. never-true condition timed out after ~4s (rejected, as expected) +``` + +## How it works + +**A predicate is just `(tx) => boolean`.** The SDK re-fetches the transaction on +each poll and hands your predicate the fresh `TransactionOnNetwork`. Return `true` +to resolve. `tx.status` exposes `isCompleted()`, `isSuccessful()`, `isPending()`, +`isFailed()`, `isInvalid()`, `isNotExecutableInBlock()`; and +`tx.smartContractResults`, `tx.logs`, `tx.nonce` and the rest are all fair game +for a condition. + +**`AwaitingOptions` tunes the loop.** Three fields: +`pollingIntervalInMilliseconds`, `timeoutInMilliseconds`, +`patienceInMilliseconds`. The SDK defaults (confirmed at runtime) are `600` / +`9000` / `0`. This recipe sets a 500ms poll and a 3s timeout for the demo. + +**When to reach for the custom condition over `awaitTransactionCompleted`.** Use +the plain completed-awaiter for "did my tx land". Use +`awaitTransactionOnCondition` when "done" for your app means more than the +protocol's notion of completed, e.g. a specific event was logged, or a cross-shard +smart-contract result arrived. For the browser/dApp equivalent (WebSocket-driven +status, no polling loop of your own), see +[Track a transaction](/sdk-and-tools/sdk-js/cookbook/transactions/track-transaction-status). + +## Pitfalls + +:::danger[Pitfall 1: timeout REJECTS, it does not resolve] +If your predicate never holds within `timeoutInMilliseconds`, the promise rejects, +it does not resolve with a partial/last-seen transaction. Any code path where a +timeout is realistic must wrap the await in try/catch, as step 3 of this recipe +does. +::: + +:::note[Pitfall 2: the thrown error is status-specific, not the documented one] +The `INetworkProvider` doc comment says these throw `ErrAwaitConditionNotReached`, +but the transaction awaiter actually throws +`ErrExpectedTransactionStatusNotReached`. Catch broadly (`catch (e: unknown)`) +rather than matching one class name. +::: + +:::warning[Pitfall 3: a slow poll can miss short-lived states] +The predicate only sees the transaction at each poll boundary. A transient status +that appears and disappears between two polls will be missed. For "did it ever +pass through state X" you need the transaction's logs/results after the fact, not +a live poll. +::: + +## See also + +- [Track a transaction (WebSocket + polling fallback)](/sdk-and-tools/sdk-js/cookbook/transactions/track-transaction-status) + is the sdk-dapp, browser-side way to watch a transaction, without writing your + own poll loop. +- [Await an account on a custom condition](/sdk-and-tools/sdk-js/cookbook/network-providers/await-account-on-condition) + applies the same poll-until-predicate pattern to an account instead of a + transaction. +- [Simulate and estimate a transaction](/sdk-and-tools/sdk-js/cookbook/network-providers/simulate-estimate-transaction) + checks what a transaction will do before you send and await it. + +--- + +### Await an account on a custom condition + +Sometimes you need to wait on an **account**, not a transaction: block until a +deposit lands (balance crosses a threshold), or until a batch of your own sends +has confirmed (nonce reaches a target). That is `awaitAccountOnCondition`, the +same poll-until-predicate machinery as +[Await a transaction on a condition](/sdk-and-tools/sdk-js/cookbook/network-providers/await-transaction-on-condition), +but the predicate receives an `AccountOnNetwork`. + +`awaitAccountOnCondition(address, predicate, options?)` is on `INetworkProvider`, +so an Api or Proxy provider both work. This recipe reads a live, funded mainnet +account, so every condition is checked against real state, with no wallet, no +funds, and no broadcast. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/await-account-on-condition +npm install +npm run build +npm start +``` + +## The awaiters + +```ts title="src/awaitAccount.ts" +// src/awaitAccount.ts — the subject of this recipe: blocking until an ACCOUNT +// reaches a state you define. Same poll-until-predicate shape as the +// transaction awaiter, but the predicate receives an AccountOnNetwork. +// +// Real uses: wait for a deposit to land (balance crosses a threshold), or wait +// for a sequence of your own transactions to confirm (nonce reaches a target). +// +// `awaitAccountOnCondition` is on INetworkProvider, so an Api or Proxy provider +// both work. This recipe reads a live, funded mainnet account, so every +// condition is checked against real state — no wallet, no funds, no broadcast. + +import type { + INetworkProvider, + AccountOnNetwork, + AwaitingOptions, + Address, +} from '@multiversx/sdk-core'; + +/** Block until `condition(account)` is true, using the SDK default poll/timeout. */ +export async function awaitAccount( + provider: INetworkProvider, + address: Address, + condition: (account: AccountOnNetwork) => boolean, +): Promise { + return provider.awaitAccountOnCondition(address, condition); +} + +/** + * The same, with your own `AwaitingOptions`. On timeout the promise REJECTS + * (it does not resolve with the last-seen account), so wrap it in try/catch + * when a timeout is a realistic outcome. + */ +export async function awaitAccountWithOptions( + provider: INetworkProvider, + address: Address, + condition: (account: AccountOnNetwork) => boolean, + options: AwaitingOptions, +): Promise { + return provider.awaitAccountOnCondition(address, condition, options); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — three awaits against one live, funded mainnet account: +// 1. nonce reaches its current value (monotonic, so already true), +// 2. balance is above zero (the "deposit landed" shape, already true), +// 3. nonce reaches a value far in the future, with a short timeout, to show +// the reject path. +// No wallet, no gas. +// +// Usage: +// npm run build && npm start [bech32Address] + +import { ApiNetworkProvider, Address, AwaitingOptions } from '@multiversx/sdk-core'; +import { awaitAccount, awaitAccountWithOptions } from './awaitAccount'; + +const MAINNET_API = 'https://api.multiversx.com'; +const DEFAULT_ADDRESS = 'erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th'; + +async function main(): Promise { + const addressArg = process.argv[2] ?? DEFAULT_ADDRESS; + const provider = new ApiNetworkProvider(MAINNET_API, { clientName: 'mvx-cookbook' }); + const address = Address.newFromBech32(addressArg); + + const current = await provider.getAccount(address); + console.log(`Account ${address.toBech32()}`); + console.log(` current nonce ${current.nonce}, balance ${current.balance}`); + + // 1. Nonce is monotonic, so "reach the current nonce" holds on the first poll. + // In real use this is how you wait for a batch of your sends to confirm. + const reached = await awaitAccount(provider, address, (a) => a.nonce >= current.nonce); + console.log(`1. awaited nonce >= ${current.nonce}: resolved at nonce ${reached.nonce}`); + + // 2. The "deposit landed" shape — balance above a threshold. + const funded = await awaitAccount(provider, address, (a) => a.balance > 0n); + console.log(`2. awaited balance > 0: resolved with balance ${funded.balance}`); + + // 3. A nonce far in the future the account will not reach in 3s — shows the + // reject-on-timeout path and AwaitingOptions. + const target = current.nonce + 1_000_000n; + const options = new AwaitingOptions(); + options.pollingIntervalInMilliseconds = 500; + options.timeoutInMilliseconds = 3000; + const startedAt = Date.now(); + try { + await awaitAccountWithOptions(provider, address, (a) => a.nonce >= target, options); + console.log('3. unexpected: condition matched'); + } catch { + console.log(`3. awaited nonce >= ${target} timed out after ~${Math.round((Date.now() - startedAt) / 1000)}s (rejected, as expected)`); + } +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start # a known funded mainnet account +npm start # await any account you like +``` + +Expected output (live values, so nonce and balance will differ when you run it): + +```text +Account erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th + current nonce 89, balance 1000010000000 +1. awaited nonce >= 89: resolved at nonce 89 +2. awaited balance > 0: resolved with balance 1000010000000 +3. awaited nonce >= 1000089 timed out after ~3s (rejected, as expected) +``` + +## How it works + +**A predicate is just `(account) => boolean`.** The SDK re-fetches the account on +each poll and hands your predicate the fresh `AccountOnNetwork`: `nonce` (bigint), +`balance` (bigint), `userName`, `isGuarded`, and the contract fields. Return +`true` to resolve. + +**Nonce is monotonic; balance is not.** "Reach nonce N" is a safe, one-way +condition, ideal for waiting on a sequence of your own transactions (see +[Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces)). A balance +condition captures the "deposit landed" case, but a balance can also decrease, so +write the condition for the direction you actually mean. + +**`AwaitingOptions` tunes the loop.** `pollingIntervalInMilliseconds`, +`timeoutInMilliseconds`, `patienceInMilliseconds` (defaults `600` / `9000` / `0`). +This recipe uses a 500ms poll and a 3s timeout for the demo. + +## Pitfalls + +:::danger[Pitfall 1: timeout REJECTS, it does not resolve] +If the account never satisfies the predicate within `timeoutInMilliseconds`, the +promise rejects, it does not hand you the last-seen account. Wrap the await in +try/catch wherever a timeout is realistic, as step 3 does. +::: + +:::warning[Pitfall 2: do not busy-poll a tiny interval against a public API] +A 100ms poll against `api.multiversx.com` will get you rate-limited. Keep the +interval realistic (the SDK default is 600ms), or run your own observing squad / +gateway if you truly need tight polling. +::: + +:::note[Pitfall 3: read the baseline before you await a relative condition] +This recipe calls `getAccount()` once up front to capture the current +nonce/balance, then writes conditions relative to that snapshot. If you hard-code +an absolute threshold instead, make sure it reflects the account's real starting +state or the await either returns instantly or never. +::: + +## See also + +- [Await a transaction on a custom condition](/sdk-and-tools/sdk-js/cookbook/network-providers/await-transaction-on-condition) + is the transaction-shaped sibling of this recipe. +- [Fetch an account's on-chain state](/sdk-and-tools/sdk-js/cookbook/accounts/fetch-account-state) + is the single-shot `getAccount` read the predicate here polls repeatedly. +- [Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) + shows why "wait until nonce reaches N" is a natural way to sequence your own + sends. + +--- + ### Basics ## Code Arrangement @@ -2767,6 +4015,332 @@ Share this guide if you found it useful. --- +### Build a relayed v3 transaction + +A relayed transaction lets a **relayer** pay the gas fee for a transaction a +**sender** builds and authorizes, the sender never needs any EGLD at all. V3 is +the current, supported iteration; V1 and V2 are being deactivated and this recipe +does not show them. + +Use this for any flow where you want users to interact on-chain without holding +EGLD for gas: a dApp's backend sponsoring its users' first transactions, an +onboarding flow, or a service that pays fees on behalf of its callers. If both +parties are the same entity, you don't need relaying, just +[send normally](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld). + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates two fresh, unfunded accounts and only needs + devnet **read** access before proving the shape via a clean rejection. No devnet + EGLD required. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/relayed-v3-transaction +npm install +npm run build +npm start +``` + +## The code + +First, find a sender and relayer that share a shard, entirely offline: + +```ts title="src/keys.ts" +// src/keys.ts — generate a sender and a relayer that share a shard. +// +// The mx-sdk-js-core cookbook (cookbook/relayed.ts) states the hard +// constraint: "the sender and the relayer must be in the same network +// shard." AddressComputer computes an address's shard purely locally (no +// network call — `computer.getShardOfAddress(addr)`), so this search runs +// entirely offline before this recipe ever touches devnet. +// +// With 3 non-meta shards, trying a handful of derivation indices from two +// independent mnemonics is guaranteed to find a same-shard pair quickly +// (pigeonhole: among any 4 addresses, at least two share one of 3 shards) — +// this recipe searches up to 8 indices per side, far more than needed in +// practice. + +import { Account, AddressComputer, Mnemonic } from '@multiversx/sdk-core'; + +const MAX_INDICES_TO_TRY = 8; + +/** + * Derives up to `MAX_INDICES_TO_TRY` accounts from a mnemonic, paired + * with their shard number. + */ +async function candidatesFromMnemonic( + mnemonic: Mnemonic, + addressComputer: AddressComputer, +): Promise> { + const candidates: Array<{ account: Account; shard: number }> = []; + for (let index = 0; index < MAX_INDICES_TO_TRY; index += 1) { + const account = await Account.newFromMnemonic(mnemonic.toString(), index); + const shard = addressComputer.getShardOfAddress(account.address); + candidates.push({ account, shard }); + } + return candidates; +} + +/** + * Generates two fresh, independent, intentionally-unfunded accounts — a + * sender and a relayer — guaranteed to be in the same shard. Two separate + * mnemonics are used (not two indices of one mnemonic) because a relayer + * is realistically a separate party (a dApp's backend, a sponsor + * service), not another account of the sender's own wallet. + */ +export async function generateSameShardPair(): Promise<{ + sender: Account; + relayer: Account; + shard: number; +}> { + const addressComputer = new AddressComputer(); + + const senderCandidates = await candidatesFromMnemonic( + Mnemonic.generate(), + addressComputer, + ); + const relayerCandidates = await candidatesFromMnemonic( + Mnemonic.generate(), + addressComputer, + ); + + for (const senderCandidate of senderCandidates) { + const match = relayerCandidates.find( + (relayerCandidate) => relayerCandidate.shard === senderCandidate.shard, + ); + if (match) { + return { + sender: senderCandidate.account, + relayer: match.account, + shard: senderCandidate.shard, + }; + } + } + + // Statistically shouldn't happen with 8x8=64 pairs across 3 shards, but + // fail loudly rather than silently returning a mismatched pair. + throw new Error( + `No same-shard pair found in ${MAX_INDICES_TO_TRY} tries per side — re-run.`, + ); +} +``` + +Then build, sign (both parties), and broadcast: + +```ts title="src/index.ts" +// src/index.ts — build, sign, and broadcast a relayed v3 transaction. +// +// V1 and V2 relayed transactions existed in earlier protocol versions and +// are being deactivated — the mx-sdk-js-core cookbook (cookbook/relayed.ts) +// says so explicitly: "We are currently on the third iteration (V3) of +// relayed transactions. V1 and V2 will be deactivated soon, so we'll focus +// on V3." This recipe only shows V3 — there is no reason to write new code +// against V1/V2 at this point, and the current cookbook does not document +// their shapes in enough detail to reproduce responsibly. +// +// The four rules for V3, restated from both sources: +// 1. Sender and relayer can sign in any order. +// 2. The `relayer` field must be set on the transaction BEFORE either +// of them signs. +// 3. Relayed transactions need an extra 50,000 gas on top of the usual +// minimum + per-byte cost. +// 4. Sender and relayer must be in the same network shard. +// +// This recipe uses two freshly generated, intentionally UNFUNDED +// accounts (see src/keys.ts) for both roles — like every devnet-touching +// recipe in this Cookbook that doesn't need a funded wallet to prove its +// point, a clean "insufficient funds" rejection from the real network is +// the strongest available evidence that the whole shape (two signatures, +// the relayer field, the gas math) was well-formed enough to reach +// evaluation, without needing real devnet EGLD. + +import { Address, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; +import { generateSameShardPair } from './keys'; + +// Default gas limits: minimum gas limit 50,000, gas per data byte 1,500. +// Relayed transactions need an extra 50,000 gas. This transaction's data is +// the 5-byte string "hello" (the same payload mx-sdk-js-core's own +// relayed.ts example uses), so: +const DATA = 'hello'; +const MIN_GAS_LIMIT = 50_000n; +const GAS_PER_DATA_BYTE = 1_500n; +const RELAYED_V3_EXTRA_GAS = 50_000n; +const GAS_LIMIT = + MIN_GAS_LIMIT + GAS_PER_DATA_BYTE * BigInt(DATA.length) + RELAYED_V3_EXTRA_GAS; + +// A fixed, well-known receiver — the same address this Cookbook's +// manage-nonces recipe uses as a generic example destination. It doesn't +// need to be in the same shard as the sender/relayer — only sender and +// relayer share that constraint. +const RECEIVER = 'erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th'; + +async function main(): Promise { + const { sender, relayer, shard } = await generateSameShardPair(); + console.log(`Sender: ${sender.address.toBech32()} (shard ${shard})`); + console.log(`Relayer: ${relayer.address.toBech32()} (shard ${shard})`); + console.log(`Receiver: ${RECEIVER}`); + + const entrypoint = new DevnetEntrypoint(); + + // Fetch the sender's real nonce — the one genuine network call before + // signing. The relayer does not need its own nonce fetched: only the + // transaction's own sender/nonce pair matters for ordering; the + // relayer is only ever a co-signer, never advances its own nonce via + // this transaction. + sender.nonce = await entrypoint.recallAccountNonce(sender.address); + console.log(`Sender nonce from network: ${sender.nonce}`); + + const transaction = new Transaction({ + chainID: 'D', + sender: sender.address, + receiver: new Address(RECEIVER), + gasLimit: GAS_LIMIT, + data: new Uint8Array(Buffer.from(DATA)), + nonce: sender.getNonceThenIncrement(), + }); + + // Rule 2: set `relayer` BEFORE either party signs. + transaction.relayer = relayer.address; + + // Rule 1: order between these two doesn't matter; sender-then-relayer + // here purely for readability. + transaction.signature = await sender.signTransaction(transaction); + transaction.relayerSignature = await relayer.signTransaction(transaction); + + console.log(`\nGas limit: ${GAS_LIMIT} (${MIN_GAS_LIMIT} min + ${GAS_PER_DATA_BYTE}×${DATA.length} data bytes + ${RELAYED_V3_EXTRA_GAS} relayed v3 extra)`); + console.log(`Sender signature (hex): ${Buffer.from(transaction.signature).toString('hex').slice(0, 24)}…`); + console.log(`Relayer signature (hex): ${Buffer.from(transaction.relayerSignature).toString('hex').slice(0, 24)}…`); + + try { + const txHash = await entrypoint.sendTransaction(transaction); + console.log(`\nBroadcast succeeded unexpectedly. Hash: ${txHash}`); + console.log(`Explorer: https://devnet-explorer.multiversx.com/transactions/${txHash}`); + } catch (err) { + console.log('\nBroadcast rejected, as expected for two unfunded accounts:'); + console.log(err instanceof Error ? err.message : err); + } +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +A real, captured run (addresses, shard, and signatures differ every run): + +```text +Sender: erd1k3v5vllugtnexmtpwqk57352gw6qcrzf0swnya8q4ssmmftf5xmshasf5f (shard 1) +Relayer: erd1s6l6jx6lfa67lnl925nwe8zn4ksqtywmlax5e8gj388nq3tqum5sswla9a (shard 1) +Receiver: erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th + +Sender nonce from network: 0 + +Gas limit: 107500 (50000 min + 1500×5 data bytes + 50000 relayed v3 extra) +Sender signature (hex): 2afc2adc826cfcaae78687b6… +Relayer signature (hex): 9e14bcda390b6cbf8ebbb130… + +Broadcast rejected, as expected for two unfunded accounts: +Request error on url [transactions]: [transaction generation failed: insufficient funds for address erd1s6l6jx6lfa67lnl925nwe8zn4ksqtywmlax5e8gj388nq3tqum5sswla9a] +``` + +:::tip[The rejection names the relayer, not the sender] +That's the network confirming the entire point of a relayed transaction: the +*relayer* is who needs funds for gas. If the gas math, signature order, or +`relayer` field were wrong, the network would reject with a generic +malformed-transaction or bad-signature error instead of this specific, +address-targeted message. +::: + +## How it works + +`src/keys.ts` generates two fresh, **independent** mnemonics (a sender and a +relayer are realistically separate parties, not two accounts of the same wallet) +and derives a handful of indices from each until it finds a pair sharing a shard. +Shard computation is a pure local calculation, so this whole search runs offline +before the recipe ever touches devnet. + +`src/index.ts` then, following the mx-sdk-js-core cookbook (`cookbook/relayed.ts`): + +1. Fetches only the **sender's** nonce from devnet. +2. Builds the transaction with `gasLimit` computed precisely: `50,000` minimum + + `1,500` per data byte + a flat `50,000` extra for relayed v3, `107,500` total, + matching the logged value exactly. +3. Sets `transaction.relayer` **before** either party signs, required ordering. +4. Signs with the sender (`transaction.signature`), then the relayer + (`transaction.relayerSignature`). The two can sign in either order. +5. Broadcasts via `entrypoint.sendTransaction(transaction)`. + +## V1/V2 deprecation + +`cookbook/relayed.ts` states the current guidance directly: "We are currently on +the third iteration (V3) of relayed transactions. V1 and V2 will be deactivated +soon, so we'll focus on V3." This recipe only shows V3, there is no reason to +write new code against V1/V2 at this point, and the current SDK cookbook does not +document their shapes in enough detail to reproduce them responsibly. + +## Creating relayed transactions via Controllers and Factories + +The manual `new Transaction({...})` shape above is the most explicit, but every +Controller in sdk-core also accepts a `relayer` argument directly: + +```ts +const transaction = await controller.createTransactionForIssuingFungible( + alice, alice.getNonceThenIncrement(), + { tokenName: "NEWFNG", tokenTicker: "FNG", /* ... */, relayer: frank.address }, +); +transaction.relayerSignature = await frank.signTransaction(transaction); +``` + +Factories, by contrast, have **no** `relayer` parameter at creation time, set +`transaction.relayer` after the factory builds the transaction, then sign both +parties. + +## Pitfalls + +:::danger[Pitfall 1: sender and relayer must share a shard] +Or the transaction is rejected regardless of funds. This recipe searches for a +matching pair entirely offline before ever calling devnet. +::: + +:::warning[Pitfall 2: set relayer before either signature, not after] +Signing before the relayer field is set produces a signature over the wrong +bytes. +::: + +:::note[Pitfall 3: the relayer pays gas, not the sender] +Confirmed by this recipe's own devnet rejection naming the relayer's address +specifically. Don't assume "insufficient funds" always points at the sender in a +relayed flow. +::: + +:::warning[Pitfall 4: relayed transactions cost an extra flat 50,000 gas] +On top of the usual minimum + per-data-byte cost, easy to under-budget if you +compute gas the same way you would for a non-relayed send. +::: + +:::note[Pitfall 5: Controllers accept relayer at construction time; Factories do not] +Set it on the built transaction afterward for the Factory path. +::: + +## See also + +- [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld) + is the non-relayed case, where sender and payer are the same account. +- [Manage nonces (fetch-then-increment)](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) + is the same nonce-fetch pattern this recipe uses for the sender only. +- [Sign a message + verify a signature](/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-message) + is another accounts-and-signing recipe, fully offline instead of + devnet-verified. + +--- + ### Build Reference ## How to: Basic build @@ -3125,6 +4699,397 @@ The source code be found here: [mx-sdk-erdcpp](https://github.com/multiversx/mx- --- +### Call a contract endpoint with native JS args (NativeSerializer) + +Call a smart contract's mutable endpoint, passing a **plain JS value** as the +argument, with no manual `TypedValue` construction. The ABI (see +[Load an ABI](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi)) is +what lets sdk-core's `NativeSerializer` do this conversion automatically. + +Target: the real, currently-deployed devnet **adder** contract, +`erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug`, the same +contract `mx-sdk-js-core`'s own cookbook uses as its running example, and the one +[Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) +reads from. + +**When NOT to use this recipe:** for a browser dApp where the *end user's own +wallet* signs, see +[Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) +and `mx-template-dapp`'s `PingPongAbi` widget (same ABI plus Factory pattern, +driven from a connected wallet). This recipe is for when **your own code holds +the private key**. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites for how to generate a throwaway one. **No devnet EGLD required**, + see "How it works" below. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/call-contract-endpoint +npm install +npm run build +npm start -- ./wallet.pem 7 +``` + +## Calling the endpoint + +```ts title="src/callAdd.ts" +// src/callAdd.ts — calling a mutable endpoint with a plain JS value as the +// argument, letting sdk-core's NativeSerializer convert it to the ABI's +// declared type (`number` / `bigint` / `BigNumber` -> numerical types, +// including BigUint). +// +// Target: the real, currently-deployed devnet **adder** contract +// (erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug) — the same +// contract mx-sdk-js-core's own cookbook uses as its running example for +// "Calling a smart contract using the controller." +// +// One spelling detail worth being explicit about: the options shape for +// `createTransactionForExecute` is `function` / `arguments`, NOT `functionName` +// / `args`. The installed SDK's `ContractExecuteInput` type +// (out/smartContracts/resources.d.ts) is: +// +// export declare type ContractExecuteInput = { +// contract: Address; +// gasLimit?: bigint; +// function: string; // NOT functionName +// arguments?: any[]; // NOT args +// nativeTransferAmount?: bigint; +// tokenTransfers?: TokenTransfer[]; +// }; +// +// This is the same shape used by both `SmartContractController` and +// `SmartContractTransactionsFactory`, and matches the real mx-sdk-js-core +// cookbook and mx-template-dapp's shipped `PingPongAbi` widget. The +// `function` / `arguments` spelling is used throughout this recipe. + +import { Address } from '@multiversx/sdk-core'; +import type { Abi, Account, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export const ADDER_CONTRACT_ADDRESS = + 'erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug'; + +export interface CallAddOutput { + txHash: string; +} + +/** + * Calls the adder contract's `add(value: BigUint)` endpoint, passing a plain + * JS `number` for `value`. The ABI (loaded by the caller) is what lets + * `NativeSerializer` convert that `number` into a `BigUint` typed value + * automatically. Without an ABI, you would have to pass a `BigUIntValue` (or + * similar `TypedValue`) instead. + */ +export async function callAdd( + entrypoint: DevnetEntrypoint, + abi: Abi, + sender: Account, + amountToAdd: number, +): Promise { + const controller = entrypoint.createSmartContractController(abi); + + const transaction = await controller.createTransactionForExecute(sender, sender.getNonceThenIncrement(), { + contract: Address.newFromBech32(ADDER_CONTRACT_ADDRESS), + function: 'add', + arguments: [amountToAdd], + gasLimit: 5_000_000n, + }); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash }; +} +``` + +## Run it + +```bash +npm start -- [amountToAdd] +``` + +Expected output: + +```text +Sender: erd1... (nonce 0) +Contract: erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug (adder, live devnet) +Calling: add(7) <- plain JS number, converted to BigUint by NativeSerializer + +Sent. Transaction hash: <64-char hex hash> +``` + +(An unfunded wallet gets a clean "insufficient funds" rejection here instead of +a hash, see "How it works.") + +## How it works + +**A plain JS `number` becomes a `BigUint` typed value automatically.** Because +the `SmartContractController` was constructed with the adder ABI, +`NativeSerializer` looks up `add`'s declared input type (`BigUint`) and converts +the plain number `7` into the right typed value before encoding it. Verified +directly: sending this transaction logged a `data` field of base64 `YWRkQDA3`, +which decodes byte-for-byte to `add@07`, the endpoint name, the `@` argument +separator, and `07`, the hex encoding of `7`. + +**The options shape is `function` / `arguments`.** Reading the installed +`out/smartContracts/resources.d.ts` shows the real type: + +```ts +export declare type ContractExecuteInput = { + contract: Address; + gasLimit?: bigint; + function: string; // NOT functionName + arguments?: any[]; // NOT args + nativeTransferAmount?: bigint; + tokenTransfers?: TokenTransfer[]; +}; +``` + +It is identical on both `SmartContractController` and +`SmartContractTransactionsFactory`, and matches the real `mx-sdk-js-core` +cookbook and `mx-template-dapp`'s shipped `PingPongAbi` widget. Older snippets +that show `functionName` / `args` will not compile against the installed SDK. + +**Why an unfunded wallet is enough to verify this end to end.** Sending this +transaction from a freshly generated, intentionally unfunded devnet wallet was +rejected with a clean, specific `insufficient funds` error, never a +malformed-request or bad-signature error. That is the strongest proof available, +without a funded wallet, that the whole pipeline (ABI loading, NativeSerializer +conversion, nonce handling, signing, serialization) produced a well-formed, +correctly-signed transaction. + +## Pitfalls + +:::danger[Pitfall 1: functionName/args will not compile] +Use `function` / `arguments` instead, see "How it works" above. This is a real +difference between older illustrative snippets and the installed SDK, not a style +preference. +::: + +:::warning[Pitfall 2: without an ABI, a plain-number argument would not work] +`NativeSerializer` only knows how to convert a plain JS value because the ABI +told it what type to convert it *to*. Calling the same endpoint without an ABI +requires constructing a `BigUIntValue(7)` (or similar `TypedValue`) yourself. +::: + +:::note[Pitfall 3: this recipe deliberately never succeeds against a real balance] +It proves the request is well-formed via a clean "insufficient funds" rejection, +not a confirmed on-chain state change. Fund the wallet first (real devnet EGLD) +and the same code actually increments the adder's stored sum; nothing in +`callAdd.ts` changes for that case. +::: + +## See also + +- [Load an ABI](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi) + is the ABI-loading step this recipe builds on. +- [Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) + reads back the adder's stored sum, no wallet needed. +- [Call a payable endpoint with EGLD](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-payable-endpoint) + is the same ABI-driven pattern, attaching a payment. + +--- + +### Call a payable endpoint with EGLD + +Call a **payable** smart contract endpoint, attaching EGLD via +`nativeTransferAmount` (the same option works for plain EGLD, no token +identifier needed). + +Target: the real, currently-deployed devnet **ping-pong** contract, +`erd1qqqqqqqqqqqqqpgqm6ad6xrsjvxlcdcffqe8w58trpec09ug9l5qde96pq`, the exact +address `mx-template-dapp`'s own `src/config/config.devnet.ts` ships as its +default `contractAddress`. Its own ABI docs: "A contract that allows anyone to +send a fixed sum, locks it for a while and then allows users to take it back... +Only the set amount can be `ping`-ed, no more, no less." This recipe queries that +fixed amount first (`getPingAmount()`, the same pattern as +[Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view)) +rather than hardcoding it. + +**When NOT to use this recipe:** for a browser dApp where the *end user's own +wallet* signs, see +[Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) +and `mx-template-dapp`'s `PingPongAbi` widget (same ABI plus Factory plus +`nativeTransferAmount` pattern, driven from a connected wallet). This recipe is +for when **your own code holds the private key**. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites for how to generate a throwaway one. **No devnet EGLD required**, + see "How it works" below. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/call-payable-endpoint +npm install +npm run build +npm start -- ./wallet.pem +``` + +## Calling the payable endpoint + +```ts title="src/callPing.ts" +// src/callPing.ts — calling a PAYABLE endpoint, attaching EGLD via +// `nativeTransferAmount` (the same option works for plain EGLD, no token +// identifier needed). +// +// Target: the real, currently-deployed devnet **ping-pong** contract — +// erd1qqqqqqqqqqqqqpgqm6ad6xrsjvxlcdcffqe8w58trpec09ug9l5qde96pq — the exact +// contract address mx-template-dapp's own config.devnet.ts ships as its default +// contractAddress, and the same contract mx-template-dapp's PingPongAbi widget +// calls. Its ABI docs: "A contract that allows anyone to send a fixed sum, +// locks it for a while and then allows users to take it back... Only the set +// amount can be `ping`-ed, no more, no less." That fixed amount is itself a +// read-only view (`getPingAmount`) — this recipe queries it first, then uses +// the result as the payment. + +import { Address } from '@multiversx/sdk-core'; +import type { Abi, Account, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export const PING_PONG_CONTRACT_ADDRESS = + 'erd1qqqqqqqqqqqqqpgqm6ad6xrsjvxlcdcffqe8w58trpec09ug9l5qde96pq'; + +/** + * Reads the exact EGLD amount this ping-pong instance requires for `ping()` — a + * read-only view, no wallet needed (same pattern as the "Query a read-only + * view" recipe). This deployed instance returns exactly 1000000000000000000 + * (1 EGLD). + */ +export async function queryPingAmount(entrypoint: DevnetEntrypoint, abi: Abi): Promise { + const controller = entrypoint.createSmartContractController(abi); + const [pingAmount] = await controller.query({ + contract: Address.newFromBech32(PING_PONG_CONTRACT_ADDRESS), + function: 'getPingAmount', + arguments: [], + }); + return BigInt((pingAmount as { toString(base: number): string }).toString(10)); +} + +export interface CallPingOutput { + txHash: string; +} + +/** + * Calls `ping()`, attaching exactly `pingAmountInSmallestDenomination` of EGLD + * via `nativeTransferAmount` — the same option `createTransactionForExecute` + * uses for ANY native EGLD payment, token payments, or both together. `ping` + * itself declares zero ABI inputs (`arguments: []`); the payment is not an + * argument, it is the transaction's attached value. + */ +export async function callPing( + entrypoint: DevnetEntrypoint, + abi: Abi, + sender: Account, + pingAmountInSmallestDenomination: bigint, +): Promise { + const controller = entrypoint.createSmartContractController(abi); + + const transaction = await controller.createTransactionForExecute(sender, sender.getNonceThenIncrement(), { + contract: Address.newFromBech32(PING_PONG_CONTRACT_ADDRESS), + function: 'ping', + arguments: [], + gasLimit: 6_000_000n, + nativeTransferAmount: pingAmountInSmallestDenomination, + }); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash }; +} +``` + +## Run it + +```bash +npm start -- +``` + +Expected output: + +```text +Contract: erd1qqqqqqqqqqqqqpgqm6ad6xrsjvxlcdcffqe8w58trpec09ug9l5qde96pq (ping-pong, live devnet) +Required ping amount (from getPingAmount()): 1000000000000000000 (smallest denomination) +Sender: erd1... (nonce 0) +Calling: ping() <- payable, attaching 1000000000000000000 EGLD via nativeTransferAmount + +Sent. Transaction hash: <64-char hex hash> +``` + +(An unfunded wallet gets a clean "insufficient funds" rejection here instead of +a hash, see "How it works.") + +## How it works + +**`nativeTransferAmount` attaches EGLD to a call without it being an ABI +argument.** `ping()`'s ABI declares zero inputs; the payment is not a function +argument, it is the transaction's attached value, same as sending EGLD to any +address, except this transaction also carries `data: "ping"` so the contract's +VM execution knows which endpoint to run. Verified directly: sending this call +logged `"value":"1000000000000000000","data":"cGluZw=="`, where +`1000000000000000000` is exactly 1 EGLD and `cGluZw==` decodes to the literal +string `"ping"` (no `@`-separated arguments, since none are declared). + +**The required amount is discovered, not hardcoded.** `queryPingAmount()` calls +`getPingAmount()`, a read-only view, no wallet needed, before `callPing()` builds +a transaction. This deployed instance requires exactly `1000000000000000000` +(1 EGLD). A different ping-pong deployment could require a different fixed +amount, set once at deploy time; querying it is what makes this recipe correct +for whichever instance you point it at. + +**Why an unfunded wallet is enough to verify this end to end.** Sending this +transaction from a freshly generated, intentionally unfunded devnet wallet was +rejected with a clean, specific `insufficient funds` error, never a +malformed-request or bad-signature error. That is proof the whole pipeline +(querying the live required amount, building the payable call, attaching +`nativeTransferAmount`, signing) produced a well-formed transaction, without +needing to fund the wallet with 1+ EGLD. + +The crowdfunding tutorial contract in `mx-sdk-rs` +(`contracts/examples/crowdfunding`) has a `fund()` endpoint of the same shape, +payable, accepts EGLD, no arguments, if you want the same pattern in a Rust +contract you deploy yourself. + +## Pitfalls + +:::danger[Pitfall 1: sending the wrong amount is an on-chain-enforced failure for this contract] +ping-pong's own logic accepts only its exact configured `ping_amount`, "no more, +no less." Always query the real value (`getPingAmount()`) instead of assuming a +round number; this recipe's default is 1 EGLD only because that is what this +particular deployed instance happens to require. +::: + +:::warning[Pitfall 2: nativeTransferAmount is separate from arguments] +It is easy to assume payment has to be encoded as an ABI argument somehow. It +does not. A payable endpoint with real arguments would use both `arguments` +(for the declared inputs) and `nativeTransferAmount` (for the payment) together. +::: + +:::note[Pitfall 3: this recipe deliberately never succeeds against a real balance] +It proves the request is well-formed via a clean "insufficient funds" rejection, +not a confirmed on-chain state change. Funding the wallet with at least 1 EGLD +(plus gas) lets the same code actually `ping` the contract for real; nothing in +`callPing.ts` changes for that case. +::: + +## See also + +- [Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) + is the `getPingAmount()` query pattern this recipe depends on. +- [Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint) + is the non-payable counterpart, calling adder's `add(value)` instead. +- [Load an ABI](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi) + is the ABI-loading step both of the above build on. + +--- + ### Chain simulator ## Overview @@ -3674,6 +5639,232 @@ By following these steps, you've mastered the basics of smart contract developme --- +### 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.13.1. +- For the default `parse` and `payload` demos: 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 + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/claim-and-redelegate-rewards +npm install +npm run build +``` + +## Claiming and re-delegating + +```ts title="src/rewards.ts" +// 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + const transactionOnNetwork = await entrypoint.getTransaction(txHash); + const parser = new DelegationTransactionsOutcomeParser(); + return parser.parseRedelegateRewards(transactionOnNetwork)[0]?.amount ?? 0n; +} +``` + +## Run it + +```bash +# 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`: + +```text +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 + +:::warning[Pitfall 1: parseClaimRewards often returns 0] +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. +::: + +:::note[Pitfall 2: re-delegating may be gated by the contract's cap check] +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. +::: + +:::note[Pitfall 3: claiming with zero pending rewards is a successful no-op] +`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](/sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake) + is the stake that earns these rewards. +- [Undelegate and withdraw](/sdk-and-tools/sdk-js/cookbook/delegation/undelegate-and-withdraw) + is the exit path, when you want the principal back too. +- [Read a delegation contract's state](/sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract) + reads your claimable rewards before claiming. + +--- + ### CLI ## Introduction @@ -4219,6 +6410,139 @@ An `Option` represents an optional value: every Option is either `Some` and cont --- +### Compute a contract address before deploy + +A smart contract's address is a deterministic function of two things: the +deployer's address and the nonce of the deploy transaction. You can compute it +**before** you broadcast the deploy, with no network call, no wallet, and no gas. +`AddressComputer.computeContractAddress` reproduces the exact rule the protocol +uses to assign the address on deploy. + +This is useful when you want to log or store the upcoming address, wire it into a +follow-up transaction in the same batch, or assert after the fact that the +address the network reports matches what you predicted (the +[deploy recipe](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/deploy-contract) +does exactly that). + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe is pure computation and runs fully offline. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/compute-contract-address +npm install +npm run build +npm start +``` + +## Computing + +```ts title="src/computeAddress.ts" +// src/computeAddress.ts - deriving a smart contract's address BEFORE you deploy +// it. A contract's address is a pure function of (deployer address, deployment +// nonce) - no network, no wallet, no signing, no gas. +// `AddressComputer.computeContractAddress` reproduces the exact rule the +// protocol uses when it assigns the address on deploy. +// +// Why you'd want this: you can log/store/print the upcoming contract address, +// wire it into a follow-up transaction in the same batch, or (as the sibling +// deploy recipe does) assert the address the network reports after deploy +// matches what you predicted. +// +// CRITICAL: the address depends on the EXACT nonce the deploy transaction is +// broadcast with. Compute it with the same nonce you will actually use, not a +// stale or placeholder value. See the Pitfalls in the recipe page. + +import { Address, AddressComputer } from '@multiversx/sdk-core'; + +/** + * Computes the (upcoming) address of a smart contract that `deployer` would + * create with a deploy transaction sent at `deploymentNonce`. + * + * `deploymentNonce` is a `bigint` because account nonces are `bigint` + * throughout sdk-core. + */ +export function predictContractAddress(deployer: Address, deploymentNonce: bigint): Address { + const computer = new AddressComputer(); + return computer.computeContractAddress(deployer, deploymentNonce); +} + +/** + * Returns the shard a given address lives in (0, 1, 2, or the metachain). A + * contract is created in the same shard as its deployer, so the predicted + * contract address and the deployer share a shard - this recipe prints both to + * show that. + */ +export function shardOf(address: Address): number { + const computer = new AddressComputer(); + return computer.getShardOfAddress(address); +} +``` + +## Run it + +```bash +npm start [deployerBech32] [nonce] +``` + +Expected output (default deployer, shard 1): + +```text +Deployer: erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th (shard 1) + nonce 0 -> erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3 (shard 1, isSmartContract=true) + nonce 1 -> erd1qqqqqqqqqqqqqpgq2j4t5v0lu0cvrwapl9z5zr88zfcepvjsd8ssc6sfq6 (shard 1, isSmartContract=true) + nonce 42 -> erd1qqqqqqqqqqqqqpgq3ytm9m8dpeud35v3us20vsafp77smqghd8ss4jtm0q (shard 1, isSmartContract=true) +``` + +## How it works + +**The address is `(deployer, deploymentNonce)`, nothing else.** +`computeContractAddress` returns the `Address` the network will assign to a +contract that `deployer` creates with that exact nonce. Because it is +deterministic, you can predict it, store it, or reference it before the deploy is +even signed. Every result is a valid smart contract address, so +`Address.isSmartContract()` returns `true`. + +**A contract lives in its deployer's shard.** `getShardOfAddress` on the +predicted address returns the same shard as the deployer, which this recipe +prints for both. That is why every address above is in shard 1: the deployer is +in shard 1. + +## Pitfalls + +:::warning[Pitfall 1: the nonce must be the one you actually deploy with] +The address changes with every nonce. If you predict at nonce 7 but the deploy +transaction lands at nonce 8 (because another transaction from the same account +went first), the real contract address will not match. A transaction built by +`SmartContractTransactionsFactory` has `nonce = 0n` until you set it, so predict +**after** you assign the intended nonce, not before. +::: + +:::note[Pitfall 2: deploymentNonce is a bigint] +Pass `7n`, not `7`. Account nonces are `bigint` throughout sdk-core. +::: + +:::note[Pitfall 3: AddressComputer defaults to 3 shards] +The constructor takes an optional `numberOfShardsWithoutMeta` (default `3`), +which matches mainnet, devnet, and testnet. Only change it for a custom network +with a different shard count. +::: + +## See also + +- [Deploy a smart contract](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/deploy-contract) + uses this prediction and confirms it against the deployed address. +- [Upgrade a smart contract](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/upgrade-contract) + is the other half of the contract lifecycle. +- [Load an ABI](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi) + is the ABI you pass to the deploy factory or controller. + +--- + ### Concept ## What is MANDOS? @@ -4852,6 +7176,245 @@ fn world() -> ScenarioWorld { --- +### Configure a network provider (Api vs Proxy) + +Every read-side call in sdk-core (fetching an account, a block, a token +balance, network config) goes through an `INetworkProvider`. Before you can read +anything, you need one. There are two implementations, and this recipe builds +both. + +- **`ApiNetworkProvider`** talks to the MultiversX HTTP API (for example + `api.multiversx.com`), backed by Elasticsearch. It has the richest, indexed + endpoints: token lists per account, transaction history, token metadata. This + is the default choice for most apps and backends. +- **`ProxyNetworkProvider`** talks to a gateway / observing-squad proxy (for + example `gateway.multiversx.com`). It sits closer to the protocol and exposes + a few endpoints the API does not, most notably block-by-nonce. + +Both implementations share one `INetworkProvider` interface, so your code should +depend on `INetworkProvider`, not the concrete class. You can swap one for the +other without touching a call site. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/configure-network-provider +npm install +npm run build +npm start +``` + +## Constructing the providers + +```ts title="src/providers.ts" +// src/providers.ts — the subject of this recipe: how to construct a network +// provider. Everything read-side in sdk-core goes through the same +// `INetworkProvider` interface, and there are two implementations: +// +// - ApiNetworkProvider → the MultiversX HTTP API (e.g. api.multiversx.com), +// backed by Elasticsearch. Richer, indexed endpoints: token lists per +// account, transaction history, token metadata. +// - ProxyNetworkProvider → a gateway / observing-squad proxy (e.g. +// gateway.multiversx.com). Closer to the protocol; exposes a few +// endpoints the API does not (notably block-by-nonce). +// +// Both implement `INetworkProvider`, so application code should depend on the +// interface, not the concrete class: you can swap Api for Proxy without +// touching a call site. + +import { + ApiNetworkProvider, + ProxyNetworkProvider, + DevnetEntrypoint, +} from '@multiversx/sdk-core'; +import type { INetworkProvider, NetworkProviderConfig } from '@multiversx/sdk-core'; + +// A `clientName` is recommended on every provider — it identifies your app in +// the network's request metrics. Omit it and the SDK logs a recommendation to +// the console on every construction. +const CLIENT_NAME = 'mvx-cookbook'; + +/** The API provider — the default choice for most apps and backends. */ +export function createApiProvider(url: string): INetworkProvider { + const config: NetworkProviderConfig = { clientName: CLIENT_NAME }; + return new ApiNetworkProvider(url, config); +} + +/** The Proxy provider — same interface, different backend. */ +export function createProxyProvider(url: string): INetworkProvider { + const config: NetworkProviderConfig = { clientName: CLIENT_NAME }; + return new ProxyNetworkProvider(url, config); +} + +/** + * `NetworkProviderConfig` extends Axios's `AxiosRequestConfig`, so you set the + * request `timeout` (milliseconds), custom `headers`, proxy agents, etc. in the + * same object as `clientName`. + */ +export function createApiProviderWithConfig(url: string): INetworkProvider { + const config: NetworkProviderConfig = { + clientName: CLIENT_NAME, + timeout: 10_000, + }; + return new ApiNetworkProvider(url, config); +} + +/** + * You rarely construct a provider by hand in a script — an entrypoint already + * holds one. `createNetworkProvider()` hands you the exact same + * `INetworkProvider` the controllers and factories use internally. A default + * `DevnetEntrypoint()` gives you an `ApiNetworkProvider` for devnet. + */ +export function providerFromEntrypoint(): INetworkProvider { + return new DevnetEntrypoint().createNetworkProvider(); +} + +/** + * To get a Proxy-backed provider from an entrypoint, pass `kind: 'proxy'` and a + * gateway URL. Everything else about the entrypoint stays the same. + */ +export function proxyProviderFromEntrypoint(gatewayUrl: string): INetworkProvider { + return new DevnetEntrypoint({ url: gatewayUrl, kind: 'proxy' }).createNetworkProvider(); +} +``` + +## Proving each one is live + +```ts title="src/index.ts" +// src/index.ts — constructs each kind of provider and proves it is live by +// calling getNetworkConfig() on it. No wallet, no PEM, no gas — every call +// below is a plain read. +// +// Usage: +// npm run build && npm start + +import { + createApiProvider, + createProxyProvider, + createApiProviderWithConfig, + providerFromEntrypoint, + proxyProviderFromEntrypoint, +} from './providers'; + +const MAINNET_API = 'https://api.multiversx.com'; +const MAINNET_GATEWAY = 'https://gateway.multiversx.com'; +const DEVNET_GATEWAY = 'https://devnet-gateway.multiversx.com'; + +async function main(): Promise { + const api = createApiProvider(MAINNET_API); + const apiConfig = await api.getNetworkConfig(); + console.log(`ApiNetworkProvider (${MAINNET_API})`); + console.log(` live — chainID ${apiConfig.chainID}, minGasPrice ${apiConfig.minGasPrice}`); + + const proxy = createProxyProvider(MAINNET_GATEWAY); + const proxyConfig = await proxy.getNetworkConfig(); + console.log(`ProxyNetworkProvider (${MAINNET_GATEWAY})`); + console.log(` live — chainID ${proxyConfig.chainID}, minGasPrice ${proxyConfig.minGasPrice}`); + + const tuned = createApiProviderWithConfig(MAINNET_API); + const tunedConfig = await tuned.getNetworkConfig(); + console.log('ApiNetworkProvider + custom timeout/config'); + console.log(` live — chainID ${tunedConfig.chainID}`); + + const fromEntry = providerFromEntrypoint(); + const entryConfig = await fromEntry.getNetworkConfig(); + console.log('DevnetEntrypoint().createNetworkProvider()'); + console.log(` live — chainID ${entryConfig.chainID} (devnet)`); + + const proxyFromEntry = proxyProviderFromEntrypoint(DEVNET_GATEWAY); + const proxyEntryConfig = await proxyFromEntry.getNetworkConfig(); + console.log(`DevnetEntrypoint({ kind: 'proxy' }).createNetworkProvider()`); + console.log(` live — chainID ${proxyEntryConfig.chainID} (devnet gateway)`); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start +``` + +Expected output: + +```text +ApiNetworkProvider (https://api.multiversx.com) + live — chainID 1, minGasPrice 1000000000 +ProxyNetworkProvider (https://gateway.multiversx.com) + live — chainID 1, minGasPrice 1000000000 +ApiNetworkProvider + custom timeout/config + live — chainID 1 +DevnetEntrypoint().createNetworkProvider() + live — chainID D (devnet) +DevnetEntrypoint({ kind: 'proxy' }).createNetworkProvider() + live — chainID D (devnet gateway) +``` + +## How it works + +**Depend on the interface, not the class.** Every function in `providers.ts` is +typed to return `INetworkProvider`. `ApiNetworkProvider` and +`ProxyNetworkProvider` both implement it, so a function that accepts an +`INetworkProvider` takes either. This recipe proves it by running the identical +`getNetworkConfig()` call against all five providers and getting the same +`NetworkConfig` shape back. + +**`NetworkProviderConfig` extends Axios's `AxiosRequestConfig`.** The second +constructor argument is where `clientName`, `timeout` (milliseconds), custom +`headers`, and proxy agents all live. It is one object, typed as +`interface NetworkProviderConfig extends AxiosRequestConfig { clientName?: string }`. + +**An entrypoint already holds a provider.** In a real script you rarely `new` a +provider up by hand. `DevnetEntrypoint().createNetworkProvider()` hands you the +exact same `INetworkProvider` the controllers and factories use internally. Pass +`kind: 'proxy'` plus a gateway URL to get a proxy-backed one instead. + +## Pitfalls + +:::note[Pitfall 1: set clientName or the SDK nags you] +Omit `clientName` and the SDK logs a recommendation to the console on every +provider construction ("We recommend providing the `clientName`..."). It is used +for the network's request metrics. This recipe always sets it; the +entrypoint-created providers in `index.ts` do not, which is why you will see +that log line when you run it. +::: + +:::warning[Pitfall 2: Api and Proxy are NOT feature-identical] +They share the `INetworkProvider` interface, but a handful of methods differ. +`ApiNetworkProvider.getBlock(hash)` takes a hash; `ProxyNetworkProvider.getBlock({ shard, blockNonce })` +takes a shard plus nonce. Pagination (`{ from, size }`) is honored by the Api +provider but ignored by the Proxy provider's token methods. Pick the provider +whose backend actually exposes what you need. +::: + +:::note[Pitfall 3: the URL is the network, not a constructor flag] +There is no `network: 'mainnet'` option. Mainnet vs devnet vs testnet is +entirely the URL you pass (`api.multiversx.com` vs `devnet-api.multiversx.com`). +The pre-configured `DevnetEntrypoint` / `MainnetEntrypoint` classes just bake the +right URL and chain ID in for you. +::: + +## See also + +- [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld) + builds a transfer with an entrypoint that wraps the same provider. +- [Read the connected account with useGetAccount](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + is the sdk-dapp counterpart, where the store holds the provider for you. +- [The versioned sdk-js cookbook](/sdk-and-tools/sdk-js/sdk-js-cookbook) covers + the wider read-side API surface. + +--- + ### Constants MultiversX uses some constants, which are specific to each chain (Mainnet, Testnet or Devnet). The updated values can be found at these sources: @@ -4906,7 +7469,7 @@ At the time of writing, the most used constants values for mainnet were: ## Overview -This guide walks you through handling common tasks using the MultiversX Javascript SDK (v14, latest stable version). +This guide walks you through handling common tasks using the MultiversX Javascript SDK (v14, old stable version). :::important This cookbook makes use of `sdk-js v14`. In order to migrate from `sdk-js v13.x` to `sdk-js v14`, please also follow [the migration guide](https://github.com/multiversx/mx-sdk-js-core/issues/576). @@ -8348,7 +10911,7 @@ Then, on the receiving side, you can use [`MessageComputer.unpackMessage()`](htt ## Overview -This guide walks you through handling common tasks using the MultiversX Javascript SDK (v14, latest stable version). +This guide walks you through handling common tasks using the MultiversX Javascript SDK (v15, latest stable version). :::important This cookbook makes use of `sdk-js v15`. In order to migrate from `sdk-js v14.x` to `sdk-js v15`, please also follow [the migration guide](https://github.com/multiversx/mx-sdk-js-core/issues/648). @@ -8560,9 +11123,9 @@ When manually instantiating a network provider, you can provide a configuration } ``` -Here you can find a full list of available methods for [`ApiNetworkProvider`](https://multiversx.github.io/mx-sdk-js-core/v14/classes/ApiNetworkProvider.html). +Here you can find a full list of available methods for [`ApiNetworkProvider`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/ApiNetworkProvider.html). -Both `ApiNetworkProvider` and `ProxyNetworkProvider` implement a common interface, which can be found [here](https://multiversx.github.io/mx-sdk-js-core/v14/interfaces/INetworkProvider.html). This allows them to be used interchangeably. +Both `ApiNetworkProvider` and `ProxyNetworkProvider` implement a common interface, which can be found [here](https://multiversx.github.io/mx-sdk-js-core/v15/interfaces/INetworkProvider.html). This allows them to be used interchangeably. The classes returned by the API expose the most commonly used fields directly for convenience. However, each object also contains a `raw` field that stores the original API response, allowing access to additional fields if needed. @@ -9291,10 +11854,10 @@ This allows arguments to be passed as native Javascript values. If the ABI is no ``` :::tip -When creating transactions using [`SmartContractController`](https://multiversx.github.io/mx-sdk-js-core/v14/classes/SmartContractController.html) or [`SmartContractTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v14/classes/SmartContractTransactionsFactory.html), even if the ABI is available and provided, -you can still use [`TypedValue`](https://multiversx.github.io/mx-sdk-js-core/v14/classes/TypedValue.html) objects as arguments for deployments and interactions. +When creating transactions using [`SmartContractController`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/SmartContractController.html) or [`SmartContractTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/SmartContractTransactionsFactory.html), even if the ABI is available and provided, +you can still use [`TypedValue`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/TypedValue.html) objects as arguments for deployments and interactions. -Even further, you can use a mix of [`TypedValue`](https://multiversx.github.io/mx-sdk-js-core/v14/classes/TypedValue.html) objects and plain JavaScript values and objects. For example: +Even further, you can use a mix of [`TypedValue`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/TypedValue.html) objects and plain JavaScript values and objects. For example: ```js let args = [new U32Value(42), "hello", { foo: "bar" }, new TokenIdentifierValue("TEST-abcdef")]; @@ -10121,8 +12684,8 @@ For scripts or quick network interactions, we recommend using the controller. Ho These are just a few examples of what you can do using the token management controller or factory. For a complete list of supported methods, please refer to the autogenerated documentation: -- [`TokenManagementController`](https://multiversx.github.io/mx-sdk-js-core/v14/classes/TokenManagementController.html) -- [`TokenManagementTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v14/classes/TokenManagementTransactionsFactory.html) +- [`TokenManagementController`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/TokenManagementController.html) +- [`TokenManagementTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/TokenManagementTransactionsFactory.html) ### Account management @@ -10369,8 +12932,8 @@ In this section, we'll cover how to: - Undelegate and withdraw funds These operations can be performed using both the controller and the **factory**. For a complete list of supported methods, please refer to the autogenerated documentation: -- [`DelegationController`](https://multiversx.github.io/mx-sdk-js-core/v14/classes/DelegationController.html) -- [`DelegationTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v14/classes/DelegationTransactionsFactory.html) +- [`DelegationController`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/DelegationController.html) +- [`DelegationTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/DelegationTransactionsFactory.html) #### Creating a New Delegation Contract Using the Controller ```js @@ -10990,8 +13553,8 @@ We can deploy a multisig smart contract, add members, propose and execute action The same as the other components, to interact with a multisig smart contract we can use either the MultisigController or the MultisigTransactionsFactory. These operations can be performed using both the **controller** and the **factory**. For a complete list of supported methods, please refer to the autogenerated documentation: -- [`MultisigController`](https://multiversx.github.io/mx-sdk-js-core/v14/classes/MultisigController.html) -- [`MultisigTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v14/classes/MultisigTransactionsFactory.html) +- [`MultisigController`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/MultisigController.html) +- [`MultisigTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/MultisigTransactionsFactory.html) #### Deploying a Multisig Smart Contract using the controller ```js @@ -11168,8 +13731,8 @@ Let's query the contract to get all board members. We can create transactions for creating a new governance proposal, vote for a proposal or query the governance contract. These operations can be performed using both the **controller** and the **factory**. For a complete list of supported methods, please refer to the autogenerated documentation: -- [`GovernanceController`](https://multiversx.github.io/mx-sdk-js-core/v14/classes/GovernanceController.html) -- [`GovernanceTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v14/classes/GovernanceTransactionsFactory.html) +- [`GovernanceController`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/GovernanceController.html) +- [`GovernanceTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/GovernanceTransactionsFactory.html) #### Creating a new proposal using the controller ```js @@ -11777,7 +14340,7 @@ To prepare a message for transmission, you can use the `MessageComputer.packMess } ``` -Then, on the receiving side, you can use [`MessageComputer.unpackMessage()`](https://multiversx.github.io/mx-sdk-js-core/v14/classes/MessageComputer.html#unpackMessage) to reconstruct the message, prior verification: +Then, on the receiving side, you can use [`MessageComputer.unpackMessage()`](https://multiversx.github.io/mx-sdk-js-core/v15/classes/MessageComputer.html#unpackMessage) to reconstruct the message, prior verification: ```js { @@ -12407,6 +14970,622 @@ As an exercise, try to add some more tests, especially ones involving the claim --- +### Create a delegation contract + +A delegation contract is a staking provider: it pools EGLD from many delegators +and stakes it across validator nodes, sharing rewards. You register one by +sending a `createNewDelegationContract` transaction to the delegation **manager** +system contract, carrying an initial stake (at least 1,250 EGLD) plus two +arguments, the total delegation cap and the service fee. This recipe builds that +transaction both ways (controller and factory) and parses the completed +transaction for the new contract's address. + +The default `npm start` parses a real, already-completed devnet create, so you +see the new-address parse work without a funded wallet. + +## Prerequisites + +- Node.js >= 20.13.1. +- For the default `parse` and `payload` demos: devnet network access only. +- For an actual create: a devnet PEM wallet holding at least 1,250 EGLD (the + protocol minimum) plus gas. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/create-delegation-contract +npm install +npm run build +``` + +## Creating + +```ts title="src/createDelegation.ts" +// src/createDelegation.ts - the subject of this recipe: creating a brand-new +// delegation (staking-provider) contract with `createNewDelegationContract`, +// two ways (controller and factory), then parsing the outcome to recover the +// new contract's address. +// +// A "delegation contract" is a staking provider: it collects EGLD from many +// delegators and stakes it across validator nodes. You create one by sending +// a `createNewDelegationContract` transaction to the DELEGATION MANAGER system +// contract, carrying an initial stake (>= 1250 EGLD) plus two arguments: +// - totalDelegationCap: the max EGLD the contract may accept (0 = uncapped); +// - serviceFee: the operator's cut, as an integer over 10,000 (1000 = 10%). +// +// Two verified SDK facts baked into this recipe (see the recipe page Pitfalls): +// 1. The receiver is the delegation manager, whose address the factory +// derives itself as +// `erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6`. +// Do not hardcode it from memory - a hand-copied value can carry a +// typo that fails the bech32 checksum. +// 2. The wire payload is `createNewDelegationContract@@`, +// with the initial stake carried in the transaction's `value`, not the +// data. + +import { Account } from '@multiversx/sdk-core'; +import type { DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +/** The protocol minimum initial stake to create a delegation contract: 1250 EGLD. */ +export const MIN_DELEGATION_STAKE_WEI = 1250n * 10n ** 18n; + +export interface CreateDelegationInput { + /** Max EGLD the contract may accept, in wei. 0n = uncapped. */ + totalDelegationCap: bigint; + /** Operator fee as an integer over 10,000 (1000 = 10%). */ + serviceFee: bigint; + /** Initial stake to seed the contract with, in wei (>= 1250 EGLD). */ + amount: bigint; +} + +/** + * Create path 1 - the controller. `createTransactionForNewDelegationContract` + * builds the transaction, sets the nonce, AND signs it (it takes the whole + * `Account`). Use this for scripts. + */ +export async function createViaController( + entrypoint: DevnetEntrypoint, + sender: Account, + input: CreateDelegationInput, +): Promise { + const controller = entrypoint.createDelegationController(); + return controller.createTransactionForNewDelegationContract(sender, sender.getNonceThenIncrement(), input); +} + +/** + * Create path 2 - the factory. The factory only BUILDS the unsigned + * transaction; the caller sets the nonce and signs. Use this when a wallet or + * hardware device signs. + */ +export async function createViaFactory( + entrypoint: DevnetEntrypoint, + sender: Account, + input: CreateDelegationInput, +): Promise { + const factory = entrypoint.createDelegationTransactionsFactory(); + const transaction = await factory.createTransactionForNewDelegationContract(sender.address, input); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; +} + +export interface CreatePayload { + function: string; + totalDelegationCap: bigint; + serviceFee: bigint; + receiver: string; + valueWei: bigint; + gasLimit: bigint; +} + +/** Decode a create transaction's wire fields, for inspection before sending. */ +export function describeCreatePayload(transaction: Transaction): CreatePayload { + const parts = Buffer.from(transaction.data).toString().split('@'); + const capHex = parts[1] ?? ''; + const feeHex = parts[2] ?? ''; + return { + function: parts[0] ?? '', + totalDelegationCap: capHex ? BigInt('0x' + capHex) : 0n, + serviceFee: feeHex ? BigInt('0x' + feeHex) : 0n, + receiver: transaction.receiver.toBech32(), + valueWei: transaction.value, + gasLimit: transaction.gasLimit, + }; +} + +/** + * Parse a completed create transaction for the new contract's address. + * `awaitCompletedCreateNewDelegationContract` waits and parses in one call; + * `parseCreateNewDelegationContract` parses a transaction you already fetched + * (used by this recipe's default mode against a historical create, so it + * needs no funded wallet). + */ +export async function parseNewContractAddress( + entrypoint: DevnetEntrypoint, + txHash: string, +): Promise { + const controller = entrypoint.createDelegationController(); + const transactionOnNetwork = await entrypoint.getTransaction(txHash); + const outcome = controller.parseCreateNewDelegationContract(transactionOnNetwork); + const first = outcome[0]; + if (!first) { + throw new Error(`Transaction ${txHash} created no delegation contract.`); + } + return first.contractAddress; +} + +// The delegation manager system contract (hex 0000...0004ffff) - the receiver +// of every `createNewDelegationContract`. This is the exact bech32 the factory +// derives internally; the CLI asserts the built transaction's receiver equals +// it. A hand-copied value (`...ylllslmq4y`) can carry a typo that fails the +// bech32 checksum; the correct value is below. +export const DELEGATION_MANAGER_ADDRESS = + 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6'; +``` + +## Run it + +```bash +# Parse a real completed devnet create - no wallet, no funds: +npm start + +# Inspect the create wire payload offline: +npm start -- payload + +# Actually create (needs a funded devnet PEM >= 1,250 EGLD); add --factory: +npm start -- send ./wallet.pem +``` + +Output of the default `parse` mode, and of `payload`: + +```text +Parsing completed create d615857f...aab9278c ... + new delegation contract: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdhllllsfymgpz + +function: createNewDelegationContract +totalDelegationCap: 7500000000000000000000 wei (7500 EGLD) +serviceFee: 1000 (10%) +value (initial): 1250000000000000000000 wei (1250 EGLD) +receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6 +receiver is manager: true +gasLimit: 60129500 +``` + +## How it works + +**Controller vs factory.** +`controller.createTransactionForNewDelegationContract(account, nonce, input)` +builds, sets the nonce, and signs in one call. +`factory.createTransactionForNewDelegationContract(address, input)` only builds +the unsigned transaction; you set `nonce` and `signature`. Both are async and take +the same `{ totalDelegationCap, serviceFee, amount }` input. + +**The three inputs.** `amount` is the initial stake, carried in the transaction's +`value` (>= 1,250 EGLD). `totalDelegationCap` is the maximum EGLD the contract may +later accept, in wei (`0n` = uncapped). `serviceFee` is the operator's cut as an +integer over 10,000 (so `1000` = 10%). + +**Parsing the new address.** +`controller.awaitCompletedCreateNewDelegationContract(txHash)` waits and parses in +one call; `parseCreateNewDelegationContract(transactionOnNetwork)` parses a +transaction you already fetched. The default `npm start` uses the second form +against a historical create, which is why it needs no funds. Both read the +`SCDeploy` event the manager emits. + +## Pitfalls + +:::warning[Pitfall 1: do not hardcode the delegation manager address from memory] +Every create is addressed to the delegation manager, hex `0000...0004ffff`, which +the SDK renders as +`erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6`. The factory +derives this itself. A hand-copied value (`...ylllslmq4y`) is a typo that fails +bech32 checksum, so the recipe asserts the built transaction's receiver equals the +correct address instead of trusting a copied constant. +::: + +:::warning[Pitfall 2: the 1,250 EGLD minimum is the initial value, not an argument] +The initial stake travels in the transaction's `value`, not the data. The wire +payload is only `createNewDelegationContract@@`. Send less than +1,250 EGLD and the manager rejects it; the SDK will not catch that for you. +::: + +:::note[Pitfall 3: service fee is over 10,000, not a percentage or basis points of 100] +`serviceFee: 1000` means 10%, because the protocol expresses it as parts per +10,000. Passing `10` for "10%" would set a 0.1% fee. +::: + +## See also + +- [Delegate (stake) EGLD](/sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake) + stakes into the contract once it exists. +- [Read a delegation contract's state](/sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract) + confirms the new contract's config and stake. +- Deploying an ordinary smart contract follows the same predict-then-parse shape, + covered in the smart-contracts recipes. + +--- + +### Create a governance proposal + +A governance proposal points at a Git commit (its `commitHash`) and opens a voting +window between two epochs. Creating one costs a proposal fee in EGLD, held by the +governance system contract until the proposal closes. This recipe reads the live +fee and config with `getConfig()`, then creates a proposal both ways, the +controller and the factory, against the real governance contract. + +The default `npm start` reads the live governance config (fee, thresholds, last +proposal nonce), so you see real output without a funded wallet, and you learn the +fee you would need before spending it. + +## Prerequisites + +- Node.js >= 20.13.1. +- For the default config read and `payload` demos: devnet network access only. +- For an actual proposal: a devnet PEM holding at least the proposal fee (500 EGLD + on devnet at time of writing) plus gas. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/governance-create-proposal +npm install +npm run build +``` + +## Creating a proposal + +```ts title="src/proposal.ts" +// src/proposal.ts - the subject of this recipe: creating a MultiversX +// governance proposal with the GovernanceController and its factory. +// +// A governance proposal points at a Git commit (its `commitHash`) and opens a +// voting window between two epochs. Creating one costs a proposal fee in EGLD, +// which the governance system contract holds until the proposal closes. The +// fee is not a constant - read it from the live config with `getConfig()` +// rather than hard-coding it. +// +// The proposal is sent to the governance system contract (a protocol contract +// at a fixed address the SDK knows), not to a contract you deploy. Unlike the +// multisig writes, governance transactions set their own gas limit from the +// SDK's config defaults, so you do not pass one. + +import { Account } from '@multiversx/sdk-core'; +import type { DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +export interface GovernanceConfigView { + proposalFeeWei: bigint; + lostProposalFeeWei: bigint; + minQuorum: number; + minPassThreshold: number; + minVetoThreshold: number; + lastProposalNonce: number; +} + +/** Read the live governance config, including the current proposal fee. */ +export async function readGovernanceConfig(entrypoint: DevnetEntrypoint): Promise { + const controller = entrypoint.createGovernanceController(); + const config = await controller.getConfig(); + return { + proposalFeeWei: config.proposalFee, + lostProposalFeeWei: config.lostProposalFee, + minQuorum: config.minQuorum, + minPassThreshold: config.minPassThreshold, + minVetoThreshold: config.minVetoThreshold, + lastProposalNonce: config.lastProposalNonce, + }; +} + +export interface ProposalInput { + commitHash: string; + startVoteEpoch: number; + endVoteEpoch: number; + feeWei: bigint; +} + +/** + * Create a proposal, path 1 - the controller. `createTransactionForNewProposal` + * builds, sets the nonce, and signs in one call. The fee travels in the + * transaction's `value`. + */ +export async function createProposalViaController( + entrypoint: DevnetEntrypoint, + proposer: Account, + input: ProposalInput, +): Promise { + const controller = entrypoint.createGovernanceController(); + return controller.createTransactionForNewProposal(proposer, proposer.getNonceThenIncrement(), { + commitHash: input.commitHash, + startVoteEpoch: input.startVoteEpoch, + endVoteEpoch: input.endVoteEpoch, + nativeTokenAmount: input.feeWei, + }); +} + +/** + * Create a proposal, path 2 - the factory. Builds the unsigned transaction + * only; the caller sets the nonce and signs. + */ +export async function createProposalViaFactory( + entrypoint: DevnetEntrypoint, + proposer: Account, + input: ProposalInput, +): Promise { + const factory = entrypoint.createGovernanceTransactionsFactory(); + const transaction = await factory.createTransactionForNewProposal(proposer.address, { + commitHash: input.commitHash, + startVoteEpoch: input.startVoteEpoch, + endVoteEpoch: input.endVoteEpoch, + nativeTokenAmount: input.feeWei, + }); + transaction.nonce = proposer.getNonceThenIncrement(); + transaction.signature = await proposer.signTransaction(transaction); + return transaction; +} + +export interface ProposalPayload { + function: string; + commitHashHex: string; + startEpochHex: string; + endEpochHex: string; + receiver: string; + feeWei: bigint; + gasLimit: bigint; +} + +/** Decode a proposal transaction's wire fields. */ +export function describeProposalPayload(transaction: Transaction): ProposalPayload { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + function: parts[0] ?? '', + commitHashHex: parts[1] ?? '', + startEpochHex: parts[2] ?? '', + endEpochHex: parts[3] ?? '', + receiver: transaction.receiver.toBech32(), + feeWei: transaction.value, + gasLimit: transaction.gasLimit, + }; +} +``` + +## Run it + +```bash +# Read the live governance config (fee, thresholds) - no wallet, no funds: +npm start + +# Inspect the proposal wire payload offline: +npm start -- payload + +# Actually create a proposal (needs a devnet PEM holding the fee); add --factory: +npm start -- propose ./wallet.pem +``` + +Output of the default config read, and of `payload`: + +```text +Governance config (live devnet): + proposal fee: 500000000000000000000 wei (500 EGLD) + lost-proposal fee: 10000000000000000000 wei (10 EGLD) + min quorum: 0.2 + min pass threshold: 0.6667 + min veto threshold: 0.33 + last proposal nonce: 138 + +function: proposal +commitHash: 616263...363738396162636465663031 (hex of the 40-char commit string) +startEpoch: 0x1850 = 6224 +endEpoch: 0x1855 = 6229 +receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla (the governance system contract) +value (fee): 500000000000000000000 wei (500 EGLD) +gasLimit: 50198500 (set automatically by the SDK) +``` + +## How it works + +**Controller vs factory.** +`controller.createTransactionForNewProposal(account, nonce, { commitHash, startVoteEpoch, endVoteEpoch, nativeTokenAmount })` +builds, sets the nonce, and signs. The factory form only builds; you set `nonce` +and `signature`. The governance controller and factory take no ABI, the governance +contract's interface is baked into the SDK. + +**Read the fee, do not hard-code it.** `nativeTokenAmount` is the proposal fee and +travels in the transaction's `value`. The fee is a network parameter, so the +recipe reads it from `getConfig().proposalFee` (500 EGLD on devnet) rather than +assuming a constant that could drift. + +**The receiver is a system contract.** Proposals go to `erd1qqq...rlllsrujgla`, the +governance system contract, whose address the SDK derives from its own constants +(`GOVERNANCE_CONTRACT_ADDRESS_HEX`). You never pass it. The wire payload is +`proposal@@@`. + +## Pitfalls + +:::warning[Pitfall 1: the commit hash must be exactly 40 characters] +Governance expects a full 40-character Git commit hash (a SHA-1). The SDK encodes +whatever string you pass as-is (`StringValue`), so a wrong length is not caught +when building, it is rejected on-chain. Pass the real 40-char commit of the change +you are proposing. +::: + +:::note[Pitfall 2: governance sets its own gas; multisig does not] +Unlike the multisig writes (which require an explicit `gasLimit`), governance +transactions set their gas limit automatically from the SDK config +(`gasLimitForProposal`, 50,000,000 plus data). Do not pass a `gasLimit`, there is +no parameter for it here. +::: + +:::note[Pitfall 3: the fee is at stake, not just a deposit] +The proposal fee travels in `value` and is held by the contract. If the proposal +fails to pass, part of it is kept as the `lostProposalFee` (10 EGLD on devnet); +only the rest is returned when the proposal is closed. Proposing is not free even +when it works. +::: + +:::note[Pitfall 4: vote epochs must be in the future] +`startVoteEpoch` / `endVoteEpoch` open the window; a window in the past is rejected +on-chain. This recipe reads the current epoch from the network and offsets from it, +rather than hard-coding epoch numbers that go stale. +::: + +## See also + +- [Vote and close a proposal](/sdk-and-tools/sdk-js/cookbook/governance/vote-close-proposal) + covers the next steps for the proposal this recipe creates. +- Staking is what gives an address the voting power to back a proposal, covered in + the delegation recipes. + +--- + +### Create an Account from a KeyPair, secret key, or mnemonic + +The `Account` is the object you sign with. This recipe builds one three ways: +from a `KeyPair`, from a raw secret key, and from a mnemonic, all fully offline, +with no network call. It also flags a real inconsistency in the named +constructors: one of them is asynchronous while the rest are not. + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates its own throwaway keys. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/account-from-keys +npm install +npm run build +npm start +``` + +## The code + +```ts title="src/index.ts" +// src/index.ts — build an Account four ways: from a KeyPair, from a raw +// secret key, from a mnemonic, and (for contrast) note the async PEM path. +// +// Fully offline. No devnet, no network call — every constructor here works +// on local key material. Fresh keys are generated on each run, so exact +// addresses differ; the equivalences printed do not. +// +// Grounded in the installed @multiversx/sdk-core v15.4.1 Account class +// (accounts/account.d.ts). + +import { Account, KeyPair, Mnemonic } from '@multiversx/sdk-core'; + +async function main(): Promise { + // === A. From a freshly generated KeyPair. === + // KeyPair.generate() makes a new Ed25519 keypair; newFromKeypair wraps it + // in an Account. Synchronous. + const keyPair = KeyPair.generate(); + const fromKeyPair = Account.newFromKeypair(keyPair); + console.log(`A. From KeyPair: ${fromKeyPair.address.toBech32()}`); + + // === B. From a raw secret key — the plain constructor. === + // We reuse the KeyPair's own secret key, so B is the same account as A and + // the addresses must match. Synchronous. + const fromSecretKey = new Account(keyPair.secretKey); + console.log(`B. From secretKey: ${fromSecretKey.address.toBech32()}`); + console.log( + ` A and B share one key, so addresses match: ${fromKeyPair.address.toBech32() === fromSecretKey.address.toBech32()}`, + ); + + // === C. From a mnemonic at addressIndex 0. === + // newFromMnemonic derives the key first, then builds the Account. + // Synchronous, despite deriving a key. + const mnemonic = Mnemonic.generate(); + const fromMnemonic = Account.newFromMnemonic(mnemonic.toString(), 0); + console.log(`C. From mnemonic: ${fromMnemonic.address.toBech32()}`); + + // === D. Every Account exposes the same surface. === + // address, publicKey, secretKey, and a LOCAL nonce (a bigint you manage + // yourself — see the Manage nonces recipe). An Account can sign, too. + console.log(`\nfromMnemonic.nonce starts at ${fromMnemonic.nonce} (a local bigint, not fetched from the network).`); + const data = new Uint8Array(Buffer.from('proof this account can sign')); + const signature = await fromMnemonic.sign(data); + const verified = await fromMnemonic.verify(data, signature); + console.log(`fromMnemonic can sign and verify its own data: ${verified}`); + + // === E. The one async constructor. === + // newFromPem is the odd one out: it returns Promise and must be + // awaited. newFromKeypair / new Account(...) / newFromMnemonic / + // newFromKeystore are all synchronous. See the "Save and load a PEM" recipe. + console.log('\nNote: Account.newFromPem(...) is async (returns a Promise); the four constructors used above are synchronous.'); + + console.log('\nExpected: A and B identical, two "true"-style confirmations.'); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +Keys are generated fresh each run, so addresses differ. A and B reuse one key, so +their addresses always match: + +```text +A. From KeyPair: erd13cj3sr6vxqknjaegpc3txvjtz3ermc3dvlznhj6lpfzm4wtu69vs2695hk +B. From secretKey: erd13cj3sr6vxqknjaegpc3txvjtz3ermc3dvlznhj6lpfzm4wtu69vs2695hk + A and B share one key, so addresses match: true +C. From mnemonic: erd1uast8l23rlhkee32xx59tmuc54gh8xp5eveayw7ecql9vqxtnqzsmg2yj6 + +fromMnemonic.nonce starts at 0 (a local bigint, not fetched from the network). +fromMnemonic can sign and verify its own data: true +``` + +## How it works + +Grounded in the installed `@multiversx/sdk-core` v15.4.1 `Account` class +(`accounts/account.d.ts`): + +1. **From a KeyPair.** `KeyPair.generate()` makes a new Ed25519 keypair; + `Account.newFromKeypair(keyPair)` wraps it. +2. **From a raw secret key.** `new Account(secretKey)`, the plain constructor. + Reusing the KeyPair's own key makes A and B the same account. +3. **From a mnemonic.** `Account.newFromMnemonic(phrase, addressIndex)` derives + the key first, then builds the Account. +4. **The shared surface.** Every `Account` exposes `address`, `publicKey`, + `secretKey`, and a *local* `nonce` (a `bigint` you manage yourself), and can + `sign` / `verify`. + +## Pitfalls + +:::warning[Pitfall 1: the named constructors are inconsistent, one is async] +`Account.newFromPem(...)` returns `Promise` and must be `await`ed. But +`Account.newFromKeypair(...)`, `new Account(...)`, `Account.newFromMnemonic(...)`, +and `Account.newFromKeystore(...)` are all **synchronous**. Forgetting to await +`newFromPem` gives you a `Promise`, not an account. Check the specific +constructor. +::: + +:::note[Pitfall 2: account.nonce starts at 0 and is not the on-chain nonce] +It is a local counter you must seed from the network before sending +transactions, see [Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces). +Constructing an account does not query the chain. +::: + +:::tip[Pitfall 3: same key means same address, always] +The address is a function of the public key alone. If two constructions share +key material (as A and B do here), they are the same account, there is no +per-object identity beyond the key. +::: + +## See also + +- [Generate a mnemonic + derive keys](/sdk-and-tools/sdk-js/cookbook/accounts/generate-mnemonic-derive-keys) + is where constructor C's key material comes from. +- [Save and load a keystore](/sdk-and-tools/sdk-js/cookbook/accounts/keystore-save-load) + persists an account as encrypted JSON. +- [Save and load a PEM (dev only)](/sdk-and-tools/sdk-js/cookbook/accounts/pem-save-load) + is the async `newFromPem` path in full. + +--- + ### Creating Wallets How to create wallets using the CLI or programmatically @@ -12459,6 +15638,215 @@ fs.writeFileSync("myKeyFile.json", JSON.stringify(jsonFileContent)); --- +### Custom API/Proxy request + +The typed provider methods (`getAccount`, `getNetworkConfig`, +`getTokenOfAccount`, ...) cover the common endpoints. The API and gateway expose +many more. When you need one the SDK does not model, call it raw, both escape +hatches are on `INetworkProvider`: + +- `doGetGeneric(resourceUrl)`, a GET, path relative to the provider's base URL. +- `doPostGeneric(resourceUrl, payload)`, a POST. + +Both return `any`, so **you** own the shape of the result. This recipe wraps +three raw calls, the `economics` and `stats` GETs, and a raw VM query POST, in +typed functions. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access to devnet. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/custom-api-request +npm install +npm run build +npm start +``` + +## The raw calls + +```ts title="src/customRequest.ts" +// src/customRequest.ts — the subject of this recipe: the escape hatch. When a +// provider has no typed method for the endpoint you need, call it raw: +// +// doGetGeneric(resourceUrl) → any (a GET, path relative to the base URL) +// doPostGeneric(resourceUrl, payload) → any (a POST) +// +// Both are on INetworkProvider. They return `any`, so YOU own the shape of the +// result — the SDK does not validate it. This file wraps three raw calls in +// typed functions so the rest of the app gets real types back. + +import type { INetworkProvider } from '@multiversx/sdk-core'; + +/** + * The API's `/economics` endpoint — total/circulating supply, staked, price, + * market cap, APR. There is NO typed provider method for it; raw is the only + * way. This interface is the shape you assert (you are responsible for it). + */ +export interface Economics { + totalSupply: number; + circulatingSupply: number; + staked: number; + price: number; + marketCap: number; + apr: number; +} + +export async function getEconomics(provider: INetworkProvider): Promise { + return (await provider.doGetGeneric('economics')) as Economics; +} + +/** The API's `/stats` endpoint — another untyped GET. */ +export interface NetworkStats { + shards: number; + blocks: number; + accounts: number; + transactions: number; +} + +export async function getStats(provider: INetworkProvider): Promise { + return (await provider.doGetGeneric('stats')) as NetworkStats; +} + +/** + * A raw VM query via POST — the same read `SmartContractController.query` + * wraps, shown at the HTTP level. `returnData` comes back as base64-encoded, + * big-endian bytes; this decodes the first return value to a bigint. + */ +export async function rawQuery( + provider: INetworkProvider, + scAddress: string, + funcName: string, +): Promise { + const result = (await provider.doPostGeneric('query', { + scAddress, + funcName, + args: [], + })) as { returnCode: string; returnData: string[] }; + + const [first] = result.returnData; + if (first === undefined || first === '') { + return 0n; + } + const bytes = Buffer.from(first, 'base64'); + return bytes.length === 0 ? 0n : BigInt(`0x${bytes.toString('hex')}`); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — three raw calls against devnet: two GETs the typed methods do +// not model (economics, stats) and one POST (a VM query) that they DO, to show +// the raw layer underneath. No wallet, no gas. +// +// Usage: +// npm run build && npm start + +import { DevnetEntrypoint } from '@multiversx/sdk-core'; +import { getEconomics, getStats, rawQuery } from './customRequest'; + +// The adder contract on devnet — the same stable fixture the query recipe uses. +const ADDER = 'erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug'; + +async function main(): Promise { + const provider = new DevnetEntrypoint().createNetworkProvider(); + + const econ = await getEconomics(provider); + console.log('GET economics (no typed provider method exists):'); + console.log(` price $${econ.price}, marketCap ${econ.marketCap}, staked ${econ.staked}`); + + const stats = await getStats(provider); + console.log('GET stats:'); + console.log(` shards ${stats.shards}, accounts ${stats.accounts}, transactions ${stats.transactions}`); + + const sum = await rawQuery(provider, ADDER, 'getSum'); + console.log('POST query — adder.getSum() at the raw HTTP level:'); + console.log(` decoded returnData: ${sum}`); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start +``` + +Expected output (economics and the adder sum are live, so they move): + +```text +GET economics (no typed provider method exists): + price $3.26, marketCap 84056590, staked 2406394 +GET stats: + shards 3, accounts 18, transactions 25902940 +POST query — adder.getSum() at the raw HTTP level: + decoded returnData: 84 +``` + +## How it works + +**`economics` is the flagship raw GET.** Total supply, price, market cap, APR, +there is no typed provider method for it, so `doGetGeneric('economics')` is the +only way. `stats` is another. This is exactly what the escape hatch is for: +endpoints that exist on the network but not (yet) in the SDK's typed surface. + +**`doPostGeneric('query', ...)` is `SmartContractController.query` at the HTTP +level.** The raw VM query returns `returnData` as base64-encoded, big-endian +bytes; the recipe decodes the first value to a bigint, adder's `getSum()`, the +same `84` the typed query-contract-view recipe returns. Use the typed controller +in real code; this shows the raw layer underneath, and the shape you would reuse +for any unwrapped POST endpoint. + +**Query params go in the path string.** `doGetGeneric` takes the whole resource +path, so pagination and filters ride along as a query string, e.g. +`doGetGeneric('accounts/erd1.../tokens?from=0&size=10')`. There is no separate +params argument. + +## Pitfalls + +:::warning[Pitfall 1: the response is any; you assert the shape] +`doGetGeneric` / `doPostGeneric` return `any`. Casting to an interface (as this +recipe does) gives your code real types, but nothing validates the cast at +runtime. Treat these responses like any other untrusted input, a renamed field +will not be a compile error, it will be `undefined` at runtime. +::: + +:::note[Pitfall 2: the path is relative and provider-specific] +`doGetGeneric` prepends the provider's base URL, so pass `economics`, not +`/economics` or a full URL. And the Api and Proxy expose different paths, +`economics` and `stats` are API routes; a gateway (Proxy) has its own +`network/...` and `address/...` routes. The escape hatch does not paper over that +difference. +::: + +:::note[Pitfall 3: prefer the typed method when one exists] +If a typed method covers your need, use it, you get decoding, types, and stability +across SDK versions for free. Reach for `doGetGeneric` / `doPostGeneric` only for +endpoints the SDK does not model yet, and consider filing an issue so it gets a +typed method. +::: + +## See also + +- [Configure a network provider](/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider) + is the provider whose `doGetGeneric` / `doPostGeneric` this recipe uses. +- [Fetch an account's token balances](/sdk-and-tools/sdk-js/cookbook/tokens/fetch-account-token-balances) + has typed methods for the token endpoints, so you rarely need the raw layer + there. +- [Fetch token metadata](/sdk-and-tools/sdk-js/cookbook/tokens/fetch-token-metadata) + is another place a typed method saves you from a raw request. + +--- + ### Custom Types We sometimes create new types that we want to serialize and deserialize directly when interacting with contracts. For `struct`s and `enum`s it is very easy to set them up, with barely any code. @@ -12751,6 +16139,367 @@ export const ExampleTable = () => ( --- +### Decode contract events + +Decode the events a smart contract emits into named, typed fields, using an ABI +and `TransactionEventsParser`. A raw event is a bag of base64 topics and data +bytes; the parser uses the ABI's event definition to turn those bytes into a +usable object. + +This recipe decodes a **real** `pongEvent` from the ping-pong contract, captured +verbatim from devnet transaction `922dbae7...`. The decoded `user` field must +equal the real sender of that transaction, which is how the recipe verifies +itself. + +## Prerequisites + +- Node.js >= 20.13.1. +- For the default (offline) decode: nothing, the raw event is baked in. +- For the `live` mode: devnet network access. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/decode-contract-events +npm install +npm run build +``` + +## Decoding + +```ts title="src/decodeEvents.ts" +// src/decodeEvents.ts - decoding the events a smart contract emits, using an +// ABI and `TransactionEventsParser`. A raw event is a bag of base64 topics and +// data bytes; the parser uses the ABI's event definition to turn those bytes +// into named, typed fields. +// +// Target: the real **ping-pong** contract's `pongEvent`, which has one indexed +// input `user: Address`. This recipe decodes a REAL pongEvent captured verbatim +// from devnet transaction +// 922dbae7f85c949add1c5971f7cb88ab2f806760851539e53cdf48e055d12740 +// (whose sender pong-ed the contract). The decoded `user` must equal that +// sender: erd179ktm46cy3hvcg6r0c9khafgtlwxq3rswj578x7wl2ltkv6k5suq7mn47d. +// +// KEY DETAIL: the parser matches the event to the ABI by its FIRST TOPIC +// ("pongEvent"), which is the ABI event's identifier. Note the log's own +// `identifier` field here is "pong" (the endpoint name), which is different. +// `firstTopicIsIdentifier` defaults to true, so the first topic wins. + +import { + TransactionEvent, + TransactionEventsParser, + findEventsByFirstTopic, +} from '@multiversx/sdk-core'; +import type { Abi, DevnetEntrypoint } from '@multiversx/sdk-core'; + +// A real pongEvent, exactly as the devnet API returned it (base64 topics). +// topics[0] "cG9uZ0V2ZW50" is base64 for "pongEvent" (the ABI event id); +// topics[1] is the 32-byte pubkey of the indexed `user` field. +export const CAPTURED_PONG_EVENT = { + address: 'erd1qqqqqqqqqqqqqpgqm6ad6xrsjvxlcdcffqe8w58trpec09ug9l5qde96pq', + identifier: 'pong', + topics: ['cG9uZ0V2ZW50', '8Wy911gkbswjQ34La/UoX9xgRHB0qeObzvq+uzNWpDg='], + data: '', + additionalData: [''], +} as const; + +export const PONG_EVENT_TX_HASH = + '922dbae7f85c949add1c5971f7cb88ab2f806760851539e53cdf48e055d12740'; + +export interface DecodedPongEvent { + /** The bech32 address of the `user` who pong-ed. */ + user: string; +} + +/** + * Decodes the real captured pongEvent above. Fully offline and deterministic: + * the raw bytes are baked in, so this always produces the same result and never + * depends on the transaction still being retrievable from the network. + */ +export function decodeCapturedPongEvent(abi: Abi): DecodedPongEvent { + const event = TransactionEvent.fromHttpResponse({ + address: CAPTURED_PONG_EVENT.address, + identifier: CAPTURED_PONG_EVENT.identifier, + topics: [...CAPTURED_PONG_EVENT.topics], + data: CAPTURED_PONG_EVENT.data, + additionalData: [...CAPTURED_PONG_EVENT.additionalData], + }); + + const parser = new TransactionEventsParser({ abi }); + const decoded = parser.parseEvent({ event }) as { user: { toBech32(): string } }; + return { user: decoded.user.toBech32() }; +} + +/** + * The production path: fetch a live transaction, gather its pongEvent(s), and + * decode them. `findEventsByFirstTopic(tx, "pongEvent")` pulls exactly the + * events whose first topic is the ABI event identifier, across the main log and + * every smart-contract-result log. Returns one decoded object per event. + * + * Devnet may prune old transactions, so this is the "how you'd do it against + * your own fresh transaction" path; the captured decode above is the durable + * one this recipe verifies against. + */ +export async function decodePongEventsFromTransaction( + entrypoint: DevnetEntrypoint, + abi: Abi, + txHash: string, +): Promise { + const transactionOnNetwork = await entrypoint.getTransaction(txHash); + const events = findEventsByFirstTopic(transactionOnNetwork, 'pongEvent'); + + const parser = new TransactionEventsParser({ abi }); + const decoded = parser.parseEvents({ events }) as Array<{ user: { toBech32(): string } }>; + return decoded.map((d) => ({ user: d.user.toBech32() })); +} +``` + +## Run it + +```bash +# Decode the captured real event - offline, deterministic: +npm start + +# Fetch a live transaction and decode its pongEvent(s): +npm start -- live +``` + +Expected output of the default mode: + +```text +Decoding the captured pongEvent (offline) from tx 922dbae7f85c949add1c5971f7cb88ab2f806760851539e53cdf48e055d12740... + decoded user: erd179ktm46cy3hvcg6r0c9khafgtlwxq3rswj578x7wl2ltkv6k5suq7mn47d + expected user: erd179ktm46cy3hvcg6r0c9khafgtlwxq3rswj578x7wl2ltkv6k5suq7mn47d + match: true +``` + +## How it works + +**The ABI event definition drives the decode.** ping-pong's ABI declares +`pongEvent` with one indexed input, `user: Address`. +`new TransactionEventsParser({ abi }).parseEvent({ event })` reads the event's +topics and returns `{ user }` as a real `Address`. The captured event is a +verbatim copy of a real on-chain pongEvent, so decoding it offline is +deterministic and genuine. + +**Getting events from a live transaction.** The `live` mode fetches with +`entrypoint.getTransaction(txHash)`, then `findEventsByFirstTopic(tx, +"pongEvent")` pulls matching events across the main log and every +smart-contract-result log, and `parseEvents({ events })` decodes them. +`gatherAllEvents(tx)` is the unfiltered version. + +## Pitfalls + +:::warning[Pitfall 1: the parser matches on the FIRST TOPIC, not the log identifier] +For `pongEvent`, the first topic is base64 `"cG9uZ0V2ZW50"` = `"pongEvent"`, the +ABI event's identifier. The log's own `identifier` field is `"pong"` (the +endpoint name), which is different. `firstTopicIsIdentifier` defaults to `true`, +so the first topic wins. Load the ABI that actually declares the event, or there +is nothing to match, and an ABI that lacks the event decodes nothing. +::: + +:::note[Pitfall 2: topics and data are base64 on the wire] +`TransactionEvent.fromHttpResponse({ topics, data, additionalData })` +base64-decodes each into a `Buffer` for you. Pass the API's base64 strings +straight through; do not decode them yourself first. +::: + +:::note[Pitfall 3: devnet prunes old transactions] +The `live` mode points at a real tx by hash; if devnet has pruned it, pass your +own recent pong transaction. The captured offline decode never rots, which is why +it is the default and the verification anchor. +::: + +## See also + +- [Decode contract return data (ABI codec)](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/decode-return-data) + is the codec that also powers event field decoding. +- [Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) + reads live state from the same ping-pong contract. +- [Load an ABI](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi) + is where the event definition comes from. + +--- + +### Decode contract return data (ABI codec) + +Use an ABI plus the low-level `BinaryCodec` to decode raw contract bytes into +typed values, and to encode typed values back into bytes. This is the layer +beneath `controller.parseQueryResponse` / `parseExecute`: reach for it when you +hold raw return data, a struct, or an enum and want to convert it by hand. + +Three real demonstrations: decode `getPingAmount`'s raw return data into a +`BigUint`, encode the multisig `EsdtTokenPayment` struct, and decode the multisig +`Action` enum. All offline and deterministic. + +## Prerequisites + +- Node.js >= 20.13.1. +- No wallet, no gas, no network. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/decode-return-data +npm install +npm run build +npm start +``` + +## Decoding and encoding + +```ts title="src/decodeReturnData.ts" +// src/decodeReturnData.ts - using an ABI plus the low-level `BinaryCodec` to +// decode raw contract bytes into typed values, and to encode typed values back +// into bytes. This is the layer beneath `controller.parseQueryResponse` / +// `parseExecute`: reach for it when you hold raw return data, a struct, or an +// enum and want to convert by hand. +// +// Three real, verified demonstrations: +// 1. Decode raw RETURN DATA: ping-pong's getPingAmount returns a BigUint; +// the raw bytes 0de0b6b3a7640000 decode to 1000000000000000000 (1 EGLD), +// the same value the live query returns. +// 2. Encode a STRUCT: the multisig contract's EsdtTokenPayment struct, +// via abi.getStruct(...) + Struct/Field + codec.encodeNested. +// 3. Decode an ENUM: the multisig contract's Action enum, via +// abi.getEnum(...) + codec.decodeNested, from a real encoded action. + +import { + BinaryCodec, + Struct, + Field, + TokenIdentifierValue, + U64Value, + BigUIntValue, +} from '@multiversx/sdk-core'; +import type { Abi } from '@multiversx/sdk-core'; + +const codec = new BinaryCodec(); + +/** + * Decode raw contract RETURN DATA into a value, using the output type the ABI + * declares for a given endpoint. `decodeTopLevel` is the top-level form (a whole + * return-data part); `decodeNested` is for values embedded inside a larger + * buffer. + * + * Returns the decimal string form, since a decoded BigUint is a BigNumber. + */ +export function decodeReturnValue(abi: Abi, endpointName: string, returnDataHex: string): string { + const endpoint = abi.getEndpoint(endpointName); + const outputType = endpoint.output[0]?.type; + if (!outputType) { + throw new Error(`Endpoint ${endpointName} declares no output type.`); + } + const decoded = codec.decodeTopLevel(Buffer.from(returnDataHex, 'hex'), outputType); + return String(decoded.valueOf()); +} + +/** + * Encode a custom STRUCT (EsdtTokenPayment) into binary, using the struct type + * looked up from the ABI. Each `Field` pairs a typed value with its field name. + * Returns the nested-encoding hex. + */ +export function encodeEsdtTokenPayment( + abi: Abi, + tokenIdentifier: string, + tokenNonce: bigint, + amount: bigint, +): string { + const paymentType = abi.getStruct('EsdtTokenPayment'); + const paymentStruct = new Struct(paymentType, [ + new Field(new TokenIdentifierValue(tokenIdentifier), 'token_identifier'), + new Field(new U64Value(tokenNonce), 'token_nonce'), + new Field(new BigUIntValue(amount), 'amount'), + ]); + return codec.encodeNested(paymentStruct).toString('hex'); +} + +/** + * Decode a custom ENUM (Action) from binary, using the enum type looked up from + * the ABI. Returns the variant name and the decoded fields object. + */ +export function decodeAction(abi: Abi, dataHex: string): { name: string; value: unknown } { + const actionType = abi.getEnum('Action'); + const [decoded] = codec.decodeNested(Buffer.from(dataHex, 'hex'), actionType); + const value = decoded.valueOf() as { name: string }; + return { name: value.name, value }; +} +``` + +## Run it + +```bash +npm start +``` + +Expected output: + +```text +1) Decode raw return data (ping-pong getPingAmount -> BigUint): + 0de0b6b3a7640000 -> 1000000000000000000 + equals 1 EGLD (1e18): true + +2) Encode a struct (multisig EsdtTokenPayment): + encoded: 0000000b544553542d3862303238660000000000000000000000022710 + matches expected: true + +3) Decode an enum (multisig Action): + variant: SendTransferExecuteEgld +``` + +## How it works + +**Decode raw return data with the ABI's declared output type.** A contract's +return data is just bytes. `abi.getEndpoint("getPingAmount").output[0].type` is +the type the ABI declares for that endpoint's result (`BigUint`), and +`codec.decodeTopLevel(buffer, type)` turns raw bytes into a typed value. +`0de0b6b3a7640000` decodes to `1000000000000000000` (1 EGLD), the same value the +live [query recipe](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) +returns. + +**Encode a struct.** `abi.getStruct("EsdtTokenPayment")` returns the struct type; +build a `Struct` from `Field`s (each a typed value plus its field name) and call +`codec.encodeNested(struct)`. + +**Decode an enum.** `abi.getEnum("Action")` returns the enum type; +`codec.decodeNested(buffer, type)` returns the decoded value and the number of +bytes consumed. The real `Action` bytes decode to the `SendTransferExecuteEgld` +variant. + +## Pitfalls + +:::note[Pitfall 1: a decoded value's static type is TypedValue] +Call `.valueOf()` and convert explicitly. A decoded `BigUint` is a `BigNumber`, +so this recipe returns `String(decoded.valueOf())`. `strict` mode will not narrow +the shape for you, the same caveat as query results. +::: + +:::warning[Pitfall 2: decodeTopLevel vs decodeNested are not interchangeable] +Top-level and nested encodings differ (nested values are length-prefixed where +top-level ones are not). Decode return-data parts with `decodeTopLevel`; decode a +value pulled from inside a larger buffer with `decodeNested`. Using the wrong one +yields garbage or throws. +::: + +:::note[Pitfall 3: getStruct / getEnum need a name that exists in the ABI] +They throw if the type name is missing. `adder` and `ping-pong` declare no custom +types, which is why this recipe uses the multisig ABI for the struct and enum +demonstrations. +::: + +## See also + +- [Decode contract events](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/decode-contract-events) + decodes event fields with the same codec. +- [Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) + is the high-level `parseQueryResponse` that sits on top of this codec. +- [Load an ABI](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi) + is where `getStruct` / `getEnum` / `getEndpoint` come from. + +--- + ### Defaults Smart contracts occasionally need to interact with uninitialized data. Most notably, whenever a smart contract is deployed, its entire storage will be uninitialized. @@ -12898,6 +16647,195 @@ The same here, `::is_default(::default() --- +### 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.13.1. +- 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 + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/delegate-stake +npm install +npm run build +``` + +## Delegating + +```ts title="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 { + 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 { + 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 { + const transactionOnNetwork = await entrypoint.getTransaction(txHash); + const parser = new DelegationTransactionsOutcomeParser(); + const outcome = parser.parseDelegate(transactionOnNetwork); + return outcome[0]?.amount ?? 0n; +} +``` + +## Run it + +```bash +# 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`: + +```text +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 + +:::warning[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. +::: + +:::note[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). +::: + +:::note[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 + +- [Create a delegation contract](/sdk-and-tools/sdk-js/cookbook/delegation/create-delegation-contract) + makes the contract you delegate into. +- [Claim and re-delegate rewards](/sdk-and-tools/sdk-js/cookbook/delegation/claim-and-redelegate-rewards) + handles the rewards this stake earns. +- [Read a delegation contract's state](/sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract) + checks your active stake after delegating. + +--- + ### delegators This page describes the structure of the `delegators` index (Elasticsearch), and also depicts a few examples of how to query it. @@ -13257,6 +17195,275 @@ We highly recommend experimenting with these interactors, as they are efficient --- +### Deploy a smart contract + +Deploy a smart contract from its WASM bytecode plus constructor arguments, then +parse the deploy outcome to recover the new contract's address. This recipe shows +both the controller path (build, nonce, sign in one call) and the factory path +(build only, you sign), against the real **adder** example contract +(`init(initial_value: BigUint)`) using the exact 687-byte WASM and ABI that +`mx-sdk-js-core` ships in its own tests. + +The default `npm start` parses a real, already-completed devnet deploy, so you +can see deploy-outcome parsing work without a funded wallet. + +## Prerequisites + +- Node.js >= 20.13.1. +- For the default `parse` demo: devnet network access only. +- For an actual deploy: a devnet PEM wallet with a little EGLD for gas (see + [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send)). + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/deploy-contract +npm install +npm run build +``` + +## Deploying + +```ts title="src/deploy.ts" +// src/deploy.ts - deploying a smart contract from its WASM bytecode plus +// constructor arguments, two ways (controller and factory), then parsing the +// deploy outcome to recover the new contract's address. +// +// Target contract: the real **adder** example contract, whose constructor is +// `init(initial_value: BigUint)`. Its 687-byte WASM (src/adder.wasm) and ABI +// (src/adder.abi.json) are the exact fixtures mx-sdk-js-core uses in its own +// tests and cookbook. +// +// Two verified SDK facts baked into this recipe (see the Pitfalls below for +// detail): +// 1. WITH an ABI, constructor arguments are plain JS values (`[42]`). +// WITHOUT an ABI, they must be TypedValue objects (`[new BigUIntValue(42)]`), +// or the factory throws "Can't convert args to TypedValues". +// 2. The deploy transaction's `data` is `@@@`. +// The default codeMetadata built by the SDK is `0504` = +// Upgradeable + Readable + PayableBySmartContract. + +import { + Account, + Address, + AddressComputer, + SmartContractTransactionsOutcomeParser, +} from '@multiversx/sdk-core'; +import type { Abi, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +export interface DeployResult { + txHash: string; + /** Address predicted BEFORE broadcasting, from (sender, nonce). */ + predictedAddress: string; +} + +/** + * Deploy path 1 - the controller. `createTransactionForDeploy` builds the + * transaction, sets the nonce, AND signs it (the controller takes the whole + * `Account`). We predict the contract address from the transaction's own sender + * and nonce before sending, so it lines up with whatever nonce the controller + * consumed. + */ +export async function deployViaController( + entrypoint: DevnetEntrypoint, + abi: Abi, + sender: Account, + bytecode: Uint8Array, + initialValue: number, +): Promise { + const controller = entrypoint.createSmartContractController(abi); + + const transaction = await controller.createTransactionForDeploy(sender, sender.getNonceThenIncrement(), { + bytecode, + gasLimit: 6_000_000n, + arguments: [initialValue], // plain JS value - allowed because we passed the ABI + }); + + const predictedAddress = predictAddress(transaction); + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash, predictedAddress }; +} + +/** + * Deploy path 2 - the factory. The factory only BUILDS the transaction; the + * caller must set the nonce and sign it. Use this when the signing happens + * elsewhere (a wallet, a hardware device, a dApp). + */ +export async function deployViaFactory( + entrypoint: DevnetEntrypoint, + abi: Abi, + sender: Account, + bytecode: Uint8Array, + initialValue: number, +): Promise { + const factory = entrypoint.createSmartContractTransactionsFactory(abi); + + const transaction = await factory.createTransactionForDeploy(sender.address, { + bytecode, + gasLimit: 6_000_000n, + arguments: [initialValue], + }); + + // The developer owns nonce + signing with the factory. + transaction.nonce = sender.getNonceThenIncrement(); + const predictedAddress = predictAddress(transaction); + transaction.signature = await sender.signTransaction(transaction); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash, predictedAddress }; +} + +/** Predict the contract address from a built deploy transaction. */ +function predictAddress(transaction: Transaction): string { + const computer = new AddressComputer(); + return computer.computeContractAddress(transaction.sender, transaction.nonce).toBech32(); +} + +/** + * Parse path 1 - the controller's one-liner. `awaitCompletedDeploy` waits for + * the transaction to complete and parses it, returning the deployed contract(s). + * Use this right after your own deploy. + */ +export async function awaitDeployedAddress( + entrypoint: DevnetEntrypoint, + abi: Abi, + txHash: string, +): Promise { + const controller = entrypoint.createSmartContractController(abi); + const outcome = await controller.awaitCompletedDeploy(txHash); + return outcome.contracts[0]!.address.toBech32(); +} + +/** + * Parse path 2 - fetch the completed transaction yourself, then parse it with + * `SmartContractTransactionsOutcomeParser`. Works on ANY completed deploy + * transaction (yours or a historical one), which is why this recipe can show + * real parse output without needing a funded wallet. + */ +export async function parseDeployFromHash( + entrypoint: DevnetEntrypoint, + txHash: string, +): Promise<{ address: string; owner: string; codeHashHex: string; returnCode: string }> { + const transactionOnNetwork = await entrypoint.getTransaction(txHash); + const parser = new SmartContractTransactionsOutcomeParser(); + const outcome = parser.parseDeploy({ transactionOnNetwork }); + + const first = outcome.contracts[0]; + if (!first) { + throw new Error(`Transaction ${txHash} deployed no contract (returnCode: ${outcome.returnCode}).`); + } + return { + address: first.address.toBech32(), + owner: first.ownerAddress.toBech32(), + codeHashHex: Buffer.from(first.codeHash).toString('hex'), + returnCode: outcome.returnCode, + }; +} + +/** Decode a deploy transaction's `data` field into its four wire parts. */ +export function describeDeployPayload(transaction: Transaction): { + codeHex: string; + vmType: string; + codeMetadata: string; + args: string[]; +} { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + codeHex: parts[0] ?? '', + vmType: parts[1] ?? '', + codeMetadata: parts[2] ?? '', + args: parts.slice(3), + }; +} + +/** Convenience: the system deploy address every deploy is addressed to. */ +export const SYSTEM_DEPLOY_ADDRESS = Address.newFromBech32( + 'erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu', +).toBech32(); +``` + +## Run it + +```bash +# Parse a real completed devnet deploy - no wallet, no funds: +npm start + +# Inspect the deploy wire payload offline: +npm start -- payload + +# Actually deploy (needs a funded devnet PEM); add --factory for the factory path: +npm start -- deploy ./wallet.pem 42 +``` + +Expected output of the default `parse` mode: + +```text +Parsing completed deploy a957cf3038a79517b1a11ee45094259989b0c7f67b61e1a39f3502ed041b964d ... + returnCode: ok + contract: erd1qqqqqqqqqqqqqpgqs6reg0rjc7tmdcz65qg9namphcat5hvk8cfs4mfuj2 + owner: erd1r69gk66fmedhhcg24g2c5kn2f2a5k4kvpr6jfw67dn2lyydd8cfswy6ede + codeHash (hex): ... +``` + +## How it works + +**Controller vs factory.** `controller.createTransactionForDeploy(account, +nonce, options)` builds, sets the nonce, and signs in one call. +`factory.createTransactionForDeploy(address, options)` only builds the unsigned +transaction; you set `nonce` and `signature`. Use the controller for scripts, the +factory when a wallet or hardware device signs. Both are async. + +**Predict, then confirm.** Both paths compute the upcoming contract address with +`AddressComputer.computeContractAddress(tx.sender, tx.nonce)` before sending, and +this recipe asserts it equals the address the network reports after completion. +See [Compute a contract address before deploy](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/compute-contract-address). + +**Parsing, two ways.** `controller.awaitCompletedDeploy(txHash)` waits and parses +in one call; `SmartContractTransactionsOutcomeParser.parseDeploy({ transactionOnNetwork })` +parses a transaction you already fetched. The default `npm start` uses the second +form so it can show real output against a historical deploy without funds. + +## Pitfalls + +:::warning[Pitfall 1: without an ABI, arguments must be TypedValue objects] +With the ABI passed to the controller or factory, `arguments: [42]` works. +Without an ABI, the same call throws `Err: Can't convert args to TypedValues`; +you must pass `arguments: [new BigUIntValue(42)]`. +::: + +:::warning[Pitfall 2: the default code metadata is permissive] +The SDK builds deploy/upgrade transactions with `codeMetadata = 0504` = +Upgradeable + Readable + PayableBySmartContract. `isPayableBySmartContract` +defaults to `true` in the factory. For a locked-down contract, pass +`isUpgradeable: false` / `isPayableBySmartContract: false` explicitly. Run +`npm start -- payload` to see the raw metadata bytes. +::: + +:::note[Pitfall 3: the receiver is the system deploy address, not the contract] +A deploy is addressed to +`erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu`. The contract +address only exists after the network assigns it. The `data` field is +`@@@`, with vmType `0500` = WASM VM. +::: + +:::note[Pitfall 4: the predicted address depends on the exact deploy nonce] +If another transaction from the same account lands first, the deploy nonce shifts +and the address changes. This recipe predicts from the transaction's own `nonce` +after it is set. +::: + +## See also + +- [Compute a contract address before deploy](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/compute-contract-address) + is the prediction this recipe confirms. +- [Upgrade a smart contract](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/upgrade-contract) + is the same bytecode-plus-args shape, for an existing contract. +- [Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint) + calls the contract once it is deployed. + +--- + ### Devcontainers In **Visual Studio Code**, you use a **container** as a [full-featured **development environment**](https://code.visualstudio.com/docs/devcontainers/containers). As of September 2023, one MultiversX devcontainer is available: @@ -17378,6 +21585,1040 @@ await transactionWatcher.awaitAnyEvents(txHash, ["mySpecialEventFoo", "mySpecial --- +### Fetch a block + +Fetching a block is the read where the Api and Proxy providers diverge the most, +so it is worth doing deliberately. + +- **`ApiNetworkProvider`** addresses a block by **hash**: `getBlock(blockHash)` + and `getLatestBlock()`. +- **`ProxyNetworkProvider`** addresses a block by **shard + nonce**: + `getBlock({ shard, blockNonce })` and `getLatestBlock(shard)`. + +The API is a cross-shard index, so a hash is a global key. The proxy talks to +observers of one shard at a time, so it needs the shard plus a coordinate within +it. The block methods are **not** on the shared `INetworkProvider` interface, so +the functions in this recipe are typed against the concrete provider classes. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/fetch-a-block +npm install +npm run build +npm start +``` + +## Fetching blocks + +```ts title="src/blocks.ts" +// src/blocks.ts — the subject of this recipe: fetching a block. This is where +// the Api and Proxy providers diverge the most, so the functions below are +// typed against the concrete provider classes, not INetworkProvider (the block +// methods are NOT on the shared interface). +// +// - ApiNetworkProvider identifies a block by HASH: +// getBlock(blockHash) / getLatestBlock() +// - ProxyNetworkProvider identifies a block by SHARD + NONCE: +// getBlock({ shard, blockNonce }) / getLatestBlock(shard) +// +// Why the difference: the API is a cross-shard index, so a hash is a global +// key. The proxy talks to observers of one shard at a time, so it needs the +// shard plus a coordinate (hash or nonce) within it. + +import type { ApiNetworkProvider, ProxyNetworkProvider, BlockOnNetwork } from '@multiversx/sdk-core'; + +/** API: the most recent block the index has seen. */ +export async function fetchLatestBlockApi(api: ApiNetworkProvider): Promise { + return api.getLatestBlock(); +} + +/** API: any block, addressed by its hash. */ +export async function fetchBlockByHashApi( + api: ApiNetworkProvider, + blockHash: string, +): Promise { + return api.getBlock(blockHash); +} + +/** + * Proxy: a block addressed by shard + nonce. Get the nonce from + * `getNetworkStatus(shard)` (use `highestFinalNonce` so the block is final). + */ +export async function fetchBlockByNonceProxy( + proxy: ProxyNetworkProvider, + shard: number, + blockNonce: bigint, +): Promise { + return proxy.getBlock({ shard, blockNonce }); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — fetch the latest block by hash (API), re-fetch that exact +// block by its hash to prove round-trip, then fetch a final block by shard + +// nonce (Proxy). No wallet, no gas. +// +// Usage: +// npm run build && npm start + +import { ApiNetworkProvider, ProxyNetworkProvider } from '@multiversx/sdk-core'; +import { fetchLatestBlockApi, fetchBlockByHashApi, fetchBlockByNonceProxy } from './blocks'; + +const MAINNET_API = 'https://api.multiversx.com'; +const MAINNET_GATEWAY = 'https://gateway.multiversx.com'; + +async function main(): Promise { + const api = new ApiNetworkProvider(MAINNET_API, { clientName: 'mvx-cookbook' }); + const proxy = new ProxyNetworkProvider(MAINNET_GATEWAY, { clientName: 'mvx-cookbook' }); + + // API — latest block, then the same block re-fetched by its hash. + const latest = await fetchLatestBlockApi(api); + console.log('API getLatestBlock():'); + console.log(` shard ${latest.shard}, nonce ${latest.nonce}, epoch ${latest.epoch}`); + console.log(` hash ${latest.hash}`); + + const byHash = await fetchBlockByHashApi(api, latest.hash); + console.log('API getBlock(hash) — round-trip:'); + console.log(` shard ${byHash.shard}, nonce ${byHash.nonce} (same block: ${byHash.hash === latest.hash})`); + + // Proxy — a final block on shard 1, addressed by nonce. + const shard = 1; + const status = await proxy.getNetworkStatus(shard); + const finalNonce = status.highestFinalNonce; + const byNonce = await fetchBlockByNonceProxy(proxy, shard, finalNonce); + console.log(`\nProxy getBlock({ shard: ${shard}, blockNonce: ${finalNonce} }):`); + console.log(` shard ${byNonce.shard}, nonce ${byNonce.nonce}, epoch ${byNonce.epoch}`); + console.log(` hash ${byNonce.hash}`); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start +``` + +Expected output (live values, so nonces and hashes will differ when you run it): + +```text +API getLatestBlock(): + shard 0, nonce 31313292, epoch 2175 + hash 5a3d6c018a5aa443ea558c0cd9c4e23738d1184bbbda3fbe072fda931c50458e +API getBlock(hash) — round-trip: + shard 0, nonce 31313292 (same block: true) + +Proxy getBlock({ shard: 1, blockNonce: 31302554 }): + shard 1, nonce 31302554, epoch 2175 + hash daf493760526846b1d0d571ce801573ddfe1383866b9338ee56afe611fbb1462 +``` + +## How it works + +**Latest, then round-trip by hash.** The recipe calls `getLatestBlock()` on the +Api provider, then feeds that block's `hash` straight back into `getBlock(hash)` +and asserts the two are the same block, a live proof that both API paths agree. + +**Proxy needs a nonce, so read the status first.** For the proxy's by-nonce +lookup, the recipe reads `getNetworkStatus(shard).highestFinalNonce` and requests +that block, so it always asks for a **final** (irreversible) block rather than a +tip that might still reorg. + +**`BlockOnNetwork` fields.** Both paths return the same shape: `shard`, `nonce` +(bigint), `hash`, `previousHash`, `timestamp`, `round`, `epoch`. + +## Pitfalls + +:::danger[Pitfall 1: ProxyNetworkProvider.getLatestBlock() is broken (sdk-core v15.4.1)] +It returns an empty block (`shard: NaN, nonce: 0n, hash: ''`) because it does not +unwrap the gateway's `response.block` envelope, unlike `getBlock()` which does. +Workaround: use `getBlock({ shard, blockNonce })` with a nonce from +`getNetworkStatus(shard)`, which is exactly what this recipe does. The **Api** +provider's `getLatestBlock()` works correctly. +::: + +:::warning[Pitfall 2: ProxyNetworkProvider.getBlock rejects the genesis nonce] +The internal guard is `else if (args.blockNonce)`, and `0n` is falsy, so +requesting block nonce `0` throws `Block hash or block nonce not provided.` If you +genuinely need the genesis block from the proxy, query it by hash instead of +nonce. +::: + +:::note[Pitfall 3: block methods are not on INetworkProvider] +`getBlock` / `getLatestBlock` exist only on the concrete `ApiNetworkProvider` and +`ProxyNetworkProvider` classes, and with different signatures. A variable typed as +`INetworkProvider` cannot call them at all. Type against the concrete provider, as +this recipe does. +::: + +## See also + +- [Fetch network config and status](/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-network-config-status) + is where the `blockNonce` this recipe queries comes from. +- [Configure a network provider](/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider) + covers Api vs Proxy, the distinction this recipe leans on hardest. +- [Custom API/Proxy request](/sdk-and-tools/sdk-js/cookbook/network-providers/custom-api-request) + reaches block sub-resources the typed methods do not model yet. + +--- + +### Fetch an account's on-chain state + +Read any account's public on-chain state through a provider: its nonce, balance, +username (herotag), guarded flag, and its key-value storage. This is the +sdk-core, read-**any**-address counterpart to sdk-dapp's `useGetAccount()`, which +only reads the connected wallet. + +Three reads, all on `INetworkProvider`: + +- `getAccount(address)` returns `nonce`, `balance`, `userName`, `isGuarded`, plus + the contract fields (`contractOwnerAddress`, `isContractUpgradable`, ...). +- `getAccountStorage(address)` returns all key-value entries. +- `getAccountStorageEntry(address, key)` returns one entry. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/fetch-account-state +npm install +npm run build +npm start +``` + +## Reading the account + +```ts title="src/accountState.ts" +// src/accountState.ts — the subject of this recipe: reading an ARBITRARY +// account's on-chain state through a provider. This is different from the +// sdk-dapp `useGetAccount()` hook, which reads the CONNECTED wallet — here you +// pass any address you like and read it directly. +// +// Three reads, all on INetworkProvider: +// - getAccount(address) → nonce, balance, username, guarded flag +// - getAccountStorage(address) → ALL key-value storage entries +// - getAccountStorageEntry(address, key) → one entry + +import type { + INetworkProvider, + AccountOnNetwork, + AccountStorage, + AccountStorageEntry, + Address, +} from '@multiversx/sdk-core'; + +/** Core account state: nonce, balance, username, guarded flag, contract fields. */ +export async function fetchAccount( + provider: INetworkProvider, + address: Address, +): Promise { + return provider.getAccount(address); +} + +/** Every key-value pair stored on the account. Values come back HEX-encoded. */ +export async function fetchAccountStorage( + provider: INetworkProvider, + address: Address, +): Promise { + return provider.getAccountStorage(address); +} + +/** + * A single storage entry by key. Unlike `getAccountStorage`, the single-entry + * endpoint returns the value already DECODED to a UTF-8 string. + */ +export async function fetchAccountStorageEntry( + provider: INetworkProvider, + address: Address, + key: string, +): Promise { + return provider.getAccountStorageEntry(address, key); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — read a live mainnet account's state and storage. No wallet, +// no gas — you are reading someone else's public on-chain state. +// +// Usage: +// npm run build && npm start [bech32Address] [storageKey] + +import { ApiNetworkProvider, Address } from '@multiversx/sdk-core'; +import { fetchAccount, fetchAccountStorage, fetchAccountStorageEntry } from './accountState'; + +const MAINNET_API = 'https://api.multiversx.com'; +const DEFAULT_ADDRESS = 'erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th'; +const DEFAULT_KEY = 'btc'; + +async function main(): Promise { + const addressArg = process.argv[2] ?? DEFAULT_ADDRESS; + const key = process.argv[3] ?? DEFAULT_KEY; + const provider = new ApiNetworkProvider(MAINNET_API, { clientName: 'mvx-cookbook' }); + const address = Address.newFromBech32(addressArg); + + const account = await fetchAccount(provider, address); + console.log(`Account ${account.address.toBech32()}`); + console.log(` nonce: ${account.nonce}`); + console.log(` balance: ${account.balance}`); + console.log(` username: ${account.userName ? account.userName : '(none)'}`); + console.log(` isGuarded: ${account.isGuarded}`); + + const storage = await fetchAccountStorage(provider, address); + console.log(`\nStorage — ${storage.entries.length} entries. Values are HEX from getAccountStorage:`); + for (const entry of storage.entries.slice(0, 3)) { + console.log(` ${entry.key} = ${entry.value}`); + } + + const single = await fetchAccountStorageEntry(provider, address, key); + console.log(`\ngetAccountStorageEntry('${key}') — value is DECODED here:`); + console.log(` ${single.key} = ${single.value}`); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start # a known mainnet account with storage +npm start # any account / key +``` + +Expected output (balance and nonce are live, they will differ): + +```text +Account erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th + nonce: 89 + balance: 1000010000000 + username: (none) + isGuarded: false + +Storage — 16 entries. Values are HEX from getAccountStorage: + btc = 626331716639747971736b3373333830713832673033686367346c637a677a7032373568617138326171 + eth = 307838433739313643643332633037623164633730353465324365423233663964443538396642334336 + ELRONDesdtBSK-baa025 = 12070001d1a94a4000 + +getAccountStorageEntry('btc') — value is DECODED here: + btc = bc1qf9tyqsk3s380q82g03hcg4lczgzp275haq82aq +``` + +## How it works + +**`getAccount` reads an arbitrary address.** You pass the address; nothing is +signed, no wallet is constructed. Contrast with `useGetAccount()` in sdk-dapp, +which is bound to whoever is logged in, this reads any account on the network. + +**`balance` and `nonce` are `bigint`.** Balance is in the smallest denomination +(10^18 = 1 EGLD). The `nonce` here is the same value +[Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) fetches +before sending, one read, two uses. + +**Bulk storage is hex; a single entry is decoded.** `getAccountStorage` returns +each value as a hex string; `getAccountStorageEntry` returns the same value +decoded to UTF-8. Verified live in the output above: `btc` is `62633171...` in +bulk and `bc1q...` as a single entry. + +For conceptual depth, see +[docs.multiversx.com/sdk-and-tools/sdk-js/sdk-js-cookbook](https://docs.multiversx.com/sdk-and-tools/sdk-js/sdk-js-cookbook). + +## Pitfalls + +:::warning[Pitfall 1: userName is typed string but can be undefined] +`AccountOnNetwork.userName` is declared `string`, but an account with no herotag +returns `undefined` at runtime (confirmed above, the default account prints +`(none)`). Guard it (`account.userName ? ... : ...`) rather than trusting the +type. +::: + +:::note[Pitfall 2: getAccountStorage values are hex, not text] +Do not print `getAccountStorage` values as-is expecting readable strings, they +are hex. Either decode them yourself (`Buffer.from(value, 'hex').toString()`) or +use `getAccountStorageEntry`, which decodes for you. The two endpoints returning +the same key in different encodings is a real inconsistency, not a bug in your +code. +::: + +:::note[Pitfall 3: contract fields are optional] +For a plain wallet, `contractOwnerAddress`, `isContractUpgradable`, +`contractCodeHash`, and friends are absent. They are populated only when the +address is a smart contract. They are typed optional for exactly this reason. +::: + +## See also + +- [Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) + is what to do with the `nonce` this recipe reads before you send transactions. +- [Configure a network provider](/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider) + is the provider all three reads run on. +- The ESDT and NFT token-balance side of the same account is covered in the + tokens recipes. + +--- + +### Fetch an account's token balances + +Read the tokens an account holds, EGLD's ESDT siblings. Three reads, all on +`INetworkProvider`: + +- `getFungibleTokensOfAccount(address, pagination?)` returns every fungible ESDT. +- `getNonFungibleTokensOfAccount(address, pagination?)` returns every NFT / SFT / + meta-ESDT. +- `getTokenOfAccount(address, token)` returns one specific token's balance. + +All read-only. This recipe reads a liquidity-pool contract (which reliably holds +several fungibles) and a known NFT-holding address, so every call returns real, +non-empty data. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/fetch-account-token-balances +npm install +npm run build +npm start +``` + +## Reading the balances + +```ts title="src/tokenBalances.ts" +// src/tokenBalances.ts — the subject of this recipe: reading the tokens an +// account holds. Three reads, all on INetworkProvider: +// +// - getFungibleTokensOfAccount(address, pagination?) → every fungible ESDT +// - getNonFungibleTokensOfAccount(address, pagination?) → every NFT / SFT / meta-ESDT +// - getTokenOfAccount(address, token) → one token's balance +// +// getTokenOfAccount has a sharp edge for NFTs — see fetchNftBalance below. + +import { Token, TokenComputer } from '@multiversx/sdk-core'; +import type { + INetworkProvider, + TokenAmountOnNetwork, + Address, + IPagination, +} from '@multiversx/sdk-core'; + +/** All fungible ESDTs the account holds. Pagination is honored by the Api + * provider; the Proxy provider ignores it and returns everything. */ +export async function fetchFungibleTokens( + provider: INetworkProvider, + address: Address, + pagination?: IPagination, +): Promise { + return pagination === undefined + ? provider.getFungibleTokensOfAccount(address) + : provider.getFungibleTokensOfAccount(address, pagination); +} + +/** All non-fungible tokens (NFT / SFT / meta-ESDT) the account holds. */ +export async function fetchNonFungibleTokens( + provider: INetworkProvider, + address: Address, + pagination?: IPagination, +): Promise { + return pagination === undefined + ? provider.getNonFungibleTokensOfAccount(address) + : provider.getNonFungibleTokensOfAccount(address, pagination); +} + +/** One fungible token's balance for the account. */ +export async function fetchFungibleBalance( + provider: INetworkProvider, + address: Address, + identifier: string, +): Promise { + return provider.getTokenOfAccount(address, new Token({ identifier })); +} + +/** + * One NFT's balance. getTokenOfAccount for an NFT wants the BASE collection + * identifier plus the nonce — it appends the nonce hex itself. Passing the + * already-extended identifier (e.g. "XPASS-423274-04") double-appends the nonce + * and the API rejects it as an invalid NFT identifier. Since + * getNonFungibleTokensOfAccount returns the EXTENDED identifier, strip it back + * to the base with TokenComputer first. + */ +export async function fetchNftBalance( + provider: INetworkProvider, + address: Address, + extendedIdentifier: string, + nonce: bigint, +): Promise { + const baseIdentifier = new TokenComputer().extractIdentifierFromExtendedIdentifier(extendedIdentifier); + return provider.getTokenOfAccount(address, new Token({ identifier: baseIdentifier, nonce })); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — read the fungible tokens of a liquidity-pool contract (which +// reliably holds several) and the NFTs of a known NFT-holding address. No +// wallet, no gas — these are public balances. +// +// Usage: +// npm run build && npm start + +import { ApiNetworkProvider, Address } from '@multiversx/sdk-core'; +import { + fetchFungibleTokens, + fetchNonFungibleTokens, + fetchFungibleBalance, + fetchNftBalance, +} from './tokenBalances'; + +const MAINNET_API = 'https://api.multiversx.com'; +// An xExchange WEGLD/USDC pair contract — reliably holds fungible ESDTs. +const FUNGIBLE_HOLDER = 'erd1qqqqqqqqqqqqqpgqeel2kumf0r8ffyhth7pqdujjat9nx0862jpsg2pqaq'; +// A known address that holds an NFT. +const NFT_HOLDER = 'erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th'; + +async function main(): Promise { + const provider = new ApiNetworkProvider(MAINNET_API, { clientName: 'mvx-cookbook' }); + + // Fungible tokens (paginated). + const fungibleHolder = Address.newFromBech32(FUNGIBLE_HOLDER); + const fungibles = await fetchFungibleTokens(provider, fungibleHolder, { from: 0, size: 5 }); + console.log(`Fungible tokens of ${FUNGIBLE_HOLDER} (up to 5):`); + for (const t of fungibles) { + console.log(` ${t.token.identifier}: ${t.amount}`); + } + + // One specific fungible balance. + const wegld = await fetchFungibleBalance(provider, fungibleHolder, 'WEGLD-bd4d79'); + console.log(`getTokenOfAccount('WEGLD-bd4d79'): ${wegld.amount}`); + + // Non-fungible tokens. + const nftHolder = Address.newFromBech32(NFT_HOLDER); + const nfts = await fetchNonFungibleTokens(provider, nftHolder, { from: 0, size: 5 }); + console.log(`\nNon-fungible tokens of ${NFT_HOLDER} (up to 5):`); + for (const t of nfts) { + console.log(` ${t.token.identifier} (nonce ${t.token.nonce}): amount ${t.amount}`); + } + + // One specific NFT balance — showing the base-identifier + nonce requirement. + const [firstNft] = nfts; + if (firstNft !== undefined) { + const back = await fetchNftBalance(provider, nftHolder, firstNft.token.identifier, firstNft.token.nonce); + console.log(`getTokenOfAccount(base id + nonce) for ${firstNft.token.identifier}: amount ${back.amount}`); + } else { + console.log('(no NFTs on this address right now — pass another address to see the NFT path)'); + } +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start +``` + +Expected output (balances are live and will differ): + +```text +Fungible tokens of erd1qqqqqqqqqqqqqpgqeel2kumf0r8ffyhth7pqdujjat9nx0862jpsg2pqaq (up to 5): + USDC-c76f1f: 733827376705 + EGLDUSDC-594e5e: 1000 + WEGLD-bd4d79: 223980465825062238009362 +getTokenOfAccount('WEGLD-bd4d79'): 223980465825062238009362 + +Non-fungible tokens of erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th (up to 5): + XPASS-423274-04 (nonce 4): amount 1 +getTokenOfAccount(base id + nonce) for XPASS-423274-04: amount 1 +``` + +## How it works + +**Fungible and non-fungible are separate calls.** A wallet's USDC and its NFTs +come from two different endpoints. Both return `TokenAmountOnNetwork[]`, each +element a `token` (identifier + nonce) plus an `amount` (bigint, in the token's +own smallest denomination). The single-token `getTokenOfAccount('WEGLD-bd4d79')` +matches the amount from the list, one balance, two ways to reach it. + +**Amounts are raw bigints.** `733827376705` for USDC is `733,827.376705` after +USDC's 6 decimals; `223980465825062238009362` for WEGLD is ~223,980 after 18 +decimals. Divide by `10 ** decimals` (from +[Fetch token metadata](/sdk-and-tools/sdk-js/cookbook/tokens/fetch-token-metadata)) +to display, never hard-code 18. + +## Pitfalls + +:::danger[Pitfall 1: getTokenOfAccount for an NFT wants the BASE identifier + nonce] +`getTokenOfAccount` appends the nonce hex to the identifier itself. +`getNonFungibleTokensOfAccount` returns the already-EXTENDED identifier +(`XPASS-423274-04`), so passing that back in double-appends the nonce (`...-04-04`) +and the API rejects it with "Invalid NFT identifier". Strip it to the base first +with `TokenComputer.extractIdentifierFromExtendedIdentifier()`, then pass +`{ identifier: base, nonce }`, reproduced and fixed against the live API. +::: + +:::warning[Pitfall 2: pagination is Api-only] +`{ from, size }` is honored by `ApiNetworkProvider` and silently ignored by +`ProxyNetworkProvider`, whose `getFungibleTokensOfAccount(address)` signature has +no pagination parameter at all. On the proxy you get everything back regardless, +page in application code if the account holds a lot. +::: + +:::note[Pitfall 3: an empty list is normal, not an error] +An account with no ESDTs returns `[]`, and `getTokenOfAccount` for a token the +account does not hold throws "Token for given account not found" rather than +returning zero. Treat "not held" as a caught error or an empty array, not an +exception you forgot to handle. +::: + +## See also + +- [Fetch token metadata](/sdk-and-tools/sdk-js/cookbook/tokens/fetch-token-metadata) + supplies the `decimals` you need to turn these raw amounts into human numbers. +- [Fetch an account's on-chain state](/sdk-and-tools/sdk-js/cookbook/accounts/fetch-account-state) + is the EGLD balance and nonce side of the same account. +- [Send an ESDT](/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt) + moves one of the tokens you just read. + +--- + +### Fetch network config and status + +Two "about the network itself" reads, side by side. They look similar but answer +different questions and have different lifetimes: + +- **`getNetworkConfig()`** returns the **static** protocol parameters: chain ID, + gas costs (`minGasLimit`, `minGasPrice`, `gasPerDataByte`, `gasPriceModifier`), + shard count, round duration. These change only across protocol upgrades, so you + fetch them once and cache them. Any fee calculation needs them. +- **`getNetworkStatus(shard)`** returns the **live** chain tip: current block + nonce, epoch, round, highest final nonce. This moves every round (~6s on + mainnet), so you re-fetch it whenever you need "where is the chain right now". + +Both are read-only, with no wallet and no gas. You need a provider first; see +[Configure a network provider](/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider). + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/fetch-network-config-status +npm install +npm run build +npm start +``` + +## Reading config and status + +```ts title="src/networkInfo.ts" +// src/networkInfo.ts — the subject of this recipe: reading the two "about the +// network itself" endpoints. They answer different questions and have +// different lifetimes: +// +// - getNetworkConfig() → the STATIC protocol parameters: chain ID, gas costs, +// shard count, round duration. These change only across protocol upgrades, +// so you fetch them once and cache them (a transaction builder needs +// minGasLimit / gasPerDataByte to compute fees). +// - getNetworkStatus(shard) → the LIVE chain tip: current block nonce, epoch, +// round, highest final nonce. This moves every round (~6s on mainnet), so +// you re-fetch it whenever you need "where is the chain right now". + +import type { + INetworkProvider, + ApiNetworkProvider, + ProxyNetworkProvider, + NetworkConfig, + NetworkStatus, +} from '@multiversx/sdk-core'; + +/** + * Static protocol parameters — safe to fetch once and cache. `getNetworkConfig` + * takes no arguments and is on the `INetworkProvider` interface, so an + * ApiNetworkProvider or a ProxyNetworkProvider both work here. + */ +export async function fetchNetworkConfig(provider: INetworkProvider): Promise { + return provider.getNetworkConfig(); +} + +/** + * The live chain tip for a given shard. `getNetworkStatus()` with no argument + * targets the metachain (shard 4294967295); pass a shard number (0, 1, 2) for a + * regular shard. + * + * Note the concrete-type parameter: the `INetworkProvider` interface declares + * `getNetworkStatus()` with NO shard argument, while both implementations + * (ApiNetworkProvider, ProxyNetworkProvider) accept `shard?`. To pass a shard + * under strict typing you must reference a concrete provider, not the + * interface. See the recipe's "Pitfalls". + */ +export async function fetchNetworkStatus( + provider: ApiNetworkProvider | ProxyNetworkProvider, + shard?: number, +): Promise { + return shard === undefined + ? provider.getNetworkStatus() + : provider.getNetworkStatus(shard); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — fetches the network config once and the status for the +// metachain plus each regular shard, then prints them. No wallet, no gas. +// +// Usage: +// npm run build && npm start [apiUrl] +// +// Defaults to mainnet; pass a devnet/testnet API URL to target another network. + +import { ApiNetworkProvider } from '@multiversx/sdk-core'; +import { fetchNetworkConfig, fetchNetworkStatus } from './networkInfo'; + +const DEFAULT_API = 'https://api.multiversx.com'; + +async function main(): Promise { + const apiUrl = process.argv[2] ?? DEFAULT_API; + const provider = new ApiNetworkProvider(apiUrl, { clientName: 'mvx-cookbook' }); + + const config = await fetchNetworkConfig(provider); + console.log(`Network config (${apiUrl}):`); + console.log(` chainID: ${config.chainID}`); + console.log(` minGasLimit: ${config.minGasLimit}`); + console.log(` minGasPrice: ${config.minGasPrice}`); + console.log(` gasPerDataByte: ${config.gasPerDataByte}`); + console.log(` gasPriceModifier:${config.gasPriceModifier}`); + console.log(` numShards: ${config.numShards}`); + console.log(` roundDuration: ${config.roundDuration} ms`); + + // Metachain status (default) then every regular shard. + const meta = await fetchNetworkStatus(provider); + console.log('\nNetwork status — metachain:'); + console.log(` blockNonce ${meta.blockNonce}, epoch ${meta.currentEpoch}, round ${meta.currentRound}`); + + for (let shard = 0; shard < config.numShards; shard++) { + const status = await fetchNetworkStatus(provider, shard); + console.log(`Network status — shard ${shard}:`); + console.log(` blockNonce ${status.blockNonce}, highestFinalNonce ${status.highestFinalNonce}, epoch ${status.currentEpoch}`); + } +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start # mainnet +npm start https://devnet-api.multiversx.com # any other network +``` + +Expected output (mainnet; live values, so the nonces and epoch will differ when +you run it): + +```text +Network config (https://api.multiversx.com): + chainID: 1 + minGasLimit: 50000 + minGasPrice: 1000000000 + gasPerDataByte: 1500 + gasPriceModifier:0.5 + numShards: 3 + roundDuration: 6000 ms + +Network status — metachain: + blockNonce 31295127, epoch 2175, round 31333830 +Network status — shard 0: + blockNonce 31313271, highestFinalNonce 31313271, epoch 2175 +Network status — shard 1: + blockNonce 31302533, highestFinalNonce 31302533, epoch 2175 +Network status — shard 2: + blockNonce 31308060, highestFinalNonce 31308060, epoch 2175 +``` + +## How it works + +**Config is static; status is live.** `getNetworkConfig()` returns numbers that +only change at a protocol upgrade: `gasPerDataByte` `1500`, `minGasLimit` `50000`, +`numShards` `3` on mainnet. Cache them. `getNetworkStatus()` returns the chain +tip, stale within one round, so never cache it. These are exactly the two +categories the SDK models as separate classes (`NetworkConfig` vs +`NetworkStatus`). + +**Status is per-shard.** MultiversX is a sharded chain, so there is no single +"current block". `getNetworkStatus()` with no argument targets the metachain +(shard `4294967295`); pass `0`, `1`, or `2` for a regular shard. This recipe +loops `0..numShards` to show all three plus the metachain. + +**`gasPerDataByte` and `minGasLimit` are the fee formula inputs.** A plain +transfer's gas is `minGasLimit + gasPerDataByte * data.length`. That is why a +fee-computing transaction builder reads `getNetworkConfig()` first. See +[Simulate and estimate a transaction](/sdk-and-tools/sdk-js/cookbook/network-providers/simulate-estimate-transaction) +for letting the network compute the cost for you instead. + +## Pitfalls + +:::warning[Pitfall 1: the shard argument is missing from the interface type] +`INetworkProvider.getNetworkStatus()` is declared with **no** parameter, but both +`ApiNetworkProvider` and `ProxyNetworkProvider` implement +`getNetworkStatus(shard?: number)`. If your variable is typed as +`INetworkProvider`, TypeScript will reject the shard argument. Type it as the +concrete provider (as `fetchNetworkStatus` does) to pass a shard. Verified against +the installed `.d.ts` files in sdk-core v15.4. +::: + +:::note[Pitfall 2: roundDuration is milliseconds, gasPriceModifier is a float] +`roundDuration` is in milliseconds (`6000` = 6s), not seconds. `gasPriceModifier` +is a plain `number` (`0.5`), the fraction of gas price actually charged on the +non-data portion of gas, not a bigint like the other gas fields. +::: + +:::note[Pitfall 3: blockNonce is a bigint] +`blockNonce`, `highestFinalNonce`, and `currentRound` are `bigint`; +`currentEpoch` is a plain `number`. Do not mix them in arithmetic without +converting; TypeScript will stop you, which is the point. +::: + +## See also + +- [Configure a network provider](/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider) + builds the provider these calls run on. +- [Fetch a block](/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-a-block) + goes from the `blockNonce` in the status to the full block. +- [Simulate and estimate a transaction](/sdk-and-tools/sdk-js/cookbook/network-providers/simulate-estimate-transaction) + lets the network compute a transaction's gas cost instead of applying the + config formula by hand. + +--- + +### Fetch token metadata + +Read a token's **definition**, its metadata and property flags, as opposed to a +balance. Two reads, both on `INetworkProvider`: + +- `getDefinitionOfFungibleToken(identifier)` returns a fungible token's `name`, + `ticker`, `decimals`, `owner`, `supply`, and the `can*` property flags. +- `getDefinitionOfTokenCollection(collection)` returns an NFT / SFT / meta-ESDT + collection's `type`, `name`, `decimals`, `owner`, and property flags. + +The `decimals` this returns is exactly what you need to turn a raw balance from +[Fetch an account's token balances](/sdk-and-tools/sdk-js/cookbook/tokens/fetch-account-token-balances) +into a human number. + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access. No wallet, no PEM, no EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/fetch-token-metadata +npm install +npm run build +npm start +``` + +## Reading the definitions + +```ts title="src/tokenMetadata.ts" +// src/tokenMetadata.ts — the subject of this recipe: reading a token's +// DEFINITION (its metadata and property flags), as opposed to a balance. +// +// - getDefinitionOfFungibleToken(identifier) → a fungible token's definition: +// name, ticker, decimals, owner, supply, and the can* property flags. +// - getDefinitionOfTokenCollection(collection) → an NFT / SFT / meta-ESDT +// collection's definition: type, name, decimals, owner, property flags. +// +// Both are on INetworkProvider. Note: pass the COLLECTION identifier +// (e.g. "MEDAL-ae074f") to the collection call, not a single NFT's extended id. + +import type { + INetworkProvider, + DefinitionOfFungibleTokenOnNetwork, + DefinitionOfTokenCollectionOnNetwork, +} from '@multiversx/sdk-core'; + +/** A fungible token's definition (metadata + property flags). */ +export async function fetchFungibleDefinition( + provider: INetworkProvider, + identifier: string, +): Promise { + return provider.getDefinitionOfFungibleToken(identifier); +} + +/** An NFT / SFT / meta-ESDT collection's definition. */ +export async function fetchCollectionDefinition( + provider: INetworkProvider, + collection: string, +): Promise { + return provider.getDefinitionOfTokenCollection(collection); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — read the definition of a well-known fungible token (WEGLD) and +// a well-known NFT collection (MEDAL). No wallet, no gas. +// +// Usage: +// npm run build && npm start [fungibleId] [collectionId] + +import { ApiNetworkProvider } from '@multiversx/sdk-core'; +import { fetchFungibleDefinition, fetchCollectionDefinition } from './tokenMetadata'; + +const MAINNET_API = 'https://api.multiversx.com'; +const DEFAULT_FUNGIBLE = 'WEGLD-bd4d79'; +const DEFAULT_COLLECTION = 'MEDAL-ae074f'; + +async function main(): Promise { + const fungibleId = process.argv[2] ?? DEFAULT_FUNGIBLE; + const collectionId = process.argv[3] ?? DEFAULT_COLLECTION; + const provider = new ApiNetworkProvider(MAINNET_API, { clientName: 'mvx-cookbook' }); + + const fungible = await fetchFungibleDefinition(provider, fungibleId); + console.log(`Fungible token ${fungible.identifier}`); + console.log(` name: ${fungible.name}`); + console.log(` ticker: ${fungible.ticker}`); + console.log(` decimals: ${fungible.decimals}`); + console.log(` owner: ${fungible.owner.toBech32()}`); + console.log(` isPaused: ${fungible.isPaused}`); + console.log(` canFreeze: ${fungible.canFreeze}, canWipe: ${fungible.canWipe}, canUpgrade: ${fungible.canUpgrade}`); + + const collection = await fetchCollectionDefinition(provider, collectionId); + console.log(`\nCollection ${collection.collection}`); + console.log(` type: ${collection.type}`); + console.log(` name: ${collection.name}`); + console.log(` decimals: ${collection.decimals}`); + console.log(` owner: ${collection.owner.toBech32()}`); + console.log(` canFreeze: ${collection.canFreeze}, canWipe: ${collection.canWipe}, canTransferNftCreateRole: ${collection.canTransferNftCreateRole}`); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start # WEGLD-bd4d79 + MEDAL-ae074f +npm start # any token / collection +``` + +Expected output: + +```text +Fungible token WEGLD-bd4d79 + name: WrappedEGLD + ticker: WEGLD + decimals: 18 + owner: erd1ss6u80ruas2phpmr82r42xnkd6rxy40g9jl69frppl4qez9w2jpsqj8x97 + isPaused: false + canFreeze: true, canWipe: true, canUpgrade: true + +Collection MEDAL-ae074f + type: NonFungibleESDT + name: GLUMedals + decimals: 0 + owner: erd126y66ear20cdskrdky0kpzr9agjul7pcut7ktlr6p0eu8syxhvrq0gsqdj + canFreeze: false, canWipe: false, canTransferNftCreateRole: false +``` + +## How it works + +**Definition, not balance.** These endpoints describe the token itself, who owns +it, how many decimals, whether it can be frozen or paused, independent of any +holder. Pair `decimals` with the raw amounts from +[Fetch an account's token balances](/sdk-and-tools/sdk-js/cookbook/tokens/fetch-account-token-balances) +to display real numbers. + +**Fungible vs collection are different calls.** A fungible identifier +(`WEGLD-bd4d79`) goes to `getDefinitionOfFungibleToken`; a collection identifier +(`MEDAL-ae074f`) goes to `getDefinitionOfTokenCollection`. The collection's `type` +field (`NonFungibleESDT`, `SemiFungibleESDT`, `MetaESDT`) tells you which kind it +is. + +**The `can*` flags mirror what you set at issuance.** `canFreeze`, `canWipe`, +`canPause`, `canUpgrade`, `canAddSpecialRoles` are the same properties +[Issue a fungible token](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token) +and [Issue an NFT collection](/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection) +configure, this is how you read them back. + +## Pitfalls + +:::warning[Pitfall 1: the collection call takes the collection id, not an NFT id] +`getDefinitionOfTokenCollection` expects `MEDAL-ae074f`, the collection +identifier, not `MEDAL-ae074f-01`, a single NFT's extended id. If you only have an +NFT's extended identifier, strip the nonce suffix with +`TokenComputer.extractIdentifierFromExtendedIdentifier()` first. +::: + +:::note[Pitfall 2: the property flag is canTransferNftCreateRole (lowercase Nft)] +On `DefinitionOfTokenCollectionOnNetwork` the flag is spelled +`canTransferNftCreateRole`, lowercase "Nft". The issuance factory uses +`canTransferNFTCreateRole` (all-caps "NFT") for the same property. Read-side and +write-side disagree on casing; match whichever class you are actually touching +(confirmed against the installed types). +::: + +:::note[Pitfall 3: supply is a BigNumber, decimals is a number] +On the fungible definition, `supply` is a bignumber.js `BigNumber` (call +`.toString()`), while `decimals` is a plain `number`. Do not assume both are the +same numeric type. +::: + +## See also + +- [Fetch an account's token balances](/sdk-and-tools/sdk-js/cookbook/tokens/fetch-account-token-balances) + holds the raw amounts whose `decimals` this recipe supplies. +- [Issue a fungible token](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token) + is the write side; this recipe reads back what it configures. +- [Issue an NFT collection](/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection) + is the collection whose definition this recipe reads. + +--- + ### Final Code Complete crowdfunding smart contract implementation with all features. @@ -19583,6 +24824,14 @@ Proxy/Gateway endpoints are referred as `https://gateway.multiversx.com/....`, w Currently, authentication is not needed to access the API. +## **Rate Limits** + +The public Gateway endpoints use a rate-limiting mechanism to ensure infrastructure stability and fair resource distribution. The limitations are as follows: + +* **gateway.multiversx.com (_Mainnet_):** Maximum of **50 requests / IP / second**. +* **devnet-gateway.multiversx.com (_Devnet_):** Maximum of **50 requests / IP / second**. + + ## **HTTP Response format** Each request against the MultiversX API will resolve to a JSON response having the following structure: @@ -19629,6 +24878,158 @@ In the case of an **error**, the `data` field is unset, the `error` field contai :::important When describing each HTTP endpoint on the following pages, the basic structure of the response is **simplified for brevity,** and, in general, only the actual payload of the response is depicted. ::: +``` + +--- + +### Generate a mnemonic + derive keys + +Generate a BIP39 mnemonic and derive secret keys from it at different address +indices, fully offline. There is no network call anywhere in this recipe: +mnemonic generation and key derivation are pure local cryptography. No devnet +wallet, no funds, no gas. + +One mnemonic is the root of many accounts: `deriveKey(0)`, `deriveKey(1)`, and so +on each produce a distinct key and address. Because derivation is deterministic, +the same phrase always regenerates the same keys, which is exactly what makes a +mnemonic a portable backup. + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates its own throwaway mnemonic and never + touches the network. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/generate-mnemonic-derive-keys +npm install +npm run build +npm start +``` + +## The code + +```ts title="src/index.ts" +// src/index.ts — generate a mnemonic and derive secret keys from it at +// several address indices, then show the derivation is deterministic. +// +// Fully offline. No devnet, no network call of any kind — mnemonic +// generation and key derivation are pure local cryptography (BIP39 +// entropy -> words, then a deterministic derivation to Ed25519 keys). A +// fresh mnemonic is generated on every run, so exact words/addresses differ +// run to run; the shape of the output does not. +// +// Modeled on the mx-sdk-js-core cookbook source (cookbook/basics.ts, +// "Generating a mnemonic" / "Deriving secret keys from a mnemonic"). + +import { Mnemonic } from '@multiversx/sdk-core'; + +function main(): void { + // === 1. Generate a fresh 24-word mnemonic. === + // This is the ROOT SECRET. Anyone holding these words controls every + // account derived from them. Real code must never log a mnemonic; this + // recipe prints only the count and the first/last word because the phrase + // is a throwaway that is never funded. + const mnemonic = Mnemonic.generate(); + const words = mnemonic.getWords(); + console.log(`Generated a ${words.length}-word mnemonic.`); + console.log( + `First word: "${words[0] ?? ''}", last word: "${words[words.length - 1] ?? ''}".`, + ); + + // === 2. One mnemonic derives many accounts, selected by addressIndex. === + // deriveKey(i) walks the MultiversX derivation path at account index i and + // returns a UserSecretKey; its public key gives the on-chain Address. + console.log('\nAddresses derived from this one mnemonic:'); + for (const addressIndex of [0, 1, 2]) { + const secretKey = mnemonic.deriveKey(addressIndex); + const address = secretKey.generatePublicKey().toAddress(); + console.log(` addressIndex ${addressIndex} -> ${address.toBech32()}`); + } + + // === 3. Derivation is deterministic. === + // Re-importing the same phrase with Mnemonic.fromString and deriving at the + // same index yields the same key/address every time — this is what makes a + // mnemonic a portable backup. + const restored = Mnemonic.fromString(mnemonic.toString()); + const original0 = mnemonic.deriveKey(0).generatePublicKey().toAddress().toBech32(); + const restored0 = restored.deriveKey(0).generatePublicKey().toAddress().toBech32(); + console.log(`\nRe-derived from the same phrase (addressIndex 0) matches: ${original0 === restored0}`); + + // === 4. deriveKey() defaults to addressIndex 0. === + const defaultAddress = mnemonic.deriveKey().generatePublicKey().toAddress().toBech32(); + console.log(`deriveKey() with no argument == deriveKey(0): ${defaultAddress === original0}`); + + console.log('\nExpected: three distinct addresses, then two "true" lines.'); +} + +main(); +``` + +## Run it + +A fresh mnemonic is generated every run, so the exact words and addresses differ; +the shape does not. Three distinct addresses, then two `true` lines: + +```text +Generated a 24-word mnemonic. +First word: "lesson", last word: "grief". + +Addresses derived from this one mnemonic: + addressIndex 0 -> erd1f03w2cg7lxx8k8e9dafw3rxe092qhzlceq3xv6rqsj4kn5rsq6fq086964 + addressIndex 1 -> erd1r64f3gmpq23zf634x80evgfcpa5n8vt9jtmpqgtd5wuwwkez94usxeke2q + addressIndex 2 -> erd1ulvfmlh43ejl42jngekfu2pn2gugrewrxxgwgzmrf4h3flywqgeqkkl78g + +Re-derived from the same phrase (addressIndex 0) matches: true +deriveKey() with no argument == deriveKey(0): true +``` + +## How it works + +Grounded in the installed `@multiversx/sdk-core` v15.4.1 `Mnemonic` class: + +1. **Generate.** `Mnemonic.generate()` returns a fresh 24-word phrase; + `getWords()` lists the words. +2. **Derive at indices.** `mnemonic.deriveKey(addressIndex)` returns a + `UserSecretKey`; its `generatePublicKey().toAddress()` gives the on-chain + `Address`. One mnemonic backs many accounts, selected by `addressIndex`. +3. **Determinism.** `Mnemonic.fromString(phrase)` re-imports the phrase and + derives the identical key/address. +4. **Default index.** `deriveKey()` with no argument is `deriveKey(0)`. + +## Pitfalls + +:::danger[Pitfall 1: a mnemonic is the root secret, never log it in real code] +Anyone holding these 24 words controls every account derived from them. This +recipe prints only the word count and first/last word because the phrase is a +throwaway that is never funded. In production, treat the phrase like a private +key: never log it, never send it over the wire. +::: + +:::note[Pitfall 2: addressIndex selects the account and is not a passphrase] +`deriveKey(0)`, `deriveKey(1)`, `deriveKey(2)` are three different accounts from +the *same* mnemonic. The optional second argument, `deriveKey(addressIndex, +password)`, is a BIP39 passphrase (a "25th word"), a separate concept. Leave it +unset unless you deliberately use one. +::: + +:::tip[Pitfall 3: this gives you a key, not a funded account] +Deriving a key is free and offline; the account does not exist on-chain until it +receives EGLD. Fetching its nonce or balance needs a network provider, see +[Fetch an account's on-chain state](/sdk-and-tools/sdk-js/cookbook/accounts/fetch-account-state). +::: + +## See also + +- [Create an Account from a KeyPair, secret key, or mnemonic](/sdk-and-tools/sdk-js/cookbook/accounts/account-from-keys) + turns a derived key into an `Account`. +- [Save and load a keystore](/sdk-and-tools/sdk-js/cookbook/accounts/keystore-save-load) + encrypts a mnemonic (or a single key) at rest. +- [Save and load a PEM (dev only)](/sdk-and-tools/sdk-js/cookbook/accounts/pem-save-load) + is the unencrypted dev-wallet format. --- @@ -19995,6 +25396,211 @@ In the future we want to publish the codebase to MultiversX TCS so that third pa --- +### Guard and unguard an account + +Once a guardian is nominated (see +[Set a guardian](/sdk-and-tools/sdk-js/cookbook/accounts/set-guardian)), two +transactions toggle protection: + +- **`GuardAccount`** activates guardianship. From here on, every transaction from + the account must carry the guardian's co-signature. +- **`UnGuardAccount`** removes guardianship. Because the account is guarded when + you run this, the unguard transaction itself must be co-signed by the current + guardian. + +This recipe builds both with sdk-core's `AccountController` (and factory), decodes +the payloads, and shows the guarded flag being set on the unguard. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites. **No devnet EGLD required**, see "How it works". + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/guard-unguard-account +npm install +npm run build +npm start -- ./wallet.pem --guardian erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th +``` + +## The code + +```ts title="src/guard.ts" +// src/guard.ts — the subject of this recipe: turning guardianship on and off +// once a guardian has been nominated (see set-guardian for the nomination). +// +// - GuardAccount — activate guardianship. From here on, every transaction +// from this account must carry the guardian's co-signature. +// - UnGuardAccount — remove guardianship. Because the account is guarded +// when you run this, the UnGuardAccount transaction ITSELF must be +// co-signed by the current guardian — otherwise a stolen primary key +// could simply unguard the account and bypass the guardian entirely. +// +// Both are builtin functions on the caller's own account, so the receiver is +// the sender. Note the wire spelling: `GuardAccount` and `UnGuardAccount` +// (capital G in "Guard"). + +import { Address, TransactionComputer } from '@multiversx/sdk-core'; +import type { Account, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +/** Build a GuardAccount transaction (activate guardianship). */ +export async function guardAccount( + entrypoint: DevnetEntrypoint, + sender: Account, + useFactory: boolean, +): Promise { + if (useFactory) { + const factory = entrypoint.createAccountTransactionsFactory(); + const transaction = await factory.createTransactionForGuardingAccount(sender.address); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; + } + + const controller = entrypoint.createAccountController(); + return controller.createTransactionForGuardingAccount(sender, sender.getNonceThenIncrement(), {}); +} + +/** + * Build an UnGuardAccount transaction (remove guardianship). + * + * Pass the current `guardian` to have the SDK mark the transaction as guarded + * (version stays 2, options gets the TX_GUARDED bit, and `guardian` is set), + * ready for the guardian's co-signature in `guardianSignature`. Without it, + * the network rejects the unguard on a guarded account. + */ +export async function unguardAccount( + entrypoint: DevnetEntrypoint, + sender: Account, + guardian: Address | undefined, + useFactory: boolean, +): Promise { + if (useFactory) { + const factory = entrypoint.createAccountTransactionsFactory(); + const transaction = await factory.createTransactionForUnguardingAccount( + sender.address, + guardian ? { guardian } : {}, + ); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; + } + + const controller = entrypoint.createAccountController(); + return controller.createTransactionForUnguardingAccount( + sender, + sender.getNonceThenIncrement(), + guardian ? { guardian } : {}, + ); +} + +/** Decode a guard/unguard transaction's shape, including the guarded bit. */ +export function describeGuardPayload(transaction: Transaction): { + function: string; + receiver: string; + options: number; + isGuardedBitSet: boolean; + guardian: string; + gasLimit: string; +} { + const guardianBech = transaction.guardian.isEmpty() ? '(none)' : transaction.guardian.toBech32(); + // The TX_GUARDED options constant is not re-exported from the sdk-core + // barrel, so read the flag through TransactionComputer instead of + // hardcoding the bit. + const computer = new TransactionComputer(); + return { + function: Buffer.from(transaction.data).toString().split('@')[0] ?? '', + receiver: transaction.receiver.toBech32(), + options: transaction.options, + isGuardedBitSet: computer.hasOptionsSetForGuardedTransaction(transaction), + guardian: guardianBech, + gasLimit: transaction.gasLimit.toString(), + }; +} +``` + +## Run it + +```bash +npm start -- [--guardian ] [--factory] +``` + +A real captured run (unfunded wallet, guardian supplied): + +```text +GUARD ACCOUNT (activate): + function: GuardAccount + receiver: erd18l2cww3g3g6uc35wy60efd87ss85xdqj9gh9qn6ga6halz3jscaqd4dl3k + options: 0 (guarded bit set: false) + guardian: (none) + gasLimit: 318000 + +UNGUARD ACCOUNT (co-signed by current guardian): + function: UnGuardAccount + options: 2 (guarded bit set: true) + guardian: erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th + gasLimit: 371000 + +Broadcasting the GuardAccount transaction... +Rejected (expected for an unfunded wallet): ... insufficient funds for address erd18l2c... +``` + +## How it works + +Both are builtin functions on the caller's own account, so the receiver is the +**sender**. `GuardAccount` carries no arguments; its gas was `318000`, matching +`50,000 + 1,500 × 12 (data bytes) + 250,000`. + +The interesting one is `UnGuardAccount`. Passing the current guardian marks the +transaction as guarded: `options` becomes `2` (the `TX_GUARDED` bit), `guardian` +is populated, and the SDK adds the `50,000` guarded-transaction gas premium, +`371000` total versus the `321000` an unguarded build would cost. This is the +network's protection: a stolen primary key alone cannot lift guardianship, +because the unguard must itself be co-signed by the guardian being removed. Add +that co-signature with the guardian's key (see +[Apply a guardian to a transaction](/sdk-and-tools/sdk-js/cookbook/accounts/apply-guardian-to-transaction)) +before broadcasting a real unguard. + +For built-in function detail, see +[docs.multiversx.com/developers/built-in-functions](https://docs.multiversx.com/developers/built-in-functions/). + +## Pitfalls + +:::danger[Pitfall 1: unguarding a guarded account needs the guardian's co-signature] +Send `UnGuardAccount` from the primary key alone and the network rejects it. Pass +the current guardian so the transaction is marked guarded, then attach the +`guardianSignature`. +::: + +:::warning[Pitfall 2: the guarded unguard costs an extra 50,000 gas] +Because it is itself a guarded transaction. The controller adds it automatically +when a guardian is present (`371000` vs `321000` here); budget for it on a +hand-built transaction. +::: + +:::note[Pitfall 3: the TX_GUARDED options constant is not exported from the barrel] +`TRANSACTION_OPTIONS_TX_GUARDED` is internal to sdk-core. Read the flag through +`TransactionComputer.hasOptionsSetForGuardedTransaction(tx)` instead of +hardcoding `2`, as this recipe does. +::: + +## See also + +- [Set a guardian on an account](/sdk-and-tools/sdk-js/cookbook/accounts/set-guardian) + nominates the guardian you activate here. +- [Apply a guardian to a transaction](/sdk-and-tools/sdk-js/cookbook/accounts/apply-guardian-to-transaction) + attaches the guardian co-signature the unguard (and every guarded transaction) + needs. +- [Manage nonces (fetch-then-increment)](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) + is the nonce-fetch pattern this recipe uses. + +--- + ### Guardians ## Introduction @@ -20057,6 +25663,202 @@ For a working example see the code used in [signing-providers](https://github.co --- +### Hash-signing a transaction + +Opt a transaction into **hash signing**, assert the version and options bits are +set, and sign it correctly by signing the transaction *hash*. Along the way this +recipe demonstrates a real v15.4.1 trap: `Account.signTransaction` does **not** +honor the hash-signing option, so its output fails verification. Fully offline, +nothing is broadcast. + +Hash signing sets the least-significant bit of the transaction `options` field. +When set, the signature must be over the **keccak-256 hash** of the serialized +transaction, not over the serialized bytes themselves. It lets a signer (for +example a hardware wallet) sign a short fixed-size digest instead of an +arbitrarily long payload. + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates its own throwaway keys and never touches + the network. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/hash-signing-transaction +npm install +npm run build +npm start +``` + +## The code + +```ts title="src/index.ts" +// src/index.ts — opt a transaction into HASH SIGNING, assert the version and +// options bits are set, and sign it correctly by signing the transaction +// HASH. Also demonstrates a real v15.4.1 trap: Account.signTransaction does +// NOT honor the hash-signing option, so its output fails verification. +// +// Fully offline. No devnet, no broadcast — this builds and signs locally and +// asserts on the bytes. Nothing is sent. Fresh keys each run. +// +// Background: hash signing sets the least-significant bit of the transaction +// `options` field. When set, the signature must be over the KECCAK-256 hash +// of the serialized transaction, not over the serialized bytes themselves. +// It lets a signer (e.g. a hardware wallet) sign a short fixed-size digest +// instead of an arbitrarily long payload. +// +// Grounded in the installed @multiversx/sdk-core v15.4.1: TransactionComputer +// (core/transactionComputer.d.ts) and Account (accounts/account.d.ts). + +import { Account, Mnemonic, Transaction, TransactionComputer } from '@multiversx/sdk-core'; +import { strict as assert } from 'node:assert'; + +async function main(): Promise { + const account = Account.newFromMnemonic(Mnemonic.generate().toString(), 0); + const other = Account.newFromMnemonic(Mnemonic.generate().toString(), 0); + const transactionComputer = new TransactionComputer(); + + const transaction = new Transaction({ + nonce: 7n, + value: 0n, + sender: account.address, + receiver: other.address, + gasLimit: 50000n, + chainID: 'D', + }); + + // === 1. Before: a default transaction is version 2, options 0. === + console.log(`1. Before: version=${transaction.version}, options=${transaction.options}`); + console.log(` hasOptionsSetForHashSigning: ${transactionComputer.hasOptionsSetForHashSigning(transaction)}`); + + // === 2. Opt into hash signing and assert the bits. === + // applyOptionsForHashSigning sets the options LSB (0b0001) and ensures + // version >= 2. This is the core of the recipe: proving the flags land. + transactionComputer.applyOptionsForHashSigning(transaction); + console.log(`\n2. After applyOptionsForHashSigning: version=${transaction.version}, options=${transaction.options}`); + + assert.equal(transaction.version, 2, 'version must be >= 2 for options to be honored'); + assert.equal(transaction.options & 0b0001, 0b0001, 'the hash-signing bit (0b0001) must be set'); + assert.equal(transactionComputer.hasOptionsSetForHashSigning(transaction), true); + // The hash-signing bit and the guarded bit (0b0010) are independent. + assert.equal(transactionComputer.hasOptionsSetForGuardedTransaction(transaction), false); + console.log(' Asserted: version === 2, options bit 0b0001 set, hasOptionsSetForHashSigning === true.'); + + // === 3. Sign CORRECTLY: sign the hash, verify against it. === + // computeHashForSigning returns the keccak-256 digest (32 bytes). + // computeBytesForVerifying already returns that same hash when the option + // is set, so signing the hash and verifying line up. + const hashForSigning = transactionComputer.computeHashForSigning(transaction); + console.log(`\n3. computeHashForSigning length: ${hashForSigning.length} bytes (keccak-256).`); + transaction.signature = account.secretKey.sign(hashForSigning); + const correct = await account.publicKey.verify( + transactionComputer.computeBytesForVerifying(transaction), + transaction.signature, + ); + console.log(` Sign the hash -> verify: ${correct} (expected true)`); + + // === 4. THE TRAP: Account.signTransaction does NOT honor the option. === + // Account.signTransaction serializes with computeBytesForSigning, which in + // v15.4.1 does NOT branch on the hash-signing option — it returns the full + // JSON bytes, not the hash. verifyTransactionSignature DOES branch (it + // verifies against the hash). So the two disagree and verification fails. + const trapSignature = await account.signTransaction(transaction); + const trapVerify = await account.verifyTransactionSignature(transaction, trapSignature); + console.log(`\n4. TRAP: account.signTransaction -> account.verifyTransactionSignature: ${trapVerify} (expected false!)`); + + const signingBytes = transactionComputer.computeBytesForSigning(transaction); + const verifyingBytes = transactionComputer.computeBytesForVerifying(transaction); + console.log( + ` Why: computeBytesForSigning is ${signingBytes.length} bytes (full JSON) but computeBytesForVerifying is ${verifyingBytes.length} bytes (the hash) — they differ.`, + ); + assert.equal(trapVerify, false, 'demonstrates the documented trap'); + assert.equal(correct, true, 'the manual hash-signing path is the correct one'); + + console.log('\nExpected: step 3 true (sign the hash), step 4 false (the trap). Use the step-3 path in real code.'); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +Keys are fresh each run; the true/false pattern and the byte counts are stable: + +```text +1. Before: version=2, options=0 + hasOptionsSetForHashSigning: false + +2. After applyOptionsForHashSigning: version=2, options=1 + Asserted: version === 2, options bit 0b0001 set, hasOptionsSetForHashSigning === true. + +3. computeHashForSigning length: 32 bytes (keccak-256). + Sign the hash -> verify: true (expected true) + +4. TRAP: account.signTransaction -> account.verifyTransactionSignature: false (expected false!) + Why: computeBytesForSigning is 250 bytes (full JSON) but computeBytesForVerifying is 32 bytes (the hash) — they differ. +``` + +The source's `assert` calls turn this behavior, including the trap, into hard +test assertions, so the run fails loudly if the SDK ever changes. + +## How it works + +Grounded in the installed `@multiversx/sdk-core` v15.4.1 `TransactionComputer` +(`core/transactionComputer.d.ts`) and `Account` (`accounts/account.d.ts`): + +1. **Before.** A default transaction is `version: 2`, `options: 0`. +2. **Opt in and assert.** `applyOptionsForHashSigning(tx)` sets the options bit + `0b0001` and ensures `version >= 2`. The recipe asserts `version === 2`, that + bit `0b0001` is set, and that `hasOptionsSetForHashSigning(tx)` is `true` (the + independent guarded bit `0b0010` stays unset). This is the core deliverable, + proving the flags land. +3. **Sign correctly.** `computeHashForSigning(tx)` returns the 32-byte keccak-256 + digest. `computeBytesForVerifying(tx)` already returns that same hash when the + option is set, so signing the hash and verifying against it line up: `true`. +4. **The trap.** `account.signTransaction(tx)` then + `account.verifyTransactionSignature(tx, sig)` returns `false`. + +## Pitfalls + +:::danger[Pitfall 1: Account.signTransaction does NOT honor the hash-signing option (v15.4.1)] +It serializes with `computeBytesForSigning`, which does not branch on the option +and returns the full ~250-byte JSON. But `computeBytesForVerifying` *does* branch +and returns the 32-byte hash. The two disagree, so a hash-flagged transaction +signed with `account.signTransaction` fails its own `verifyTransactionSignature`, +and would be rejected by the network. **Sign `computeHashForSigning(tx)` +directly instead** (step 3). +::: + +:::warning[Pitfall 2: options is a bitfield, do not overwrite it] +`applyOptionsForHashSigning` OR-s in bit `0b0001`; the guarded flag is `0b0010`. +Setting `tx.options = 1` by hand would clobber a guarded flag. Use the `apply...` +/ `hasOptionsSet...` helpers rather than assigning the field. +::: + +:::note[Pitfall 3: applyOptionsForHashSigning bumps the version if needed] +Options are only honored at `version >= 2`. The helper raises the version for +you; if you set the bit by hand on a `version: 1` transaction, the network +ignores the options entirely. +::: + +## See also + +- [Sign + verify a transaction (offline)](/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-transaction) + is the ordinary (non-hash) signing path, where signing and verifying bytes + match. +- [Sign a message + verify a signature](/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-message) + is message signing, a related but distinct primitive family. +- [Build a relayed v3 transaction](/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction) + is another advanced transaction shape using the `options`/`version` machinery. + +--- + ### How to fix Elasticsearch mapping errors Starting with the February 2023 mainnet upgrade new constrains for Elasticsearch indices were added. Therefore, one can notice @@ -20682,6 +26484,712 @@ ESDT tokens can be issued, owned and held by any account on the MultiversX netwo --- +### Issue a fungible ESDT + +Issue a new fungible ESDT token using sdk-core's `TokenManagementController` +directly against `DevnetEntrypoint`. + +If instead you are building a browser dApp where the end user's own wallet should +sign the issuance, wire the same `TokenManagementTransactionsFactory` methods +through a connected-wallet flow instead of a PEM-holding script. This recipe is +for when **your own code holds the private key**. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites for how to generate a throwaway one. **No devnet EGLD required** + to verify the payload, see "How it works" below. (Issuing for real costs a fixed + 0.05 EGLD plus gas.) + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/issue-fungible-token +npm install +npm run build +npm start -- ./wallet.pem CookbookToken COOK 1000000000000 6 +``` + +## Issuing + +```ts title="src/issueToken.ts" +// src/issueToken.ts — the actual subject of this recipe: issuing a +// fungible ESDT token via sdk-core's TokenManagementController. +// +// Three confirmed, real discrepancies between a commonly-copied issuance +// snippet and the actually-installed SDK, found by reading +// node_modules/@multiversx/sdk-core/out/tokenManagement/resources.d.ts +// directly (all worth being explicit about, the same way other recipes +// flag the ESDT-casing and Account.signTransaction() gaps): +// +// 1. A commonly-copied snippet includes +// `canTransferNftCreateRole: false`. The real type, +// `IssueFungibleInput = IssueInput & { initialSupply: bigint; numDecimals: bigint }`, +// has NO such field anywhere in `IssueInput` — it doesn't exist for +// fungible tokens at all (this makes sense: an NFT-create role is +// meaningless on a token with no NFT nonce). Including it is a +// `tsc --strict` "object literal may only specify known properties" +// error, not just a style nit. This recipe's options object omits it +// entirely, matching the real type and the real +// mx-sdk-js-core cookbook/tokens.ts example. +// 2. (Relevant to the sibling "Issue an NFT collection" recipe, not this +// one — the field DOES exist for NFT/SFT issuance, spelled +// `canTransferNFTCreateRole`, all-caps "NFT".) +// 3. (Also relevant to the sibling NFT recipe's mint step, not this +// one — `royalties` is a plain `number`, not `bigint`.) +// +// Issuing a token costs a fixed 0.05 EGLD PLUS gas — this recipe's own +// devnet verification (an intentionally unfunded wallet) confirms the +// network rejects for insufficient funds at that combined cost, proving +// the issuance payload itself is well-formed without needing to actually +// spend real EGLD. + +import type { Account, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export interface IssueTokenInput { + tokenName: string; + tokenTicker: string; + /** Total supply in the token's own smallest denomination (see numDecimals). */ + initialSupply: bigint; + /** This token's own decimal count — chosen by the issuer, not fixed like EGLD's 18. */ + numDecimals: bigint; +} + +export interface IssueTokenOutput { + txHash: string; +} + +/** + * Issues a new fungible ESDT token. `sender.nonce` must already reflect + * the network's current nonce — see src/account.ts's loadDevnetAccount. + */ +export async function issueFungibleToken( + entrypoint: DevnetEntrypoint, + sender: Account, + input: IssueTokenInput, +): Promise { + const controller = entrypoint.createTokenManagementController(); + + const transaction = await controller.createTransactionForIssuingFungible( + sender, + sender.getNonceThenIncrement(), + { + tokenName: input.tokenName, + tokenTicker: input.tokenTicker, + initialSupply: input.initialSupply, + numDecimals: input.numDecimals, + canFreeze: false, + canWipe: true, + canPause: true, + canChangeOwner: true, + canUpgrade: true, + canAddSpecialRoles: false, + // No canTransferNftCreateRole field here — see the file header: + // it does not exist on IssueFungibleInput at all. + }, + ); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash }; +} +``` + +## Run it + +```bash +npm start -- +``` + +Expected output: + +```text +Sender: erd1... (nonce 0) +Token name: CookbookToken +Token ticker: COOK +Initial supply: 1000000000000 (6 decimals) +Issue cost: 0.05 EGLD + gas + +========== +IMPORTANT! +========== +You are about to issue (register) a new token. This will set the role "ESDTRoleBurnForAll" (globally). +Once the token is registered, you can unset this role by calling "unsetBurnRoleGlobally" (in a separate transaction). +``` + +(An unfunded wallet gets a clean "insufficient funds" rejection after the notice, +instead of a hash, see "How it works.") + +## How it works + +**Three confirmed, real discrepancies between a commonly-copied "Issue Fungible +Token" snippet and the actually-installed SDK**, found by reading +`node_modules/@multiversx/sdk-core/out/tokenManagement/resources.d.ts` directly: + +1. The naive snippet includes `canTransferNftCreateRole: false`. The real type, + `IssueFungibleInput = IssueInput & { initialSupply: bigint; numDecimals: bigint }`, + has **no such field anywhere** in `IssueInput` for fungible tokens, an + NFT-create role is meaningless on a token with no NFT nonce. Including it is a + `tsc --strict` excess-property error. Confirmed by hand-decoding the real, + logged issuance payload, the wire format has no such field either. +2. The field DOES exist for NFT/SFT issuance, spelled `canTransferNFTCreateRole` + (all-caps "NFT"), see + [Issue an NFT collection + mint an NFT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection). +3. `royalties` (used only in the NFT mint step) is a plain `number`, not a + `bigint`. + +**Issuance costs a fixed 0.05 EGLD, sent to the ESDT system contract, plus gas for +the data payload.** Verified by hand-decoding a real logged issuance request: +`value: "50000000000000000"` (exactly 0.05 EGLD), +`receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u` (the +ESDT system contract address), and `gasLimit: 60426500`, matching +`50,000 + 1,500 x 251 (data bytes) + 60,000,000 (gasLimitIssue)` to the unit. The +decoded `data` field (`issue@436f6f6b626f6f6b546f6b656e@434f4f4b@e8d4a51000@06@...`) +confirms every flag encoded correctly, with no `canTransferNftCreateRole` anywhere +in the wire payload. + +**The SDK itself prints an unconditional warning on every issuance call**, traced +to `notifyAboutUnsettingBurnRoleGlobally()`, called at the start of all 5 issuance +methods (fungible, semi-fungible, non-fungible, meta-ESDT, register-and-set-roles), +not specific to this recipe's flag combination. +`createTransactionForUnsettingBurnRoleGlobally(...)` is the escape hatch it +mentions. + +## Pitfalls + +:::danger[Pitfall 1: canTransferNftCreateRole does not exist for fungible tokens] +Copying a naive fungible-issuance snippet that includes it fails `tsc --strict`, +see "How it works" above. +::: + +:::note[Pitfall 2: the burn-role notice is informational only] +It prints client-side and does not indicate anything went wrong, it appears even +when this recipe's unfunded wallet then gets rejected for insufficient funds. +Worth reading once, since unsetting the role later needs its own transaction. +::: + +:::warning[Pitfall 3: numDecimals is chosen once, at issuance, and cannot change later] +Unlike EGLD's fixed 18, an ESDT's decimal count is whatever you pass here, see +[Send an ESDT](/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt)'s Pitfall 1 +for why getting this wrong later is a real footgun. +::: + +## See also + +- [Issue an NFT collection + mint an NFT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection) + is the non-fungible sibling, including the `canTransferNFTCreateRole` casing this + recipe omits. +- [Send an ESDT](/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt) + spends a token once it is issued. +- [Multi-token transfer in one tx](/sdk-and-tools/sdk-js/cookbook/transactions/multi-token-transfer) + sends several tokens together. + +--- + +### Issue an NFT collection + mint an NFT + +Two steps, both via sdk-core's `TokenManagementController`: issue a new NFT +collection, then create (mint) one NFT into it. Per `mx-sdk-js-core`'s own +`cookbook/tokens.ts`, no separate "set special role" transaction is needed in +between, issuing your own collection automatically grants you the NFT-create role +on it. + +If instead you are building a browser dApp where the end user's own wallet should +sign, wire the same `TokenManagementTransactionsFactory` methods through a +connected-wallet flow. This recipe is for when **your own code holds the private +key**. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites for how to generate a throwaway one. **No devnet EGLD required** to + verify both payloads, see "How it works" below. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/issue-nft-collection +npm install +npm run build +npm start -- ./wallet.pem CookbookNFT CNFT +``` + +## Issuing and minting + +```ts title="src/issueNftCollection.ts" +// src/issueNftCollection.ts — the actual subject of this recipe: issuing +// an NFT collection, then creating (minting) one NFT into it. +// +// Two confirmed, real corrections to commonly-copied snippets, found by +// reading node_modules/@multiversx/sdk-core/out/tokenManagement/resources.d.ts +// and .../tokenManagementController.d.ts / tokenManagementTransactionsFactory.d.ts +// directly: +// +// 1. The field is `canTransferNFTCreateRole` — all-caps "NFT" — not +// `canTransferNftCreateRole` (a common miscasing). Confirmed: +// `IssueNonFungibleInput = IssueInput & { canTransferNFTCreateRole: boolean }`. +// (Contrast with fungible issuance, which has no such field at all — +// see the sibling "Issue a fungible ESDT" recipe.) +// 2. `royalties` is a plain `number`, not a `bigint` — a commonly-copied +// `royalties: 500n` (with the `n` suffix) does not match +// `MintInput.royalties: number`. +// +// A THIRD confirmed casing trap, the same shape as the already-documented +// `createTransactionForEsdtTokenTransfer` (Controller, lowercase "sdt") vs +// `createTransactionForESDTTokenTransfer` (Factory, uppercase "ESDT") one +// the `send-esdt` recipe flags — this is not a one-off: +// +// TokenManagementController.createTransactionForCreatingNft (lowercase "Nft") +// TokenManagementTransactionsFactory.createTransactionForCreatingNFT (uppercase "NFT") +// +// Confirmed from both .d.ts files side by side. This recipe uses the +// Controller throughout, so `createTransactionForCreatingNft` (lowercase) +// is correct here — swapping in the Factory's casing is a real +// `tsc --strict` error, not a lint nit. +// +// Per mx-sdk-js-core's own cookbook/tokens.ts: issuing an NFT collection +// and then immediately minting into it, with no separate "set special +// role" transaction in between, is the documented pattern — the issuer +// automatically receives the NFT-create role on their own freshly issued +// collection. + +import type { Account, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export interface IssueNftCollectionInput { + tokenName: string; + tokenTicker: string; +} + +export interface IssueNftCollectionOutput { + txHash: string; +} + +/** + * Issues a new NFT collection. `sender.nonce` must already reflect the + * network's current nonce — see src/account.ts's loadDevnetAccount. Once + * this transaction completes, `awaitCompletedIssueNonFungible(txHash)` + * (not called by this recipe directly against a real balance, but shown + * in mx-sdk-js-core's cookbook) parses out the real collection identifier + * — this recipe's mint step uses a placeholder instead. + */ +export async function issueNftCollection( + entrypoint: DevnetEntrypoint, + sender: Account, + input: IssueNftCollectionInput, +): Promise { + const controller = entrypoint.createTokenManagementController(); + + const transaction = await controller.createTransactionForIssuingNonFungible( + sender, + sender.getNonceThenIncrement(), + { + tokenName: input.tokenName, + tokenTicker: input.tokenTicker, + canFreeze: false, + canWipe: true, + canPause: false, + canTransferNFTCreateRole: true, + canChangeOwner: true, + canUpgrade: true, + canAddSpecialRoles: true, + }, + ); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash }; +} + +export interface MintNftInput { + /** + * The collection identifier to mint into — e.g. "COOKNFT-abcdef". In + * real usage this comes from parsing the issuance transaction's + * outcome (`awaitCompletedIssueNonFungible`), not a hand-typed value. + */ + tokenIdentifier: string; + name: string; + /** Basis points, e.g. 500 = 5%. A plain number — see the file header. */ + royalties: number; + uris: string[]; +} + +export interface MintNftOutput { + txHash: string; +} + +/** + * Creates (mints) one NFT into an already-issued collection. Note the + * Controller method name: `createTransactionForCreatingNft` (lowercase + * "Nft") — see the file header for why the Factory's + * `createTransactionForCreatingNFT` (uppercase) would be wrong here. + */ +export async function mintNft( + entrypoint: DevnetEntrypoint, + sender: Account, + input: MintNftInput, +): Promise { + const controller = entrypoint.createTokenManagementController(); + + const transaction = await controller.createTransactionForCreatingNft( + sender, + sender.getNonceThenIncrement(), + { + tokenIdentifier: input.tokenIdentifier, + initialQuantity: 1n, + name: input.name, + royalties: input.royalties, + hash: '', + attributes: Buffer.from(''), + uris: input.uris, + }, + ); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash }; +} +``` + +## Two independently-verified steps, not a real end-to-end mint + +Because an unfunded wallet's issuance transaction never actually completes +on-chain, this recipe cannot obtain a real collection identifier to mint into, +`src/index.ts` uses a **placeholder** identifier (`COOKBOOK-000000`) for the mint +step. In real usage: send the issuance transaction, parse the real identifier from +its outcome (`awaitCompletedIssueNonFungible(txHash)` returns `[{ tokenIdentifier }]`), +then pass that into `mintNft()`. Both steps are verified independently below. + +## How it works + +**Confirmed casing: `canTransferNFTCreateRole`, all-caps "NFT"**, not +`canTransferNftCreateRole` (a common miscasing). Confirmed from +`IssueNonFungibleInput = IssueInput & { canTransferNFTCreateRole: boolean }`. +Hand-decoding the real issuance payload confirms the wire format uses this exact +spelling too, the flag's literal name on the wire matches the TypeScript property +name character for character. + +**A third instance of the Controller/Factory casing trap** (already documented for +ESDT transfers in `send-esdt`'s Pitfall 2): + +```text +TokenManagementController.createTransactionForCreatingNft (lowercase "Nft") +TokenManagementTransactionsFactory.createTransactionForCreatingNFT (uppercase "NFT") +``` + +This recipe uses the Controller, so `createTransactionForCreatingNft` (lowercase) +is correct here. + +**Issuance payload, hand-decoded and gas-formula-verified.** A real logged request: +`value: "50000000000000000"` (0.05 EGLD), `gasLimit: 60503000`, matching +`50,000 + 1,500 x 302 (data bytes) + 60,000,000` to the unit. The decoded `data` +field confirms every flag encoded correctly, including +`canTransferNFTCreateRole: true`. + +**Mint payload, hand-decoded.** A real logged mint request has `receiver` set to +the **sender's own address**, not a contract. This is expected: `ESDTNFTCreate` is +a builtin function executed in the context of the token-holder/creator's own +account, not a separate contract call. The decoded `data` field, +`ESDTNFTCreate@434f4f4b424f4f4b2d303030303030@01@436f6f6b626f6f6b2054657374204e4654202331@01f4@@@...`, +confirms the collection identifier, quantity (`01` = 1), name, and `01f4` (hex for +decimal `500`), confirming `royalties: 500` (a plain **number**, not `500n`) +encodes correctly. A commonly-copied `royalties: 500n` snippet does not match +`MintInput.royalties: number` and fails `tsc --strict`. + +## Pitfalls + +:::danger[Pitfall 1: canTransferNftCreateRole (the miscasing) fails tsc --strict] +The real field is `canTransferNFTCreateRole` (all-caps "NFT"). Contrast with +fungible issuance, where the field does not exist at all, see +[Issue a fungible ESDT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token). +::: + +:::warning[Pitfall 2: createTransactionForCreatingNft (Controller) vs createTransactionForCreatingNFT (Factory)] +Mixing up the casing for the API level you are using is a real `tsc --strict` +error, not a lint nit, a third confirmed instance of this SDK's recurring +Controller/Factory casing-mismatch pattern. +::: + +:::danger[Pitfall 3: royalties is a plain number, not a bigint] +`500n` fails `tsc --strict` against `MintInput.royalties: number`. +::: + +:::note[Pitfall 4: this recipe's mint step uses a placeholder token identifier] +It cannot mint into a collection that was never actually issued (the unfunded +wallet's issuance transaction is rejected before it reaches the network). See "Two +independently-verified steps" above for the real-usage sequence. +::: + +## See also + +- [Issue a fungible ESDT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token) + is the fungible sibling, including why `canTransferNftCreateRole` does not apply + there at all. +- The general Controller-vs-Factory casing pattern also applies to smart contract + calls, covered in the smart-contract recipes. +- [Multi-token transfer in one tx](/sdk-and-tools/sdk-js/cookbook/transactions/multi-token-transfer) + sends an NFT (once minted) alongside other tokens in a single transaction. + +--- + +### Issue an SFT collection + create, add, and burn quantity + +A semi-fungible token (SFT) is a fungible **quantity** living at an NFT-style +nonce inside a collection: 100 identical festival tickets, 50 copies of an in-game +item. This recipe walks the full lifecycle with sdk-core's +`TokenManagementController`: + +1. **Issue** the collection (`issueSemiFungible`, costs 0.05 EGLD). +2. **Create** the first batch (`ESDTNFTCreate` with quantity > 1). +3. **Add quantity** to that batch (`ESDTNFTAddQuantity`). +4. **Burn quantity** from it (`ESDTNFTBurn`). + +An SFT collection is issued with the *same* input shape as an NFT collection (both +carry `canTransferNFTCreateRole`), and an SFT is created with the *same* +`ESDTNFTCreate` builtin as an NFT, only the quantity differs. Contrast the +single-unit case in +[Issue an NFT collection](/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection). + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites. **No devnet EGLD required** to verify the payloads, see "How it + works". + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/issue-sft-collection +npm install +npm run build +npm start -- ./wallet.pem CookbookSFT CSFT +``` + +## The code + +```ts title="src/sft.ts" +// src/sft.ts — the subject of this recipe: the full semi-fungible (SFT) +// lifecycle via sdk-core's TokenManagementController. +// +// 1. issueSemiFungibleCollection — register the collection (costs 0.05 EGLD) +// 2. createSft — mint the first batch (quantity > 1) +// 3. addSftQuantity — increase an existing batch's supply +// 4. burnSftQuantity — destroy some of a batch's supply +// +// An SFT is a fungible quantity that lives at a specific NFT-style nonce +// inside a collection. Its issuance has the SAME shape as an NFT collection +// (IssueSemiFungibleInput === IssueNonFungibleInput — both carry +// `canTransferNFTCreateRole`, all-caps NFT), and creation reuses the exact +// same `ESDTNFTCreate` builtin as an NFT, only with an initialQuantity above +// 1. See issue-nft-collection for the quantity-1 case. +// +// Two casing facts baked in (see the recipe page's Pitfalls): +// - Controller: createTransactionForCreatingNft (lowercase "Nft"). +// Factory: createTransactionForCreatingNFT (all-caps "NFT"). +// - royalties is a plain number, not a bigint. + +import type { Account, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +export interface IssueSftInput { + tokenName: string; + tokenTicker: string; +} + +export interface CreateSftInput { + tokenIdentifier: string; + /** How many units of this new batch to mint — the point of an SFT (> 1). */ + initialQuantity: bigint; + name: string; + /** Basis points, e.g. 500 = 5%. A plain number, NOT a bigint. */ + royalties: number; + uris: string[]; +} + +export interface QuantityInput { + tokenIdentifier: string; + /** The nonce of the batch to change (from the create step's outcome). */ + tokenNonce: bigint; + quantity: bigint; +} + +/** Step 1 — register the SFT collection. Costs a fixed 0.05 EGLD + gas. */ +export async function issueSemiFungibleCollection( + entrypoint: DevnetEntrypoint, + sender: Account, + input: IssueSftInput, +): Promise { + const controller = entrypoint.createTokenManagementController(); + return controller.createTransactionForIssuingSemiFungible(sender, sender.getNonceThenIncrement(), { + tokenName: input.tokenName, + tokenTicker: input.tokenTicker, + canFreeze: true, + canWipe: true, + canPause: true, + // All-caps "NFT" — the field exists for SFT/NFT issuance, unlike + // fungible issuance where it does not exist at all. + canTransferNFTCreateRole: true, + canChangeOwner: true, + canUpgrade: true, + canAddSpecialRoles: true, + }); +} + +/** Step 2 — create (mint) the first batch. Needs the ESDTRoleNFTCreate role. */ +export async function createSft( + entrypoint: DevnetEntrypoint, + sender: Account, + input: CreateSftInput, +): Promise { + const controller = entrypoint.createTokenManagementController(); + // createTransactionForCreatingNft — lowercase "Nft" on the controller. + return controller.createTransactionForCreatingNft(sender, sender.getNonceThenIncrement(), { + tokenIdentifier: input.tokenIdentifier, + initialQuantity: input.initialQuantity, + name: input.name, + royalties: input.royalties, // plain number + hash: '', + attributes: new Uint8Array(), + uris: input.uris, + }); +} + +/** Step 3 — add supply to an existing batch. Needs the ESDTRoleNFTAddQuantity role. */ +export async function addSftQuantity( + entrypoint: DevnetEntrypoint, + sender: Account, + input: QuantityInput, +): Promise { + const controller = entrypoint.createTokenManagementController(); + return controller.createTransactionForAddingQuantity(sender, sender.getNonceThenIncrement(), { + tokenIdentifier: input.tokenIdentifier, + tokenNonce: input.tokenNonce, + quantity: input.quantity, + }); +} + +/** Step 4 — destroy supply from a batch. Needs the ESDTRoleNFTBurn role. */ +export async function burnSftQuantity( + entrypoint: DevnetEntrypoint, + sender: Account, + input: QuantityInput, +): Promise { + const controller = entrypoint.createTokenManagementController(); + return controller.createTransactionForBurningQuantity(sender, sender.getNonceThenIncrement(), { + tokenIdentifier: input.tokenIdentifier, + tokenNonce: input.tokenNonce, + quantity: input.quantity, + }); +} + +/** Decode a transaction's `data` field into `` + hex ``. */ +export function describePayload(transaction: Transaction): { + function: string; + receiver: string; + value: string; + args: string[]; +} { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + function: parts[0] ?? '', + receiver: transaction.receiver.toBech32(), + value: transaction.value.toString(), + args: parts.slice(1), + }; +} +``` + +## Run it + +```bash +npm start -- +``` + +A real captured run (unfunded wallet), abbreviated: + +```text +ISSUE semi-fungible collection: + function: issueSemiFungible + receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + value: 50000000000000000 + +CREATE SFT (CSFT-000000, quantity 100): + function: ESDTNFTCreate + receiver: erd18l2cww3g3g6uc35wy60efd87ss85xdqj9gh9qn6ga6halz3jscaqd4dl3k + args: [435346542d303030303030, 64, 4261746368202331, 01f4, , , 6874747073...] + +ADD quantity (nonce 1, +50): ESDTNFTAddQuantity ... [CSFT..., 01, 32] +BURN quantity (nonce 1, -10): ESDTNFTBurn ... [CSFT..., 01, 0a] + +Broadcasting the issuance... +Rejected (expected for an unfunded wallet): ... insufficient funds for address erd18l2c... +``` + +## Two independently-verified steps, not a real end-to-end mint + +An unfunded wallet's issuance never lands on-chain, so there is no real collection +identifier to mint into, the create/add/burn steps use a **placeholder** +(`CSFT-000000`). In real usage: send the issuance, parse the identifier from its +outcome (`awaitCompletedIssueSemiFungible(txHash)` returns `[{ tokenIdentifier }]`), +then feed that into the create step. + +## How it works + +**Issuance** is addressed to the ESDT system contract with +`value: 50000000000000000` (exactly 0.05 EGLD), and its flags include +`canTransferNFTCreateRole` (all-caps "NFT", same as NFT issuance). + +**Create/add/burn** are builtin functions executed in the creator's own account, +so their receiver is the **sender**, not a contract, hand-verified against the +decoded payloads above: + +- `ESDTNFTCreate@@@@@@@`. + The quantity `64` is hex for 100, the whole point of an SFT. Royalties `01f4` is + hex for 500 (a plain **number**, not a bigint, `500n` fails `tsc --strict` + against `MintInput.royalties: number`). +- `ESDTNFTAddQuantity@@@` and + `ESDTNFTBurn@@@` both take the batch's **nonce** (from + the create outcome) plus an amount. + +## Pitfalls + +:::warning[Pitfall 1: createTransactionForCreatingNft (Controller) vs ...CreatingNFT (Factory)] +The controller spells it lowercase `Nft`; the factory spells it all-caps `NFT`. +Mixing them up is a `tsc --strict` error. This recipe uses the controller. See +[Issue an NFT collection](/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection)'s +Pitfall 2. +::: + +:::danger[Pitfall 2: royalties is a plain number, not a bigint] +`royalties: 500n` fails `tsc --strict` against `MintInput.royalties: number`. Use +`500`. +::: + +:::note[Pitfall 3: add/burn quantity need the matching roles] +`ESDTNFTAddQuantity` needs `ESDTRoleNFTAddQuantity` and `ESDTNFTBurn` needs +`ESDTRoleNFTBurn` on the creator address. Grant them like any special role, see +[Set and unset special roles](/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles) +(the SFT role setter is +`createTransactionForSettingSpecialRoleOnSemiFungibleToken`). +::: + +## See also + +- [Issue an NFT collection + mint an NFT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection) + is the single-unit sibling; same issuance and `ESDTNFTCreate` shape with quantity + 1. +- [Issue a fungible ESDT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token) + is the fully fungible case, and why `canTransferNFTCreateRole` does not apply + there. +- [Set and unset special roles](/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles) + grants the create / add-quantity / burn roles this recipe relies on. + +--- + ### Iterate keys ## Overview @@ -22192,945 +28700,2133 @@ Just like deploy, upgrade also comes in two flavors: --- -### logs - -This page describes the structure of the `logs` index (Elasticsearch), and also depicts a few examples of how to query it. - +### Load an ABI -## _id +Load a contract's ABI (Application Binary Interface), the description of its +endpoints, custom types, and events, three ways: from a local file, from a URL, +and by hand when no ABI file exists at all. This is the prerequisite every other +recipe in this category builds on: +[Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint), +[Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view), +and [Call a payable endpoint with EGLD](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-payable-endpoint) +all load an ABI first, the same way. -:::warning Important +This recipe never sends a transaction or calls a devnet API. Loading and +inspecting an ABI is entirely local, plus one plain HTTPS fetch for the "from a +URL" case. -**The `logs` index will be deprecated and removed in the near future.** -We recommend using the [events](/sdk-and-tools/indices/es-index-events) index, which contains all the events included in a log. +## Prerequisites -Please make the necessary updates to ensure a smooth transition. -If you need further assistance, feel free to reach out. -::: +- Node.js >= 20.13.1. +- Network access to fetch one ABI JSON file over HTTPS. -The `_id` field for this index is composed of hex-encoded hash of the transaction of the smart contract result that generated the log. +## Install +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/load-abi +npm install +npm run build +npm start +``` + +## Loading + +```ts title="src/loadAbi.ts" +// src/loadAbi.ts — three ways to load a contract's ABI, plus the lookup +// methods each one gives you afterward. +// +// A naming subtlety worth being explicit about: `AbiRegistry` is the class +// with the real implementation — a protected constructor plus the static +// `create()` factory used below. `Abi` is a subclass that adds its own public +// constructor (taking already-built `EndpointDefinition`/`CustomType` objects, +// not raw JSON, which is not what you want for loading a JSON file) but does +// NOT override `create()`, so it inherits AbiRegistry's version as-is. +// `create()` always does `new AbiRegistry(...)` internally, regardless of +// which class name you called it through — so `Abi.create(json)` does not +// actually give you an `Abi` instance despite the name; both `Abi.create(json)` +// and `AbiRegistry.create(json)` return the exact same `AbiRegistry` instance +// (this is also the declared TypeScript return type of `create()` on both +// classes). The mx-sdk-js-core cookbook uses `Abi.create(...)`; mx-template-dapp's +// shipped widgets use `AbiRegistry.create(...)`. Both are correct and +// interchangeable — this recipe uses `Abi.create(...)`. + +import * as fs from 'fs'; +import axios from 'axios'; +import { Abi } from '@multiversx/sdk-core'; +import type { AbiRegistry } from '@multiversx/sdk-core'; + +/** + * Loads a contract's ABI from a local JSON file. + */ +export function loadAbiFromFile(filePath: string): AbiRegistry { + const json = fs.readFileSync(filePath, { encoding: 'utf8' }); + return Abi.create(JSON.parse(json) as Record); +} + +/** + * Loads a contract's ABI from a URL — e.g. straight from a GitHub raw content + * URL, the same source this recipe's own src/adder.abi.json was copied from. + */ +export async function loadAbiFromUrl(url: string): Promise { + const response = await axios.get>(url); + return Abi.create(response.data); +} + +/** + * Manually constructs an ABI when no ABI file is available but the endpoint + * names and argument types are known. `foo` and `bar` are illustrative names + * only; this does not correspond to any real deployed contract. + */ +export function buildAbiManually(): AbiRegistry { + return Abi.create({ + endpoints: [ + { + name: 'foo', + inputs: [{ type: 'BigUint' }, { type: 'u32' }, { type: 'Address' }], + outputs: [{ type: 'u32' }], + }, + { + name: 'bar', + inputs: [{ type: 'counted-variadic' }, { type: 'variadic' }], + outputs: [], + }, + ], + }); +} -## Fields +export interface EndpointSummary { + name: string; + mutability: string; + inputs: string[]; + outputs: string[]; +} +/** + * Summarizes every endpoint an ABI declares: name, mutability (readonly vs + * mutable, from `EndpointModifiers.isReadonly()`), and each parameter's type + * name — using `abi.getEndpoints()` and `EndpointDefinition.input` / `.output`. + * Those field names are singular, each an array; the ABI JSON itself uses the + * plural `inputs`/`outputs`, but the parsed `EndpointDefinition` object does not. + */ +export function summarizeEndpoints(abi: AbiRegistry): EndpointSummary[] { + return abi.getEndpoints().map((endpoint) => ({ + name: endpoint.name, + mutability: endpoint.modifiers.isReadonly() ? 'readonly' : 'mutable', + inputs: endpoint.input.map((param) => `${param.name}: ${param.type.getName()}`), + outputs: endpoint.output.map((param) => param.type.getName()), + })); +} +``` -| Field | Description | -|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| address | The address field holds the address in bech32 encoding. It can be the address of the smart contract that generated the log or the address of the receiver address of the transaction. | -| events | The events field holds a list of events. | -| originalTxHash | The originalTxHash field holds the hex-encoded hash of the initial transaction. When this field is not empty the log is generated by a smart contract result and this field represents the hash of the initial transaction. | -| timestamp | The timestamp field represents the timestamp of the block in which the log was generated. | +## Run it -Event structure +```ts title="src/index.ts" +// src/index.ts — CLI entry point. Loads the same ABI three ways and prints its +// endpoint summary each time (should be identical for the file/URL cases — +// same JSON, two sources), then shows the manual-construct path with its own +// illustrative endpoints. +// +// Usage: +// npm run build && npm start +// +// No wallet, no PEM, no network write — this recipe only reads a JSON file and +// (for the URL case) fetches one over HTTPS. The recipe bundles src/adder.abi.json. +import * as path from 'path'; +import { loadAbiFromFile, loadAbiFromUrl, buildAbiManually, summarizeEndpoints } from './loadAbi'; -| Field | Description | -|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| identifier | This field represents the identifier of the event. | -| address | The address field holds the address in bech32 encoding. It can be the address of the smart contract that generated the event or the address of the receiver address of the transaction. | -| topics | The topics field holds a list with extra information. They don't have a specific order because the smart contract is free to log anything that could be helpful. | -| data | The data field can contain information added by the smart contract that generated the event. | -| order | The order field represents the index of the event indicating the execution order. | +const ADDER_ABI_URL = + 'https://raw.githubusercontent.com/multiversx/mx-sdk-js-core/main/src/testdata/adder.abi.json'; +async function main(): Promise { + console.log('--- Loading from a local file (src/adder.abi.json) ---'); + const fromFile = loadAbiFromFile(path.join(__dirname, '..', 'src', 'adder.abi.json')); + console.log(JSON.stringify(summarizeEndpoints(fromFile), null, 2)); -## Query examples + console.log(`\n--- Loading the same ABI from a URL (${ADDER_ABI_URL}) ---`); + const fromUrl = await loadAbiFromUrl(ADDER_ABI_URL); + console.log(JSON.stringify(summarizeEndpoints(fromUrl), null, 2)); + console.log('\n--- Manually constructed ABI (no file, illustrative "foo"/"bar" only) ---'); + const manual = buildAbiManually(); + console.log(JSON.stringify(summarizeEndpoints(manual), null, 2)); -### Fetch all the logs generated by a transaction + console.log('\n--- Single-endpoint lookup: abi.getEndpoint("add") ---'); + const addEndpoint = fromFile.getEndpoint('add'); + console.log(`add(${addEndpoint.input.map((p) => `${p.name}: ${p.type.getName()}`).join(', ')})`); +} -``` -curl --request GET \ - --url ${ES_URL}/logs/_search \ - --header 'Content-Type: application/json' \ - --data '{ - "query": { - "match": { - "_id":"d6.." - } - } -}' +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); ``` +Expected output (abridged): -### Fetch all the logs generated by a transaction and the smart contract results triggered by it +```text +--- Loading from a local file (src/adder.abi.json) --- +[ + { "name": "getSum", "mutability": "readonly", "inputs": [], "outputs": ["BigUint"] }, + { "name": "add", "mutability": "mutable", "inputs": ["value: BigUint"], "outputs": [] } +] -``` -curl --request GET \ - --url ${ES_URL}/logs/_search \ - --header 'Content-Type: application/json' \ - --data '{ - "query": { - "bool": { - "should": [ - { - "match": { - "_id":"d6.." - } - }, - { - "match": { - "originalTxHash": "d6.." - } - } - ] - } - } -}' -``` +--- Loading the same ABI from a URL (...) --- +[ ...identical output... ] ---- +--- Manually constructed ABI (no file, illustrative "foo"/"bar" only) --- +[ + { "name": "foo", "mutability": "mutable", "inputs": ["?: BigUint", "?: u32", "?: Address"], "outputs": ["u32"] }, + { "name": "bar", "mutability": "mutable", "inputs": ["?: Variadic", "?: Variadic"], "outputs": [] } +] -### Managed Decimal +--- Single-endpoint lookup: abi.getEndpoint("add") --- +add(value: BigUint) +``` + +## How it works + +**`Abi.create(json)` and `AbiRegistry.create(json)` are the same inherited +method.** `AbiRegistry` is the class with the real implementation: a protected +constructor plus the static `create()` factory. `Abi extends AbiRegistry` and +adds its own public constructor (for already-built `EndpointDefinition` / +`CustomType` objects, not raw JSON), but it does not override `create()`, which +always runs `new AbiRegistry(...)` internally. So `Abi.create(json)` does not +actually give you an `Abi` instance; both spellings return the same +`AbiRegistry` instance. Both work identically; this recipe follows the +`Abi.create(...)` naming. + +**`src/adder.abi.json` is the real ABI of a real, currently-deployed devnet +contract**, `erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug`, +copied from `mx-sdk-js-core`'s own `src/testdata/adder.abi.json`, the exact file +its cookbook uses as its running example. +[Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint) +and [Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) +call this same live contract. + +**`abi.getEndpoints()` / `abi.getEndpoint(name)`** return `EndpointDefinition` +objects whose parameter arrays are named `input` / `output` (singular). The raw +ABI JSON uses the plural `inputs`/`outputs`; the parsed object does not carry +that naming through. Each parameter's `.type` is a `Type` object, and +`.getName()` gives its readable type name. + +## Pitfalls + +:::note[Pitfall 1: a freshly-loaded ABI's types start out raw, not known] +`AbiRegistry.remapToKnownTypes()` increases the specificity of a registry's +field and parameter types on a best-effort basis. `Abi.create()` / +`AbiRegistry.create()` already call this internally before returning, so you do +not need to call it yourself for the common case of loading from JSON, only if +you construct a registry through some other path. +::: -## Managed Decimal Overview +:::warning[Pitfall 2: this recipe never touches devnet] +Loading an ABI is a local operation, plus (for the URL case) one plain HTTPS GET +for a JSON file, nothing MultiversX-specific about that request, and no wallet +or devnet EGLD is needed. If you expected a devnet wallet requirement here, you +are thinking of +[Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint). +::: -`ManagedDecimal` is a generic type representing a fixed-point decimal number managed by the API. -It is designed to handle decimal values with a specific number of decimals, providing operations such as addition, subtraction, multiplication, division, scaling, and conversion between different decimal precision and also more complex operations such as logarithm and nth root with a customizable degree of precision. +:::danger[Pitfall 3: the manually-constructed ABI is illustrative only] +`foo` and `bar` are not endpoints on any real deployed contract. This shape +exists only to show what `Abi.create()` expects when you know a contract's +interface but do not have its ABI JSON file. Do not copy it expecting it to call +anything real. +::: -Essentially, `ManagedDecimal` is simply a struct containing a wrapper over a `BigIntHandle` (`BigUint`) and the number of decimals associated with that value (precision). Having such a light design helps `ManagedDecimal` become the optimal solution for dealing with fixed-point decimal numbers in terms of efficiency and readability. +## See also -```rust title=managed_decimal.rs -#[derive(Debug, Clone)] -pub struct ManagedDecimal { - data: BigUint, // value * scaling_factor - decimals: D, // number_of_decimals (precision) -} -``` +- [Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint) + uses this same adder ABI to call `add(value)` on the real deployed contract. +- [Query a read-only view](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view) + queries the same contract's `getSum()`. +- [Call a payable endpoint with EGLD](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-payable-endpoint) + is the ABI-driven pattern applied to a payable endpoint. -The `scaling factor` of the decimal is `10^num_of_decimals` and is a cached value across multiple `ManagedDecimal` instances for increased efficiency. +--- -```rust title=example.rs -pub fn main() { - let decimal = ManagedDecimal::>::from(BigUint::from(1u64)); - let cached_decimal = ManagedDecimal::>::from(BigUint::from(5u64)); -} -``` +### Local HTTPS for dApp dev (mkcert + Vite + Next.js) +The first time a builder hits the wallet button over plain HTTP, they get a wall +of silence. The DeFi extension does nothing. Ledger does nothing. Most docs say +"make sure you run your app on https" and stop there. This recipe is the missing how. -## Number of decimals +The short version: install mkcert, run `setup.sh`, copy the matching framework +snippet. Five minutes total. The longer version walks the per-framework +configuration, Vite (two options), Next.js (two options), framework-agnostic plain +Node, plus the "I can't install root certs on this corp laptop" Cloudflare Tunnel +escape hatch. -`Decimals` is a trait representing the number of decimals associated with a decimal value and is only -implemented for `NumDecimals` and `ConstDecimals`. -`NumDecimals` is a type alias for `usize` and is used to specify the number of decimals dynamically, while `ConstDecimals` is a type that represents a constant number of decimals and is used to statically specify the number of decimals for ManagedDecimal instances. +## Why HTTPS in dev -```rust title=example.rs -pub fn main() { - let decimal: ManagedDecimal = ManagedDecimal::from_raw_units(BigUint::from(100u64), 2usize); - let const_decimal = ManagedDecimal::>::const_decimals_from_raw(BigUint::from(500u64)) -} -``` +Most wallet providers require a secure context to connect: +| Provider | Works under HTTP? | HTTPS required? | +| --- | --- | --- | +| Extension (DeFi) | No | Yes | +| Ledger | No | Yes | +| WalletConnect / xPortal | Yes | Optional | +| Web Wallet (cross-window) | Yes | Optional | +| Passkey / WebAuthn | No | Yes (browser API requires secure context) | +| InMemory (dev) | Yes | Optional | -## Operations +If you only test against xPortal during dev, plain HTTP works. The moment a +teammate tries the DeFi extension, things break, and it does not fail with a useful +message; it just silently does not connect. -`ManagedDecimal` supports various types of simple and complex operations, as well as conversions and scaling. More code examples can be found at `mx-sdk-rs/framework/scenario/tests/managed_decimal_test.rs` +## Prerequisites -### Available methods: -- Simple Operations: - - `add`, `mul`, `div`, `sub` are all implemented for `ConstDecimals` of the same scale. - ```rust title=example.rs - pub fn main() { - let fixed = ManagedDecimal::>::from(BigUint::from(1u64)); - let fixed_2 = ManagedDecimal::>::from(BigUint::from(5u64)); - - let addition = fixed + fixed_2; - assert_eq!( - addition, - ManagedDecimal::>::from(BigUint::from(6u64)) - ); - } - ``` - - `trunc(&self) -> BigUint` returns the `data` field without the `scaling_factor` applied, by dividing the value to the scaling factor and truncating. - ```rust title=example.rs - pub fn main() { - ... - assert_eq!(addition.trunc(), BigUint::from(6u64)); - } - ``` -- Complex Operations: - - `log(self, target_base: BigUint, precision: T) -> ManagedDecimal` returns the value of log in any base with customizable precision level. - ```rust title=example.rs - pub fn logarithm() { - let fixed = ManagedDecimal::::from_raw_units(BigUint::from(10u64), 1usize); - let fixed_const = ManagedDecimal::>::const_decimals_from_raw(BigUint::from(10u64)); +- macOS, Linux, or Windows. +- A package manager (Homebrew on macOS / apt on Linux / Chocolatey on Windows). +- Admin rights for the one-time root cert install. - let log2_fixed = fixed.log(BigUint::from(2u64), 10_000usize); - assert_eq!( - log2_fixed, - ManagedDecimal::::from_raw_units(BigUint::from(33219u64), 10_000usize) - ); - } - ``` -- Scaling: - - `scale(&self) -> usize` returns the number of decimals (the scale). - - `scaling_factor(&self) -> BigUint` returns the scaling factor value (`10^num_decimals`). - - `rescale(self, scale_to: T) -> ManagedDecimal` returns the correspondent of the decimal in the newly specified scale. It can also convert between `NumDecimal` and `ConstDecimals` instances. +## Step 1: Install mkcert and trust the local CA (one-time per machine) - ```rust title=example.rs - pub fn main() { - ... - let fixed_8: ManagedDecimal = ManagedDecimal::from_raw_units(BigUint::from(5u64), 5usize); - let fixed_9 = fixed_8.rescale(ConstDecimals::<3>); - assert_eq!( - fixed_9, - ManagedDecimal::>::const_decimals_from_raw(BigUint::from(500u64)) - ); - } - ``` -- Conversions: - - `into_raw_units(&self) -> &BigUint` returns the `data` field value. +The bundled `setup.sh` does the install, trust, and cert generation in one shot: - ```rust title=example.rs - pub fn main() { - ... - assert_eq!(addition.into_raw_units(), &BigUint::from(600u64)); - } - ``` - - `from_raw_units(data: BigUint, decimals: D) -> Self` returns a `ManagedDecimal` from a data field value without applying scaling factor. +```bash +#!/usr/bin/env bash +# setup.sh — install mkcert and generate localhost certs for the current +# directory. Idempotent: re-running is safe. +# +# Usage: +# bash setup.sh +# +# What this does: +# 1. Detects OS (macOS / Linux / Windows-via-Git-Bash) and installs +# mkcert via the appropriate package manager if it's not present. +# 2. Runs `mkcert -install` to add the local CA to the OS trust store. +# Requires admin rights on first run; later runs are no-ops. +# 3. Generates cert + key files in the current directory: +# ./localhost+2.pem (cert) +# ./localhost+2-key.pem (key) +# 4. Adds both file globs to .gitignore so the key is never committed. +# +# After running this, configure your dev server to use the cert/key — +# see snippets/vite.config.mkcert.ts or snippets/server.mkcert.js. + +set -euo pipefail + +echo ">>> Local HTTPS setup for dApp dev" +echo + +# ---- 1. Install mkcert ---- +if command -v mkcert >/dev/null 2>&1; then + echo "mkcert already installed: $(mkcert --version 2>&1)" +else + echo "Installing mkcert..." + if [[ "$OSTYPE" == "darwin"* ]]; then + if ! command -v brew >/dev/null 2>&1; then + echo "Homebrew not found. Install from https://brew.sh and re-run." + exit 1 + fi + brew install mkcert nss + elif [[ "$OSTYPE" == "linux"* ]]; then + echo "Linux detected. Install mkcert and libnss3-tools manually:" + echo " Debian/Ubuntu: sudo apt install libnss3-tools && \\" + echo " curl -JLO 'https://dl.filippo.io/mkcert/latest?for=linux/amd64' && \\" + echo " chmod +x mkcert-* && sudo mv mkcert-* /usr/local/bin/mkcert" + echo " Arch: sudo pacman -S mkcert nss" + echo " Fedora: sudo dnf install mkcert nss-tools" + exit 1 + elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then + if ! command -v choco >/dev/null 2>&1; then + echo "Chocolatey not found. Install from https://chocolatey.org and re-run, or install mkcert directly:" + echo " choco install mkcert (PowerShell as admin)" + echo " scoop bucket add extras && scoop install mkcert" + exit 1 + fi + choco install -y mkcert + else + echo "Unsupported OS: $OSTYPE" + echo "Install mkcert manually from https://github.com/FiloSottile/mkcert" + exit 1 + fi +fi + +# ---- 2. Trust the local CA ---- +echo +echo ">>> Installing local CA (requires admin rights on first run)" +mkcert -install + +# ---- 3. Generate the cert ---- +echo +echo ">>> Generating cert for localhost, 127.0.0.1, ::1" +mkcert localhost 127.0.0.1 ::1 + +# ---- 4. .gitignore safety ---- +echo +echo ">>> Adding cert globs to .gitignore" +GITIGNORE=".gitignore" +touch "$GITIGNORE" +for pattern in "localhost*.pem" "localhost*-key.pem"; do + if ! grep -qx "$pattern" "$GITIGNORE"; then + echo "$pattern" >> "$GITIGNORE" + echo " added: $pattern" + else + echo " already present: $pattern" + fi +done + +echo +echo ">>> Done." +echo "Configure your dev server to use ./localhost+2.pem and ./localhost+2-key.pem." +echo "See:" +echo " snippets/vite.config.mkcert.ts (Vite)" +echo " snippets/server.mkcert.js (Next.js)" +echo " snippets/server.plain-node.ts (other)" +``` + +Run it from your project root: - ```rust title=example.rs - pub fn main() { - ... - let fixed_4: ManagedDecimal = ManagedDecimal::from_raw_units(BigUint::from(100u64), 2usize); - let fixed_5 = fixed_4.rescale(2usize); - assert_eq!( - fixed_5, - ManagedDecimal::from_raw_units(BigUint::from(100000000u64), 8usize) - ); - } - ``` - - `const_decimals_from_raw(data: BigUint) -> Self` returns a `ConstDecimals` type of `ManagedDecimal` from a data field value without applying scaling factor. - ```rust title=example.rs - pub fn main() { - ... - let fixed_const: ManagedDecimal> = ManagedDecimal::const_decimals_from_raw(BigUint::from(1u64)); - } - ``` - - `num_decimals(&self) -> NumDecimals` returns the number of decimals. - - `to_big_float(&self) -> BigFloat` returns the decimal as `BigFloat`. - - `to_big_int(self) -> BigInt` returns the decimal as `BigInt`. - - `from_big_int(big_int: BigInt, num_decimals: T) -> ManagedDecimal` constructs a `ManagedDecimal` from a `BigInt` with customizable `num_decimals`. - - `from_big_float(big_float: BigFloat, num_decimals: T) -> ManagedDecimal` constructs a `ManagedDecimal` from a `BigFloat` with customizable `num_decimals`. - ```rust title=example.rs - pub fn main() { - ... - let float_1 = BigFloat::::from_frac(3i64, 2i64); - let fixed_float_1 = ManagedDecimal::>::from_big_float(float_1.clone(),ConstDecimals::<1>); - let fixed_float_2 = ManagedDecimal::::from_big_float(float_1, 1usize); +```bash +bash setup.sh +``` - assert_eq!( - fixed_float_1, - ManagedDecimal::>::const_decimals_from_raw(BigUint::from(15u64)) - ); - assert_eq!( - fixed_float_2, - ManagedDecimal::::from_raw_units(BigUint::from(15u64), 1usize) - ); - } - ``` +It detects the OS, installs mkcert via the appropriate package manager, runs +`mkcert -install` (adds a root CA to your OS trust store, admin rights required +first time), generates `./localhost+2.pem` plus `./localhost+2-key.pem`, and adds +both globs to `.gitignore`. ---- +## Step 2: Configure your dev server -### Memory allocation +### Vite (mkcert): fully trusted, no browser warning (recommended) -MultiversX smart contracts are compiled to WebAssembly, which does not come with memory allocation out of the box. In general WebAssembly programs need special memory allocation functionality to work with the heap. +```ts +// snippets/vite.config.mkcert.ts — Vite HTTPS via mkcert-generated certs. +// +// Pros: fully trusted certificate. No browser warning. Same cert can be +// trusted on the phone via mkcert.dev's mobile guide for end-to-end +// trusted dev. +// +// Cons: one-time `mkcert -install` per machine; cert + key files in the +// project root that MUST be gitignored (otherwise you've shipped a key +// that signs as your hostname). +// +// Generation steps (one-time per project): +// mkcert localhost 127.0.0.1 ::1 +// → produces ./localhost+2.pem and ./localhost+2-key.pem +// +// Add both to .gitignore: +// localhost*.pem +// localhost*-key.pem + +import { readFileSync } from 'node:fs'; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// readFileSync at config-load time: Vite's config is evaluated in Node, +// so this is fine. It runs once at server start, not per request. +const cert = readFileSync('./localhost+2.pem'); +const key = readFileSync('./localhost+2-key.pem'); + +export default defineConfig({ + plugins: [react()], + server: { + https: { cert, key }, + port: 5173, + }, +}); +``` -Using traditional memory allocation is highly discouraged on MultiversX. There are several reasons: -- We have “managed types”, which are handled by the VM, and which offer a cheaper and more reliable alternative. -- “Memory grow” operations can be expensive and unreliable. For the stability of the blockchain we have chosen to limit them drastically. -- Memory allocators end up in smart contract code, bloating it with something that is not related in any way to its specifications. This contradicts our design philosophy. +### Vite (basic-ssl): zero-config but self-signed -Even so, it is unreasonable to forbid the use of allocators altogether, whether to use them or not ultimately needs to be the developers' choice. Before framework version 0.41.0, the only allocator solution offered was `wee_alloc`. Unfortunately, it has not been maintained for a few years and has some known vulnerabilities. This was also causing Github’s Dependabot to produce critical warnings, not only to our framework, but to all contract projects, despite most of them not really using it. +```bash +npm install --save-dev @vitejs/plugin-basic-ssl +``` -First of all, we made the allocator [configurable](/developers/meta/sc-config#single-contract-configuration) from multicontract.toml, currently the main source of contract build specifications. Developers currently have 4 allocators to choose from. +```ts +// snippets/vite.config.basic-ssl.ts — minimum-config Vite HTTPS via the +// @vitejs/plugin-basic-ssl plugin. +// +// Pros: zero filesystem fingerprint. No certs to generate, manage, or +// gitignore. Good for solo work. +// +// Cons: the cert is self-signed and untrusted. Your browser shows a +// "not private" warning every dev session; the wallet extensions still +// connect, but the warning is annoying and can confuse new contributors +// to your project. +// +// Do NOT also set `server.https` yourself. Two independent reasons: +// 1. Vite's `server.https` type is `https.ServerOptions | undefined` +// (node_modules/vite/dist/node/index.d.ts) — it has never accepted a +// bare `boolean`. `https: true` fails `tsc --strict` with "no overload +// matches this call" on the actually-installed vite@5.4.21. +// 2. The plugin's own README (node_modules/@vitejs/plugin-basic-ssl/README.md) +// shows usage as `plugins: [basicSsl()]` with no `server.https` at all — +// the plugin injects the generated cert into `server.https` itself via +// its Vite `config` hook. Setting it yourself risks the two configs +// merging in an order you don't control. + +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import basicSsl from '@vitejs/plugin-basic-ssl'; + +export default defineConfig({ + plugins: [react(), basicSsl()], + server: { + port: 5173, + }, +}); +``` -Then, we added the following allocators to our framework: -- `FailAllocator` (the default) simply crashes whenever any memory allocation or deallocation is attempted. For the first time we have a tool that completely prevents accidental memory allocation. We already had an "alloc" feature in Cargo.toml, but it is only operating high-level and can easily (and sometimes accidentally) be circumvented. -- `StaticAllocator64k` pre-allocates a static 2-page buffer, where all memory is allocated. It can never call memory.grow . It never deallocates and crashes when the buffer is full. It can be suitable for small contracts with limited data being processed, who want to avoid the pitfalls of a memory.grow . -- `LeakingAllocator` uses memory.grow to get hold of memory pages. It also never deallocates. This is because contracts do not generally fill up so much memory and all memory is erased at the end of execution anyway. Suitable for contracts with a little more data. -- `wee_alloc` is still supported. It is, however, not included in the framework. Contracts need to import it explicitly. +### Next.js (mkcert): fully trusted, via a custom server -:::caution -While these allocators are functional, they should be avoided by all contracts. Only consider this functionality when all else fails, in extremely niche situations, or for dealing with very old code. -::: +Used by `mx-template-dapp-nextjs/server.js`. ---- +```js +// snippets/server.mkcert.js — Next.js custom HTTPS dev server with mkcert. +// +// Run with `node server.mkcert.js` instead of `next dev`. +// +// Mirrors the pattern used by mx-template-dapp-nextjs/server.js. +// The Node `https` module wraps the Next.js request handler; mkcert +// supplies the trusted certificate so the browser doesn't show a +// warning. +// +// Generation steps (one-time per project): +// mkcert localhost 127.0.0.1 ::1 +// → produces ./localhost+2.pem and ./localhost+2-key.pem +// +// Add both to .gitignore: +// localhost*.pem +// localhost*-key.pem +// +// This file is .js (CommonJS) deliberately — Next 14 expects a CJS +// custom server. If you'd rather use ESM, rename to .mjs and switch to +// `import` syntax. + +const { createServer } = require('node:https'); +const { parse } = require('node:url'); +const { readFileSync } = require('node:fs'); +const next = require('next'); + +const port = 3000; +const dev = process.env.NODE_ENV !== 'production'; +const app = next({ dev, hostname: 'localhost', port }); +const handle = app.getRequestHandler(); + +const httpsOptions = { + key: readFileSync('./localhost+2-key.pem'), + cert: readFileSync('./localhost+2.pem'), +}; -### Messages +void app.prepare().then(() => { + createServer(httpsOptions, (req, res) => { + const parsedUrl = parse(req.url ?? '/', true); + void handle(req, res, parsedUrl); + }).listen(port, () => { + // eslint-disable-next-line no-console + console.log(`> Ready on https://localhost:${port}`); + }); +}); +``` -The SC framework supports message interpolation in a variety of situations. +Then point your `dev` script at it: -The mechanism makes full use of managed types, and does not require memory allocation on the heap. +```json +{ + "scripts": { + "dev": "node server.js", + "build": "next build", + "start": "NODE_ENV=production node server.js" + } +} +``` -It resembles the standard message formatting in Rust, but it does not have all the features and is a completely separate implementation. +### Next.js (experimental): zero-config, browser warns -To see these features in action, we have [an example contract just for that](https://github.com/multiversx/mx-sdk-rs/blob/master/contracts/feature-tests/formatted-message-features/src/formatted_message_features.rs). +```json +{ + "scripts": { + "dev": "next dev --experimental-https" + } +} +``` +`npm run dev` starts the server on `https://localhost:3000` with a self-signed cert. +### Plain Node: Express, Fastify, Koa, or vanilla -## Errors +Same pattern, different request handler. -The most common place to see message interpolation is in the error messages. These messages will be seen in the explorer and tools if a transaction fails. +```ts +// snippets/server.plain-node.ts — framework-agnostic HTTPS dev server. +// +// For the rare reader who isn't on Vite or Next.js — Express, Fastify, +// Koa, or vanilla Node. The pattern is the same: load the cert, hand it +// to https.createServer. The framework's request handler slots in the +// same way the Next.js custom server does. +// +// This snippet is illustrative. In production you'd put a real reverse +// proxy (Cloudflare, NGINX, Caddy) in front of your service — local +// HTTPS only matters for the dev loop. + +import { createServer } from 'node:https'; +import { readFileSync } from 'node:fs'; +import type { IncomingMessage, ServerResponse } from 'node:http'; + +const httpsOptions = { + key: readFileSync('./localhost+2-key.pem'), + cert: readFileSync('./localhost+2.pem'), +}; +// Replace this with your framework's request handler. For Express: +// const app = express(); +// ... +// const handler = app; +const handler = (_req: IncomingMessage, res: ServerResponse): void => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('hello over https'); +}; -### sc_panic! +createServer(httpsOptions, handler).listen(3000, () => { + // eslint-disable-next-line no-console + console.log('> Ready on https://localhost:3000'); +}); +``` -Whenever encountered, it stops execution immediately, and returns the given error message. Also note that for all errors originating in the smart contract, the status code is "4". +## Pitfalls -The macro simplifies the process of throwing exceptions with custom error messages. It works with formatted messages with arguments, and with static messages without arguments. +:::warning[Pitfall 1: mkcert -install fails silently on locked-down corp laptops] +On macOS, IT can lock the System Keychain so `mkcert` can add to `nss` but not +Keychain. Browsers then do not trust the cert. Escape hatch: Cloudflare Tunnel. -```rust - sc_panic!("Formatted error message with arguments: {}", arg); // message with argument - sc_panic!("Static error message"); // static messages +```bash +cloudflared tunnel --url http://localhost:3000 ``` +A free public HTTPS URL that wallets accept. The tunnel URL changes per session +unless you register a stable hostname through the Cloudflare Zero Trust dashboard. +::: -### require! +:::warning[Pitfall 2: committing the cert key] +The cert and key files are .gitignored, but only after you set them up that way. +`setup.sh` does it; the manual flow does not unless you remember. If you committed +the key, **regenerate** with a fresh `mkcert localhost 127.0.0.1` and rotate. +Anyone with the committed key can MITM your traffic on the local network. +::: -`require!` is a macro used to enforce preconditions by checking whether an expression evaluates to *true*. If the expression evaluates to *false*, it will trigger a panic with a specific error message. It is very useful when validating pre-conditions. +:::note[Pitfall 3: mkcert's root CA expires after ~3 years] +You will start seeing "not private" warnings on what was a working setup. Run +`mkcert -install` again; it regenerates and re-trusts. +::: -```rust -require!(expression, "formatted error message with argument: {}", arg); // error message with argument -require!(expression, "error message"); // static error messages -``` +:::note[Pitfall 4: mobile testing] +Run `mkcert -CAROOT` to find the CA cert location (typically +`~/Library/Application Support/mkcert/rootCA.pem` on macOS), copy it to the iOS / +Android device, and trust manually. mkcert.dev has the per-platform trust +instructions. +::: +:::note[Pitfall 5: staging is not dev] +The "not private" click-through is fine for `localhost`. Staging deploys should use +a real cert (Let's Encrypt via Caddy / Cloudflare / whatever your hosting provides), +not mkcert. mkcert root CAs are local-machine-only; they do not propagate to +teammates' machines. +::: +## See also -## Formatting string +- [Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) + uses `next dev --experimental-https` by default; switch to mkcert via `server.js` + for fully trusted. +- [Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) + uses `@vitejs/plugin-basic-ssl` by default; switch to mkcert for fully trusted. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the wallet flow this recipe makes accessible. +- [mkcert](https://github.com/FiloSottile/mkcert) is the upstream tool. -We might want to interpolate a string for other uses than throwing an error, such as returning it or saving it to storage. +--- +### Local mint and burn (change a fungible token's supply) -### sc_format! +Once a fungible ESDT is issued, its circulating supply is not fixed. An address +holding the right role can mint more (`ESDTLocalMint`, needs `ESDTRoleLocalMint`) +or burn its own (`ESDTLocalBurn`, needs `ESDTRoleLocalBurn`). Both are **builtin +functions** executed in the caller's own account, so the transaction's receiver is +the sender itself, not the ESDT system contract. -The macro creates a formatted managed buffer in contracts. Just like all other format functionality, it supports both formatted messages with arguments and static messages without arguments. The returned value of this macro is a `ManagedBuffer` that can be used within smart contracts. +This recipe builds both, decodes the payloads, and broadcasts the mint. It also +walks straight into a real sdk-core method-name trap. -```rust -let number_i32: i32 = 16; -let message_with_i32: ManagedBuffer = sc_format!("i32: {}", number_i32); +## Prerequisites -let number_bigUint = BigUint::from(16u32); -let message_with_bigUint = sc_format!("BigUint: {}", number_bigUint); +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites. **No devnet EGLD required**, see "How it works". -let buffer: ManagedBuffer = ManagedBuffer::new_from_bytes(b"message"); -let message_with_buffer = sc_format!("ManagedBuffer: {}", buffer); -let message_with_buffer_hex = sc_format!("ManagedBuffer hex: {:x}", buffer); -``` +## Install +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/local-mint-burn-supply +npm install +npm run build +npm start -- ./wallet.pem COOK-123456 1000 +``` + +## The code + +```ts title="src/supply.ts" +// src/supply.ts — the subject of this recipe: changing a fungible ESDT's +// circulating supply after issuance, using the ESDTLocalMint / ESDTLocalBurn +// builtin functions. +// +// - localMint — requires the ESDTRoleLocalMint role (see set-special-roles) +// - localBurn — requires the ESDTRoleLocalBurn role +// +// Both are builtin functions executed in the caller's OWN account context, +// so the transaction's receiver is the sender itself (not the ESDT system +// contract). This is the correct, on-chain-verified shape for these two +// operations — contrast the freeze/pause/wipe manager operations in the +// token-lifecycle-operations recipe, which must instead target the ESDT +// system contract. +// +// TWO method-name traps, both confirmed against the installed v15.4.1 (see +// the recipe page's Pitfalls): +// - MINT, controller: createTransactionForLocaMinting (note the typo: +// "Loca", missing an "l" — it is NOT createTransactionForLocalMinting). +// - MINT, factory: createTransactionForLocalMint (correct spelling, +// but no "-ing" suffix). +// - BURN is createTransactionForLocalBurning on BOTH — the only consistent +// one of the pair. + +import type { Account, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +export interface SupplyInput { + tokenIdentifier: string; + /** Amount in the token's smallest denomination (respect its numDecimals). */ + amount: bigint; +} + +/** Mint additional supply of a fungible token you hold the local-mint role on. */ +export async function localMint( + entrypoint: DevnetEntrypoint, + sender: Account, + input: SupplyInput, + useFactory: boolean, +): Promise { + if (useFactory) { + const factory = entrypoint.createTokenManagementTransactionsFactory(); + // Factory: createTransactionForLocalMint — no "-ing", correct spelling. + const transaction = await factory.createTransactionForLocalMint(sender.address, { + tokenIdentifier: input.tokenIdentifier, + supplyToMint: input.amount, + }); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; + } + const controller = entrypoint.createTokenManagementController(); + // Controller: createTransactionForLocaMinting — "Loca", a real SDK typo. + return controller.createTransactionForLocaMinting(sender, sender.getNonceThenIncrement(), { + tokenIdentifier: input.tokenIdentifier, + supplyToMint: input.amount, + }); +} -## Printing to console +/** Burn supply of a fungible token you hold the local-burn role on. */ +export async function localBurn( + entrypoint: DevnetEntrypoint, + sender: Account, + input: SupplyInput, + useFactory: boolean, +): Promise { + if (useFactory) { + const factory = entrypoint.createTokenManagementTransactionsFactory(); + const transaction = await factory.createTransactionForLocalBurning(sender.address, { + tokenIdentifier: input.tokenIdentifier, + supplyToBurn: input.amount, + }); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; + } -Smart contracts cannot print messages on-chain, because they do not have access to any console or standard output. They can, however, do so when run in tests. This printing feature is designed to come to complement the debugger when testing contracts. + const controller = entrypoint.createTokenManagementController(); + return controller.createTransactionForLocalBurning(sender, sender.getNonceThenIncrement(), { + tokenIdentifier: input.tokenIdentifier, + supplyToBurn: input.amount, + }); +} +/** Decode a transaction's `data` field into `@@`. */ +export function describeSupplyPayload(transaction: Transaction): { + function: string; + receiver: string; + tokenIdentifier: string; + amountHex: string; +} { + const parts = Buffer.from(transaction.data).toString().split('@'); + const tokenHex = parts[1] ?? ''; + return { + function: parts[0] ?? '', + receiver: transaction.receiver.toBech32(), + tokenIdentifier: Buffer.from(tokenHex, 'hex').toString(), + amountHex: parts[2] ?? '', + }; +} +``` -### sc_print! +## Run it -This macro is the primary way to output messages to console, when running tests. +```bash +npm start -- [--factory] +``` -Using it doesn't impact wasm builds in any way, so it is safe to use in any situation. To avoid any overhead, not even the formatting code will end up compiled to WebAssembly. +A real captured run (unfunded wallet): -Despite this, we recommend removing the `sc_print!` commands, once you are done with debugging. +```text +LOCAL MINT (ESDTLocalMint): + function: ESDTLocalMint + receiver: erd18l2cww3g3g6uc35wy60efd87ss85xdqj9gh9qn6ga6halz3jscaqd4dl3k + tokenIdentifier: COOK-123456 + amount (hex): 03e8 + +LOCAL BURN (ESDTLocalBurn): + function: ESDTLocalBurn + amount (hex): 03e8 + +Broadcasting the mint... +Rejected (expected for an unfunded wallet): ... insufficient funds for address erd18l2c... +``` + +## How it works + +Both payloads decode to `@@` addressed to the +**sender**. `03e8` is hex for 1000, remember this is in the token's smallest +denomination, so respect its `numDecimals` (a 6-decimal token needs `1000000` for +one whole unit). The unfunded broadcast is rejected with a clean `insufficient +funds` keyed to the sender, proving the payload is well-formed; a real mint +additionally needs the token to exist and the sender to hold the matching local +role (see [Set and unset special roles](/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles)). + +**The method names are a minefield, this is the recipe's real value.** Against the +installed v15.4.1: + +- **Mint, controller:** `createTransactionForLocaMinting`, note the typo, **"Loca"** + with a missing "l". It is *not* `createTransactionForLocalMinting`. +- **Mint, factory:** `createTransactionForLocalMint`, correct spelling, but **no + "-ing"** suffix. +- **Burn:** `createTransactionForLocalBurning` on *both*, the only consistent name + of the pair. + +## Pitfalls + +:::danger[Pitfall 1: the controller mint method is misspelled createTransactionForLocaMinting] +"Loca", not "Local". Autocomplete for `createTransactionForLocal…` will not surface +it on the controller. The factory instead uses `createTransactionForLocalMint` (no +"-ing"). Copy the exact name from the code above. +::: -To produce logs on-chain, consider using [event logs](sc-annotations#events) instead. +:::warning[Pitfall 2: amounts are in the smallest denomination] +`ESDTLocalMint@…@03e8` mints 1000 base units. For a token issued with 6 decimals +that is 0.001 of a whole unit. See +[Send an ESDT](/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt)'s decimals +pitfall. +::: +:::note[Pitfall 3: local mint/burn needs the local role, not ownership] +Being the token's owner is not enough; the sender needs `ESDTRoleLocalMint` / +`ESDTRoleLocalBurn` explicitly granted. Grant it with +[Set and unset special roles](/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles). +::: +## See also -## Interpolation format +- [Set and unset special roles](/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles) + grants the `ESDTRoleLocalMint` / `ESDTRoleLocalBurn` roles these operations + require. +- [Issue a fungible ESDT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token) + issues the token whose supply you then change. +- [Token lifecycle operations](/sdk-and-tools/sdk-js/cookbook/tokens/token-lifecycle-operations) + covers freeze, pause, and wipe (the manager-side operations, which target the + ESDT contract instead of the sender). -Values can be interpolated into messages in several ways. -- `{}` - The human-readable representation. - - For **number types**, it is the base 10 representation. - ```rust - // "Printing u64: 372036854775807" - sc_print!("Printing u64: {}", 372036854775807u64;); +--- - // "Printing u32: 800000008" - sc_print!("Printing u32: {}", 800000008u32); +### Log in with a Ledger hardware wallet - // "Printing usize: 1800000000" - sc_print!("Printing usize: {}", 1800000000usize); +A dedicated, single-provider login button for a Ledger hardware wallet. It is the +same `ProviderFactory` pattern as +[Login via the DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login) +and +[Login via xPortal / WalletConnect](/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login), +this time for `ProviderTypeEnum.ledger`. Ledger needs the same real DOM anchor +WalletConnect does, but for a different reason: instead of a QR code, the SDK +renders its own device-connect screen, a paginated account picker, and a confirm +screen into it. - // "Printing u16: 60123" - sc_print!("Printing u16: {}", 60123u16); +Use this when you want a dedicated "Connect Ledger" button, and when your users +are expected to bring physical hardware wallets. This recipe cannot be exercised +end to end without one, see Pitfall 1. - // "Printing u8: 233" - sc_print!("Printing u8: {}", 233u8); - - // "Printing i64: -372036854775807" - sc_print!("Printing i64: {}", -372036854775807i64); +## Prerequisites - // "Printing i32: -800000008" - sc_print!("Printing i32: {}", -800000008i32); +- A working sdk-dapp v5 setup (Next.js or Vite, see the start-here recipes). +- A physical Ledger device with the MultiversX app installed and open on it. +- A Chromium-based browser. WebHID/WebUSB, what `@multiversx/sdk-hw-provider` + uses to talk to the device, are not implemented in Firefox or Safari. - // "Printing isize: -1800000000" - sc_print!("Printing isize: {}", -1800000000isize); +## Install - // "Printing i16: -30123" - sc_print!("Printing i16: {}", -30123i16); +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/ledger-login +npm install +npm run dev +# open https://localhost:5173 +``` + +## The button + +```tsx title="src/LedgerLoginButton.tsx" +// src/LedgerLoginButton.tsx — a dedicated Ledger hardware-wallet login +// button with an anchor element for the SDK's own device/account UI. +// +// Same overall pattern as "Login via the DeFi extension" and "Login via +// xPortal / WalletConnect" — ProviderFactory.create() + provider.login() — +// with the anchor requirement specific to Ledger: +// +// ProviderFactory.create({ type: ProviderTypeEnum.ledger, anchor }) needs +// a real HTMLElement. The SDK renders a `` web +// component (from the optional @multiversx/sdk-dapp-ui package — it +// installs automatically as an npm optionalDependency of sdk-dapp, see +// this recipe's README) into that anchor: first a "connecting" screen, +// then a paginated list of the accounts derived from the device (10 per +// page), then a confirm screen while you approve the login on the +// physical device itself. +// +// Everything below is verified against the actually-installed +// @multiversx/sdk-dapp source (not just its .d.ts files), the same standard +// this Cookbook's WalletConnect/DeFi-extension recipes were held to: +// +// ProviderFactory.create({ type: ProviderTypeEnum.ledger, anchor }) +// → constructs `new LedgerProviderStrategy({ anchor })`, then calls +// `LedgerIdleStateManager.getInstance().init()` +// (node_modules/@multiversx/sdk-dapp/out/providers/ProviderFactory.cjs) +// → internally calls setAccountProvider() for you, same as every other +// provider type — no separate call needed. +// strategy.init() +// → connects to the physical device via @multiversx/sdk-hw-provider's +// HWProvider (WebUSB/WebHID under the hood). Because no account is +// logged in yet, this happens eagerly, as part of create() itself — +// not deferred to login() — confirmed from +// .../LedgerProviderStrategy/helpers/getLedgerProvider/getLedgerProvider.cjs: +// `shouldInitProvider = options?.shouldInitProvider || !isLoggedIn`. +// Practically: clicking "Connect Ledger" immediately triggers the +// browser's native device-permission prompt, before any app UI shows. +// provider.login() +// → renders the account list + confirm screen into the anchor via +// LedgerConnectStateManager (confirmed from +// .../LedgerProviderStrategy/helpers/authenticateLedgerAccount/authenticateLedgerAccount.cjs). +// You do NOT pass an addressIndex yourself — the SDK's own UI collects +// it and feeds it back internally. Once the user approves on-device, +// login() resolves the same way it does for every other provider: +// useGetIsLoggedIn() / useGetAccount() are correct immediately after. + +import { useRef, useState } from 'react'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { ProviderFactory } from '@multiversx/sdk-dapp/out/providers/ProviderFactory'; +import { ProviderTypeEnum } from '@multiversx/sdk-dapp/out/providers/types/providerFactory.types'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; - // "Printing i8: -126" - sc_print!("Printing i8: {}", -126i8); +export function LedgerLoginButton(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const [status, setStatus] = useState<'idle' | 'connecting' | 'error'>('idle'); + const [error, setError] = useState(null); + // The device-connection status, account list, and confirm screen all + // render into this div via a web component the SDK defines at runtime. + // It must exist in the DOM before create() runs — rendered unconditionally + // below, so by the time a click handler fires, anchorRef.current is set. + const anchorRef = useRef(null); + + const handleConnect = async (): Promise => { + if (!anchorRef.current) { + // Should not happen — the anchor div always renders — but keeps the + // types honest under strict null checks rather than asserting `!`. + setError('Ledger anchor not mounted yet.'); + setStatus('error'); + return; + } - let x_bigint: BigInt = BigInt::from(-3272036854775807i64); - // "Printing x_bigint: -3272036854775807" - sc_print!("Printing x_bigint: {}", x_bigint); - - let x_biguint: BigUint = BigUint::from(3272036854775807u64); - // "Printing x_biguint: 3272036854775807" - sc_print!("Printing x_biguint: {}", x_biguint); - ``` - - For `ManagedBuffer`, it is the contained text. - ```rust - let managed_buffer: ManagedBuffer = ManagedBuffer::new_from_bytes(b"Welcome to MultiversX!"); - // "Printing managed_buffer: Welcome to MultiversX!" - sc_print!("Printing managed_buffer: {}", managed_buffer); - ``` - - For **bool**, it indicates whether the condition is true or false. - ```rust - let x_bool: bool = true; - // "Printing x_bool: true" - sc_print!("Printing x_bool: {}", x_bool); - ``` - - For **byte values**, it is the string character. - ```rust - let x_bytes: &[u8] = b"MVX"; - // "Printing x_bytes: MVX" - sc_print!("Printing x_bytes: {}", x_bytes); - ``` - - For `CodeMetadata`, it is the flag represented as a text. - ```rust - let code_metadata: CodeMetadata = CodeMetadata::UPGRADEABLE; - // "Printing code_metadata: Upgradeable" - sc_print!("Printing code_metadata: {}", code_metadata); - ``` - - For `TokenIdentifier`, it is the token ticker. - ```rust - let token_identifier = TokenIdentifier::from(&b"TESTTOK-2345"[..]); - // "Printing token_identifier: TESTTOK-2345" - sc_print!("Printing token_identifier: {}", token_identifier); - ``` - - for `EgldOrEsdtTokenIdentifier`, it is the token ticker. - ```rust - let egld_or_esdt_token_identifier: EgldOrEsdtTokenIdentifier = EgldOrEsdtTokenIdentifier::egld(); - // "Printing egld_or_esdt_token_identifier: EGLD" - sc_print!("Printing egld_or_esdt_token_identifier: {}", egld_or_esdt_token_identifier); - ``` - -- `{:x}` - Lowercase hexadecimal encoding. - - For **number types**, it is the base 16 representation. - ```rust - // "Printing u64: 1525d94927fff" - sc_print!("Printing u64: {:x}", 372036854775807u64); + setStatus('connecting'); + setError(null); + try { + // This call itself triggers the browser's WebHID/WebUSB device picker + // — it happens here, not inside login() — see the file header. + const provider = await ProviderFactory.create({ + type: ProviderTypeEnum.ledger, + anchor: anchorRef.current, + }); + // nativeAuth is enabled in this recipe's dappConfig, so login() + // generates its own native-auth token internally, same as every + // other provider. + await provider.login(); + setStatus('idle'); + } catch (err) { + // Two distinct failure classes reach this catch, and this recipe + // cannot tell them apart without a physical device to reproduce + // against — flagged honestly rather than guessed: + // 1. The device/transport layer failing (no device plugged in, + // wrong app open, browser without WebHID/WebUSB support — + // Firefox and Safari do not implement either API). + // 2. The user cancelling from the account list or confirm screen + // rendered inside the anchor. + // sdk-dapp's own internal recovery path (rebuildProvider, used before + // signing) shows a toast reading "Unlock your device & open the + // MultiversX App" for case 1 — a reasonable model for what to tell + // the user here too, but this recipe surfaces the raw message rather + // than pattern-matching on it. + const message = err instanceof Error ? err.message : String(err); + setError(message); + setStatus('error'); + } + }; - // "Printing u32: 2faf0808" - sc_print!("Printing u32: {:x}", 800000008u32); + const handleDisconnect = async (): Promise => { + const provider = getAccountProvider(); + await provider.logout(); + }; - // "Printing usize: 6b49d200" - sc_print!("Printing usize: {:x}", 1800000000usize); + if (isLoggedIn) { + return ( + + ); + } - // "Printing u16: eadb" - sc_print!("Printing u16: {:x}", 60123u16); + return ( +
+ + + {/* The SDK renders its device-connect / account-picker / confirm UI + into this element once ProviderFactory.create() runs. Give it real + dimensions — an empty 0x0 div means that UI has nowhere visible to + draw, the same requirement as the WalletConnect recipe's QR anchor. */} +
+ + {status === 'error' && error && ( +

+ Connection failed: {error} +

+ )} +
+ ); +} - // "Printing u8: e9" - sc_print!("Printing u8: {:x}", 233u8); - - // "Printing i64: fffeada26b6d8001" - sc_print!("Printing i64: {:x}", -372036854775807i64); +const buttonStyle: React.CSSProperties = { + padding: '0.75rem 1.25rem', + fontSize: '1rem', + cursor: 'pointer', +}; - // "Printing i32: d050f7f8" - sc_print!("Printing i32: {:x}", -800000008i32); +const anchorStyle: React.CSSProperties = { + marginTop: '1rem', + minHeight: '280px', + minWidth: '280px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}; +``` - // "Printing isize: 94b62e00" - sc_print!("Printing isize: {:x}", -1800000000isize); +## The demo page - // "Printing i16: 8a55" - sc_print!("Printing i16: {:x}", -30123i16); +```tsx title="src/App.tsx" +// src/App.tsx — demo page for LedgerLoginButton. - // "Printing i8: 82" - sc_print!("Printing i8: {:x}", -126i8); - ``` - - For `ManagedBuffer`, it is the contained text as hexadecimal. - ```rust - let managed_buffer: ManagedBuffer = ManagedBuffer::new_from_bytes(b"MultiversX!"); - // "Printing managed_buffer: 4d756c7469766572735821" - sc_print!("Printing managed_buffer: {:x}", managed_buffer); - ``` - - For `ManagedAddress`, it is the contained address as hexadecimal. - ```rust - let address: [u8; 32] = hex!("fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe"); - let managed_address: ManagedAddress = ManagedAddress::from(&address); - // "Printing managed_address: fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe" - sc_print!("Printing managed_address: {:x}", managed_address); - ``` - - For `ManagedByteArray`, it is the hexadecimal representation for the ASCII characters. - ```rust - let managed_byte_array: ManagedByteArray = ManagedByteArray::new_from_bytes(b"MVX"); - // "Printing managed_byte_array: 4d5658" - sc_print!("Printing managed_byte_array: {:x}", managed_byte_array); - ``` - - For `CodeMetadata`, it is the metadata stored in the flag as hexadecimal. - ```rust - let code_metadata: CodeMetadata = CodeMetadata::UPGRADEABLE; - // "Printing code_metadata: 0100" - sc_print!("Printing code_metadata: {}", code_metadata); - ``` - - For `TokenIdentifier`, it is the token ticker as hexadecimal. - ```rust - let token_identifier = TokenIdentifier::from(&b"TESTTOK-2345"[..]); - // "Printing token_identifier: 54455354544f4b2d32333435" - sc_print!("Printing token_identifier: {:x}", token_identifier); - ``` - - for `EgldOrEsdtTokenIdentifier`, it is the token ticker as hexadecimal. - ```rust - let egld_or_esdt_token_identifier: EgldOrEsdtTokenIdentifier = EgldOrEsdtTokenIdentifier::egld(); - // "Printing egld_or_esdt_token_identifier: 45474C44" - sc_print!("Printing egld_or_esdt_token_identifier: {:x}", egld_or_esdt_token_identifier); - ``` +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { LedgerLoginButton } from './LedgerLoginButton'; -- `{:b}` - Binary encoding ("0" and "1"). - - For **number types**, it is the base 2 representation. - ```rust - // "Printing u64: 1010100100101110110010100100100100111111111111111" - sc_print!("Printing u64: {:b}", 372036854775807u64); +export function App(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const account = useGetAccount(); + const { network } = useGetNetworkConfig(); - // "Printing u32: 101111101011110000100000001000" - sc_print!("Printing u32: {:b}", 800000008u32); + return ( +
+

Login via Ledger

+

+ Network: {network.chainId} +

+

+ Requires a physical Ledger device with the MultiversX app installed + and open, plus a Chromium-based browser (Chrome, Edge) — WebHID/WebUSB + are not implemented in Firefox or Safari. +

+ + + + {isLoggedIn && ( +

+ Connected as {account.address}. +

+ )} +
+ ); +} +``` - // "Printing usize: 1101011010010011101001000000000" - sc_print!("Printing usize: {:b}", 1800000000usize); +## Provider bootstrap - // "Printing u16: 1110101011011011" - sc_print!("Printing u16: {:b}", 60123u16); +`providers.tsx` calls `initApp()` once and gates rendering until the store is +ready; `lib/multiversx.ts` holds the environment config it passes in. - // "Printing u8: 11101001" - sc_print!("Printing u8: {:b}", 233u8); - ``` - - For `CodeMetadata`, we get the individual bits. - ```rust - let code_metadata: CodeMetadata = CodeMetadata::UPGRADEABLE; - // "Printing code_metadata: 0000000100000000" - sc_print!("Printing code_metadata: {:b}", code_metadata); - ``` - - For `ManagedBuffer`, it is the contained text as bits. - ```rust - let managed_buffer: ManagedBuffer = ManagedBuffer::new_from_bytes(b"MVX"); - // "Printing managed_buffer: 010011010101011001011000" - sc_print!("Printing managed_buffer: {}", managed_buffer); - ``` +```tsx title="src/providers.tsx" +// src/providers.tsx — sdk-dapp v5 init wrapper. +// +// Deliberately does NOT configure UnlockPanelManager — this recipe bypasses +// the generic multi-provider picker and talks to ProviderFactory directly +// for one specific provider. See src/LedgerLoginButton.tsx. -- `{:c}` - Encoded the same as the [codec representation](/developers/data/serialization-overview). Helpful when trying to visualize the encoding of an object. It is also the one available for most types, so it is a good fallback when other types of formatting are not available, e.g. for custom structs. - - For all types, it is the [top-encoded](/developers/data/serialization-overview#the-concept-of-top-level-vs-nested-objects) representation. - ```rust - // "Printing u64: 01525d94927fff" - sc_print!("Printing u64: {:c}", 372036854775807u64); - ``` +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { dappConfig } from './lib/multiversx'; ---- +let initStarted = false; -### Migration +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); -There is an older syntax for producing contract calls, detailed [here](./tx-legacy-calls.md). It is already in use in many projects. + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; -Upgrading to framework version 0.49.0 is almost completely backwards compatible, but the new syntax is nicer and more reliable, so we encourage everyone to migrate. + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + setReady(true); + }); -This will be a migration guide, as well as some frequently encountered pitfalls. + return () => { + cancelled = true; + }; + }, []); -:::caution -Even though the syntax is backwards compatible, the implementation of the [old syntax](./tx-legacy-calls.md) has been replaced. + if (!ready) { + return ( +
+ Initializing wallet SDK… +
+ ); + } -To the best of our knowledge, all code should continue to behave the same. However, if you upgrade beyond 0.49.0, please make sure to **test your smart contract fully** once again, even if you do not change a single line of code in your code base. + return <>{children}; +} +``` -Do not be fooled by the identical legacy syntax, the implementation for **all** contract calls is new. +```ts title="src/lib/multiversx.ts" +// src/lib/multiversx.ts — environment configuration for sdk-dapp's initApp. +// Same shape as every other Vite recipe in this Cookbook — see +// vite-react-minimal's README for why the fields are set this way. +// walletConnectV2ProjectId isn't exercised by this recipe (LedgerLoginButton +// talks to ProviderFactory with ProviderTypeEnum.ledger only) but is kept +// here for copy-paste consistency with the rest of the corpus. + +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +const WALLET_CONNECT_PROJECT_ID = + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; + +export const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + nativeAuth: true, + // `theme` is a ThemesEnum member (runtime value 'mvx:dark-theme'), not a + // bare string — see node_modules/@multiversx/sdk-dapp/out/methods/initApp/initApp.types.d.ts. + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + }, +}; + +export { EnvironmentsEnum }; +``` + +## How it works + +**The anchor is not optional in practice, the same requirement as the +WalletConnect QR code.** `ProviderFactory.create({ type: ProviderTypeEnum.ledger, anchor })` +passes the anchor straight into `new LedgerProviderStrategy({ anchor })` +(`node_modules/@multiversx/sdk-dapp/out/providers/ProviderFactory.cjs`). The SDK +renders a `` web component into it, from the optional +`@multiversx/sdk-dapp-ui` package, which installs automatically as an +`optionalDependencies` entry of `@multiversx/sdk-dapp` itself; nothing extra to +add to `package.json`. + +**Connecting to the device happens inside `create()`, before `login()` is ever +called.** Because no account is logged in yet on a fresh page load, +`getLedgerProvider()` computes +`shouldInitProvider = options?.shouldInitProvider || !isLoggedIn`, which +evaluates to `true`. That is what triggers the browser's native WebHID/WebUSB +device-permission prompt, as soon as you click "Connect Ledger," before any +account list appears. + +**No `addressIndex` is passed from this component.** The SDK's own UI (rendered +into the anchor) lists the accounts derivable from the device and calls back into +the login flow with the chosen index internally. This component only calls +`provider.login()` with no arguments, the same call shape as every other provider +in this Cookbook. + +## Pitfalls + +:::warning[Pitfall 1: this recipe cannot be verified end-to-end without physical hardware] +Everything above the "physical device" line, the `ProviderFactory` dispatch, the +anchor requirement, the `shouldInitProvider` timing, the account-list hand-off, +is confirmed against the actually-installed `@multiversx/sdk-dapp` source. What a +real device-pairing session looks like in detail (exact error messages on cancel, +exact WebHID permission-prompt wording) is not independently reproduced here. +Treat error messages as unstructured strings to display. ::: +:::danger[Pitfall 2: Firefox and Safari cannot run this recipe at all] +WebHID and WebUSB are Chromium-only browser APIs. This is a platform limitation, +not something the SDK or this recipe can work around. +::: +:::warning[Pitfall 3: the Ledger deep-import fix is load-bearing here, not dormant] +The other wallet recipes in this Cookbook carry the same `vite.config.ts` +`optimizeDeps.exclude` / `rollupOptions.external` fix for three `@ledgerhq/devices` +deep-import paths, but only because `ProviderFactory` transitively references the +Ledger strategy. This recipe actually calls into that code path, so dropping the +fix here breaks both `npm run dev` and `npm run build`, not just a path you never +exercise. +::: -## Imports +:::note[Pitfall 4: the MultiversX app must be open on the device] +If it is not, or the device goes idle mid-session, sdk-dapp's own recovery path +(used before signing) shows a toast reading "Unlock your device & open the +MultiversX App", confirmed from the compiled source's `rebuildProvider` catch block. +::: -This is not strictly related to the unified syntax, but the imports were recently cleaned up. A single line should be enough for each of the contexts, as follows: -- In contracts: `use multiversx_sc::imports::*;` -- In tests: `use multiversx_sc_scenario::imports::*;` -- In interactors: `use multiversx_sc_snippets::imports::*;` +## See also -We also have `use multiversx_sc::derive_imports::*;`, which gives you derives like `TypeAbi` and the codec derives. +- [Login via the DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login) + is the same `ProviderFactory` pattern for the browser-extension provider. +- [Login via xPortal / WalletConnect](/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login) + is the same pattern, QR-code anchor instead of a device UI. +- [Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button) + is the multi-provider picker alternative to this dedicated button. +- [Read the connected account](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + is what to do once `provider.login()` resolves. -The proxies have `use multiversx_sc::proxy_imports::*;`, but that gets generated automatically, so develoeprs shouldn't worry about it. +--- +### Log in with the DeFi Wallet browser extension +[Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button) +covers the common case: one button, `UnlockPanelManager` picks the provider. +This recipe is for the other case. You want a single, dedicated button for +exactly one provider, the DeFi Wallet browser extension, with no picker in +between. That means talking to `ProviderFactory` directly instead of +`UnlockPanelManager`. +Use this when your dApp only supports one wallet, or when you want +provider-specific buttons side by side (a "Connect DeFi Wallet" next to a +"Connect xPortal", see +[Login via xPortal / WalletConnect](/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login) +for that sibling). -## Old `Proxy` type caveat +## Prerequisites -We must start with the only instance of backwards incompatibility that we have after 0.49.0. +- A working sdk-dapp v5 setup (Next.js or Vite, see the start-here recipes). +- The DeFi Wallet browser extension, to test the connected path. The recipe + handles its absence gracefully (see below) so you can also see that path + without installing anything. -It is very uncommon to encounter this problem, we don't expect developers to encounter it, but it did pop up in the DEX when migrating. +## Install -The old `Proxy` type has been split in two: `Proxy` no longer includes the field for the recipient, whereas there is a new `ProxyTo` type that does include it. +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/defi-extension-login +npm install +npm run dev +# open https://localhost:5173 +``` + +## The button + +```tsx title="src/ExtensionLoginButton.tsx" +// src/ExtensionLoginButton.tsx — a dedicated, single-provider login button. +// +// Unlike "Adding a wallet login button" (which opens a picker listing every +// registered provider via UnlockPanelManager), this component talks to +// ProviderFactory directly for exactly one provider: the DeFi Wallet +// browser extension. Use this pattern when your dApp only supports one +// wallet, or when you want a dedicated "Connect with DeFi Wallet" button +// alongside (not inside) the generic picker. +// +// The full sequence — verified against the actual sdk-dapp source, not +// just its .d.ts files, because the .d.ts alone doesn't say whether extra +// steps are needed after provider.login(): +// +// ProviderFactory.create({ type: ProviderTypeEnum.extension }) +// → internally calls setAccountProvider() for you (see +// node_modules/@multiversx/sdk-dapp/out/providers/ProviderFactory.cjs) +// provider.login() +// → internally dispatches BOTH the login-info store action and the +// account store action (see +// node_modules/@multiversx/sdk-dapp/out/providers/DappProvider/helpers/login/helpers/accountLogin.cjs) +// +// So after `await provider.login()` resolves, useGetIsLoggedIn() and +// useGetAccount() already reflect the new session — no manual store +// wiring needed beyond calling these two functions in order. + +import { useState } from 'react'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { ProviderFactory } from '@multiversx/sdk-dapp/out/providers/ProviderFactory'; +import { ProviderTypeEnum } from '@multiversx/sdk-dapp/out/providers/types/providerFactory.types'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; -Whenever you encounter code of this sort, everything should remain unchanged: +// The DeFi Wallet extension injects `window.multiversxWallet` once it's +// installed and active (`window.elrondWallet` is the deprecated predecessor +// — the SDK still checks both). This is the SAME check +// ExtensionProviderStrategy performs internally +// (node_modules/@multiversx/sdk-extension-provider/out/extensionProvider.js); +// checking it here up-front lets us show an install link instead of a +// cryptic error after the user clicks. The ambient `Window.multiversxWallet` +// type comes from @multiversx/sdk-extension-provider's own `declare global` +// block (its extensionProvider.d.ts) — a transitive dependency of sdk-dapp. +function isExtensionInstalled(): boolean { + return ( + typeof window !== 'undefined' && + Boolean(window.multiversxWallet || window.elrondWallet) + ); +} -```rust - #[proxy] - fn vault_proxy(&self) -> vault::Proxy; -``` +export function ExtensionLoginButton(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const [status, setStatus] = useState<'idle' | 'connecting' | 'error'>('idle'); + const [error, setError] = useState(null); -If, however, your proxy getter looks like this, the return type has changed, the return type is now `ProxyTo`: + const handleConnect = async (): Promise => { + setStatus('connecting'); + setError(null); + try { + const provider = await ProviderFactory.create({ + type: ProviderTypeEnum.extension, + }); + // nativeAuth is enabled in this recipe's dappConfig, so login() + // generates its own native-auth token internally — no `token` arg + // needed here. Pass one only if you're supplying a custom token. + await provider.login(); + setStatus('idle'); + } catch (err) { + // ExtensionProviderStrategy throws "Extension provider is not + // initialised, call init() first" if the extension wasn't detected at + // provider-creation time (see + // node_modules/@multiversx/sdk-extension-provider/out/extensionProvider.js). + // The isExtensionInstalled() pre-check above should catch this case + // before we ever get here, but the catch stays as defense in depth — + // e.g. the user disables the extension between the check and the click. + const message = err instanceof Error ? err.message : String(err); + setError(message); + setStatus('error'); + } + }; -```rust - #[proxy] - fn vault_proxy(&self, sc_address: ManagedAddress) -> vault::Proxy; -``` + const handleDisconnect = async (): Promise => { + const provider = getAccountProvider(); + await provider.logout(); + }; -To preserve backwards compatibility in this case as well, we placed a hack in the pre-processor stage: the `Proxy` return type is silently replaced by `ProxyTo` in the background, unbeknownst to the developer. For all practical purposes, the code should still be functioning the same way. + if (isLoggedIn) { + return ( + + ); + } -If, however, the developer does not use the legacy proxy object directly, i.e. it returns it, or passes it on to another function, the framework cannot do the replacement there, and you might get a compilation error. + if (!isExtensionInstalled()) { + return ( + + Install DeFi Wallet extension + + ); + } -The solution in this case is simple: replace `Proxy` with `ProxyTo` in code. + return ( + <> + + {status === 'error' && error && ( +

+ Connection failed: {error} +

+ )} + + ); +} +const buttonStyle: React.CSSProperties = { + padding: '0.75rem 1.25rem', + fontSize: '1rem', + cursor: 'pointer', +}; +``` +## The Window type declaration + +The extension injects `window.multiversxWallet`, but that global's type does not +arrive through the deep `@multiversx/sdk-dapp/out/...` import paths this recipe +uses. Declare it locally, or `tsc --strict` reports +`Property 'multiversxWallet' does not exist on type 'Window'`: + +```ts title="src/vite-env.d.ts" +// src/vite-env.d.ts — in a real Vite project this file also carries the line +// `/// ` for import.meta.env typing. The +// augmentation below is the part this recipe specifically needs. +// +// @multiversx/sdk-extension-provider declares this same augmentation in its +// own extensionProvider.d.ts (a `declare global { interface Window { ... } }` +// block), but that file only enters a project's compilation if something +// imports a TYPE from that package directly. This project only imports deep +// @multiversx/sdk-dapp/out/... paths, so the augmentation never gets pulled +// in transitively — redeclared here with the identical shape so +// `window.multiversxWallet` / `window.elrondWallet` type-check. Verified +// against node_modules/@multiversx/sdk-extension-provider/out/extensionProvider.d.ts +// on the actually-installed @multiversx/sdk-dapp@5.6.23's dependency tree. +interface Window { + /** @deprecated Use `multiversxWallet` instead. */ + elrondWallet?: { + extensionId: string; + }; + multiversxWallet?: { + extensionId: string; + }; +} +``` +## The demo page -## Replace `#[derive(TypeAbi)]` with `#[type_abi]` +```tsx title="src/App.tsx" +// src/App.tsx — demo page for ExtensionLoginButton. -To use the new proxies, one must first [generate](./tx-proxies.md#how-to-generate) them. The proxy is designed to be self-contained, so unless configured otherwise, it will also output a copy of the contract types involved in the ABI. +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { ExtensionLoginButton } from './ExtensionLoginButton'; -No methods of these types are copied, but the annotations are important most of the time, so they need to be copied too. These types will need the encode/decode annotations, as well as `Clone`, `Eq`, etc. +export function App(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const account = useGetAccount(); + const { network } = useGetNetworkConfig(); -In order to do this, the proxy generator (via `TypeAbi`) needs to know what type annotations were originally declared. It turns out, derive annotations in Rust don't have access to the other derives that were declared on the same line. For instance, `B` does not see `A` in `#[derive(A, B)]`. + return ( +
+

Login via the DeFi Wallet extension

+

+ Network: {network.chainId} +

+ + + + {isLoggedIn && ( +

+ Connected as {account.address}. +

+ )} +
+ ); +} +``` -The solution is to have another annotation, called `#[type_abi]` **before** the derives. +## Provider bootstrap -Currently, `#[type_abi]` takes no arguments and works the same way as `#[derive(TypeAbi)]`, but it might be extended in the future. +`providers.tsx` calls `initApp()` once and gates rendering until the store is +ready; `lib/multiversx.ts` holds the environment config it passes in. This +recipe does not call `UnlockPanelManager.init()` in the bootstrap, because it +never uses the panel. +```tsx title="src/providers.tsx" +// src/providers.tsx — sdk-dapp v5 init wrapper. +// +// Deliberately does NOT configure UnlockPanelManager. This recipe bypasses +// the generic multi-provider picker entirely and talks to ProviderFactory +// directly for one specific provider — see src/ExtensionLoginButton.tsx. +// If you want the picker instead, see "Adding a wallet login button." -## Generate the new proxies +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { dappConfig } from './lib/multiversx'; -Just like for a new project, you will need to [generate](./tx-proxies.md#how-to-generate) the new proxies and embed them in your project. +let initStarted = false; +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; -## Replace the old proxies in calls + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + setReady(true); + }); -You might have this kind of syntax in your contract. You can easily find it by searching in your project for `#[proxy]` or `.contract(`. + return () => { + cancelled = true; + }; + }, []); -```rust title="Variant A" -#[proxy] -fn vault_proxy(&self) -> vault::Proxy; + if (!ready) { + return ( +
+ Initializing wallet SDK… +
+ ); + } -#[endpoint] -fn do_call(&self, to: ManagedAddress, args: MultiValueEncoded) { - self.vault_proxy() - .contract(to) - .echo_arguments(args) - .async_call() - .with_callback(self.callbacks().echo_args_callback()) - .call_and_exit(); + return <>{children}; } ``` -```rust title="Variant B" -#[proxy] -fn vault_proxy(&self, sc_address: ManagedAddress) -> vault::Proxy; +```ts title="src/lib/multiversx.ts" +// src/lib/multiversx.ts — environment configuration for sdk-dapp's initApp. +// Same shape as every other Vite recipe in this Cookbook. -#[endpoint] -fn do_call(&self, to: ManagedAddress, args: MultiValueEncoded) { - self.vault_proxy(to) - .echo_arguments(args) - .async_call() - .with_callback(self.callbacks().echo_args_callback()) - .call_and_exit(); -} -``` +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; -```rust title="Replace by" -#[endpoint] -fn do_call(&self, to: ManagedAddress, args: MultiValueEncoded) { - self.tx() - .to(&to) - .typed(vault_proxy::VaultProxy) - .echo_arguments(args) - .async_call() - .with_callback(self.callbacks().echo_args_callback()) - .call_and_exit(); -} +const WALLET_CONNECT_PROJECT_ID = + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; + +export const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + nativeAuth: true, + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + }, +}; + +export { EnvironmentsEnum }; ``` -Both variants above should be replaced by this pattern. +## How it works -:::info -The new proxies no longer have a recipient field in them. The [recipient (to) field](./tx-to.md) is completely independent. It is set the same way for transactions with or without proxies. +**`ProviderFactory.create()` already registers the provider.** Verified against +the actual sdk-dapp source +(`node_modules/@multiversx/sdk-dapp/out/providers/ProviderFactory.cjs`), not +inferred from the `.d.ts` alone: `create()`'s last step before returning is +calling `setAccountProvider()` internally. You never call that yourself. -This feature has proven not worth the complication, we are happy to see it go. +**`provider.login()` already populates the whole store.** Also verified against +source +(`node_modules/@multiversx/sdk-dapp/out/providers/DappProvider/helpers/login/helpers/accountLogin.cjs`): +it dispatches the login-info store action, fetches and dispatches the account, +registers the websocket listener, and starts transaction tracking, all before +the returned promise resolves. So this really is the complete sequence: + +```ts +const provider = await ProviderFactory.create({ type: ProviderTypeEnum.extension }); +await provider.login(); +// useGetIsLoggedIn() / useGetAccount() already reflect the new session here. +``` + +**Detecting the extension before the click, not after.** +`ExtensionProviderStrategy` checks `window.multiversxWallet || window.elrondWallet` +internally +(`node_modules/@multiversx/sdk-extension-provider/out/extensionProvider.js`). If +neither is present it silently sets an internal flag rather than throwing, and +the throw only happens later, on `login()`. This recipe runs the identical check +in the UI layer first, so a user without the extension sees an install link +instead of a button that is guaranteed to fail. + +## Pitfalls + +:::warning[Pitfall 1: the not-installed error throws from login(), not create()] +`ProviderFactory.create({ type: ProviderTypeEnum.extension })` calls the +strategy's `init()` internally, which just sets a flag to `false` if the +extension is not found. It does not throw, and `create()` still returns a +provider object successfully. The actual throw +(`"Extension provider is not initialised, call init() first"`) only happens on +the next call, e.g. `login()`. Pre-check with `isExtensionInstalled()` (as this +recipe does) rather than relying on `create()` to fail fast. ::: -Once you've done this, you can safely delete all the old proxy getters (all methods annotated with `#[proxy]`). +:::warning[Pitfall 2: window.multiversxWallet needs a local type declaration] +`@multiversx/sdk-extension-provider` declares this global itself, but that +`.d.ts` only enters your compilation if something imports a *type* from that +package directly. This recipe only imports deep `@multiversx/sdk-dapp/out/...` +paths, so the augmentation never arrives transitively. `src/vite-env.d.ts` +redeclares the identical shape locally. +::: -You can also delete all proxy trait imports of the form `my_module::ProxyTrait as _`. The new proxies require no trait imports. +:::warning[Pitfall 3: this button only ever shows the extension option] +It is provider-specific by design, but that means a mobile user, or a desktop +user without the extension, hits a dead end unless you also offer another path. +Pair it with +[Login via xPortal / WalletConnect](/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login), +or use +[Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button)'s +picker instead if you would rather the SDK decide which providers to surface. +::: +:::warning[Pitfall 4: HTTPS required] +The DeFi extension refuses `http://localhost`. See +[Local HTTPS for dApp dev](/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup) +for a fully trusted mkcert alternative to this recipe's self-signed dev certificate. +::: -:::info -The explicit proxies (traits annotated with `#[multiversx_sc::proxy]`) do not yet have an equivalent in the new syntax. +## See also -It's ok to not migrate them yet. +- [Login via xPortal / WalletConnect](/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login) + is the same `ProviderFactory` pattern, for the mobile-QR provider. +- [Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button) + is the multi-provider picker alternative to this dedicated single-provider button. +- [Read the connected account](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + is what to do once `provider.login()` resolves. -Of course, it is possible rewrite them in the style of the new proxies, but in the absence of code generation, this might be tedious. +--- -A solution is planned for the near future. -::: +### Log in with xPortal via WalletConnect +The sibling to +[Login via the DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login): +a dedicated, single-provider button, this time for xPortal via WalletConnect v2. +Same `ProviderFactory` pattern, with two things specific to WalletConnect: a +`walletConnectV2ProjectId` configured up front, and a real DOM anchor for the QR +code to render into. -In case you want to migrate to the unified syntax and you cannot, or do not want to get rid of the old proxies, this is an alternative transitional syntax: +Use this when you want a dedicated "Connect xPortal" button next to (or instead +of) +[Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button)'s +generic picker. -```rust title="Transitional variant" -#[proxy] -fn vault_proxy(&self, sc_address: ManagedAddress) -> vault::Proxy; +## Prerequisites -#[endpoint] -fn do_call(&self, to: ManagedAddress, args: MultiValueEncoded) { - self.tx() - .legacy_proxy_call(self.vault_proxy(to).echo_arguments(args)) - .async_call() - .with_callback(self.callbacks().echo_args_callback()) - .call_and_exit(); -} +- A working sdk-dapp v5 setup (Next.js or Vite, see the start-here recipes). +- A WalletConnect v2 project ID from + [cloud.walletconnect.com](https://cloud.walletconnect.com). The recipe ships + with a shared, rate-limited demo ID so it runs out of the box, but replace it + before shipping. +- The xPortal mobile app, to actually scan the QR code and test the connected path. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/walletconnect-login +npm install +npm run dev +# open https://localhost:5173 ``` +## Configuring the project ID -## (Optional) Remove contract dependencies +```ts title="src/lib/multiversx.ts" +// src/lib/multiversx.ts — environment configuration for sdk-dapp's initApp. +// +// walletConnectV2ProjectId here is not optional in practice: ProviderFactory +// throws `WalletConnectV2Error.invalidConfig` ("Invalid WalletConnect +// setup") if you try to create a walletConnect provider without one +// configured (verified in +// node_modules/@multiversx/sdk-dapp/out/providers/ProviderFactory.cjs). +// Register a real project ID at https://cloud.walletconnect.com before +// shipping — the demo one here is shared and rate-limited. -The new proxies can be copy-pasted between contract crates. This means that a caller's contract no longer needs to depend on its callees, in order to obtain the proxy. Once the new proxies are set up, you might be able to get rid of the dependency. +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; -This also means contracts no longer need to be on the same framework version. The same proxy, once generated, should work with any framework version the caller uses, irrespective of the callee. +const WALLET_CONNECT_PROJECT_ID = + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; -:::info -Dependencies might still be needed for shared structures and modules. -::: +export const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + nativeAuth: true, + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + }, +}; +export { EnvironmentsEnum }; +``` + +## The button + +```tsx title="src/WalletConnectLoginButton.tsx" +// src/WalletConnectLoginButton.tsx — a dedicated xPortal / WalletConnect +// login button with a QR-code anchor. +// +// Same overall pattern as the "Login via the DeFi extension" recipe — +// ProviderFactory.create() + provider.login() — with two differences +// specific to WalletConnect: +// +// 1. It needs an `anchor`: an HTMLElement the SDK renders the QR code +// (desktop) / deep-link prompt (mobile) into. Without one, there's +// nowhere for the user to actually see the code to scan. +// 2. It needs `walletConnectV2ProjectId` configured in initApp()'s +// dAppConfig — see src/lib/multiversx.ts. Omitting it makes +// ProviderFactory.create() throw immediately (Pitfall 1). +// +// The rest of the sequence is identical to the DeFi extension recipe and +// is verified the same way — against the actual sdk-dapp source, not just +// its .d.ts files: +// +// ProviderFactory.create({ type, anchor }) +// → internally calls setAccountProvider() for you (see +// node_modules/@multiversx/sdk-dapp/out/providers/ProviderFactory.cjs) +// provider.login() +// → internally dispatches both the login-info and account store +// actions (see +// node_modules/@multiversx/sdk-dapp/out/providers/DappProvider/helpers/login/helpers/accountLogin.cjs) + +import { useRef, useState } from 'react'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { ProviderFactory } from '@multiversx/sdk-dapp/out/providers/ProviderFactory'; +import { ProviderTypeEnum } from '@multiversx/sdk-dapp/out/providers/types/providerFactory.types'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +export function WalletConnectLoginButton(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const [status, setStatus] = useState<'idle' | 'connecting' | 'error'>('idle'); + const [error, setError] = useState(null); + // The QR code / deep-link UI renders into this div. It must exist in the + // DOM before ProviderFactory.create() runs — it's rendered unconditionally + // below, so by the time a click handler fires, anchorRef.current is set. + const anchorRef = useRef(null); + + const handleConnect = async (): Promise => { + if (!anchorRef.current) { + // Should not happen — the anchor div always renders — but keeps the + // types honest under strict null checks rather than asserting `!`. + setError('QR anchor not mounted yet.'); + setStatus('error'); + return; + } + setStatus('connecting'); + setError(null); + try { + const provider = await ProviderFactory.create({ + type: ProviderTypeEnum.walletConnect, + anchor: anchorRef.current, + }); + // nativeAuth is enabled in this recipe's dappConfig, so login() + // generates its own native-auth token internally. + await provider.login(); + setStatus('idle'); + } catch (err) { + // If walletConnectV2ProjectId isn't configured, this throws + // "Invalid WalletConnect setup" (WalletConnectV2Error.invalidConfig, + // node_modules/@multiversx/sdk-dapp/out/providers/strategies/WalletConnectProviderStrategy/types/walletConnect.types.d.ts) + // — before the QR code ever renders. That specific case is confirmed + // from source. What happens if the user closes the QR modal without + // scanning is NOT independently confirmed here — the strategy's + // compiled source logs a `WalletConnectV2Error.userRejected` label in + // a catch block around the approval flow, but tracing exactly what + // (if anything) it re-throws to this call site would need a live + // WalletConnect session to observe, not just reading the bundle. + // Don't assume the caught message always matches one of the + // WalletConnectV2Error enum strings — display it, but treat it as + // unstructured for now. + const message = err instanceof Error ? err.message : String(err); + setError(message); + setStatus('error'); + } + }; -## Replace method names + const handleDisconnect = async (): Promise => { + const provider = getAccountProvider(); + await provider.logout(); + }; -To achieve backwards compatibility, all the old methods were kept (even though their implementations changed). + if (isLoggedIn) { + return ( + + ); + } -We did not yet deprecate the old ones, but we encourage everyone to switch to the new ones. They have shorter names and tend to be more expressive. + return ( +
+ + + {/* The SDK renders the QR code / deep-link prompt into this element + once ProviderFactory.create() runs. Give it real dimensions — + an empty 0x0 div means the QR code has nowhere visible to draw. */} +
+ + {status === 'error' && error && ( +

+ Connection failed: {error} +

+ )} +
+ ); +} -| Old method | New method | Comments | -| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| `.with_egld_transfer(amount)` | `.egld(amount)` | | -| `.with_esdt_transfer((token, nonce, amount))` | `.esdt((token, nonce, amount))`
or
`.single_esdt(&token, nonce, &amount)` | `single_esdt` can deal with references, instead of taking owned objects. | -| `.with_multi_token_transfer(p)` | `.payment(p)` | `payment` is universal. | -| `.with_egld_or_single_esdt_transfer(p)` | `.payment(p)` | Method `payment` is universal. | -| `.with_gas_limi(gas)` | `.gas(gas)` | | -| `.with_extra_gas_for_callback(gas)` | `.gas_for_callback(gas)` | Method `payment` is universal. | -| `.async_call()` | - | Does nothing, can be removed with no consequences. | -| `.async_call_promise()` | - | Does nothing, can be removed with no consequences. | -| `.with_callback(cb)` | `.callback(cb)` | | -| `.deploy_contract(code, code_metadata)` | `.code(code)`
`.code_metadata(code_metadata)`
`.sync_call()` | Also add result handlers for decoding the result. | -| `.deploy_from_source(address, code_metadata)` | `.from_source(code)`
`.code_metadata(code_metadata) .sync_call()` | Also add result handlers for decoding the result. | -| `.upgrade_contract(code, code_metadata)` | `.code(code) .code_metadata(code_metadata) .upgrade_async_call_and_exit()` | Upgrades are async calls. | -| `.upgrade_from_source(address, code_metadata)` | `.from_source(code)`
`.code_metadata(code_metadata)`
`.upgrade_async_call_and_exit()` | Upgrades are async calls. | -| `.execute_on_dest_context()` | `.sync_call()` | Also add result handlers for decoding the result. | -| `.execute_on_dest_context`
`_with_back_transfers()` | `.returns(ReturnsBackTransfers)`
`.sync_call()` | Add additional result handlers for decoding the result. | +const buttonStyle: React.CSSProperties = { + padding: '0.75rem 1.25rem', + fontSize: '1rem', + cursor: 'pointer', +}; +const anchorStyle: React.CSSProperties = { + marginTop: '1rem', + minHeight: '280px', + minWidth: '280px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}; +``` +## The demo page +```tsx title="src/App.tsx" +// src/App.tsx — demo page for WalletConnectLoginButton. -## Black-box tests +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { WalletConnectLoginButton } from './WalletConnectLoginButton'; -We have not extensively advertised the scenario-based black-box tests, knowing they would eventually be superseded by the unified transaction syntax. +export function App(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const account = useGetAccount(); + const { network } = useGetNetworkConfig(); -If, however, you got to develop on that syntax, here is how to migrate to the new one. + return ( +
+

Login via xPortal / WalletConnect

+

+ Network: {network.chainId} +

+ + + + {isLoggedIn && ( +

+ Connected as {account.address}. +

+ )} +
+ ); +} +``` -```rust title=before_migration.rs -use multiversx_sc_scenario::{scenario_model::*, *}; +## Provider bootstrap -const ADDER_PATH_EXPR: &str = "mxsc:output/adder.mxsc.json"; +`providers.tsx` calls `initApp()` once and gates rendering until the store is ready. -fn world() -> ScenarioWorld { - let mut blockchain = ScenarioWorld::new(); - blockchain.register_contract(ADDER_PATH_EXPR, adder::ContractBuilder); - blockchain +```tsx title="src/providers.tsx" +// src/providers.tsx — sdk-dapp v5 init wrapper. +// +// Deliberately does NOT configure UnlockPanelManager — this recipe bypasses +// the generic multi-provider picker and talks to ProviderFactory directly +// for one specific provider. See src/WalletConnectLoginButton.tsx. + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { dappConfig } from './lib/multiversx'; + +let initStarted = false; + +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; + + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + setReady(true); + }); + + return () => { + cancelled = true; + }; + }, []); + + if (!ready) { + return ( +
+ Initializing wallet SDK… +
+ ); + } + + return <>{children}; } +``` -#[test] -fn adder_blackbox_raw() { - let mut world = world(); - let adder_code = world.code_expression(ADDER_PATH_EXPR); +## How it works - world - .set_state_step( - SetStateStep::new() - .put_account("address:owner", Account::new().nonce(1)) - .new_address("address:owner", 1, "sc:adder"), - ) - .sc_deploy( - ScDeployStep::new() - .from("address:owner") - .code(adder_code) - .argument("5") - .expect(TxExpect::ok().no_result()), - ) - .sc_query( - ScQueryStep::new() - .to("sc:adder") - .function("getSum") - .expect(TxExpect::ok().result("5")), - ) - .sc_call( - ScCallStep::new() - .from("address:owner") - .to("sc:adder") - .function("add") - .argument("3") - .expect(TxExpect::ok().no_result()), - ) - .check_state_step( - CheckStateStep::new() - .put_account("address:owner", CheckAccount::new()) - .put_account( - "sc:adder", - CheckAccount::new().check_storage("str:sum", "8"), - ), - ); -} -``` +**The anchor is not optional in practice.** +`ProviderFactory.create({ type: ProviderTypeEnum.walletConnect, anchor })` needs a +real `HTMLElement` to draw the QR code (desktop) or deep-link prompt (mobile +browser) into, verified from `WalletConnectProviderStrategyConfigType`, which +extends `WalletConnectConfig` with `anchor?: HTMLElement`. This recipe uses a +`useRef` div rendered unconditionally, so it exists before any +click handler runs. -This was the old blackbox code from the `adder` contract, present in framework version `0.48.0`. +**The project ID gate happens before the QR code ever renders.** +`ProviderFactory.create()` reads `walletConnectV2ProjectId` from the store (the +same config object passed to `initApp()`) and throws `"Invalid WalletConnect setup"` +immediately if it is missing, confirmed directly from the compiled source +(`node_modules/@multiversx/sdk-dapp/out/providers/ProviderFactory.cjs`): +`if (!config?.walletConnectV2ProjectId) throw new Error(WalletConnectV2Error.invalidConfig)`. -Migrating this test to unified syntax means, in a nutshell, separating each action into different steps, replacing verbose `CheckStateStep` and `SetStateStep` with their specific state builders, and replacing the interactions with actual transactions (deploy, query, call). +**Everything after `create()` matches the DeFi extension recipe exactly.** +`ProviderFactory.create()` calls `setAccountProvider()` internally; +`provider.login()` dispatches both the login-info and account store actions +internally. Once `await provider.login()` resolves, `useGetIsLoggedIn()` and +`useGetAccount()` already reflect the session. -This is how the migrated test looks like: -```rust title=after_migration.rs -use multiversx_sc_scenario::imports::*; +## Pitfalls -use adder::*; +:::danger[Pitfall 1: missing walletConnectV2ProjectId fails immediately] +`ProviderFactory.create({ type: ProviderTypeEnum.walletConnect })` throws +`"Invalid WalletConnect setup"` (`WalletConnectV2Error.invalidConfig`) if +`dAppConfig.providers.walletConnect.walletConnectV2ProjectId` was not set in +`initApp()`. Register a real project ID at +[cloud.walletconnect.com](https://cloud.walletconnect.com) before shipping. The +shared demo ID here is rate-limited and meant for local development only. +::: -// new types -const OWNER_ADDRESS: TestAddress = TestAddress::new("owner"); -const ADDER_ADDRESS: TestSCAddress = TestSCAddress::new("adder"); -const CODE_PATH: MxscPath = MxscPath::new("output/adder.mxsc.json"); +:::warning[Pitfall 2: give the anchor real dimensions] +An anchor `
` with no width or height renders a QR code with nowhere visible +to draw. This recipe's anchor style sets an explicit `min-height`/`min-width`. +Don't drop that when you copy the pattern into your own layout. +::: -fn world() -> ScenarioWorld { - let mut blockchain = ScenarioWorld::new(); +:::warning[Pitfall 3: the closed-modal error path is not independently confirmed here] +The compiled strategy source references a `WalletConnectV2Error.userRejected` +label inside a `catch` block around its approval flow, but confirming exactly what +(if anything) propagates to your own `catch` when the user closes the modal +without scanning would need a live WalletConnect session to observe, not just +reading the minified bundle. Display whatever you get, do not pattern-match on it. +::: - blockchain.register_contract(CODE_PATH, adder::ContractBuilder); - blockchain -} +:::warning[Pitfall 4: this button only ever offers WalletConnect] +Pair it with +[Login via the DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login) +for a desktop-first option, or use +[Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button)'s +picker if you would rather let the SDK decide which providers to surface. +::: -#[test] -fn adder_blackbox() { - let mut world = world(); // ScenarioWorld +## See also - // starting mandos trace - world.start_trace(); +- [Login via the DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login) + is the same `ProviderFactory` pattern for the browser-extension provider. +- [Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button) + is the multi-provider picker alternative to this dedicated button. +- [Read the connected account](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + is what to do once `provider.login()` resolves. - // set state for owner - world.account(OWNER_ADDRESS).nonce(1); +--- - // deploy the contract - let new_address = world - .tx() // tx with test environment - .from(OWNER_ADDRESS) - .typed(adder_proxy::AdderProxy) // typed call - proxy - .init(5u32) // deploy call - .code(CODE_PATH) - .new_address(ADDER_ADDRESS) // custom deploy address for tests - .returns(ReturnsNewAddress) // returns new address after deploy - .run(); // send transaction +### logs - assert_eq!(new_address, ADDER_ADDRESS.to_address()); +This page describes the structure of the `logs` index (Elasticsearch), and also depicts a few examples of how to query it. - // query the contract, `sum` view - world - .query() // tx with test query environment - .to(ADDER_ADDRESS) - .typed(adder_proxy::AdderProxy) // typed call - proxy - .sum() - .returns(ExpectValue(5u32)) // asserts returned value == 5u32 - .run(); // send transaction - // contract call, `add` endpoint - world - .tx() // tx with test environment - .from(OWNER_ADDRESS) - .to(ADDER_ADDRESS) - .typed(adder_proxy::AdderProxy) // typed call - proxy - .add(1u32) - .run(); // send transaction +## _id - // query the contract, `sum view` - world - .query() // tx with test query environment - .to(ADDER_ADDRESS) - .typed(adder_proxy::AdderProxy) // typed call - proxy - .sum() - .returns(ExpectValue(6u32)) // asserts returned value == 6u32 - .run(); // send transaction +:::warning Important - // check state for owner - world.check_account(OWNER_ADDRESS); +**The `logs` index will be deprecated and removed in the near future.** +We recommend using the [events](/sdk-and-tools/indices/es-index-events) index, which contains all the events included in a log. - // check state for adder - world - .check_account(ADDER_ADDRESS) - .check_storage("str:sum", "6"); +Please make the necessary updates to ensure a smooth transition. +If you need further assistance, feel free to reach out. +::: - // write mandos trace to file - world.write_scenario_trace("trace1.scen.json"); -} -``` +The `_id` field for this index is composed of hex-encoded hash of the transaction of the smart contract result that generated the log. ---- -### miniblocks +## Fields -This page describes the structure of the `miniblocks` index (Elasticsearch), and also depicts a few examples of how to query it. +| Field | Description | +|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| address | The address field holds the address in bech32 encoding. It can be the address of the smart contract that generated the log or the address of the receiver address of the transaction. | +| events | The events field holds a list of events. | +| originalTxHash | The originalTxHash field holds the hex-encoded hash of the initial transaction. When this field is not empty the log is generated by a smart contract result and this field represents the hash of the initial transaction. | +| timestamp | The timestamp field represents the timestamp of the block in which the log was generated. | -## _id +Event structure -The _id field of this index is represented by the miniblock hash, in a hexadecimal encoding. +| Field | Description | +|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| identifier | This field represents the identifier of the event. | +| address | The address field holds the address in bech32 encoding. It can be the address of the smart contract that generated the event or the address of the receiver address of the transaction. | +| topics | The topics field holds a list with extra information. They don't have a specific order because the smart contract is free to log anything that could be helpful. | +| data | The data field can contain information added by the smart contract that generated the event. | +| order | The order field represents the index of the event indicating the execution order. | -## Fields +## Query examples -| Field | Description | -|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| senderShard | The senderShard field represents the shard ID of the source block. | -| receiverShard | The receiverShard field represents the shard ID of the destination block. | -| senderBlockHash | The senderBlockHash field represents the hash (hex encoded) of the source block in which the miniblock was included. | -| receiverBlockHash | The receiverBlockHash field represents the hash (hex encoded) of the destination block in which the miniblock was included. | -| type | The type field represents the type of the miniblock. It can be `TxBlock` (if it contains transactions) or `SmartContractResultBlock` (if it contains smart contracts results). | -| procTypeS | The procTypeS field represents the processing type at the source shard. It can be `Normal` or `Scheduled`. | -| procTypeD | The procTypeD field represents the processing type at the destination shard. It can be `Normal` or `Scheduled`. | -| timestamp | The timestamp field represents the timestamp of the block in which the miniblock was executed. | -| reserved | The reserved field ensures the possibility to extend the mini block. | +### Fetch all the logs generated by a transaction -## Query examples +``` +curl --request GET \ + --url ${ES_URL}/logs/_search \ + --header 'Content-Type: application/json' \ + --data '{ + "query": { + "match": { + "_id":"d6.." + } + } +}' +``` -### Fetch all the miniblocks of a block +### Fetch all the logs generated by a transaction and the smart contract results triggered by it ``` curl --request GET \ - --url ${ES_URL}/miniblocks/_search \ + --url ${ES_URL}/logs/_search \ --header 'Content-Type: application/json' \ --data '{ - "query": { + "query": { "bool": { "should": [ { - "match": { - "senderBlockHash": "ddc..." - } + "match": { + "_id":"d6.." + } }, { "match": { - "receiverBlockHash": "ddc..." + "originalTxHash": "d6.." } } ] @@ -23141,17908 +30837,30244 @@ curl --request GET \ --- -### Multi-Values +### Manage nonces (fetch-then-increment) -## Single values vs. multi-values +The pattern behind sdk-core's single most emphasized rule: fetch the account's +nonce from the network **once**, then increment it **locally** for every +transaction sent afterward in the same batch, never re-fetch mid-batch. This +recipe makes that pattern concrete with a small burst of sequential sends, plus +the recovery step for when a batch partially fails. -To recap, we have discussed about data being represented either in a: -- nested encoding, as part of the byte representation of a larger object; -- top encoding, the full byte representation of an object. +This is the sdk-core side of a problem that also exists in sdk-dapp's +connected-wallet flow (see +[Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send)'s +Pitfall 1), the failure mode is identical; only the concrete API differs. -But even the top encoding only refers to a _single_ object, being represented as a _single_ array of bytes. This encoding, no matter how simple or complex, is the representation for a _single_ argument, result, log topic, log event, NFT attribute, etc. +## Prerequisites -However, we sometimes want to work with _multiple_, variadic arguments, or an arbitrary number of results. An elegant solution is modelling them as special collections of top-encodable objects that each represent an individual item. For instance, we could have a list of separate arguments, of arbitrary length. +- Node.js >= 20.13.1. +- A devnet wallet with some devnet EGLD, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites for how to generate and fund a throwaway one. -Multi-values work similarly to varargs in other languages, such as C, where you can write `void f(int arg, ...) { ... }`. In the smart contract framework they do not need specialized syntax, we use the type system to define their behavior. +## Install -:::info Note -In the framework, single values are treated as a special case of multi-value, one that consumes exactly one argument, or returns exactly one value. +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/manage-nonces +npm install +npm run build +npm start -- ./wallet.pem erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th 3 +``` + +## The pattern + +```ts title="src/batchSend.ts" +// src/batchSend.ts — the actual subject of this recipe: sending more than +// one transaction per process run with a SINGLE nonce fetch, using +// `getNonceThenIncrement()` locally for each one. +// +// The sdk-core nonce rule: you MUST fetch the account nonce from the network +// before sending transactions, then increment locally with +// getNonceThenIncrement(). Stale nonces cause transaction rejection. This +// file is that pattern, applied to a batch. +// +// THE WRONG PATTERN (do not copy this) is calling +// `entrypoint.recallAccountNonce(address)` again before building each +// transaction in the loop below, instead of relying on the account's local +// counter. It looks safer ("always get the freshest nonce!") but does the +// opposite: the network only advances an account's nonce once a +// transaction is PROCESSED, not merely broadcast. If you fire transaction +// #2's nonce fetch before #1 has been processed, both fetches return the +// SAME value — transaction #2 then either overwrites #1 in the mempool +// (same nonce, different hash) or gets rejected outright, depending on +// timing. This is the exact failure mode the "Sign and send a transaction" +// recipe's Pitfall 1 describes for the sdk-dapp + connected-wallet flow; +// the fix is structurally identical here even though the concrete API +// differs (sdk-dapp's AccountType.nonce is read-only with no +// getNonceThenIncrement() equivalent — you'd track a local counter +// yourself; here, Account already has one built in). +// +// Fetch once, per PROCESS RUN (or per logical batch), before the loop — +// not once per transaction inside it. + +import { Address } from '@multiversx/sdk-core'; +import type { Account, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export interface BatchSendResult { + nonce: bigint; + txHash: string; +} + +/** + * Sends one EGLD transfer per entry in `amounts`, all to the same + * `receiver`, using a single local nonce counter incremented once per + * transaction. Does NOT wait for any transaction to confirm before + * building and sending the next — that's the whole point: this is exactly + * the "send a burst, don't wait" scenario where re-fetching the nonce + * per-send would break (see the file header). + * + * `sender.nonce` must already reflect the network's current nonce before + * calling this — see src/account.ts's loadDevnetAccount, called once by the + * caller, not once per entry in `amounts`. + */ +export async function sendBatch( + entrypoint: DevnetEntrypoint, + sender: Account, + receiver: string, + amounts: bigint[], +): Promise { + const controller = entrypoint.createTransfersController(); + const receiverAddress = Address.newFromBech32(receiver); + const results: BatchSendResult[] = []; + + for (const amount of amounts) { + // ONE local increment per transaction. No network call in this loop — + // that's the fix. Capture the nonce used for this transaction before + // the controller call increments it further, so the log below reflects + // reality even though getNonceThenIncrement() already advanced + // sender.nonce internally. + const nonceForThisTx = sender.nonce; + + const transaction = await controller.createTransactionForNativeTokenTransfer( + sender, + sender.getNonceThenIncrement(), + { + receiver: receiverAddress, + nativeAmount: amount, + }, + ); -In effect, all serializable types implement the multi-value traits. -::: + const txHash = await entrypoint.sendTransaction(transaction); + results.push({ nonce: nonceForThisTx, txHash }); + } + return results; +} -## Parsing and limitations +/** + * Recovery for when a batch partially fails and leaves a nonce gap (one + * transaction rejected or expired mid-batch stalls every transaction + * queued behind it, up to the mempool's own timeout). Re-fetches the real + * nonce from the network and overwrites the local one — resyncing rather + * than continuing to trust a local counter that may now be wrong. + * + * There is no way to detect a gap from the client side alone before + * attempting to send; this is a recovery step to call after a send in the + * batch fails, not a pre-check. + */ +export async function resyncNonce(entrypoint: DevnetEntrypoint, sender: Account): Promise { + sender.nonce = await entrypoint.recallAccountNonce(sender.address); +} +``` -It is important to understand that arguments get read one by one from left to right, so there are some limitations as to how var-args can be positioned. Argument types also define how the arguments are consumed, so, for instance, if a type specifies that all remaining arguments will be consumed, it doesn't really make sense to have any other argument after that. +## Run it -For instance, let's consider the behavior of `MultiValueEncoded`, which consumes all subsequent arguments. Hence, it's advisable to place it as the last argument in the function, like so: +Expected output, three distinct, sequential nonces from a **single** network +fetch at the start: -```rust -#[endpoint(myEndpoint)] -fn my_endpoint(&self, first_arg: ManagedBuffer, second_arg: TokenIdentifier, last_arg: MultiValueEncoded) -``` -Placing any argument after `MultiValueEncoded` will not initialize that argument, because `MultiValueEncoded` will consume all arguments following it. An important rule to remember is that an endpoint can have only one `MultiValueEncoded` argument, and it should always occupy the last position in order to achieve the desired outcome. +```text +Sender: erd1... (starting nonce 42) +Sending 3 transfers of 0.001 EGLD each to erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th… -Another scenario to consider involves the use of multiple `Option` arguments. Take, for instance, the following endpoint: +nonce 42 -> +nonce 43 -> +nonce 44 -> -```rust -#[endpoint(myOptionalEndpoint)] -fn my_optional_endpoint(&self, first_arg: OptionalValue, second_arg: OptionalValue) +Ending local nonce: 45 ``` -In this context, both arguments (or none) should be provided at the same time in order to get the desired effect. Since arguments are processed sequentially from left to right, supplying a single value will automatically assign it to the first argument, making it impossible to determine which argument should receive that value. -The same rule applies when any regular argument is placed after a var-arg, thus, a strong restriction regarding arguments' order has been enforced. Regular arguments `must not` be placed after var-args. +## How it works -To further enhance clarity and minimize potential errors related to var-args, starting from framework version `v0.44.0`, it is no longer allowed by default to have multiple var-args. This restriction can be lifted by using the #[allow_multiple_var_args] annotation. +**One fetch, then N local increments, never N fetches.** `sender.nonce` is set +once from `entrypoint.recallAccountNonce(address)`. Inside `sendBatch()`'s loop, +each iteration calls `sender.getNonceThenIncrement()`, a purely local, +synchronous read-then-increment on the `Account` object, with **no** network +call. Verified directly against real devnet: sending 3 transfers in a row from an +intentionally unfunded wallet produced transactions with `nonce: 0`, `nonce: 1`, +and `nonce: 2` respectively, each rejected for "insufficient funds", never for a +stale or duplicate nonce, confirming no re-fetch happened between iterations. -:::info Note -`#[allow_multiple_var_args]` is required when using more than one var-arg in an endpoint and is placed at the endpoint level, alongside the `#[endpoint]` annotation. Utilizing `#[allow_multiple_var_args]` in any other manner will not work. +**Why re-fetching per-send is actively wrong, not just slower.** The network only +advances an account's nonce once a transaction is *processed*, not merely +broadcast. Calling `recallAccountNonce()` again for transaction #2 before +transaction #1 has been processed returns the **same** value both times; +transaction #2 then collides with #1 or gets rejected, depending on timing. -Considering this, our optional endpoint from the example before becomes: -```rust -#[allow_multiple_var_args] -#[endpoint(myOptionalEndpoint)] -fn my_optional_endpoint(&self, first_arg: OptionalValue, second_arg: OptionalValue) -``` -::: +**A nonce gap stalls everything queued behind it.** If one transaction in a batch +is rejected or expires, every transaction already built with a *higher* local +nonce is now invalid until that gap is filled or the queued transactions time +out. `resyncNonce()` recovers by re-fetching the real value and overwriting the +local counter, a recovery step to call after a failure, not a defensive pre-check +before every send. -The absence of #[allow_multiple_var_args] as an endpoint attribute, along with the use of multiple var-args and/or the placement of regular arguments after var-args, leads to build failure, as the parsing validations now consider the count and positions of var-args. +For conceptual depth, see +[docs.multiversx.com/sdk-and-tools/sdk-js/sdk-js-cookbook](https://docs.multiversx.com/sdk-and-tools/sdk-js/sdk-js-cookbook). -However, when `#[allow_multiple_var_args]` is used, there is no other parsing validation (except the ones from above) to enforce the var-args rules mentioned before. In simpler terms, using the annotation implies that the developer is assuming responsibility for handling multiple var-args and anticipating the outcomes, effectively placing trust in their ability to manage the situation. +## Pitfalls +:::note[Pitfall 1: this recipe does not wait for each transaction to confirm before sending the next] +That's the point, waiting between every send would sidestep the whole problem, at +the cost of one network round-trip per transaction. If your workload can tolerate +that latency, waiting is simpler and this recipe doesn't apply to you. +::: -## Standard multi-values +:::warning[Pitfall 2: a failure partway through the loop is not automatically retried or detected] +`sendBatch()` throws on the first rejected transaction, aborting the remaining +iterations, verified directly: an intentionally unfunded wallet's first send fails +and the loop never reaches its second or third iteration. A production version +needs its own decision about whether to abort, skip, or resync-and-retry. +::: -These are the common multi-values provided by the framework: -- Straightforward var-args, an arbitrary number of arguments of the same type. - - Can be defined in code as: - - `MultiValueVec` - the unmanaged version. To be used outside of contracts. - - `MultiValueManagedVec` - the equivalent managed version. The only limitation is that `T` must implement `ManagedVecItem`, because we are working with an underlying `ManagedVec`. Values are deserialized eagerly, the endpoint receives them already prepared. - - `MultiValueEncoded` - the lazy version of the above. Arguments are only deserialized when iterating over this structure. This means that `T` does not need to implement `ManagedVecItem`, since it never gets stored in a `ManagedVec`. - - In all these 3 cases, `T` can be any serializable type, either single or multi-value. - - Such a var-arg will always consume all the remaining arguments, so it doesn't make sense to place any other arguments after it, single or multi-value. The framework doesn't forbid it, but single values will crash at runtime, since they always need a value, and multi-values will always be empty. -- Multi-value tuples. - - Defined as `MultiValueN`, where N is a number between 2 and 16, and `T1`, `T2`, ..., `TN` can be any serializable types, either single or multi-value; e.g. `MultiValue3`. - - It doesn't make much sense to use them as arguments on their own (it is easier and equivalent to just have separate named arguments), but they do have the following uses: - - They can be embedded in a regular var-arg to obtain groups of arguments. For example `MultiValueVec>` defines pairs of numbers. There is no more need to check in code that an even number of arguments was passed, the deserializer resolves this on its own. - - Rust does not allow returning more than one result, but by returning a multi-value tuple we can have an endpoint return several values, of different types. -- Optional arguments. - - Defined as `OptionalValue`, where `T` can be any serializable type, either single or multi-value. - - At most one argument will be consumed. For this reason it sometimes makes sense to have several optional arguments one after the other, or optional arguments followed by var-args. - - Do note, however, that an optional argument cannot be missing if there is anything else coming after it. For example if an endpoint has arguments `a: OptionalValue, b: OptionalValue`, `b` can be missing, or both can be missing, but there is no way to have `a` missing and `b` to be there, because passing any argument will automatically get it assigned to `a`. -- Counted var-args. - - Suppose we actually do want two sets of var-args in an endpoint. One solution would be to explicitly state how many arguments each of them contain (or at least the first one). Counted var-args make this simple. - - Defined as `MultiValueManagedVecCounted`, where `T` can be any serializable type, either single or multi-value. - - Always takes a number argument first, which represents how many arguments follow. Then it consumes exactly that many arguments. - - Can be followed by other arguments, single or multi-value. -- Async call result. - - Asynchronous call callbacks also need to know whether or not the call failed, so they have a special format for transmitting arguments. The first argument is always the error code. If the error code is `0`, then the call result follows. Otherwise, we get one additional argument, which is the error message. To easily deserialize this, we use a special type. - - Defined as `ManagedAsyncCallResult`. - - There is also an unmanaged version, `AsyncCallResult`, but it is no longer used nowadays. - - They are both enums, the managed part only refers to the error message. -- Ignored arguments. - - Sometimes, for backwards compatibility or other reasons it can happen to have (optional) arguments that are never used and not of interest. To avoid any useless deserialization, it is possible to define an argument of type `IgnoreValue` at the end. - - By doing so, any number of arguments are allowed at the end, all of which will be completely ignored. +:::warning[Pitfall 3: resyncNonce() is a recovery step, not a pre-check] +There is no way to detect a nonce gap from the client side before attempting to +send. Call `resyncNonce()` after a failure, not defensively before every send, +doing the latter reintroduces the "why re-fetching per-send is wrong" problem +above. +::: -So, to recap: +:::danger[Pitfall 4: concurrent processes sharing one account will race regardless of this pattern] +`getNonceThenIncrement()` only protects against the caller re-fetching nonces it +already knows. If two separate process instances both load the same PEM and both +call `recallAccountNonce()` around the same time, they can still both observe the +same starting value. Use one process (or an external lock) per account for +nonce-sensitive sends. +::: -| Managed Version | Unmanaged version | What it represents | Similar single value | -| ---------------------------------- | -------------------- | ------------------------------- | ------------------------------ | -| `MultiValueN` | | A fixed number of arguments | Tuple `(T1, T2, ..., TN)` | -| `OptionalValue` | | An optional argument | `Option` | -| `MultiValueEncoded` | `MultiValueVec` | Variable number of arguments | `Vec` | -| `MultiValueManagedVec` | `MultiValueVec` | Variable number of arguments | `Vec` | -| `MultiValueManagedVecCounted` | | Counted number of arguments | `(usize, Vec)` | -| `ManagedAsyncCallResult` | `AsyncCallResult` | Async call result in callback | `Result` | -| `IgnoreValue` | | Any number of ignored arguments | Unit `()` | +## See also +- [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld) + is the single-send case this recipe extends to a batch. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the same nonce problem, in the sdk-dapp + connected-wallet flow. +- The same Controller pattern applies to ESDT token transfers, covered in the + transactions recipes. -## Storage mapper contents as multi-values +--- -The storage mapper declaration is a method that can normally also be made into a public view endpoint. If so, when calling them, the entire contents of the mapper will be read from storage and serialized as multi-value. Only recommended when there is little data, or in tests. +### Managed Decimal -These storage mappers are, in no particular order: -- BiDiMapper -- LinkedListMapper -- MapMapper -- QueueMapper -- SetMapper -- SingleValueMapper -- UniqueIdMapper -- UnorderedSetMapper -- UserMapper -- VecMapper -- FungibleTokenMapper -- NonFungibleTokenMapper +## Managed Decimal Overview + +`ManagedDecimal` is a generic type representing a fixed-point decimal number managed by the API. +It is designed to handle decimal values with a specific number of decimals, providing operations such as addition, subtraction, multiplication, division, scaling, and conversion between different decimal precision and also more complex operations such as logarithm and nth root with a customizable degree of precision. +Essentially, `ManagedDecimal` is simply a struct containing a wrapper over a `BigIntHandle` (`BigUint`) and the number of decimals associated with that value (precision). Having such a light design helps `ManagedDecimal` become the optimal solution for dealing with fixed-point decimal numbers in terms of efficiency and readability. -## Multi-values in action +```rust title=managed_decimal.rs +#[derive(Debug, Clone)] +pub struct ManagedDecimal { + data: BigUint, // value * scaling_factor + decimals: D, // number_of_decimals (precision) +} +``` -To clarify the way multi-values work in real life, let's provide some examples of how one would go avout calling an endpoint with variadic arguments. +The `scaling factor` of the decimal is `10^num_of_decimals` and is a cached value across multiple `ManagedDecimal` instances for increased efficiency. +```rust title=example.rs +pub fn main() { + let decimal = ManagedDecimal::>::from(BigUint::from(1u64)); + let cached_decimal = ManagedDecimal::>::from(BigUint::from(5u64)); +} +``` -### Option vs. OptionalValue -Assume we want to have an endpoint that takes a token identifier, and, optionally, a token nonce. There are two ways of doing this: +## Number of decimals -```rust -#[endpoint(myOptArgEndpoint1)] -fn my_opt_arg_endpoint_1(&self, token_id: TokenIdentifier, opt_nonce: Option) {} -``` +`Decimals` is a trait representing the number of decimals associated with a decimal value and is only +implemented for `NumDecimals` and `ConstDecimals`. +`NumDecimals` is a type alias for `usize` and is used to specify the number of decimals dynamically, while `ConstDecimals` is a type that represents a constant number of decimals and is used to statically specify the number of decimals for ManagedDecimal instances. -```rust -#[endpoint(myOptArgEndpoint2)] -fn my_opt_arg_endpoint_2(&self, token_id: TokenIdentifier, opt_nonce: OptionalValue) {} +```rust title=example.rs +pub fn main() { + let decimal: ManagedDecimal = ManagedDecimal::from_raw_units(BigUint::from(100u64), 2usize); + let const_decimal = ManagedDecimal::>::const_decimals_from_raw(BigUint::from(500u64)) +} ``` -We want to call these endpoints with arguments: `TOKEN-123456` (`0x544f4b454e2d313233343536`) and `5`. To contrast for the two endpoints: -- Endpoint 1: `myOptArgEndpoint1@544f4b454e2d313233343536@010000000000000005` -- Endpoint 2: `myOptArgEndpoint2@544f4b454e2d313233343536@05` -:::info Note -In the first case, we are dealing with an [Option](/developers/data/composite-values#options), whose first encoded byte needs to be `0x01`, to signal `Some`. In the second case there is no need for `Option`, `Some` is signalled simply by the fact that the argument was provided. +## Operations -Also note that the nonce itself is nested-encoded in the first case (being _nested_ in an `Option`), whereas in the second case it can be top-encoded directly. -::: +`ManagedDecimal` supports various types of simple and complex operations, as well as conversions and scaling. More code examples can be found at `mx-sdk-rs/framework/scenario/tests/managed_decimal_test.rs` -Now let's do the same exercise for the case where we want to omit the nonce altogether: -- Endpoint 1: - - `myOptArgEndpoint1@544f4b454e2d313233343536@`, or - - `myOptArgEndpoint1@544f4b454e2d313233343536@00` - also accepted -- Endpoint 2: - - `myOptArgEndpoint2@544f4b454e2d313233343536` +### Available methods: +- Simple Operations: + - `add`, `mul`, `div`, `sub` are all implemented for `ConstDecimals` of the same scale. + ```rust title=example.rs + pub fn main() { + let fixed = ManagedDecimal::>::from(BigUint::from(1u64)); + let fixed_2 = ManagedDecimal::>::from(BigUint::from(5u64)); + + let addition = fixed + fixed_2; + assert_eq!( + addition, + ManagedDecimal::>::from(BigUint::from(6u64)) + ); + } + ``` + - `trunc(&self) -> BigUint` returns the `data` field without the `scaling_factor` applied, by dividing the value to the scaling factor and truncating. + ```rust title=example.rs + pub fn main() { + ... + assert_eq!(addition.trunc(), BigUint::from(6u64)); + } + ``` +- Complex Operations: + - `log(self, target_base: BigUint, precision: T) -> ManagedDecimal` returns the value of log in any base with customizable precision level. + ```rust title=example.rs + pub fn logarithm() { + let fixed = ManagedDecimal::::from_raw_units(BigUint::from(10u64), 1usize); + let fixed_const = ManagedDecimal::>::const_decimals_from_raw(BigUint::from(10u64)); -:::info Note -The difference is less striking in this case. + let log2_fixed = fixed.log(BigUint::from(2u64), 10_000usize); + assert_eq!( + log2_fixed, + ManagedDecimal::::from_raw_units(BigUint::from(33219u64), 10_000usize) + ); + } + ``` +- Scaling: + - `scale(&self) -> usize` returns the number of decimals (the scale). + - `scaling_factor(&self) -> BigUint` returns the scaling factor value (`10^num_decimals`). + - `rescale(self, scale_to: T) -> ManagedDecimal` returns the correspondent of the decimal in the newly specified scale. It can also convert between `NumDecimal` and `ConstDecimals` instances. -In the first case, we encoded `None` as an empty byte array (encoding it as `0x00` is also accepted). In any case, we do need to pass it as an explicit argument. + ```rust title=example.rs + pub fn main() { + ... + let fixed_8: ManagedDecimal = ManagedDecimal::from_raw_units(BigUint::from(5u64), 5usize); + let fixed_9 = fixed_8.rescale(ConstDecimals::<3>); + assert_eq!( + fixed_9, + ManagedDecimal::>::const_decimals_from_raw(BigUint::from(500u64)) + ); + } + ``` +- Conversions: + - `into_raw_units(&self) -> &BigUint` returns the `data` field value. -In the second case, the last argument is omitted altogether. -::: + ```rust title=example.rs + pub fn main() { + ... + assert_eq!(addition.into_raw_units(), &BigUint::from(600u64)); + } + ``` + - `from_raw_units(data: BigUint, decimals: D) -> Self` returns a `ManagedDecimal` from a data field value without applying scaling factor. -We also want to point out that the multi-value implementation is more efficient in terms of gas. It is more easier for the smart contract to count the number of arguments and top-decode, than parse a composite type, like `Option`. + ```rust title=example.rs + pub fn main() { + ... + let fixed_4: ManagedDecimal = ManagedDecimal::from_raw_units(BigUint::from(100u64), 2usize); + let fixed_5 = fixed_4.rescale(2usize); + assert_eq!( + fixed_5, + ManagedDecimal::from_raw_units(BigUint::from(100000000u64), 8usize) + ); + } + ``` + - `const_decimals_from_raw(data: BigUint) -> Self` returns a `ConstDecimals` type of `ManagedDecimal` from a data field value without applying scaling factor. + ```rust title=example.rs + pub fn main() { + ... + let fixed_const: ManagedDecimal> = ManagedDecimal::const_decimals_from_raw(BigUint::from(1u64)); + } + ``` + - `num_decimals(&self) -> NumDecimals` returns the number of decimals. + - `to_big_float(&self) -> BigFloat` returns the decimal as `BigFloat`. + - `to_big_int(self) -> BigInt` returns the decimal as `BigInt`. + - `from_big_int(big_int: BigInt, num_decimals: T) -> ManagedDecimal` constructs a `ManagedDecimal` from a `BigInt` with customizable `num_decimals`. + - `from_big_float(big_float: BigFloat, num_decimals: T) -> ManagedDecimal` constructs a `ManagedDecimal` from a `BigFloat` with customizable `num_decimals`. + ```rust title=example.rs + pub fn main() { + ... + let float_1 = BigFloat::::from_frac(3i64, 2i64); + let fixed_float_1 = ManagedDecimal::>::from_big_float(float_1.clone(),ConstDecimals::<1>); + let fixed_float_2 = ManagedDecimal::::from_big_float(float_1, 1usize); + assert_eq!( + fixed_float_1, + ManagedDecimal::>::const_decimals_from_raw(BigUint::from(15u64)) + ); + assert_eq!( + fixed_float_2, + ManagedDecimal::::from_raw_units(BigUint::from(15u64), 1usize) + ); + } + ``` -### ManagedVec vs. MultiValueEncoded +--- -In this example, let's assume we want to receive any number of triples of the form (token ID, nonce, amount). This can be implemented in two ways: +### Memory allocation -```rust -#[endpoint(myVarArgsEndpoint1)] -fn my_var_args_endpoint_1(&self, args: ManagedVec<(TokenIdentifier, u64, BigUint)>) {} -``` +MultiversX smart contracts are compiled to WebAssembly, which does not come with memory allocation out of the box. In general WebAssembly programs need special memory allocation functionality to work with the heap. -```rust -#[endpoint(myVarArgsEndpoint2)] -fn my_var_args_endpoint_2(&self, args: MultiValueManagedVec) {} -``` +Using traditional memory allocation is highly discouraged on MultiversX. There are several reasons: +- We have “managed types”, which are handled by the VM, and which offer a cheaper and more reliable alternative. +- “Memory grow” operations can be expensive and unreliable. For the stability of the blockchain we have chosen to limit them drastically. +- Memory allocators end up in smart contract code, bloating it with something that is not related in any way to its specifications. This contradicts our design philosophy. -The first approach seems a little simpler from the perspective of the smart contract implementation, since we only have a `ManagedVec` of tuples. But when we try to encode this argument, to call the endpoint, we are struck with a format that is quite devastating, both for performance and usability. +Even so, it is unreasonable to forbid the use of allocators altogether, whether to use them or not ultimately needs to be the developers' choice. Before framework version 0.41.0, the only allocator solution offered was `wee_alloc`. Unfortunately, it has not been maintained for a few years and has some known vulnerabilities. This was also causing Github’s Dependabot to produce critical warnings, not only to our framework, but to all contract projects, despite most of them not really using it. -Let's call these endpoints with triples: `(TOKEN-123456, 5, 100)` and `(TOKEN-123456, 10, 500)`. The call data would have to look like this: -`myVarArgsEndpoint1@0000000c_544f4b454e2d313233343536_0000000000000005_00000001_64_0000000c_544f4b454e2d313233343536_000000000000000a_00000002_01f4`. +First of all, we made the allocator [configurable](/developers/meta/sc-config#single-contract-configuration) from multicontract.toml, currently the main source of contract build specifications. Developers currently have 4 allocators to choose from. -:::info Note -Above, we've separated the parts with `_` for readability purposes only. On the real blockchain, there would be no underscores, everything would be concatenated. +Then, we added the following allocators to our framework: +- `FailAllocator` (the default) simply crashes whenever any memory allocation or deallocation is attempted. For the first time we have a tool that completely prevents accidental memory allocation. We already had an "alloc" feature in Cargo.toml, but it is only operating high-level and can easily (and sometimes accidentally) be circumvented. +- `StaticAllocator64k` pre-allocates a static 2-page buffer, where all memory is allocated. It can never call memory.grow . It never deallocates and crashes when the buffer is full. It can be suitable for small contracts with limited data being processed, who want to avoid the pitfalls of a memory.grow . +- `LeakingAllocator` uses memory.grow to get hold of memory pages. It also never deallocates. This is because contracts do not generally fill up so much memory and all memory is erased at the end of execution anyway. Suitable for contracts with a little more data. +- `wee_alloc` is still supported. It is, however, not included in the framework. Contracts need to import it explicitly. -Every single value in this call data needs to be nested-encoded. We need to lay out the length or each token identifier, nonces are spelled out in full 8 bytes, and we also need the length of each `BigUint` value. +:::caution +While these allocators are functional, they should be avoided by all contracts. Only consider this functionality when all else fails, in extremely niche situations, or for dealing with very old code. ::: -As you can see, that endpoint is very hard to work with. All arguments are concatenated into one big chunk, and every single value needs to be nested-encoded. This is why we need to lay out the length for each `TokenIdentifier` (e.g. the 0000000c in front, which is length 12) as well as for each `BigUint` (e.g. the `00000001` before `64`). The nonces are spelled out in their full 8 bytes. +--- -The second endpoint is a lot easier to use. For the same arguments, the call data looks like this: -`myVarArgsEndpoint2@544f4b454e2d313233343536@05@64@544f4b454e2d313233343536@0a@01f4`. +### Messages -It is a lot more readable, for several reasons: -- We have 6 arguments instead of 1; -- The argument separator makes it much easier for both us and the smart contract to distinguish where each value ends and where the next one begins; -- All values are top-encoded, so there is no more need for lengths; the nonces can be expressed in a more compact form. +The SC framework supports message interpolation in a variety of situations. -Once again, the multi-value implementation is more efficient in terms of gas. All the contract needs to do is to make sure that the number of arguments is a multiple of 3, and then top-decode each value. Conversely, in the first example, a lot more memory needs to be moved around when splitting the large argument into pieces. +The mechanism makes full use of managed types, and does not require memory allocation on the heap. +It resembles the standard message formatting in Rust, but it does not have all the features and is a completely separate implementation. -## Implementation details +To see these features in action, we have [an example contract just for that](https://github.com/multiversx/mx-sdk-rs/blob/master/contracts/feature-tests/formatted-message-features/src/formatted_message_features.rs). -All serializable types will implement traits `TopEncodeMulti` and `TopDecodeMulti`. -The components that do argument parsing, returning results, or handling of event logs all work with these two traits. -All serializable types (the ones that implement `TopEncode`) are explicitly declared to also be multi-value in this declaration: +## Errors -```rust -/// All single top encode types also work as multi-value encode types. -impl TopEncodeMulti for T -where - T: TopEncode, -{ - fn multi_encode_or_handle_err(&self, output: &mut O, h: H) -> Result<(), H::HandledErr> - where - O: TopEncodeMultiOutput, - H: EncodeErrorHandler, - { - output.push_single_value(self, h) - } -} -``` +The most common place to see message interpolation is in the error messages. These messages will be seen in the explorer and tools if a transaction fails. -To create a custom multi-value type, one needs to manually implement these two traits for the type. Unlike for single values, there is no [equivalent derive syntax](/developers/data/custom-types). ---- +### sc_panic! -### MultiversX API +Whenever encountered, it stops execution immediately, and returns the given error message. Also note that for all errors originating in the smart contract, the status code is "4". -## About MultiversX API +The macro simplifies the process of throwing exceptions with custom error messages. It works with formatted messages with arguments, and with static messages without arguments. -`api.multiversx.com` is the public instance of MultiversX API and is a wrapper over `gateway.multiversx.com` that brings a robust caching mechanism, alongside Elasticsearch -historical queries support, tokens media support, delegation & staking data, and many others. +```rust + sc_panic!("Formatted error message with arguments: {}", arg); // message with argument + sc_panic!("Static error message"); // static messages +``` -## Public URLs +### require! -Mainnet: [https://api.multiversx.com](https://api.multiversx.com). +`require!` is a macro used to enforce preconditions by checking whether an expression evaluates to *true*. If the expression evaluates to *false*, it will trigger a panic with a specific error message. It is very useful when validating pre-conditions. -Testnet: [https://testnet-api.multiversx.com](https://testnet-api.multiversx.com). +```rust +require!(expression, "formatted error message with argument: {}", arg); // error message with argument +require!(expression, "error message"); // static error messages +``` -Devnet: [https://devnet-api.multiversx.com](https://devnet-api.multiversx.com). -## External Providers +## Formatting string -**Blastapi** +We might want to interpolate a string for other uses than throwing an error, such as returning it or saving it to storage. -Mainnet: [https://multiversx-api.public.blastapi.io](https://multiversx-api.public.blastapi.io). -Devnet: [https://multiversx-api-devnet.public.blastapi.io](https://multiversx-api-devnet.public.blastapi.io). +### sc_format! -Checkout information about [pricing](https://blastapi.io/pricing) and API [limitations per plan](https://docs.blastapi.io/blast-documentation/apis-documentation/core-api/multiversx). +The macro creates a formatted managed buffer in contracts. Just like all other format functionality, it supports both formatted messages with arguments and static messages without arguments. The returned value of this macro is a `ManagedBuffer` that can be used within smart contracts. -More details on how to get your private endpoint can be found [here](https://docs.blastapi.io/blast-documentation/tutorials-and-guides/using-blast-to-get-a-blockchain-endpoint-1). +```rust +let number_i32: i32 = 16; +let message_with_i32: ManagedBuffer = sc_format!("i32: {}", number_i32); -**Kepler** +let number_bigUint = BigUint::from(16u32); +let message_with_bigUint = sc_format!("BigUint: {}", number_bigUint); -High-performance infrastructure layer purpose-built for the MultiversX ecosystem. +let buffer: ManagedBuffer = ManagedBuffer::new_from_bytes(b"message"); +let message_with_buffer = sc_format!("ManagedBuffer: {}", buffer); +let message_with_buffer_hex = sc_format!("ManagedBuffer hex: {:x}", buffer); +``` -Affordable plans, high limits for API, Gateway, Elastic Search, Event Notifier, and many more! -[https://kepler.projectx.mx](https://kepler.projectx.mx) +## Printing to console -## Dependencies +Smart contracts cannot print messages on-chain, because they do not have access to any console or standard output. They can, however, do so when run in tests. This printing feature is designed to come to complement the debugger when testing contracts. -### Core dependencies +### sc_print! -For its basic functionality (without including caching or storage improvements), api.multiversx.com depends on the following external systems: +This macro is the primary way to output messages to console, when running tests. -- `gateway`: also referred as Proxy, provides access to node information, such as network settings, account balance, sending transactions, etc - docs: [Proxy](/sdk-and-tools/proxy). -- `index`: a database that indexes data that can be queries, such as transactions, blocks, nfts, etc. - docs: [Elasticsearch](/sdk-and-tools/elastic-search). -- `delegation`: a microservice used to fetch providers list from the delegation API. Not currently open for public access. +Using it doesn't impact wasm builds in any way, so it is safe to use in any situation. To avoid any overhead, not even the formatting code will end up compiled to WebAssembly. +Despite this, we recommend removing the `sc_print!` commands, once you are done with debugging. -### Other dependencies +To produce logs on-chain, consider using [event logs](sc-annotations#events) instead. -It uses on the following internal systems: -- redis: used to cache various data, for performance purposes -- rabbitmq: pub/sub for sending mainly NFT process information from the transaction processor instance to the queue worker instance -It depends on the following optional external systems: +## Interpolation format -- events notifier rabbitmq: queue that pushes logs & events which are handled internally e.g. to trigger NFT media fetch -- data: provides EGLD price information for transactions -- xexchange: provides price information regarding various tokens listed on xExchange -- ipfs: ipfs gateway for fetching mainly NFT metadata & media files -- media: ipfs gateway which will be used as prefix for NFT media & metadata returned in the NFT details -- media internal: caching layer for ipfs data to fetch from a centralized system such as S3 for performance reasons -- AWS S3: used to process newly minted NFTs & uploads their thumbnails -- github: used to fetch provider profile & node information from github +Values can be interpolated into messages in several ways. +- `{}` - The human-readable representation. + - For **number types**, it is the base 10 representation. + ```rust + // "Printing u64: 372036854775807" + sc_print!("Printing u64: {}", 372036854775807u64;); -It uses the following optional internal systems: + // "Printing u32: 800000008" + sc_print!("Printing u32: {}", 800000008u32); -- mysql database: used to store mainly NFT media & metadata information -- mongo database: used to store mainly NFT media & metadata information + // "Printing usize: 1800000000" + sc_print!("Printing usize: {}", 1800000000usize); + // "Printing u16: 60123" + sc_print!("Printing u16: {}", 60123u16); -## Ways to start MultiversX API + // "Printing u8: 233" + sc_print!("Printing u8: {}", 233u8); + + // "Printing i64: -372036854775807" + sc_print!("Printing i64: {}", -372036854775807i64); -An API instance can be started with the following behavior: + // "Printing i32: -800000008" + sc_print!("Printing i32: {}", -800000008i32); -- public API: provides REST API for the consumers -- private API: used to report prometheus metrics & health checks -- transaction processor & completed: fetches real-time transactions & logs from the blockchain; takes action based on various events, such as clearing cache values and publishing events on a queue -- cache warmer: used to proactively fetch data & pushes it to cache, to improve performance & scalability -- elastic updater: used to attach various extra information to items in the elasticsearch, for not having to fetch associated data from other external systems when performing listing requests -- events notifier: perform various decisions based on incoming logs & events -- subscription: used to manage subscriptions, fetch and broadcast data to subscribers + // "Printing isize: -1800000000" + sc_print!("Printing isize: {}", -1800000000isize); -## Rate limiting + // "Printing i16: -30123" + sc_print!("Printing i16: {}", -30123i16); -Public MultiversX APIs have a rate limit mechanism that brings the following limitations: + // "Printing i8: -126" + sc_print!("Printing i8: {}", -126i8); -- api.multiversx.com (_mainnet_): 2 requests / IP / second -- devnet-api.multiversx.com (_devnet_): 5 requests / IP / second + let x_bigint: BigInt = BigInt::from(-3272036854775807i64); + // "Printing x_bigint: -3272036854775807" + sc_print!("Printing x_bigint: {}", x_bigint); + + let x_biguint: BigUint = BigUint::from(3272036854775807u64); + // "Printing x_biguint: 3272036854775807" + sc_print!("Printing x_biguint: {}", x_biguint); + ``` + - For `ManagedBuffer`, it is the contained text. + ```rust + let managed_buffer: ManagedBuffer = ManagedBuffer::new_from_bytes(b"Welcome to MultiversX!"); + // "Printing managed_buffer: Welcome to MultiversX!" + sc_print!("Printing managed_buffer: {}", managed_buffer); + ``` + - For **bool**, it indicates whether the condition is true or false. + ```rust + let x_bool: bool = true; + // "Printing x_bool: true" + sc_print!("Printing x_bool: {}", x_bool); + ``` + - For **byte values**, it is the string character. + ```rust + let x_bytes: &[u8] = b"MVX"; + // "Printing x_bytes: MVX" + sc_print!("Printing x_bytes: {}", x_bytes); + ``` + - For `CodeMetadata`, it is the flag represented as a text. + ```rust + let code_metadata: CodeMetadata = CodeMetadata::UPGRADEABLE; + // "Printing code_metadata: Upgradeable" + sc_print!("Printing code_metadata: {}", code_metadata); + ``` + - For `TokenIdentifier`, it is the token ticker. + ```rust + let token_identifier = TokenIdentifier::from(&b"TESTTOK-2345"[..]); + // "Printing token_identifier: TESTTOK-2345" + sc_print!("Printing token_identifier: {}", token_identifier); + ``` + - for `EgldOrEsdtTokenIdentifier`, it is the token ticker. + ```rust + let egld_or_esdt_token_identifier: EgldOrEsdtTokenIdentifier = EgldOrEsdtTokenIdentifier::egld(); + // "Printing egld_or_esdt_token_identifier: EGLD" + sc_print!("Printing egld_or_esdt_token_identifier: {}", egld_or_esdt_token_identifier); + ``` + +- `{:x}` - Lowercase hexadecimal encoding. + - For **number types**, it is the base 16 representation. + ```rust + // "Printing u64: 1525d94927fff" + sc_print!("Printing u64: {:x}", 372036854775807u64); + // "Printing u32: 2faf0808" + sc_print!("Printing u32: {:x}", 800000008u32); -## Rest API documentation + // "Printing usize: 6b49d200" + sc_print!("Printing usize: {:x}", 1800000000usize); -Rest API documentation of `api.multiversx.com` can be found on the [Swagger docs](https://api.multiversx.com). + // "Printing u16: eadb" + sc_print!("Printing u16: {:x}", 60123u16); + // "Printing u8: e9" + sc_print!("Printing u8: {:x}", 233u8); + + // "Printing i64: fffeada26b6d8001" + sc_print!("Printing i64: {:x}", -372036854775807i64); -## WebSocket Subscription Documentation + // "Printing i32: d050f7f8" + sc_print!("Printing i32: {:x}", -800000008i32); -Real-time blockchain streaming is supported through the MultiversX WebSocket Subscription API. -A dedicated guide is available here [WebSocket Subscription Guide](./ws-subscriptions.md) + // "Printing isize: 94b62e00" + sc_print!("Printing isize: {:x}", -1800000000isize); + // "Printing i16: 8a55" + sc_print!("Printing i16: {:x}", -30123i16); -## References + // "Printing i8: 82" + sc_print!("Printing i8: {:x}", -126i8); + ``` + - For `ManagedBuffer`, it is the contained text as hexadecimal. + ```rust + let managed_buffer: ManagedBuffer = ManagedBuffer::new_from_bytes(b"MultiversX!"); + // "Printing managed_buffer: 4d756c7469766572735821" + sc_print!("Printing managed_buffer: {:x}", managed_buffer); + ``` + - For `ManagedAddress`, it is the contained address as hexadecimal. + ```rust + let address: [u8; 32] = hex!("fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe"); + let managed_address: ManagedAddress = ManagedAddress::from(&address); + // "Printing managed_address: fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe" + sc_print!("Printing managed_address: {:x}", managed_address); + ``` + - For `ManagedByteArray`, it is the hexadecimal representation for the ASCII characters. + ```rust + let managed_byte_array: ManagedByteArray = ManagedByteArray::new_from_bytes(b"MVX"); + // "Printing managed_byte_array: 4d5658" + sc_print!("Printing managed_byte_array: {:x}", managed_byte_array); + ``` + - For `CodeMetadata`, it is the metadata stored in the flag as hexadecimal. + ```rust + let code_metadata: CodeMetadata = CodeMetadata::UPGRADEABLE; + // "Printing code_metadata: 0100" + sc_print!("Printing code_metadata: {}", code_metadata); + ``` + - For `TokenIdentifier`, it is the token ticker as hexadecimal. + ```rust + let token_identifier = TokenIdentifier::from(&b"TESTTOK-2345"[..]); + // "Printing token_identifier: 54455354544f4b2d32333435" + sc_print!("Printing token_identifier: {:x}", token_identifier); + ``` + - for `EgldOrEsdtTokenIdentifier`, it is the token ticker as hexadecimal. + ```rust + let egld_or_esdt_token_identifier: EgldOrEsdtTokenIdentifier = EgldOrEsdtTokenIdentifier::egld(); + // "Printing egld_or_esdt_token_identifier: 45474C44" + sc_print!("Printing egld_or_esdt_token_identifier: {:x}", egld_or_esdt_token_identifier); + ``` -- Github repository: [https://github.com/multiversx/mx-api-service](https://github.com/multiversx/mx-api-service) -- Swagger docs: [https://api.multiversx.com](https://api.multiversx.com) -- Raw JSON Swagger OpenAPI definitions: [https://api.multiversx.com/-json](https://api.multiversx.com/-json) -- MultiversX blog: [https://multiversx.com/blog/elrond-api-internet-scale-defi](https://multiversx.com/blog/elrond-api-internet-scale-defi) +- `{:b}` - Binary encoding ("0" and "1"). + - For **number types**, it is the base 2 representation. + ```rust + // "Printing u64: 1010100100101110110010100100100100111111111111111" + sc_print!("Printing u64: {:b}", 372036854775807u64); ---- + // "Printing u32: 101111101011110000100000001000" + sc_print!("Printing u32: {:b}", 800000008u32); -### MultiversX API WebSocket + // "Printing usize: 1101011010010011101001000000000" + sc_print!("Printing usize: {:b}", 1800000000usize); -## MultiversX WebSocket Subscription API + // "Printing u16: 1110101011011011" + sc_print!("Printing u16: {:b}", 60123u16); -Starting with the release [v1.17.0](https://github.com/multiversx/mx-api-service/releases/tag/v1.17.0) we introduced WebSocket Subscription functionality. + // "Printing u8: 11101001" + sc_print!("Printing u8: {:b}", 233u8); + ``` + - For `CodeMetadata`, we get the individual bits. + ```rust + let code_metadata: CodeMetadata = CodeMetadata::UPGRADEABLE; + // "Printing code_metadata: 0000000100000000" + sc_print!("Printing code_metadata: {:b}", code_metadata); + ``` + - For `ManagedBuffer`, it is the contained text as bits. + ```rust + let managed_buffer: ManagedBuffer = ManagedBuffer::new_from_bytes(b"MVX"); + // "Printing managed_buffer: 010011010101011001011000" + sc_print!("Printing managed_buffer: {}", managed_buffer); + ``` -It is useful for subscribing to new events in real-time, rather than performing polling (requesting latest events with a given refresh period). +- `{:c}` - Encoded the same as the [codec representation](/developers/data/serialization-overview). Helpful when trying to visualize the encoding of an object. It is also the one available for most types, so it is a good fallback when other types of formatting are not available, e.g. for custom structs. + - For all types, it is the [top-encoded](/developers/data/serialization-overview#the-concept-of-top-level-vs-nested-objects) representation. + ```rust + // "Printing u64: 01525d94927fff" + sc_print!("Printing u64: {:c}", 372036854775807u64); + ``` -## Update Frequency and Stream Modes +--- -The WebSocket API supports two primary modes of data consumption: **Pulse Stream** and **Filtered Stream**. +### Migrate sdk-dapp v4 to v5: hook-by-hook diffs -### 1. Pulse Stream (Snapshot & Loop) -Subscribers receive the most recent events for a specific timeframe at regular intervals defined by the API. -* **Behavior:** You receive an update every round (or configured interval). -* **Content:** Each update contains the latest events for the requested buffer (e.g., latest 25 blocks). -* **Duplicates:** Because of the repeating interval, **duplicate events may appear across batches**. It is the user’s responsibility to filter these duplicates. -* **Available Streams:** Transactions, Blocks, Pool, Events, Stats. +Many teams have stayed on v4 because the migration cost was unclear. The official +guide is a patch-notes-style doc that lists renames; this recipe shows runnable +diffs for the six load-bearing changes plus the rename table, so you can see the +shape of your migration before you start. -### 2. Filtered Stream (Custom Real-time Streams) -Subscribers receive events strictly as they occur on the blockchain, filtered by specific criteria. -* **Behavior:** You are notified immediately when a new event matches your filter. -* **Content:** Data flows in real-time from the moment of subscription. -* **Duplicates:** **No duplicate events are sent.** You receive each item exactly once. -* **Available Streams:** `Transactions`, `Events`, and `Transfers` are supported in this mode. +Effort estimate, very loosely: -## Rest API models compatibility -The MultiversX WebSocket Subscription API provides real-time blockchain data identical in structure to REST API responses: +| Codebase size | Estimated effort | +| --- | --- | +| Small dApp: login plus a handful of transactions | Half a day | +| Medium: 20+ pages, custom login UI, multi-step txs | 2 to 3 days | +| Large: heavily customized provider tree, custom hooks | 1 to 2 weeks | +Most of the time goes into hook renames and the `` to `initApp()` +restructure. The actual code shape is similar; the imports and the +wrapper-vs-imperative shift are what take time. -[https://api.multiversx.com](https://api.multiversx.com) -[https://devnet-api.multiversx.com](https://devnet-api.multiversx.com) -[https://testnet-api.multiversx.com](https://testnet-api.multiversx.com) +In each pair below, the "before (v4)" block is v4 code shown for contrast only. It +imports v4-only paths and is not part of the compiled recipe; the "after (v5)" +block compiles against the installed v5 SDK. +## The structural change: `` is gone -All updates mirror REST responses and include a `Count` field representing **the total number of existing items at the moment the update was delivered**. +v4 wrapped your app in a declarative React component; v5 calls a function before +rendering. -## Selecting the WebSocket Endpoint +```tsx +// before (v4): wraps the app +``` -Before connecting, fetch the WebSocket cluster: +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed. +// +// migrations/01-dapp-provider/before.tsx — v4 declarative provider. +// +// In v4 you wrapped the entire app tree with . The provider +// component took the network environment as a prop and handled +// initialisation under the hood. Hooks worked anywhere inside the tree. + +import { DappProvider } from '@multiversx/sdk-dapp/wrappers'; +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/types'; + +const customNetworkConfig = { + walletConnectV2ProjectId: 'multiversx-cookbook-demo', +}; -### Mainnet -```text -https://api.multiversx.com/websocket/config +export function App() { + return ( + + + + ); +} ``` -### Testnet -```text -https://testnet-api.multiversx.com/websocket/config -``` +```tsx title="migrations/01-dapp-provider/after.tsx" +// migrations/01-dapp-provider/after.tsx — v5 imperative init. +// +// v5 replaces with an imperative initApp() call. The +// component below mirrors the structure of the Next.js / Vite minimal +// recipes' Providers wrapper: useEffect-driven init, ready-gate, +// children pass-through. +// +// Key changes from v4: +// - wrapping is gone — call initApp(config) instead. +// - The config shape is { storage, dAppConfig: { environment, providers, ... } }. +// - initApp must run AFTER the component mounts. In Next.js this means +// a "use client" component; in Vite there is no SSR concern. +// - Hooks still work, but only after init resolves. + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; -### Devnet -```text -https://devnet-api.multiversx.com/websocket/config -``` +const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + providers: { + walletConnect: { + walletConnectV2ProjectId: 'multiversx-cookbook-demo', + }, + }, + }, +}; -### Response example -```json -{ - "url": "socket-api-xxxx.multiversx.com" -} -``` +let initStarted = false; -### WebSocket endpoint -```text -https:///ws/subscription -``` +export function Providers({ children }: { children: ReactNode }): JSX.Element { + const [ready, setReady] = useState(false); ---- + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; -## Subscription Events Overview + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + setReady(true); + }); -| Stream Type | Stream Name | Subscribe Event | Update Event | Description | -|---|---|---|---|---| -| **Pulse** | Transactions | `subscribeTransactions` | `transactionUpdate` | Recurring latest buffer | -| **Pulse** | Blocks | `subscribeBlocks` | `blocksUpdate` | Recurring latest buffer | -| **Pulse** | Pool | `subscribePool` | `poolUpdate` | Recurring mempool dump | -| **Pulse** | Events | `subscribeEvents` | `eventsUpdate` | Recurring latest events | -| **Pulse** | Stats | `subscribeStats` | `statsUpdate` | Recurring chain stats | -| **Filtered** | Custom Txs | `subscribeCustomTransactions` | `customTransactionUpdate` | Real-time filtered Txs | -| **Filtered** | Custom Transfers | `subscribeCustomTransfers` | `customTransferUpdate` | Real-time filtered Transfers | -| **Filtered** | Custom Events| `subscribeCustomEvents` | `customEventUpdate` | Real-time filtered Events | + return () => { + cancelled = true; + }; + }, []); ---- + if (!ready) { + return
Initializing…
; + } + return <>{children}; +} +``` -## Pulse Stream Subscriptions +The mechanism changes (`` becomes `await initApp(config)`), where it +runs changes (client-side, in a `useEffect`, behind a `"use client"` directive and +a re-init guard), and SSR behavior changes (v5's `initApp()` hits `sessionStorage` +during rehydration and crashes server-side, so it must be gated). See +[Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) +Pitfall 3 for the canonical workaround. -**Note:** This mode pushes the latest buffer of data repeatedly. **Duplicate events may appear across batches**, and it is the user’s responsibility to filter or handle those duplicates on their side. +## `useGetAccountInfo` to `useGetAccount` + `useGetIsLoggedIn` + `useGetLoginInfo` -### Transactions (Pulse) +v5 splits the v4 monolith into three hooks. Login state and on-chain account state +live in different store slices and have different update cadences. -#### Payload (DTO) +```tsx +// before (v4): useGetAccountInfo returned everything +``` -| Field | Type | Required | -|-------------------------|----------------------------------------------------|----------| -| from | number | YES | -| size | number (1–50) | YES | -| status | `"success" \| "pending" \| "invalid" \| "fail"` | NO | -| order | `"asc" \| "desc"` | NO | -| isRelayed | boolean | NO | -| isScCall | boolean | NO | -| withScResults | boolean | NO | -| withRelayedScresults | boolean | NO | -| withOperations | boolean | NO | -| withLogs | boolean | NO | -| withScamInfo | boolean | NO | -| withUsername | boolean | NO | -| withBlockInfo | boolean | NO | -| withActionTransferValue | boolean | NO | -| fields | string[] | NO | +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed. +// +// migrations/02-get-account-info/before.tsx — v4 useGetAccountInfo. +// +// In v4, useGetAccountInfo() returned everything: address, balance, +// nonce, plus assorted "loginInfo" fields like isLoggedIn and the +// auth token. One hook, one big object. -#### Example usage +import { useGetAccountInfo } from '@multiversx/sdk-dapp/hooks'; -```js -import { io } from "socket.io-client"; +export function AccountSummary() { + const { + address, + account: { balance, nonce }, + isLoggedIn, + tokenLogin, + } = useGetAccountInfo(); + + if (!isLoggedIn) { + return

Not logged in.

; + } -async function main() { - const { url } = await fetch("https://api.multiversx.com/websocket/config") - .then((r) => r.json()); + return ( +
+

Address: {address}

+

Balance: {balance}

+

Nonce: {nonce}

+

Token: {tokenLogin?.nativeAuthToken}

+
+ ); +} +``` - const socket = io(`https://${url}`, { path: "/ws/subscription" }); +```tsx title="migrations/02-get-account-info/after.tsx" +// migrations/02-get-account-info/after.tsx — v5 split: useGetAccount + useGetLoginInfo. +// +// v5 splits the v4 monolith. The login state and the on-chain account +// state live in different store slices and have different update +// cadences (login info changes rarely; account balance / nonce change +// per transaction), so they got their own hooks. +// +// Migration rule of thumb: +// address, balance, nonce, shard, username → useGetAccount() +// isLoggedIn → useGetIsLoggedIn() +// tokenLogin, providerType, expires, loginMethod → useGetLoginInfo() - const payload = { from: 0, size: 25 }; +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetLoginInfo } from '@multiversx/sdk-dapp/out/react/loginInfo/useGetLoginInfo'; - socket.emit("subscribeTransactions", payload); +export function AccountSummary(): JSX.Element { + const { address, balance, nonce } = useGetAccount(); + const isLoggedIn = useGetIsLoggedIn(); + const { tokenLogin } = useGetLoginInfo(); - socket.on("transactionUpdate", (data) => { - console.log("Transactions update:", data); - }); -} + if (!isLoggedIn) { + return

Not logged in.

; + } -main().catch(console.error); + return ( +
+

Address: {address}

+

Balance: {balance}

+

Nonce: {nonce}

+

Token: {tokenLogin?.nativeAuthToken}

+
+ ); +} ``` -#### Update Example +Migration rule of thumb: `address`, `balance`, `nonce`, `shard`, `username` go to +`useGetAccount()`; `isLoggedIn` goes to `useGetIsLoggedIn()`; `tokenLogin`, +`providerType`, `expires`, `loginMethod` go to `useGetLoginInfo()`. See +[Migration: useGetAccountInfo v4 vs v5](/sdk-and-tools/sdk-js/cookbook/migration/get-account-info-v4-vs-v5) +for why the shim that survives the rename is the dangerous case. -```json -{ - "transactions": [ - { - "txHash": "7f172e468e61210805815f33af8500d827aff36df6196cc96783c6d592a5fc76", - "sender": "erd1srdxd75cg7nkaxxy3llz4hmwqqkmcej0jelv8ults8m86g29aj3sxjkc45", - "receiver": "erd19waq9tlhj32ane9duhkv6jusm58ca5ylnthhg9h8fcumtp8srh4qrl3hjj", - "nonce": 211883, - "status": "pending", - "timestamp": 1763718888 - } - ], - "transactionsCount": 1234567 -} +## `useSignTransactions` to `provider.signTransactions(txs)` + +In v5 signing is on the provider, not a hook. The signing call is fundamentally +imperative ("user clicks Send, wallet pops up"); putting it on the provider makes +the call site honest about that. + +```tsx +// before (v4): useSignTransactions hook ``` ---- +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed. +// +// migrations/03-sign-transactions/before.tsx — v4 useSignTransactions. +// +// In v4, signing was a hook. It returned a signTransactions function +// plus a "signing state" object you could destructure to render +// signing-pending UI. + +import { useSignTransactions } from '@multiversx/sdk-dapp/hooks'; +import { Transaction } from '@multiversx/sdk-core'; + +export function useV4SignFlow() { + const { signTransactions } = useSignTransactions(); + + const handleSign = async (txs: Transaction[]) => { + const signedTransactions = await signTransactions(txs); + return signedTransactions; + }; -### Blocks (Pulse) + return { handleSign }; +} +``` -#### Payload (DTO) +```tsx title="migrations/03-sign-transactions/after.tsx" +// migrations/03-sign-transactions/after.tsx — v5 provider.signTransactions. +// +// v5 moves signing onto the provider. The signing call is fundamentally +// imperative — "user clicks Send, wallet pops up". Putting it on the +// provider makes the call site honest about that and avoids paying the +// hook overhead (Rules of Hooks, render-phase identity, etc.) for a +// call that doesn't benefit from being a hook. +// +// Migration: replace useSignTransactions() with getAccountProvider() + +// .signTransactions(). The signature is the same — same input array, +// same return-array shape. -| Field | Type | Required | -|-----------------------|---------------------|----------| -| from | number | YES | -| size | number (1–50) | YES | -| shard | number | NO | -| order | `"asc" \| "desc"` | NO | -| withProposerIdentity | boolean | NO | +import { Transaction } from '@multiversx/sdk-core'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; -#### Example usage +// No more hook — the function below is callable from any context, not +// just inside a React component. Useful if you also want to sign from a +// service worker, a CLI, or a server action. +export async function signFlow(txs: Transaction[]): Promise { + const provider = getAccountProvider(); + const signed = await provider.signTransactions(txs); + return signed; +} +``` -```js -import { io } from "socket.io-client"; +A side benefit: the v5 version is not tied to React. You can call `signFlow(txs)` +from a CLI script, a service worker, or a server action, anywhere +`getAccountProvider()` works. -async function main() { - const { url } = await fetch("https://api.multiversx.com/websocket/config") - .then((r) => r.json()); +## `sendTransactions` to `TransactionManager.send` + `.track` - const socket = io(`https://${url}`, { path: "/ws/subscription" }); +v5 splits the v4 monolithic `sendTransactions()` into three steps: sign, send, +track. The split lets you do meaningful things between steps and lets you skip +steps if you only need a subset. - const payload = { from: 0, size: 25 }; +```tsx +// before (v4): services.sendTransactions +``` - socket.emit("subscribeBlocks", payload); +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed. +// +// migrations/04-send-transactions/before.tsx — v4 sendTransactions. +// +// In v4, sendTransactions() was a single function exported from the +// services module. It sign+send+track in one shot, returning a +// session id you could pass to other hooks. + +import { sendTransactions } from '@multiversx/sdk-dapp/services'; +import { Transaction } from '@multiversx/sdk-core'; + +export async function v4Send(transactions: Transaction[]) { + const { sessionId, error } = await sendTransactions({ + transactions, + transactionsDisplayInfo: { + processingMessage: 'Processing…', + successMessage: 'Done', + errorMessage: 'Failed', + }, + }); + if (error) { + throw new Error(error); + } + return sessionId; +} +``` + +```tsx title="migrations/04-send-transactions/after.tsx" +// migrations/04-send-transactions/after.tsx — v5 sign + TransactionManager. +// +// v5 splits the v4 monolithic sendTransactions() into three steps: +// 1. provider.signTransactions(txs) — opens the wallet UI +// 2. TransactionManager.send(signed) — POSTs to the network +// 3. TransactionManager.track(sent, opts) — starts the WebSocket / polling tracker +// +// The split lets you do meaningful things between steps (e.g., show a +// "preparing to send" UI while the wallet popup is open, or pre-flight +// validate the signed transactions before posting). It also lets you +// skip steps — e.g., if you sign now and send later from a different +// session, you can do that. +// +// The returned sessionId from .track() is the same string the v4 call +// returned. Existing components that consume sessionId via +// useGetPendingTransactions(sessionId) etc. continue to work. + +import { Transaction } from '@multiversx/sdk-core'; +import { TransactionManager } from '@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; - socket.on("blocksUpdate", (data) => { - console.log("Blocks update:", data); +export async function v5Send(transactions: Transaction[]): Promise { + // 1. Sign on the provider. + const provider = getAccountProvider(); + const signed = await provider.signTransactions(transactions); + + // 2. Send. + const txManager = TransactionManager.getInstance(); + const sent = await txManager.send(signed); + + // 3. Track. The returned sessionId is the lookup key for + // useGetPendingTransactions(sessionId), etc. + const sessionId = await txManager.track(sent, { + transactionsDisplayInfo: { + processingMessage: 'Processing…', + successMessage: 'Done', + errorMessage: 'Failed', + }, }); + + return sessionId; } +``` -main().catch(console.error); +The returned `sessionId` from `.track()` is the same string the v4 call returned, +so existing components that consume it keep working. For grouped/batch sends, see +[Migration: useSendTransactions to TransactionManager.send](/sdk-and-tools/sdk-js/cookbook/migration/send-transactions-to-transaction-manager). + +## Per-provider login buttons to `UnlockPanelManager` + +v4 exported a button component per provider. v5 collapses all of them into a single +panel and a single open call. + +```tsx +// before (v4): plus siblings ``` -#### Update Example +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed. +// +// migrations/05-login-buttons/before.tsx — v4 per-provider login buttons. +// +// In v4 you imported a button component per provider and rendered them +// all on your login page. Each one was tightly bound to a single +// provider; choosing which to show meant rendering or not rendering +// each component. -```json -{ - "blocks": [ - { - "hash": "8576bb346bc95680f1ab0eb1fb8c43bbd03ef6e6ac8fd24a3c6e85d4c81be16b", - "epoch": 1939, - "nonce": 27918028, - "round": 27933551, - "shard": 0, - "timestamp": 1763718906 - } - ], - "blocksCount": 111636242 +import { + ExtensionLoginButton, + WalletConnectLoginButton, + LedgerLoginButton, + WebWalletLoginButton, +} from '@multiversx/sdk-dapp/UI'; + +export function LoginPage() { + const callbackRoute = '/dashboard'; + + return ( +
+

Login

+ + DeFi extension + + + xPortal + + Ledger + + Web Wallet + +
+ ); } ``` ---- +```tsx title="migrations/05-login-buttons/after.tsx" +// migrations/05-login-buttons/after.tsx — v5 UnlockPanelManager. +// +// v5 collapses all per-provider buttons into a single UnlockPanelManager +// that renders a panel listing every available provider. You configure +// which providers to show via `allowedProviders`. The actual panel is a +// web component from @multiversx/sdk-dapp-ui; you don't render it +// yourself. +// +// Migration: replace N per-provider button components with one button +// that calls UnlockPanelManager.openUnlockPanel(). The init() call — +// configuring loginHandler / onClose — happens once at app startup, +// inside the same wrapper as initApp(). +// +// CRITICAL: loginHandler and onClose return Promise. The docs +// example uses sync callbacks; that fails strict-mode compile. Use +// async or () => Promise.resolve(). + +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; + +// At app startup (inside the same Providers wrapper that calls initApp): +export function setupUnlockPanel(): void { + UnlockPanelManager.init({ + loginHandler: async (): Promise => { + // Replace with your post-login navigation, e.g. router.push('/dashboard'). + }, + onClose: async (): Promise => { + // Optional: handle the user dismissing the panel without logging in. + }, + // Restrict which providers appear in the panel. Omit this to show all. + allowedProviders: ['extension', 'walletConnect', 'ledger', 'crossWindow'], + }); +} -### Pool (Pulse) +// Anywhere you'd previously have rendered a login button: +export function LoginPage(): JSX.Element { + const handleConnect = (): void => { + // openUnlockPanel() returns Promise; this handler is a sync + // onClick, so discard it explicitly with `void` (eslint + // no-floating-promises). + void UnlockPanelManager.getInstance().openUnlockPanel(); + }; -#### Payload (DTO) + return ( +
+

Login

+ +
+ ); +} +``` -| Field | Type | Required | -|--------|-----------------------------------------------------|----------| -| from | number | YES | -| size | number (1–50) | YES | -| type | `"Transaction" \| "SmartContractResult" \| "Reward"`| NO | +Filter which providers appear in the panel via `allowedProviders` on `init()`. +Omit it to show all. -#### Example usage +## `useGetNotification` to `NotificationsFeedManager` -```js -import { io } from "socket.io-client"; +v5 ships a pre-built notifications feed UI as a web component. You no longer render +the list yourself. -async function main() { - const { url } = await fetch("https://api.multiversx.com/websocket/config") - .then((r) => r.json()); +```tsx +// before (v4): hook plus hand-rolled list +``` - const socket = io(`https://${url}`, { path: "/ws/subscription" }); +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed. +// +// migrations/06-notifications/before.tsx — v4 useGetNotification. +// +// In v4, you'd render notifications by hand using the data the hook +// returned. The hook gave you the array; you styled and positioned the +// list yourself. - const payload = { from: 0, size: 25, type: "Transaction" }; +import { useGetNotification } from '@multiversx/sdk-dapp/hooks'; - socket.emit("subscribePool", payload); +export function NotificationsList() { + const notifications = useGetNotification(); - socket.on("poolUpdate", (data) => { - console.log("Pool update:", data); - }); + return ( +
    + {notifications.map((n) => ( +
  • {n.message}
  • + ))} +
+ ); } - -main().catch(console.error); ``` -#### Update Example +```tsx title="migrations/06-notifications/after.tsx" +// migrations/06-notifications/after.tsx — v5 NotificationsFeedManager. +// +// v5 ships a pre-built notifications feed UI as a web component. You no +// longer render the list yourself; the manager + the +// `` element handle it. You only need to call +// .getInstance() to get a reference, and call .open() / .close() / etc. +// to drive the UI. +// +// If you need fine-grained custom rendering (e.g. branding the +// notification card differently), drop down to the store directly via +// useSelector — the data is still there. The manager is the convenience +// layer; bypass it when convenience isn't enough. + +import { NotificationsFeedManager } from '@multiversx/sdk-dapp/out/managers/NotificationsFeedManager/NotificationsFeedManager'; + +export function NotificationsButton(): JSX.Element { + // The manager wires itself to the store automatically once initApp() + // has resolved; no per-component setup is required. If you want to + // register a custom on-notification handler at mount-time, do it in a + // useEffect here. + + const open = (): void => { + // openNotificationsFeed() returns Promise (see + // node_modules/@multiversx/sdk-dapp/out/managers/NotificationsFeedManager/NotificationsFeedManager.d.ts) — + // same floating-promise gate as UnlockPanelManager.openUnlockPanel(). + void NotificationsFeedManager.getInstance().openNotificationsFeed(); + }; -```json -{ - "pool": [ - { - "txHash": "0b0cd3932689c6853e50ccc0f49feeb9c5f2a68858cbd213fd0825dd4bc0632b", - "sender": "erd1jfwjg6tl87rhe73zmd5ygm8xmc9u3ys80mjvakdc7ca3kknr2kjq7s98h3", - "receiver": "erd1qqqqqqqqqqqqqpgq0dsmyccxtlkrjvv0czyv2p4kcy72xvt3nzgq8j2q3y", - "nonce": 1166, - "function": "claim", - "type": "Transaction" - } - ], - "poolCount": 1902 + return ( + + ); } ``` ---- +If you need bespoke notification rendering, drop down to the store directly via +`useSelector`; the data is still there. The manager is the convenience layer. -### Events (Pulse) +## The import-path renames -#### Payload (DTO) +Most v4 to v5 changes are pure path renames: -| Field | Type | Required | -|-------|---------------|----------| -| from | number | YES | -| size | number (1–50) | YES | -| shard | number | NO | +| v4 path | v5 path | Semantic change? | +| --- | --- | --- | +| `/hooks` | `/out/react//...` | Path only | +| `/services` | `/out/methods/...` and `/out/managers/...` | Path only, split | +| `/UI` | `/out/managers/...` (web components) | **UI replaced** | +| `/wrappers` | (gone, see `initApp()`) | **Replaced** | +| `/utils` | `/out/utils/...` | Path only | +| `/types` | `/out/types/...` | Path only | +| `/providers` | `/out/providers/...` | Path only | -#### Example usage +Two semantic gotchas worth flagging loudly: -```js -import { io } from "socket.io-client"; +1. **`getAccount` is a store read in both versions**, instant return, no network + call. There is **also** a new function called `getAccountFromApi` in v5 that + hits the API. Do not conflate them. +2. **`useGetAccountInfo` is still exported** in v5 for back-compat, but it is a + thinner shim. The recommended v5 path is the three-way split shown above. -async function main() { - const { url } = await fetch("https://api.multiversx.com/websocket/config") - .then((r) => r.json()); +## The smallest viable patch - const socket = io(`https://${url}`, { path: "/ws/subscription" }); +The shortest possible diff to make a v4 codebase compile under v5: - const payload = { from: 0, size: 25 }; +1. Replace `` with the `Providers` wrapper from the Next.js / Vite + minimal recipes. Move the environment plus walletConnect projectId props into + `dappConfig.dAppConfig`. +2. Bulk find-and-replace import paths (`/hooks` to `/out/react//...`, + `/services` to `/out/methods/...` or `/managers/...`, `/types` to + `/out/types/...`, delete `/wrappers` references). +3. Replace `` et al. with one button that calls + `UnlockPanelManager.openUnlockPanel()`. Initialize the manager once at startup. +4. Replace `useSignTransactions` and `sendTransactions` with + `provider.signTransactions(txs)` plus `TransactionManager.send(signed)` plus + `TransactionManager.track(sent, opts)`. +5. Run `tsc --noEmit`. Fix what breaks. Most v4-vs-v5 bugs are caught at + compile-time once you are on v5 deps. - socket.emit("subscribeEvents", payload); +## Pitfalls - socket.on("eventsUpdate", (data) => { - console.log("Events update:", data); - }); -} +:::warning[Pitfall 1: getAccount vs getAccountFromApi] +Same prefix, very different semantics. `getAccount()` is a store read (free, +instant); `getAccountFromApi()` makes a network round-trip every call. Search your +codebase for `getAccountFromApi`. If it is used inside a render loop or a +frequently-fired event handler, you have added an unintended network call. +::: -main().catch(console.error); -``` +:::warning[Pitfall 2: Redux DevTools is gone] +v4 used Redux; v5 uses Zustand. Your existing Redux DevTools setup will not surface +state changes. Zustand has its own devtools middleware, but sdk-dapp does not +enable it by default. +::: -#### Update Example +:::warning[Pitfall 3: sessionStorage layout changed] +v4 sessions are not re-readable post-upgrade. Document this in your release notes, +users will be silently logged out on first load after the upgrade. +::: -```json -{ - "events": [ - { - "txHash": "b5bde891df72e26fb36e7ab3acc14b74044bd9aa82b4852692f5b9a767e0391f-1-0", - "identifier": "signalError", - "address": "erd1jv5m4v3yr0wy6g2jtz2v344sfx572rw6aclum9c6r7rd4ej4l6csjej2wh", - "timestamp": 1763718864, - "topics": [ - "9329bab2241bdc4d21525894c8d6b049a9e50ddaee3fcd971a1f86dae655feb1", - "4865616c7468206e6f74206c6f7720656e6f75676820666f72206c69717569646174696f6e2e" - ], - "shardID": 1 - } - ], - "eventsCount": 109432 -} -``` +:::warning[Pitfall 4: OnCloseUnlockPanelType requires Promise<void>] +The v5 docs example for `UnlockPanelManager.init` uses sync callbacks; that fails +strict-mode compile. Use `async () => {}`. +::: + +:::warning[Pitfall 5: 'use client' boundaries in Next.js] +v4 was loose about SSR; v5 is strict. Any component that calls a sdk-dapp hook (or +`getAccountProvider`, or `getAccount` from the methods directory) must be inside a +`"use client"` tree. +::: + +## See also + +- [Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) + is the target shape for Next.js codebases. +- [Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) + is the target shape for Vite codebases. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the new sign, send, track flow used by every transaction in v5. +- [Migration: DappProvider to initApp](/sdk-and-tools/sdk-js/cookbook/migration/dapp-provider-to-initapp) + is the full-app version of the first change above. --- -### Stats (Pulse) +### Migration -#### Payload (DTO) +There is an older syntax for producing contract calls, detailed [here](./tx-legacy-calls.md). It is already in use in many projects. -Stats subscription does not accept any payload. +Upgrading to framework version 0.49.0 is almost completely backwards compatible, but the new syntax is nicer and more reliable, so we encourage everyone to migrate. -#### Example usage +This will be a migration guide, as well as some frequently encountered pitfalls. -```js -import { io } from "socket.io-client"; +:::caution +Even though the syntax is backwards compatible, the implementation of the [old syntax](./tx-legacy-calls.md) has been replaced. -async function main() { - const { url } = await fetch("https://api.multiversx.com/websocket/config") - .then((r) => r.json()); +To the best of our knowledge, all code should continue to behave the same. However, if you upgrade beyond 0.49.0, please make sure to **test your smart contract fully** once again, even if you do not change a single line of code in your code base. - const socket = io(`https://${url}`, { path: "/ws/subscription" }); +Do not be fooled by the identical legacy syntax, the implementation for **all** contract calls is new. +::: - socket.emit("subscribeStats"); - socket.on("statsUpdate", (data) => { - console.log("Stats update:", data); - }); -} -main().catch(console.error); -``` +## Imports -#### Update Example +This is not strictly related to the unified syntax, but the imports were recently cleaned up. A single line should be enough for each of the contexts, as follows: +- In contracts: `use multiversx_sc::imports::*;` +- In tests: `use multiversx_sc_scenario::imports::*;` +- In interactors: `use multiversx_sc_snippets::imports::*;` -```json -{ - "shards": 3, - "blocks": 111636242, - "accounts": 9126654, - "transactions": 569773975, - "scResults": 402596990, - "epoch": 1939, - "roundsPassed": 9478, - "roundsPerEpoch": 14400, - "refreshRate": 6000 -} -``` +We also have `use multiversx_sc::derive_imports::*;`, which gives you derives like `TypeAbi` and the codec derives. ---- +The proxies have `use multiversx_sc::proxy_imports::*;`, but that gets generated automatically, so develoeprs shouldn't worry about it. -## Filtered Stream Subscriptions (Custom Streams) -**Note:** These streams provide real-time data (with a small delay after the actions are committed on-chain) for specific criteria. You must provide at least one filter criteria when subscribing. -### Custom Transactions (Filtered) -Subscribes to transactions matching specific criteria (Sender, Receiver, or Function) as they happen. +## Old `Proxy` type caveat -#### Subscribe Event -`subscribeCustomTransactions` +We must start with the only instance of backwards incompatibility that we have after 0.49.0. -#### Payload (DTO) +It is very uncommon to encounter this problem, we don't expect developers to encounter it, but it did pop up in the DEX when migrating. -| Field | Type | Required | Description | -|---|---|---|---| -| sender | string | NO* | Filter by sender address (bech32) | -| receiver | string | NO* | Filter by receiver address (bech32) | -| function | string | NO* | Filter by smart contract function name | +The old `Proxy` type has been split in two: `Proxy` no longer includes the field for the recipient, whereas there is a new `ProxyTo` type that does include it. -*\*At least one field must be provided.* +Whenever you encounter code of this sort, everything should remain unchanged: -#### Example usage +```rust + #[proxy] + fn vault_proxy(&self) -> vault::Proxy; +``` -```js -import { io } from "socket.io-client"; +If, however, your proxy getter looks like this, the return type has changed, the return type is now `ProxyTo`: -async function main() { - const { url } = await fetch("https://api.multiversx.com/websocket/config") - .then((r) => r.json()); +```rust + #[proxy] + fn vault_proxy(&self, sc_address: ManagedAddress) -> vault::Proxy; +``` - const socket = io(`https://${url}`, { path: "/ws/subscription" }); +To preserve backwards compatibility in this case as well, we placed a hack in the pre-processor stage: the `Proxy` return type is silently replaced by `ProxyTo` in the background, unbeknownst to the developer. For all practical purposes, the code should still be functioning the same way. - // Subscribe to all transactions sent by a specific address - const payload = { - sender: "erd1..." - }; +If, however, the developer does not use the legacy proxy object directly, i.e. it returns it, or passes it on to another function, the framework cannot do the replacement there, and you might get a compilation error. - socket.emit("subscribeCustomTransactions", payload); +The solution in this case is simple: replace `Proxy` with `ProxyTo` in code. - socket.on("customTransactionUpdate", (data) => { - // data.transactions: Transaction[] - // data.timestampMs: number - console.log("New Custom Transaction:", data); - }); -} -``` -#### Update Example -```json -{ - "transactions": [ - { - "txHash": "7f172e468e61210805815f33af8500d827aff36df6196cc96783c6d592a5fc76", - "sender": "erd1srdxd75cg7nkaxxy3llz4hmwqqkmcej0jelv8ults8m86g29aj3sxjkc45", - "receiver": "erd19waq9tlhj32ane9duhkv6jusm58ca5ylnthhg9h8fcumtp8srh4qrl3hjj", - "nonce": 211883, - "status": "pending", - "timestamp": 1763718888 - } - ], - "timestampMs": 1763718888000 -} -``` ---- +## Replace `#[derive(TypeAbi)]` with `#[type_abi]` -### Custom Transfers (Filtered) +To use the new proxies, one must first [generate](./tx-proxies.md#how-to-generate) them. The proxy is designed to be self-contained, so unless configured otherwise, it will also output a copy of the contract types involved in the ABI. -Subscribes to the complete stream of actions associated with a specific Address or Token. This includes not only standard transactions but all blockchain operations: Transactions, Smart Contract Results (SCRs), Rewards, ... . It is the preferred mode for tracking the full real-time activity of an account or the global movement of a specific token. +No methods of these types are copied, but the annotations are important most of the time, so they need to be copied too. These types will need the encode/decode annotations, as well as `Clone`, `Eq`, etc. -#### Subscribe Event -`subscribeCustomTransfers` +In order to do this, the proxy generator (via `TypeAbi`) needs to know what type annotations were originally declared. It turns out, derive annotations in Rust don't have access to the other derives that were declared on the same line. For instance, `B` does not see `A` in `#[derive(A, B)]`. -#### Payload (DTO) +The solution is to have another annotation, called `#[type_abi]` **before** the derives. -| Field | Type | Required | Description | -|---|---|---|---| -| sender | string | NO* | Filter by sender address (bech32) | -| receiver | string | NO* | Filter by receiver address (bech32) | -| relayer | string | NO* | Filter by the relayer address (for meta-transactions) | -| function | string | NO* | Filter by smart contract function name | -| token | string | NO* | Filter by Token Identifier (e.g., `USDC-c76f1f` or `EGLD` for native egld) | -| address | string | NO** | **Universal Filter:** Matches if the address is the Sender **OR** Receiver **OR** Relayer. | +Currently, `#[type_abi]` takes no arguments and works the same way as `#[derive(TypeAbi)]`, but it might be extended in the future. -*\*At least one field from the list above must be provided.* -*\*\*The `address` field cannot be combined with `sender`, `receiver`, or `relayer` in the same payload.* -#### Example usage +## Generate the new proxies -**Scenario: Listen for specific Token transfers (e.g., USDC)** -Useful for tracking volume on a specific token. +Just like for a new project, you will need to [generate](./tx-proxies.md#how-to-generate) the new proxies and embed them in your project. -```js -import { io } from "socket.io-client"; -async function main() { - const { url } = await fetch("https://api.multiversx.com/websocket/config") - .then((r) => r.json()); - const socket = io(`https://${url}`, { path: "/ws/subscription" }); +## Replace the old proxies in calls - const payload = { - token: "USDC-c76f1f" - }; +You might have this kind of syntax in your contract. You can easily find it by searching in your project for `#[proxy]` or `.contract(`. - socket.emit("subscribeCustomTransfers", payload); +```rust title="Variant A" +#[proxy] +fn vault_proxy(&self) -> vault::Proxy; - socket.on("customTransferUpdate", (data) => { - // data.transfers: Transaction[] (filtered) - // data.timestampMs: number - console.log("New USDC Transfer:", data); - }); +#[endpoint] +fn do_call(&self, to: ManagedAddress, args: MultiValueEncoded) { + self.vault_proxy() + .contract(to) + .echo_arguments(args) + .async_call() + .with_callback(self.callbacks().echo_args_callback()) + .call_and_exit(); } ``` -**Scenario: Listen for ANY activity related to an address** -Using the `address` field is a shorthand to avoid creating 3 separate subscriptions (sender, receiver, relayer). - -```js - const payload = { - address: "erd1..." // Will capture incoming, outgoing, and relayed transfers - }; +```rust title="Variant B" +#[proxy] +fn vault_proxy(&self, sc_address: ManagedAddress) -> vault::Proxy; - socket.emit("subscribeCustomTransfers", payload); +#[endpoint] +fn do_call(&self, to: ManagedAddress, args: MultiValueEncoded) { + self.vault_proxy(to) + .echo_arguments(args) + .async_call() + .with_callback(self.callbacks().echo_args_callback()) + .call_and_exit(); +} ``` -#### Update Example - -```json -{ - "transfers": [ - { - "txHash": "cb5d0644ef40943db8035ff50913c4a974a469c2479a73c3cd3ab8de9027be0f", - "receiver": "erd1qqqqqqqqqqqqqpgqxn6hj5m9x33zuq0xynjkusd8tsz3u6a94fvsn2m2ry", - "receiverShard": 1, - "sender": "erd1qqqqqqqqqqqqqpgqcc69ts8409p3h77q5chsaqz57y6hugvc4fvs64k74v", - "senderAssets": { - ... - }, - "senderShard": 1, - "status": "success", - "value": "0", - "timestamp": 1765963650, - "function": "exchange", - "action": { - "category": "esdtNft", - "name": "transfer", - "description": "Transfer", - "arguments": { - "transfers": [ - { - "type": "FungibleESDT", - "ticker": "USDC", - "svgUrl": "https://tools.multiversx.com/assets-cdn/tokens/USDC-c76f1f/icon.svg", - "token": "USDC-c76f1f", - "decimals": 6, - "value": "4087442" - } - ], - "receiver": "erd1qqqqqqqqqqqqqpgqxn6hj5m9x33zuq0xynjkusd8tsz3u6a94fvsn2m2ry", - "functionName": "exchange", - "functionArgs": [ - "01" - ] - } - }, - "type": "SmartContractResult", - "originalTxHash": "46bb841a087c5ce95ca28d0e95c860c661b4d32514bb2970137536036bf591b3" - }, - ], - "timestampMs": 1763718888000 +```rust title="Replace by" +#[endpoint] +fn do_call(&self, to: ManagedAddress, args: MultiValueEncoded) { + self.tx() + .to(&to) + .typed(vault_proxy::VaultProxy) + .echo_arguments(args) + .async_call() + .with_callback(self.callbacks().echo_args_callback()) + .call_and_exit(); } ``` ---- +Both variants above should be replaced by this pattern. -### Custom Events (Filtered) +:::info +The new proxies no longer have a recipient field in them. The [recipient (to) field](./tx-to.md) is completely independent. It is set the same way for transactions with or without proxies. -Subscribes to smart contract events matching specific criteria as they happen. +This feature has proven not worth the complication, we are happy to see it go. +::: -#### Subscribe Event -`subscribeCustomEvents` +Once you've done this, you can safely delete all the old proxy getters (all methods annotated with `#[proxy]`). -#### Payload (DTO) +You can also delete all proxy trait imports of the form `my_module::ProxyTrait as _`. The new proxies require no trait imports. -| Field | Type | Required | Description | -|---|---|---|---| -| address | string | NO* | Filter by the address associated with the event | -| identifier | string | NO* | Filter by event identifier (name) | -| logAddress | string | NO* | Filter by the contract address that emitted the log | -*\*At least one field must be provided.* +:::info +The explicit proxies (traits annotated with `#[multiversx_sc::proxy]`) do not yet have an equivalent in the new syntax. -#### Example usage +It's ok to not migrate them yet. -```js -import { io } from "socket.io-client"; +Of course, it is possible rewrite them in the style of the new proxies, but in the absence of code generation, this might be tedious. -async function main() { - const { url } = await fetch("https://api.multiversx.com/websocket/config") - .then((r) => r.json()); +A solution is planned for the near future. +::: - const socket = io(`https://${url}`, { path: "/ws/subscription" }); - // Subscribe to a specific event identifier - const payload = { - identifier: "swap" - }; +In case you want to migrate to the unified syntax and you cannot, or do not want to get rid of the old proxies, this is an alternative transitional syntax: - socket.emit("subscribeCustomEvents", payload); +```rust title="Transitional variant" +#[proxy] +fn vault_proxy(&self, sc_address: ManagedAddress) -> vault::Proxy; - socket.on("customEventUpdate", (data) => { - // data.events: Events[] - // data.timestampMs: number - console.log("New Custom Event:", data); - }); +#[endpoint] +fn do_call(&self, to: ManagedAddress, args: MultiValueEncoded) { + self.tx() + .legacy_proxy_call(self.vault_proxy(to).echo_arguments(args)) + .async_call() + .with_callback(self.callbacks().echo_args_callback()) + .call_and_exit(); } ``` -#### Update Example - -```json -{ - "events": [ - { - "txHash": "b5bde891df72e26fb36e7ab3acc14b74044bd9aa82b4852692f5b9a767e0391f-1-0", - "identifier": "signalError", - "address": "erd1jv5m4v3yr0wy6g2jtz2v344sfx572rw6aclum9c6r7rd4ej4l6csjej2wh", - "timestamp": 1763718864, - "topics": [ - "9329bab2241bdc4d21525894c8d6b049a9e50ddaee3fcd971a1f86dae655feb1", - "4865616c7468206e6f74206c6f7720656e6f75676820666f72206c69717569646174696f6e2e" - ], - "shardID": 1 - } - ], - "timestampMs": 1763718864000 -} -``` ---- +## (Optional) Remove contract dependencies -## Unsubscribing +The new proxies can be copy-pasted between contract crates. This means that a caller's contract no longer needs to depend on its callees, in order to obtain the proxy. Once the new proxies are set up, you might be able to get rid of the dependency. -To stop receiving updates for any stream, you must emit the corresponding unsubscribe event. +This also means contracts no longer need to be on the same framework version. The same proxy, once generated, should work with any framework version the caller uses, irrespective of the callee. -**The Rule:** -1. Add the prefix `un` to the subscription event name (e.g., `subscribeTransactions` → `unsubscribeTransactions`, `subscribeCustomTransactions` → `unsubscribeCustomTransactions`). -2. Send the **exact same payload** used for the subscription. +:::info +Dependencies might still be needed for shared structures and modules. +::: -### Example: Unsubscribe from Custom Transactions -If you subscribed with: -```js -const payload = { sender: "erd1..." }; -socket.emit("subscribeCustomTransactions", payload); -``` -You must unsubscribe with: -```js -socket.emit("unsubscribeCustomTransactions", payload); -``` -### Example: Unsubscribe from Custom Transfers -If you subscribed with: -```js -const payload = { token: "USDC-c76f1f" }; -socket.emit("subscribeCustomTransfers", payload); -``` +## Replace method names -You must unsubscribe with: -```js -socket.emit("unsubscribeCustomTransfers", payload); -``` +To achieve backwards compatibility, all the old methods were kept (even though their implementations changed). -### Example: Unsubscribe from Blocks -If you subscribed with: -```js -const payload = { from: 0, size: 25 }; -socket.emit("subscribeBlocks", payload); -``` +We did not yet deprecate the old ones, but we encourage everyone to switch to the new ones. They have shorter names and tend to be more expressive. -You must unsubscribe with: -```js -socket.emit("unsubscribeBlocks", payload); -``` +| Old method | New method | Comments | +| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| `.with_egld_transfer(amount)` | `.egld(amount)` | | +| `.with_esdt_transfer((token, nonce, amount))` | `.esdt((token, nonce, amount))`
or
`.single_esdt(&token, nonce, &amount)` | `single_esdt` can deal with references, instead of taking owned objects. | +| `.with_multi_token_transfer(p)` | `.payment(p)` | `payment` is universal. | +| `.with_egld_or_single_esdt_transfer(p)` | `.payment(p)` | Method `payment` is universal. | +| `.with_gas_limi(gas)` | `.gas(gas)` | | +| `.with_extra_gas_for_callback(gas)` | `.gas_for_callback(gas)` | Method `payment` is universal. | +| `.async_call()` | - | Does nothing, can be removed with no consequences. | +| `.async_call_promise()` | - | Does nothing, can be removed with no consequences. | +| `.with_callback(cb)` | `.callback(cb)` | | +| `.deploy_contract(code, code_metadata)` | `.code(code)`
`.code_metadata(code_metadata)`
`.sync_call()` | Also add result handlers for decoding the result. | +| `.deploy_from_source(address, code_metadata)` | `.from_source(code)`
`.code_metadata(code_metadata) .sync_call()` | Also add result handlers for decoding the result. | +| `.upgrade_contract(code, code_metadata)` | `.code(code) .code_metadata(code_metadata) .upgrade_async_call_and_exit()` | Upgrades are async calls. | +| `.upgrade_from_source(address, code_metadata)` | `.from_source(code)`
`.code_metadata(code_metadata)`
`.upgrade_async_call_and_exit()` | Upgrades are async calls. | +| `.execute_on_dest_context()` | `.sync_call()` | Also add result handlers for decoding the result. | +| `.execute_on_dest_context`
`_with_back_transfers()` | `.returns(ReturnsBackTransfers)`
`.sync_call()` | Add additional result handlers for decoding the result. | ---- -## Error Handling -Unexpected behaviors, such as sending an invalid payload or exceeding the server's subscription limits, will trigger an `error` event emitted by the server. -You should listen to this event to handle failures gracefully. +## Black-box tests -### Payload (DTO) +We have not extensively advertised the scenario-based black-box tests, knowing they would eventually be superseded by the unified transaction syntax. -The error object contains context about which subscription failed and why. +If, however, you got to develop on that syntax, here is how to migrate to the new one. -| Field | Type | Description | -|---------|--------|------------------------------------------------------------------------------------| -| pattern | string | The subscription topic (event name) that was requested (e.g., `subscribePool`). | -| data | object | The original payload sent by the client that caused the error. | -| error | object | The specific error returned by the server. | +```rust title=before_migration.rs +use multiversx_sc_scenario::{scenario_model::*, *}; -### Example usage +const ADDER_PATH_EXPR: &str = "mxsc:output/adder.mxsc.json"; -```js -import { io } from "socket.io-client"; +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + blockchain.register_contract(ADDER_PATH_EXPR, adder::ContractBuilder); + blockchain +} -// ... setup socket connection ... +#[test] +fn adder_blackbox_raw() { + let mut world = world(); + let adder_code = world.code_expression(ADDER_PATH_EXPR); -// Listen for generic errors from the server -socket.on("error", (errorData) => { - console.error("Received error from server:"); - console.dir(errorData, { depth: null }); -}); + world + .set_state_step( + SetStateStep::new() + .put_account("address:owner", Account::new().nonce(1)) + .new_address("address:owner", 1, "sc:adder"), + ) + .sc_deploy( + ScDeployStep::new() + .from("address:owner") + .code(adder_code) + .argument("5") + .expect(TxExpect::ok().no_result()), + ) + .sc_query( + ScQueryStep::new() + .to("sc:adder") + .function("getSum") + .expect(TxExpect::ok().result("5")), + ) + .sc_call( + ScCallStep::new() + .from("address:owner") + .to("sc:adder") + .function("add") + .argument("3") + .expect(TxExpect::ok().no_result()), + ) + .check_state_step( + CheckStateStep::new() + .put_account("address:owner", CheckAccount::new()) + .put_account( + "sc:adder", + CheckAccount::new().check_storage("str:sum", "8"), + ), + ); +} ``` -### Error Example +This was the old blackbox code from the `adder` contract, present in framework version `0.48.0`. -**Scenario:** The client attempts to open more subscriptions than the server allows (e.g., limit of X). +Migrating this test to unified syntax means, in a nutshell, separating each action into different steps, replacing verbose `CheckStateStep` and `SetStateStep` with their specific state builders, and replacing the interactions with actual transactions (deploy, query, call). -```json -{ - "pattern": "subscribePool", - "data": { - "from": 0, - "size": 25, - "type": "badInput" - }, - "error": [ - { - "target": { - "from": 0, - "size": 25, - "type": "badInput" - }, - "value": "badInput", - "property": "type", - "children": [], - "constraints": { - "isEnum": "type must be one of the following values: Transaction, SmartContractResult, Reward" - } - } - ] +This is how the migrated test looks like: +```rust title=after_migration.rs +use multiversx_sc_scenario::imports::*; + +use adder::*; + +// new types +const OWNER_ADDRESS: TestAddress = TestAddress::new("owner"); +const ADDER_ADDRESS: TestSCAddress = TestSCAddress::new("adder"); +const CODE_PATH: MxscPath = MxscPath::new("output/adder.mxsc.json"); + +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + + blockchain.register_contract(CODE_PATH, adder::ContractBuilder); + blockchain } -``` ---- +#[test] +fn adder_blackbox() { + let mut world = world(); // ScenarioWorld -## Summary + // starting mandos trace + world.start_trace(); -- WebSocket endpoint is dynamically obtained via `/websocket/config`. -- **Pulse Stream Subscriptions:** periodic updates with possible duplicates (Transactions, Blocks, Pool, Events, Stats). -- **Filtered Stream Subscriptions:** real-time updates with only new data (CustomTransactions, **CustomTransfers**, CustomEvents). -- **Unsubscribing:** Use `un` prefix + same payload. -- Payload DTOs define allowed fields and required/optional rules. -- Update messages mirror REST API and include `Count` fields. -- Errors are emitted via the standard `error` event. -- Uses `socket.io-client`. + // set state for owner + world.account(OWNER_ADDRESS).nonce(1); -This document contains everything required to use MultiversX WebSocket Subscriptions effectively. + // deploy the contract + let new_address = world + .tx() // tx with test environment + .from(OWNER_ADDRESS) + .typed(adder_proxy::AdderProxy) // typed call - proxy + .init(5u32) // deploy call + .code(CODE_PATH) + .new_address(ADDER_ADDRESS) // custom deploy address for tests + .returns(ReturnsNewAddress) // returns new address after deploy + .run(); // send transaction ---- + assert_eq!(new_address, ADDER_ADDRESS.to_address()); -### MultiversX Smart Contracts + // query the contract, `sum` view + world + .query() // tx with test query environment + .to(ADDER_ADDRESS) + .typed(adder_proxy::AdderProxy) // typed call - proxy + .sum() + .returns(ExpectValue(5u32)) // asserts returned value == 5u32 + .run(); // send transaction -## MultiversX Smart Contracts + // contract call, `add` endpoint + world + .tx() // tx with test environment + .from(OWNER_ADDRESS) + .to(ADDER_ADDRESS) + .typed(adder_proxy::AdderProxy) // typed call - proxy + .add(1u32) + .run(); // send transaction -The MultiversX Blockchain's virtual machine (VM) executes [WebAssembly](https://en.wikipedia.org/wiki/WebAssembly). This means that it can execute smart contracts -written in _any programming language_ that can be compiled to WASM bytecode. **Though, we only provide support for Rust.** + // query the contract, `sum view` + world + .query() // tx with test query environment + .to(ADDER_ADDRESS) + .typed(adder_proxy::AdderProxy) // typed call - proxy + .sum() + .returns(ExpectValue(6u32)) // asserts returned value == 6u32 + .run(); // send transaction -Developers are encouraged to use Rust for their smart contracts, however. MultiversX provides a [Rust framework](https://github.com/multiversx/mx-sdk-rs) -which allows for unusually clean and efficient code in smart contracts, a rarity in the blockchain field. -A declarative testing framework is bundled as well. + // check state for owner + world.check_account(OWNER_ADDRESS); -Read more about the MultiversX VM [here](/learn/space-vm). + // check state for adder + world + .check_account(ADDER_ADDRESS) + .check_storage("str:sum", "6"); -Navigate through the sidebar to see some tutorials to get you started, as well as the Rust framework documentation. + // write mandos trace to file + world.write_scenario_trace("trace1.scen.json"); +} +``` --- -### MultiversX Smart Contracts API limits +### Migration: DappProvider to initApp, side-by-side -## MultiversX Smart Contracts API limits +This Cookbook's +[Migrate v4 to v5: hook-by-hook diffs](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration) +shows the `` to `initApp()` change as a single before/after +component. That is the compressed version. This recipe shows the same change at +the scale it actually happens at, a whole entry point plus its provider tree, +because that is where the real migration cost lives: not the line that changed, +but everything that now has to be sequenced around it. -Starting with the Polaris release (February 2023), we have added blockchain data call limits for Smart Contracts. This means that in a single transaction, a Smart Contract cannot perform more than a protocol-level configured number of transfers, trie reads, or built-in function calls. +Come here once you are actually doing the `` removal and want to +see every file that touches it. Start with the hook-by-hook overview first if you +have not already. -This approach comes as a better alternative than increasing the gas costs of those operations since the limits are still very high, so most probably only the most expensive contracts' functions will suffer from these limitations. +## Prerequisites -These limits are set in the gas schedule files in the `MaxPerTransaction` section. For example, this -gas schedule file https://github.com/multiversx/mx-chain-mainnet-config/blob/master/gasSchedules/gasScheduleV7.toml has the following limits: +- An existing sdk-dapp v4.x codebase using ``. +- Familiarity with React `useEffect` and component lifecycles. +- Node.js >= 20.13.1. -```toml -[MaxPerTransaction] - MaxBuiltInCallsPerTx = 100 - MaxNumberOfTransfersPerTx = 250 - MaxNumberOfTrieReadsPerTx = 1500 -``` +## Before (v4): what DappProvider did implicitly -which translates to: -* each transaction can make a maximum 100 built-in functions calls, such as "get last nonce", "get last randomness", "ESDT unpause", "ESDT NFT create" and so on; -* each transaction can create maximum 250 transfers (as in produced smart contract results). For example, a call to "ESDT NFT transfer" will create 1 -smart contract results and a call to "multi ESDT NFT transfer" will consume the number of transfers defined in the function call; -* each transaction can access its data trie for a maximum 1500 get operations (can read a maximum 1500 values stored in the contract). +The v4 entry point wraps the app in a declarative component. These two files are +v4 code and are shown for contrast only; they are not part of the compiled recipe. -These limits are subject to change, a new release can activate a new gas schedule at a defined epoch. +```tsx +// before/App.tsx (v4) +``` ---- +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed in this +// recipe (we only install v5 packages — see package.json). This file is +// reference material, not compiled or linted. Excluded via tsconfig.json +// `exclude` and .eslintrc.cjs `ignorePatterns`. +// +// before/App.tsx — the entire v4 app tree, wrapped once at the root. +// +// did five things under the hood that v5 makes explicit +// (see after/Providers.tsx's header comment for the v5 side of each): +// 1. Read `environment` and picked the matching network config. +// 2. Restored a previous session from sessionStorage/localStorage, if any. +// 3. Registered the WebSocket transaction-status listener. +// 4. Made every hook in `hooks/` work anywhere inside the tree. +// 5. Exposed `customNetworkConfig` for WalletConnect's project ID and any +// API-address overrides. +// +// None of this was awaited by the caller — rendered +// synchronously and hooks like useGetIsLoggedIn() simply returned `false` +// (or stale-but-safe defaults) until the async restore finished, then +// re-rendered. There was no explicit "not ready yet" state to handle. + +import { DappProvider } from '@multiversx/sdk-dapp/wrappers'; +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/types'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/hooks'; +import { ExtensionLoginButton } from '@multiversx/sdk-dapp/UI'; + +const customNetworkConfig = { + name: 'customConfig', + walletConnectV2ProjectId: 'multiversx-cookbook-demo', +}; -### MultiversX tools on multiple platforms +function Dashboard() { + // This hook "just works" here because it's rendered inside + // , below in the tree. v4 didn't distinguish + // "provider mounted" from "session restore finished" — both + // logged-out and still-restoring render as isLoggedIn === false. + const isLoggedIn = useGetIsLoggedIn(); -Generally speaking, the MultiversX tools should work on all platforms. However, platform-specific issues can occur. This page aims to be an entry point for troubleshooting platform-specific issues, in regards to the MultiversX toolset. + if (!isLoggedIn) { + return ; + } -:::note -If you discover a platform-specific issue, please let us known, on the [corresponding GitHub repository](/sdk-and-tools/overview). + return

Connected.

; +} -If you are blocked by a platform-specific issue, please consider using a **devcontainer**, as described [here](/sdk-and-tools/devcontainers). -::: +export function App() { + return ( + + + + ); +} +``` -## Linux +```tsx +// before/index.tsx (v4) +``` -All tools are expected to work on Linux. They are generally tested on Ubuntu-based distributions. +```tsx +// @ts-nocheck — illustrative v4 code; see App.tsx's header comment. +// +// before/index.tsx — v4 entry point. +// +// Nothing app-specific happens here beyond the standard React root mount. +// All of the SDK setup is inside because it's a component +// (), not an imperative call — that's the whole point of +// this recipe. Contrast with after/index.tsx, which is almost as short but +// for a different reason: the setup moved into Providers.tsx instead. + +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; -## MacOS +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error('No #root element found in index.html'); +} -All tools are expected to work on MacOS. Though, even if the tests within the continuous integration flows cover MacOS, some inconveniences might still occur. +createRoot(rootElement).render( + + + , +); +``` -### Apple Silicon (M1, M2) +## After (v5): initApp made explicit + +The v5 entry point calls `initApp()` inside a `Providers` wrapper, then renders. +These three files compile against the installed v5 SDK. + +```tsx title="after/Providers.tsx" +// after/Providers.tsx — v5 imperative init, side-by-side with before/App.tsx. +// +// Same verified pattern as recipes/vite-react-minimal/src/providers.tsx — +// copied here (not cross-imported; each Cookbook recipe is self-contained) +// because the WHOLE POINT of this recipe is to show this file existing at +// all, where v4 had no equivalent. Read this alongside before/App.tsx: +// every one of 's five under-the-hood jobs (listed in that +// file's header comment) now has an explicit, visible line of code here: +// +// 1. Environment selection -> dappConfig.dAppConfig.environment +// 2. Session restore -> awaited inside initApp(); ready gate +// below makes the "still restoring" +// state explicit, unlike v4. +// 3. WebSocket listener -> registered internally by initApp() +// when a restored session exists +// (during initApp() startup). +// 4. Hooks work anywhere -> still true, but only for descendants +// of , and only after `ready` +// flips true. +// 5. WalletConnect project ID -> dappConfig.dAppConfig.providers.walletConnect +// +// The one thing v4 did NOT make you handle: an explicit "not ready yet" +// render. v5's initApp() is a Promise; until it resolves, hooks return +// defaults that are indistinguishable from "logged out" (same as v4 during +// restore) — but now the developer decides whether to show a loading +// state instead of silently rendering "logged out" for a few hundred ms. +// This recipe chooses to show one (see the `!ready` branch below); +// omitting it is also valid and matches v4's actual behavior more closely. + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; -As of February 2024, the Node can only be compiled using the AMD64 version of Go. Thus, dependent tools, such as [localnets](/developers/setup-local-testnet), the [Chain Simulator](/sdk-and-tools/chain-simulator) etc. will rely on the [Apple Rosetta binary translator](https://en.wikipedia.org/wiki/Rosetta_(software)). +const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + // theme is a ThemesEnum member, not a bare string literal — see + // node_modules/@multiversx/sdk-dapp/out/methods/initApp/initApp.types.d.ts. + // A `theme: 'dark' as const` cast (what the naive port of the v4 prop + // would look like) does not satisfy InitAppType; this was a real, + // confirmed tsc --strict failure found while verifying this Cookbook's + // start-here recipes. + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: 'multiversx-cookbook-demo', + }, + }, + }, +}; -:::note -As of February 2024, a native ARM64 version of the Node is in the works. This will allow the dependent tools to run natively on Apple Silicon. -::: +// Module-scope flag prevents double-init under React Strict Mode, which +// deliberately double-invokes effects in dev. v4 needed no equivalent +// because was a component, not an effect. +let initStarted = false; + +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; -If you'd like to manually build a Go tool that only works on AMD64 (for now), download & extract the Go toolchain for AMD64. For example: + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + setReady(true); + }); -```sh -wget https://go.dev/dl/go1.20.7.darwin-amd64.tar.gz -tar -xf go1.20.7.darwin-amd64.tar.gz -``` + return () => { + cancelled = true; + }; + }, []); -Then, export `GOPATH` and `GOENV` variables into your shell: + if (!ready) { + return
Initializing…
; + } + return <>{children}; +} +``` + +```tsx title="after/App.tsx" +// after/App.tsx — the v5 half of the side-by-side, structurally identical +// to before/App.tsx's child: read login state, render a login +// button or a connected message. The difference this recipe is actually +// about is entirely in Providers.tsx / index.tsx — this file is here to +// prove the hook still "just works" once it's inside , same +// promise v4 made inside . +// +// For the full connect/disconnect button and account-field walkthrough, +// see the wallets/wallet-login-button and wallets/read-connected-account +// recipes — this file stays intentionally minimal so the provider/init +// contrast above isn't buried under unrelated UI code. + +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; + +export function App(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + + const handleConnect = (): void => { + // openUnlockPanel() returns Promise; this handler is a sync + // onClick, so the promise is explicitly discarded to satisfy + // @typescript-eslint/no-floating-promises. + void UnlockPanelManager.getInstance().openUnlockPanel(); + }; -```sh -export GOPATH=/(path to extracted toolchain)/go -export GOENV=/(path to extracted toolchain)/go/env + if (!isLoggedIn) { + return ( + + ); + } + + return

Connected.

; +} ``` -Afterwards, build the tools, as needed. The obtained binaries will be AMD64, and they will run on your ARM64 system. +```tsx title="after/index.tsx" +// after/index.tsx — v5 entry point. UnlockPanelManager also needs a one- +// time init() call somewhere; the Next.js / Vite minimal recipes do this +// inside Providers.tsx right after initApp() resolves. This recipe's +// Providers.tsx keeps that out to stay focused purely on the initApp() +// side of the story — see wallets/wallet-login-button for the +// UnlockPanelManager.init({ loginHandler, onClose }) call this app would +// need before openUnlockPanel() does anything useful in a real app. -## Windows +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; +import { Providers } from './Providers'; -Some tools can be difficult to install or run **directly on Windows**. For example, when building smart contracts, the encountered issues might be harder to tackle, especially for beginners. +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error('No #root element found in index.html'); +} -Therefore, we recommend using the [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/en-us/windows/wsl/install), instead of running the tools directly on Windows. +createRoot(rootElement).render( + + + + + , +); +``` ---- +## What `` actually did, made explicit -### mxpy CLI cookbook +`` did five things under the hood that v5 makes explicit: -## mxpy (Command Line Interface) +| v4 (``, implicit) | v5 (`initApp()`, explicit) | +| --- | --- | +| Environment selection | `dappConfig.dAppConfig.environment` | +| Session restore | Awaited inside `initApp()`; this recipe's `ready` gate makes the "still restoring" state visible | +| WebSocket listener | Registered internally by `initApp()` when a restored session exists | +| Hooks work anywhere | Still true, but only for descendants of ``, and only after `ready` flips `true` | +| WalletConnect project ID | `dappConfig.dAppConfig.providers.walletConnect.walletConnectV2ProjectId` | -**mxpy**, as a command-line tool, can be used to simplify and automate the interaction with the MultiversX network - it can be easily used in shell scripts, as well. It implements a set of **commands**, organized within **groups**. +The one thing v4 never made you handle explicitly: a distinct "not ready yet" +render. `initApp()` is a Promise; until it resolves, hooks return defaults +indistinguishable from "logged out", same as v4 during restore, but v5 hands you +the choice of showing a loading state instead of letting that ambiguous instant +flash by. -:::important -In order to migrate to the newer `mxpy`, please follow [the migration guide](https://github.com/multiversx/mx-sdk-py-cli/issues?q=label:migration). +:::tip[Reused, not reinvented] +`after/Providers.tsx` is copied from the already-verified +[Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) +pattern, not written fresh for this recipe. That pattern is proven to compile and run. ::: -The complete Command Line Interface is listed [**here**](https://github.com/multiversx/mx-sdk-py-cli/blob/main/CLI.md). Command usage and description are available through the `--help` or `-h` flags. +## Pitfalls -For example: +:::danger[Pitfall 1: module-scope initApp() calls crash under SSR] +If you lift the `useEffect` body to module scope (or call `initApp()` directly in +a Next.js Server Component), it crashes during server-side rendering. `initApp()` +touches `sessionStorage`, which does not exist on the server. See +[Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) +Pitfall 3 for the `"use client"` plus `useEffect` fix. +::: -```sh -mxpy --help -mxpy tx --help -mxpy tx new --help -``` +:::warning[Pitfall 2: React Strict Mode double-invokes useEffect in dev] +Without the module-scope `initStarted` guard, `initApp()` fires twice on first +mount in development. `initApp()` is idempotent, but the guard avoids a redundant +round of session-restore work. +::: -This page will guide you through the process of handling common tasks using **mxpy**. +:::note[Pitfall 3: v4 hid that restore is async; v5 does not have to] +The `ready` gate in `Providers.tsx` is optional. Skipping it means your app's +first render always shows "logged out", then flips to "logged in" a moment later +for a returning user. Decide deliberately whether that flash is acceptable, rather +than inheriting v4's behavior by default. +::: +## See also -## Managing dependencies +- [Migrate v4 to v5: hook-by-hook diffs](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration) + is the compressed six-pair overview this recipe expands on. +- [Migration: useGetAccountInfo v4 vs v5](/sdk-and-tools/sdk-js/cookbook/migration/get-account-info-v4-vs-v5) + is the next thing that breaks once your provider tree compiles. +- [Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) + is the target shape `after/` is drawn from directly. -Using `mxpy` you can either check if a dependency is installed or install a new dependency. +--- -To check if a dependency is installed you can use: +### Migration: useGetAccountInfo v4 vs v5 -```sh -mxpy deps check -``` +The +[hook-by-hook migration overview](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration) +shows the v4 shape and the v5-recommended replacement for `useGetAccountInfo`. +This recipe adds the piece that overview leaves out: **`useGetAccountInfo` is not +removed in v5**. The same import name still resolves, still compiles, and still +returns real data. It just returns *less* data, silently, which makes this one of +the easiest migration bugs to miss entirely. -To install a new dependency you can use: +## Prerequisites -```sh -mxpy deps install -``` +- An existing sdk-dapp v4.x codebase calling `useGetAccountInfo()`. +- Node.js >= 20.13.1. -Both `mxpy deps check ` and `mxpy deps install ` use the `` as a positional argument. +## Before (v4): one hook returned everything -To find out which dependencies can be managed using `mxpy`, you can type one of the following commands to see the positional arguments it accepts: +v4's `useGetAccountInfo()` returned address, balance, nonce, `isLoggedIn`, and +`tokenLogin` from a single call. This is v4 code, shown for contrast only. -```sh -mxpy deps check -h -mxpy deps install -h +```tsx +// before.tsx (v4) ``` -For example, in order to check if the `testwallets` dependency is installed, you would type: +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed. +// +// before.tsx — v4's useGetAccountInfo(): one hook, one big object. +// address + balance/nonce + login flag + auth token, all in one place. -```sh -mxpy deps check testwallets -``` +import { useGetAccountInfo } from '@multiversx/sdk-dapp/hooks'; -For example, to install the `testwallets` dependency, you can simply type the command: +export function AccountSummary() { + const { + address, + account: { balance, nonce }, + isLoggedIn, + tokenLogin, + } = useGetAccountInfo(); + + if (!isLoggedIn) { + return

Not logged in.

; + } -```sh -mxpy deps install testwallets + return ( +
+

Address: {address}

+

Balance: {balance}

+

Nonce: {nonce}

+

Token: {tokenLogin?.nativeAuthToken}

+
+ ); +} ``` -When installing dependencies, the `--overwrite` argument can be used to overwrite an existing version. - -For example, to overwrite the installation of a dependency, you can simply type the command: +## After (v5): same name, thinner shape + +The same import name still resolves under its new `out/react/account/...` path, +same call-site shape, but `isLoggedIn` and `tokenLogin` are gone from the return +type. `tsc --strict` only catches this if you actually destructure those fields. + +```tsx title="after-shim.tsx" +// after-shim.tsx — the SAME import name resolves in v5, but the shape it +// returns changed. This is the trap this recipe exists to name: a v4 +// codebase that only bulk-renamed import paths (the "smallest viable +// patch" from the v4-to-v5-migration recipe) will still find +// `useGetAccountInfo` at +// `@multiversx/sdk-dapp/out/react/account/useGetAccountInfo` and will +// still compile a call to it — right up until it reaches for +// `.isLoggedIn` or `.tokenLogin`, which no longer exist on the return +// type. `tsc --strict` catches this the moment you destructure a field +// that's gone; it will NOT catch it if you only ever access `.address` or +// `.account`, which still work identically. That's what makes this a +// sneaky migration bug rather than an obvious one: the hook doesn't +// disappear, so nothing forces you to look at it. +// +// Confirmed against the real, installed +// `@multiversx/sdk-dapp@5.6.23` `.d.ts`: the v5 shape is +// { address, account, publicKey, ledgerAccount, walletConnectAccount, +// websocketEvent, websocketBatchEvent } +// — no `isLoggedIn`, no `tokenLogin`. Both moved to useGetLoginInfo() / +// useGetIsLoggedIn() — see after-recommended.tsx. + +import { useGetAccountInfo } from '@multiversx/sdk-dapp/out/react/account/useGetAccountInfo'; + +export function AccountSummaryShim(): JSX.Element { + const { address, account } = useGetAccountInfo(); + + // The next two lines are what the v4 code looked like. Uncommenting + // either one is a real, reproducible tsc --strict failure against the + // installed v5 types — left here as comments rather than deleted, so + // the failure is visible without needing to break the build to see it: + // + // const { isLoggedIn } = useGetAccountInfo(); + // // error TS2339: Property 'isLoggedIn' does not exist on type + // // 'AccountInfoType'. + // + // const { tokenLogin } = useGetAccountInfo(); + // // error TS2339: Property 'tokenLogin' does not exist on type + // // 'AccountInfoType'. -```sh -mxpy deps install testwallets --overwrite + return ( +
+

Address: {address}

+

Balance: {account.balance}

+

Nonce: {account.nonce}

+

+ Not shown: login flag and auth token — this shim no longer carries + them. See after-recommended.tsx. +

+
+ ); +} ``` -## Configuring mxpy - -The configuration can be altered using the `mxpy config` command. +## After (v5): the recommended three-hook split -:::tip -mxpy's configuration is stored in the file `~/multiversx-sdk/mxpy.json`. -::: +The recommended replacement: `useGetAccount()` plus `useGetIsLoggedIn()` plus +`useGetLoginInfo()`. +```tsx title="after-recommended.tsx" +// after-recommended.tsx — the v5-recommended replacement: three hooks, +// one per concern, instead of reaching for the still-exists-but-thinner +// useGetAccountInfo() shim in after-shim.tsx. +// +// Migration rule of thumb: +// address, balance, nonce, shard, username -> useGetAccount() +// isLoggedIn -> useGetIsLoggedIn() +// tokenLogin, providerType, expires, loginMethod -> useGetLoginInfo() -### Viewing the current mxpy configuration +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetLoginInfo } from '@multiversx/sdk-dapp/out/react/loginInfo/useGetLoginInfo'; -As of `mxpy v11`, we've introduced more configuration options, such as `environments` and `wallets`. +export function AccountSummaryRecommended(): JSX.Element { + const { address, balance, nonce } = useGetAccount(); + const isLoggedIn = useGetIsLoggedIn(); + const { tokenLogin } = useGetLoginInfo(); -In order to view the current configuration, one can issue the command `mxpy config dump`. Output example: + if (!isLoggedIn) { + return

Not logged in.

; + } -```json -{ - "dependencies.testwallets.tag": "" + return ( +
+

Address: {address}

+

Balance: {balance}

+

Nonce: {nonce}

+

Token: {tokenLogin?.nativeAuthToken}

+
+ ); } ``` -For viewing the default configuration the following command can be used: +## Why the shim is the dangerous one -```sh -mxpy config dump --defaults -``` +A codebase migrated only via the "smallest viable patch" (bulk find-and-replace +of import paths) keeps calling `useGetAccountInfo()` under its new +`out/react/account/...` path. If that codebase only ever reads `.address` or +`.account`, **it keeps compiling and keeps working**. Nothing forces a second look +at this hook. The bug surfaces only when code reaches for `.isLoggedIn` or +`.tokenLogin`, at which point `tsc --strict` reports a real, specific error (see +the commented-out lines in `after-shim.tsx` for the exact message). +Confirmed against the real, installed `@multiversx/sdk-dapp@5.6.23` `.d.ts`: the +v5 return type is +`{ address, account, publicKey, ledgerAccount, walletConnectAccount, websocketEvent, websocketBatchEvent }`, +with no `isLoggedIn` and no `tokenLogin`. -### Updating the mxpy configuration +**Practical takeaway:** grep your v4 codebase for every `useGetAccountInfo()` call +site and check what it destructures, rather than trusting that "it still compiles" +means "it still works the same way." -One can alter the current configuration using the command `mxpy config set`. +## How it works -The default config contains the **log level** of the CLI. The default log level is set to `info`, but can be changed. The available values are: [debug, info, warning, error]. To set the log level, we can use the following command: -```sh -mxpy config set log_level debug -``` +`useGetAccount()`, `useGetIsLoggedIn()`, and `useGetLoginInfo()` read three +different Zustand store slices, each with a different update cadence: account +balance/nonce change per transaction, the login flag changes twice per session, +and the login info (auth token, provider type) changes only on login. v4's +`useGetAccountInfo()` bundled all three into one subscription, so any change to any +of the three re-rendered every consumer. v5's split means a component that only +cares about the balance does not re-render when the auth token refreshes, a real +performance improvement that falls out of doing the migration properly instead of +leaning on the shim indefinitely. -:::note -Previously, the `default_address_hrp` was also stored in the config. As of `mxpy v11` it has been moved to the `env` config, which we'll talk about in the next section. +## Pitfalls + +:::danger[Pitfall 1: "it still compiles" is not proof the migration is done] +`useGetAccountInfo` surviving the rename means nothing here forces a re-read. +Audit every call site's destructuring, not just whether the import resolves. ::: -### Configuring environments +:::warning[Pitfall 2: isLoggedIn and tokenLogin move to different hooks entirely] +There is no `useGetAccountInfo().isLoggedIn` replacement shortcut; you need the +separate `useGetIsLoggedIn()` / `useGetLoginInfo()` calls. +::: -An `env config` is a named environment configuration that stores commonly used settings (like proxy url, hrp, and flags) for use with the **mxpy CLI**. Environments can be useful when switching between networks, such as Mainnet and Devnet. +:::note[Pitfall 3: don't assume every surviving v4 hook kept its full shape] +This Cookbook found the same "same name, thinner shape" pattern is worth checking +case-by-case per hook, not assumed either way. +::: -The values that are available for configuration and their default values are the following: -```json -{ - "default_address_hrp": "erd", - "proxy_url": "", - "explorer_url": "", - "ask_confirmation": "false", -} -``` +## See also -#### Creating a new env config +- [Migrate v4 to v5: hook-by-hook diffs](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration) + is the six-pair overview this recipe expands on. +- [Migration: DappProvider to initApp](/sdk-and-tools/sdk-js/cookbook/migration/dapp-provider-to-initapp) + is the structural change that usually comes first in a real migration. +- [Read the connected account](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + is the full `useGetAccount()` field reference. -To create a new env config, we use the following command: +--- -```sh -mxpy config-env new -``` +### Migration: useSendTransactions to TransactionManager.send -Additionally, `--template` can be used to create a config from an existing env config. After a new env config is created, it becomes the active one. You can then set its values using the `mxpy config-env set` command. +## A correction before this recipe starts -#### Setting the default hrp +Some migration notes name the v4 hook this recipe replaces as +`useSendTransactions()`. While authoring this recipe we checked the real, +published `@multiversx/sdk-dapp@4.6.4` (the last v4 release, fetched from the npm +registry and unpkg): **no hook by that exact name exists anywhere in it.** The two +real v4 surfaces for sending transactions are: -The `default_address_hrp` might need to be changed depending on the network you plan on using (e.g Sovereign Chain). Most of the commands that might need the `address hrp` already provide a parameter called `--hrp` or `--address-hrp`, that can be explicitly set, but there are system smart contract addresses that cannot be changed by providing the parameter. If those addresses need to be changed, we can use the following command to set the `default hrp` that will be used throughout mxpy. Here we set the default hrp to `test`: +- the plain `sendTransactions()` function from + `services/transactions/sendTransactions`, already covered by + [Migrate v4 to v5: hook-by-hook diffs](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration)'s + hook-by-hook pair, for the single-array case. +- `useSendBatchTransactions()`, a dedicated hook for grouped/sequential sends, + which that pair does not cover. -```sh -mxpy config-env set default_address_hrp test --env test-env -``` +This recipe covers the second one, in depth, since it maps onto a genuinely +distinct v5 capability (nested-array batch sends). -:::note -Explicitly providing `--hrp` will **always** be used over the one fetched from the network or the `hrp` set in the config. -::: +## Prerequisites -#### Setting the proxy url +- An existing sdk-dapp v4.x codebase using `useSendBatchTransactions()`. +- Node.js >= 20.13.1. +- Familiarity with + [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send); + this recipe assumes the single-transaction flow and focuses on what changes for groups. -If `proxy_url` is set in the active environment, the `--proxy` argument is no longer required for the commands that need this argument. +## Before (v4): the dedicated batch-sending hook -To set the proxy url, use the following command: +This is v4 code, shown for contrast only. -```sh -mxpy config-env set proxy_url https://devnet-api.multiversx.com --env devnet +```tsx +// before.tsx (v4) — useSendBatchTransactions() ``` -#### Setting the explorer url +```tsx +// @ts-nocheck — illustrative v4 code; v4 imports are not installed. +// +// before.tsx — v4's dedicated batch-sending hook. +// +// A note on the name of this recipe: some migration notes say +// "useSendTransactions() replaced by +// TransactionManager.getInstance().send()". Checked against the real, +// published @multiversx/sdk-dapp@4.6.4 (the last v4 release, fetched from +// the npm registry / unpkg while authoring this recipe): there is no hook +// literally named useSendTransactions anywhere in that package. The two +// real v4 surfaces for sending transactions are: +// - the plain `sendTransactions()` function from +// `services/transactions/sendTransactions` — already covered by +// the v4-to-v5-migration recipe's migrations/04-send-transactions/ +// pair, for the single-array case. +// - `useSendBatchTransactions()`, shown below — a dedicated HOOK for +// grouped/sequential sends, which pair 04 does not cover at all. +// This recipe covers the batch hook specifically, since it's the one +// real gap pair 04 leaves open, and it maps onto a genuinely distinct v5 +// capability (see after.tsx) rather than repeating pair 04's content. + +import { useSendBatchTransactions } from '@multiversx/sdk-dapp/hooks/transactions/batch/useSendBatchTransactions'; + +export function useBatchStakeFlow() { + const { send, batchId } = useSendBatchTransactions(); + + const submitBatch = async (transactions: unknown[]) => { + const result = await send({ + transactions, + transactionsDisplayInfo: { + processingMessage: 'Processing batch…', + successMessage: 'Batch complete', + errorMessage: 'Batch failed', + }, + }); -**mxpy** already knows the explorer urls for all three networks (Mainnet, Devnet, Testnet). This is particularly useful when running the CLI on custom networks where an explorer is also available. This key is not required to be present in the `env config` for the config to be valid. To set the explorer url use the following command: + if ('error' in result && result.error) { + throw new Error(result.error); + } -```sh -mxpy config-env set explorer_url https://url-to-explorer.com --env test-env -``` + return result.batchId; + }; -#### Setting the ask for confirmation flag + return { submitBatch, batchId }; +} +``` + +## After (v5): TransactionManager with a nested array + +```tsx title="after.tsx" +// after.tsx — v5's grouped-send path: a nested Transaction[][] fed to the +// SAME TransactionManager.send()/.track() calls the flat, single-group +// case uses (see v4-to-v5-migration/migrations/04-send-transactions/after.tsx +// for that simpler case). +// +// Confirmed directly from the installed +// node_modules/@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager.d.ts +// and its compiled .cjs: +// +// send(signedTransactions: Transaction[] | Transaction[][]): +// Promise +// track(sentTransactions: SignedTransactionType[] | SignedTransactionType[][], options?): +// Promise +// +// `isBatchTransaction(t)` (the internal helper send() calls first) is +// exactly `Array.isArray(t[0])` — a NESTED array is what selects batch +// mode. Concretely, from reading the compiled source: +// - Flat Transaction[] -> send() POSTs each transaction individually +// to `${apiAddress}/transactions` (parallel, +// one request per tx). +// - Nested Transaction[][] -> send() POSTs the WHOLE nested structure +// ONCE to `${apiAddress}/batch`, as +// { transactions: [[...], [...]], id: batchId }. +// The outer arrays are what the source calls "sequential" groups +// (`getIsSequential` / `sequentialToFlatArray`) — that naming strongly +// implies the API processes group 1 before group 2, but this recipe only +// verifies the CLIENT-SIDE contract (the type signature, the batch +// trigger, and the endpoint choice) by reading the compiled source; it +// does not independently confirm server-side group ordering against a +// live multi-group broadcast, which would need funded wallets and is out +// of scope here. Treat "sequential" as the SDK's own claim, not this +// recipe's independently-verified one. +// +// track() ALWAYS flattens the structure back down +// (`sequentialToFlatArray`, triggered when every top-level element is +// itself an array) before building the transaction session — so whether +// you sent flat or grouped, you get exactly ONE sessionId covering every +// transaction in the call, not one per group. + +import { Transaction } from '@multiversx/sdk-core'; +import { TransactionManager } from '@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; -If set to `true`, whenever sending a transaction, mxpy will display the transaction and will ask for your confirmation. To set the flag, use the following command: +/** + * Splits a flat array back into the group sizes it came from. Used below + * to re-nest a signed, flat array of transactions after signing — signing + * itself only accepts a flat Transaction[] (see Pitfall 1), so grouping + * has to happen before signing (to know the sizes) and again after (to + * rebuild the nested shape send()/track() expect for batch mode). + */ +function regroup(flat: T[], groupSizes: number[]): T[][] { + const groups: T[][] = []; + let offset = 0; + for (const size of groupSizes) { + groups.push(flat.slice(offset, offset + size)); + offset += size; + } + return groups; +} + +/** + * Sends `groups` — e.g. [[approveTx1, approveTx2], [claimTx]] — as one + * grouped submission, and tracks all of it under a single sessionId. + * + * Replaces v4's useSendBatchTransactions() hook (see before.tsx). Unlike + * the hook, this is plain async code — usable from anywhere + * getAccountProvider() works (a click handler, a CLI-adjacent browser + * script, a service worker), not tied to a React render. + */ +export async function sendGrouped(groups: Transaction[][]): Promise { + const groupSizes = groups.map((g) => g.length); + const flat = groups.flat(); + + // 1. Sign. signTransactions() only accepts a flat array — see Pitfall 1. + const provider = getAccountProvider(); + const signedFlat = await provider.signTransactions(flat); + + // 2. Re-nest using the ORIGINAL group sizes, now that everything is + // signed. The order signTransactions() returns is stable (one + // signed transaction per input, same index), so slicing by the + // sizes we recorded before flattening reconstructs the same groups. + const signedGroups = regroup(signedFlat, groupSizes); + + // 3. Send. A nested array here (signedGroups is Transaction[][]) makes + // isBatchTransaction() return true, which routes this call to the + // dedicated POST /batch endpoint instead of N individual + // POST /transactions calls. + const txManager = TransactionManager.getInstance(); + const sent = await txManager.send(signedGroups); + + // 4. Track. One call, one sessionId, even though the input was nested — + // track() flattens internally before building the session. + const sessionId = await txManager.track(sent, { + transactionsDisplayInfo: { + processingMessage: 'Processing batch…', + successMessage: 'Batch complete', + errorMessage: 'Batch failed', + }, + }); -```sh -mxpy config-env set ask_confirmation true --env mainnet + return sessionId; +} ``` -#### Dumping the active env config +## The v5 mechanism: a nested array picks batch mode -We can see the values set in our active env config. To do so, we use the following command: +Confirmed directly from the installed +`node_modules/@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager.d.ts` +and its compiled source: -```sh -mxpy config-env dump +```ts +send(signedTransactions: Transaction[] | Transaction[][]): + Promise + +track(sentTransactions: SignedTransactionType[] | SignedTransactionType[][], options?): + Promise ``` -#### Dumping all the available env configs +The internal `isBatchTransaction(t)` check is exactly `Array.isArray(t[0])`. +Passing a **nested** array selects batch mode: -We may have multiple env configs, maybe one for each network. To dump all the available env configs, we use the following command: +| Input shape | What `send()` actually does | +| --- | --- | +| Flat `Transaction[]` | POSTs each transaction individually to `${apiAddress}/transactions` (parallel, one request per tx) | +| Nested `Transaction[][]` | POSTs the whole nested structure **once** to `${apiAddress}/batch`, as `{ transactions: [[...], [...]], id: batchId }` | -```sh -mxpy config-env list -``` +The outer groups are what the compiled source's own internal naming calls +"sequential" (`getIsSequential` / `sequentialToFlatArray`), naming that implies the +API processes group 1 before group 2. This recipe verifies the **client-side +contract** (the type signature, the batch trigger, and the endpoint choice) by +reading the compiled source directly; it does not independently confirm +server-side group ordering against a live multi-group broadcast. -#### Deleting a value from the active env config +`track()` always flattens the structure back down before building the transaction +session, so whether you sent flat or grouped, you get exactly **one** `sessionId` +covering every transaction in the call, not one per group. -We can also delete the key-value pairs saved in the env config. For example, let's say we want to delete the explorer url, so we use the following command: +## Pitfalls -```sh -mxpy config-env delete explorer_url --env test-env -``` +:::danger[Pitfall 1: signTransactions() only accepts a flat array] +There is no nested-array overload for signing, confirmed from +`node_modules/@multiversx/sdk-dapp/out/providers/DappProvider/DappProvider.d.ts`: +`signTransactions(transactions: Transaction[], options?): Promise`. +This is why `after.tsx` flattens before signing and re-nests afterward, rather +than trying to sign group-by-group. +::: -#### Getting a value from the active env config +:::warning[Pitfall 2: re-nesting depends on signTransactions() preserving order and count] +`regroup()` in `after.tsx` slices the signed, flat result using the group sizes +recorded before flattening. This is correct as long as the signed array has +exactly one entry per input transaction, in the same order. +::: -If we want to see just the value of a env config key from a specific environment, we can use the following command: +:::warning[Pitfall 3: batch mode requires a resolvable logged-in address] +Reading `TransactionManager.cjs`'s `sendSignedBatchTransactions`: it builds the +batch id from `getAccount().address` and returns an explicit error if that is +empty. The flat-array path has no equivalent check at this layer. +::: -```sh -mxpy config-env get --env mainnet -``` +:::note[Pitfall 4: don't treat "sequential" as independently proven here] +See "The v5 mechanism" above; this recipe verified the client contract, not +on-chain group ordering. +::: -#### Deleting an env config +## See also -To delete an env config, we use the following command: +- [Migrate v4 to v5: hook-by-hook diffs](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration) + is the six-pair overview, including the flat single-array send/track case. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the canonical single-transaction sign, send, track flow this recipe extends + to groups. +- [Migration: useGetAccountInfo v4 vs v5](/sdk-and-tools/sdk-js/cookbook/migration/get-account-info-v4-vs-v5) + is another "the old name still resolves, but check the real shape" trap from the + same migration. -```sh -mxpy config-env remove -``` +--- -#### Switching to a different env config +### miniblocks -To switch to a new env config, we use the following command: +This page describes the structure of the `miniblocks` index (Elasticsearch), and also depicts a few examples of how to query it. -```sh -mxpy config-env switch --env -``` -You can manage multiple environment configurations with ease using `mxpy config-env`. This feature helps streamline workflows when working with multiple networks or projects. Use `mxpy config-env list` to see all available configs, and `switch` to quickly toggle between them. +## _id -### Configuring wallets +The _id field of this index is represented by the miniblock hash, in a hexadecimal encoding. -Wallets can be configured in the wallet config. Among all configured wallets, one must be set as the active wallet. This active wallet will be used by default in all mxpy commands, unless another wallet is explicitly provided using `--pem`, `--keystore`, or `--ledger`. Alternatively, the `--sender` argument can be used to specify a particular sender address from the address config (e.g. --sender alice). -The values that are available for configuring wallets are the following: -```json -{ - "path": "", - "index": "", -} -``` +## Fields -Supported wallet types include PEM files and keystores. The CLI will determine the type based on the given path and act accordingly. If the wallet is of type `keystore`, you'll be prompted to enter the wallet's password. -The `path` field represents the absolute path to the wallet. +| Field | Description | +|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| senderShard | The senderShard field represents the shard ID of the source block. | +| receiverShard | The receiverShard field represents the shard ID of the destination block. | +| senderBlockHash | The senderBlockHash field represents the hash (hex encoded) of the source block in which the miniblock was included. | +| receiverBlockHash | The receiverBlockHash field represents the hash (hex encoded) of the destination block in which the miniblock was included. | +| type | The type field represents the type of the miniblock. It can be `TxBlock` (if it contains transactions) or `SmartContractResultBlock` (if it contains smart contracts results). | +| procTypeS | The procTypeS field represents the processing type at the source shard. It can be `Normal` or `Scheduled`. | +| procTypeD | The procTypeD field represents the processing type at the destination shard. It can be `Normal` or `Scheduled`. | +| timestamp | The timestamp field represents the timestamp of the block in which the miniblock was executed. | +| reserved | The reserved field ensures the possibility to extend the mini block. | -The `index` field represents the index that will be used when deriving the wallet from the secret key. This field is optional, the default index is `0`. -#### Creating a new wallet config +## Query examples -When configuring a new wallet we need to give it an alias. An alias is a user-defined name that identifies a configured wallet (e.g. alice, bob, dev-wallet). To create a new wallet config, we use the following command: -```sh -mxpy config-wallet new +### Fetch all the miniblocks of a block + +``` +curl --request GET \ + --url ${ES_URL}/miniblocks/_search \ + --header 'Content-Type: application/json' \ + --data '{ + "query": { + "bool": { + "should": [ + { + "match": { + "senderBlockHash": "ddc..." + } + }, + { + "match": { + "receiverBlockHash": "ddc..." + } + } + ] + } + } +}' ``` -This command accepts the `--path` argument, so the path to the wallet can be set directly, without needing to call `mxpy config-wallet set` afterwards. +--- -#### Setting the wallet config fields +### Minimal sdk-dapp v5 app in Next.js (App Router) -For a config to be valid, we need to set at least the `path` field for an already created wallet alias. To do so, we use the following command: +This recipe gives you the smallest working sdk-dapp v5 setup in a Next.js 14 App +Router app. It is the path the official sdk-dapp docs describe in passing without +ever showing a complete working file. Use this as your starting point if you are +scaffolding a new dApp on Next.js: clone the `recipes/nextjs-minimal` directory +and rename it. -```sh -mxpy config-wallet set path absolute/path/to/pem/wallet.pem --alias alice -``` +This recipe is **not** the right starting point if you are on Vite + React (use +[Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal)) +or if you have an existing v4 codebase to upgrade (use +[Migrate v4 to v5](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration)). -#### Getting the value of a field +## Prerequisites -We can get the value of a field from an alias using the following command: +- Node.js >= 20.13.1 (sdk-dapp's stated minimum). +- pnpm or npm. +- A devnet wallet, either the DeFi browser extension or xPortal (mobile via + WalletConnect). +- Familiarity with the Next.js App Router (`app/` directory). -```sh -mxpy config-wallet get path --alias alice +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/nextjs-minimal +npm install +npm run dev +# open https://localhost:3000 (note the s, see Pitfalls below) ``` -#### Dumping the active wallet config +## package.json: pin the SDK and peer deps -To view all the properties of the active address, use the following command: +`bignumber.js` and `protobufjs` are peer dependencies of sdk-core and must be +installed explicitly. The Next.js template installs them as direct deps; we do +the same. -```sh -mxpy config-wallet dump +```json +{ + "name": "cookbook-recipe-nextjs-minimal", + "version": "0.1.0", + "private": true, + "description": "Cookbook recipe — minimal sdk-dapp v5 in Next.js 14 App Router. Compiles strict.", + "scripts": { + "dev": "next dev --experimental-https", + "build": "next build", + "start": "next start", + "typecheck": "tsc --noEmit --strict", + "lint": "next lint --max-warnings=0" + }, + "dependencies": { + "@multiversx/sdk-core": "^15.4.0", + "@multiversx/sdk-dapp": "^5.6.0", + "bignumber.js": "^9.1.2", + "next": "^14.2.18", + "protobufjs": "^7.4.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^20.17.6", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "eslint": "^8.57.1", + "eslint-config-next": "^14.2.18", + "typescript": "^5.6.3" + }, + "engines": { + "node": ">=20.13.1" + } +} ``` -#### Dumping all configured wallets +## next.config.js: the real config, not the README's -To view all the wallets configured, use the following command: +This matches the actual `next.config.js` shipped in `mx-template-dapp-nextjs`, not +the simplified version in that repo's README. The four entries do four things: +`transpilePackages: ['@multiversx/sdk-dapp-ui']` (sdk-dapp-ui ships untranspiled +web components); `webpack(config) { config.resolve.fallback = { fs: false } }` +(silences the `fs` lookup in transitive deps that do not run server-side anyway); +`config.externals.push('pino-pretty', 'lokijs', 'encoding', ...)` (Node-only code +pulled in via the WalletConnect transitive chain); and the `bufferutil` / +`utf-8-validate` externals (optional WebSocket performance binaries). -```sh -mxpy config-wallet list -``` +```js +// next.config.js — sdk-dapp v5 + Next.js 14 App Router +// +// This file mirrors the actual configuration shipped in +// https://github.com/multiversx/mx-template-dapp-nextjs/blob/main/next.config.js +// (NOT the simplified version shown in that repo's README). +// +// What each entry does: +// transpilePackages: sdk-dapp-ui ships ESM web components untranspiled. +// Without this, Next.js's default Babel pipeline rejects the syntax. +// webpack().resolve.fallback.fs: false: silences `fs` lookups in +// transitive deps that don't actually run server-side. +// externals: pino-pretty, lokijs, encoding pull in Node-only code via +// the WalletConnect transitive chain. Externalising them avoids +// bundling them and the warnings they produce. +// bufferutil / utf-8-validate: optional WebSocket native acceleration — +// graceful fallback if absent, but webpack tries to bundle them +// unless externalised. + +/** @type {import('next').NextConfig} */ +const nextConfig = { + transpilePackages: ['@multiversx/sdk-dapp-ui'], + webpack: (config) => { + config.resolve.fallback = { ...(config.resolve.fallback || {}), fs: false }; + config.externals.push( + 'pino-pretty', + 'lokijs', + 'encoding', + { + bufferutil: 'bufferutil', + 'utf-8-validate': 'utf-8-validate', + }, + ); + return config; + }, +}; -#### Switching to a different wallet +module.exports = nextConfig; +``` -We may have multiple wallets configured, so to switch between them, we use the following command: +## app/layout.tsx: root layout, just the shell -```sh -mxpy config-wallet switch --alias alice -``` +`layout.tsx` is a server component (no `"use client"`). It imports ``, +which is the client-only wrapper. -#### Removing an address from the config +```tsx +// app/layout.tsx — Next.js 14 App Router root layout. +// +// This is a server component (no "use client" directive). It does nothing +// blockchain-related — it only renders the html/body shell and delegates +// to which is the client-only sdk-dapp init wrapper. +// +// Why this split matters: sdk-dapp's initApp() touches sessionStorage, +// which is undefined during SSR / build. Putting initApp in a server +// component crashes the build. Putting it in a "use client" component that +// runs in useEffect makes it safe. + +import type { Metadata, Viewport } from 'next'; +import type { ReactNode } from 'react'; +import { Providers } from './providers'; + +export const metadata: Metadata = { + title: 'Cookbook recipe — Minimal sdk-dapp v5 in Next.js', + description: + 'A working sdk-dapp v5 + Next.js 14 App Router setup that compiles under TypeScript strict mode.', +}; -We can remove an address from the config using the alias of the address and the following command: +export const viewport: Viewport = { + width: 'device-width', + initialScale: 1, +}; -```sh -mxpy config-wallet remove --alias alice +export default function RootLayout({ + children, +}: { + children: ReactNode; +}): JSX.Element { + return ( + + + {children} + + + ); +} ``` -## Estimating the Gas Limit for transactions +## app/providers.tsx: the client-only init wrapper + +This is the file the docs leave you to invent. Three things happen here: +`"use client"` keeps `initApp()` off the server (it touches `sessionStorage`, +which crashes during SSR); `useEffect` runs `initApp(config)` once on mount and +gates rendering on `ready`; `UnlockPanelManager.init()` sets up the wallet +selection panel, with an **`async`** `onClose` handler (see Pitfall 1). + +```tsx title="app/providers.tsx" +'use client'; + +// app/providers.tsx — client-only sdk-dapp v5 init wrapper. +// +// This component is the file the official docs leave to the reader to +// invent. It does three things: +// +// 1. Marks itself "use client" so it never runs on the server. sdk-dapp's +// initApp reads from sessionStorage, which is undefined during SSR. +// 2. Calls initApp(config) once on mount inside useEffect. Renders a +// lightweight loader until init resolves; then renders children. +// 3. Configures the UnlockPanelManager — the v5 replacement for the +// per-provider login button components from v4. +// +// IMPORTANT: the OnCloseUnlockPanelType requires Promise. The docs +// example uses sync `() => navigate(...)` +// which fails strict-mode compile. We use `async` callbacks. + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { dappConfig } from '../lib/multiversx'; + +// Module-scope flag prevents double-init if React Strict Mode mounts the +// component twice (it does, deliberately, in dev). initApp is idempotent +// in v5 but skipping the second call avoids redundant work. +let initStarted = false; + +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; + + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + + // UnlockPanelManager.init must be called AFTER initApp resolves. + // Both callbacks return Promise — see Pitfall 1 on the recipe page. + UnlockPanelManager.init({ + loginHandler: async (): Promise => { + // Replace with your post-login navigation (e.g. router.push). + // For this minimal recipe we just leave the user on /. + // eslint-disable-next-line no-console + console.log('Logged in.'); + }, + onClose: async (): Promise => { + // Called if the user dismisses the unlock panel without logging in. + // eslint-disable-next-line no-console + console.log('Unlock panel closed.'); + }, + }); -mxpy (version 11.1.0 and later) can automatically estimate the required gas limit for transactions when a proxy URL is provided. The estimation works by simulating the transaction before sending it. + setReady(true); + }); -While the estimation is generally accurate, it's recommended to add a safety margin to account for potential state changes. This can be done in two ways: + return () => { + cancelled = true; + }; + }, []); -1. Per transaction, using the `--gas-limit-multiplier` flag: + if (!ready) { + return ( +
+ Initializing wallet SDK… +
+ ); + } -```sh -mxpy tx new --gas-limit-multiplier 1.1 ... + return <>{children}; +} ``` -2. As a global default setting in the config: +## app/page.tsx: landing page with login and account info -```sh -mxpy config set gas_limit_multiplier 1.1 -``` +`useGetIsLoggedIn()` and `useGetAccount()` are sdk-dapp's React hooks. The +component renders a connect button when logged out and the account address plus +EGLD balance when logged in. -A multiplier of 1.1 (10% increase) is typically sufficient for most transactions. +```tsx title="app/page.tsx" +'use client'; +// app/page.tsx — landing page. +// +// Two states: +// 1. Logged out → "Connect wallet" button that opens the unlock panel. +// 2. Logged in → address (bech32) and EGLD balance. +// +// All blockchain state comes from sdk-dapp React hooks. These read from a Zustand store that initApp populates, +// so they MUST run inside — they return empty defaults until +// init has resolved. -## Creating wallets - -There are a couple available wallet formats: - -- `raw-mnemonic` - secret phrase in plain text -- `keystore-mnemonic` - secret phrase, as a password-encrypted JSON keystore file -- `keystore-secret-key` - secret key (irreversibly derived from the secret phrase), as a password-encrypted JSON keystore file -- `pem` - secret key (irreversibly derived from the secret phrase), as a PEM file - -For this example, we are going to create a `keystore-mnemonic` wallet. +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import { formatAmount } from '@multiversx/sdk-dapp/out/lib/sdkDappUtils'; + +export default function Home(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const account = useGetAccount(); + const { network } = useGetNetworkConfig(); + + // The UnlockPanelManager is a singleton. .getInstance() works once it has + // been initialised inside . Calling .openUnlockPanel() raises + // the wallet selector. + const handleConnect = (): void => { + // openUnlockPanel() returns Promise (see + // node_modules/@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager.d.ts). + // This handler is a sync onClick, so we explicitly discard the promise — + // matching the eslint no-floating-promises gate. + void UnlockPanelManager.getInstance().openUnlockPanel(); + }; -Let's create a keystore wallet: + const handleLogout = async (): Promise => { + const provider = getAccountProvider(); + await provider.logout(); + }; -```sh -mxpy wallet new --format keystore-mnemonic --outfile test_wallet.json + return ( +
+

Minimal sdk-dapp v5 in Next.js

+

+ Network: {network.chainId} · API: {network.apiAddress} +

+ + {!isLoggedIn ? ( + + ) : ( +
+

Connected

+

+ Address: {account.address} +

+

+ Balance:{' '} + + {formatAmount({ + input: account.balance, + decimals: 18, + digits: 4, + showLastNonZeroDecimal: true, + })}{' '} + {network.egldLabel} + +

+ +
+ )} +
+ ); +} ``` -The wallet's mnemonic will appear, followed by a prompt to set a password for the file. Once you input the password and press "Enter", the file will be generated at the location specified by the `--outfile` argument. - +## lib/multiversx.ts: environment config -## Converting a wallet +Centralized config so swapping environments is a one-line change. Exports +`dappConfig`, consumed by ``. -As you have read above, there are multiple ways in which you can store your secret keys. +```ts title="lib/multiversx.ts" +// lib/multiversx.ts — environment configuration for sdk-dapp's initApp. +// +// Centralising config here means that swapping environments +// (devnet -> testnet -> mainnet) is a one-file change. It also lets us keep +// secret-ish values (WalletConnect project IDs) in a single place and read +// them from environment variables. +// +// The full `dAppConfig` shape accepts more keys than this. We +// only set the keys we actually use; sdk-dapp fills the rest with defaults. -To convert a wallet from a type to another you can use: +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +// `WalletConnect` requires a project ID from https://cloud.walletconnect.com. +// Read from env so the same code ships across environments without leaks. +// Fallback is the demo project ID — works in dev but rate-limited; replace it +// with your own before going to production. +const WALLET_CONNECT_PROJECT_ID = + process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; + +// `dappConfig` is consumed by `app/providers.tsx` -> `initApp(...)`. +export const dappConfig = { + storage: { + // sessionStorage on the client. SSR-safe because only calls + // initApp inside a useEffect. + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + // nativeAuth: true issues a JWT-shaped token on login that backends can + // verify. Set to false for the + // simplest possible flow. + nativeAuth: true, + // Theme is a ThemesEnum member (not a bare string) — sdk-dapp's own + // InitAppType JSDoc example uses `theme: ThemesEnum.light` (see + // node_modules/@multiversx/sdk-dapp/out/methods/initApp/initApp.types.d.ts). + // The runtime value is the literal 'mvx:dark-theme'; the web components + // key off that exact string. + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + }, +}; -```sh -mxpy wallet convert +// Re-export EnvironmentsEnum so consumers don't need a deep import. +export { EnvironmentsEnum }; ``` -:::info -Keep in mind that the conversion isn't always possible (due to irreversible derivations of the secret phrase): +## Run it -- `raw-mnemonic` can be converted to any other format -- `keystore-mnemonic` can be converted to any other format -- `keystore-secret-key` can only be converted to `pem` -- `pem` can only be converted to `keystore-secret-key` +```bash +npm run dev +``` -It's mandatory that you keep a backup of your secret phrase somewhere safe. -::: +Then open `https://localhost:3000` (HTTPS, see Pitfall 2). You should see a +"Connect wallet" button; after clicking, the sdk-dapp unlock panel listing your +installed providers; after connecting, the account address (bech32) and the EGLD +balance, formatted via `formatAmount`. -Let's convert the previously created `keystore-mnemonic` to a `PEM` wallet. We discourage the use of PEM wallets for storing cryptocurrencies due to their lower security level. However, they prove to be highly convenient and user-friendly for application testing purposes. +## How it works -To convert the wallet we type the following command: +Three sdk-dapp v5 concepts are in play here. -```sh -mxpy wallet convert --infile test_wallet.json --in-format keystore-mnemonic --outfile converted_wallet.pem --out-format pem -``` +**`initApp()` is imperative, not declarative.** Unlike wagmi or +`@solana/wallet-adapter-react`, sdk-dapp's setup is a function call you must +`await` once before any hook works. The `` component wraps that call +so React does not render until it resolves. This is the workaround for the +SSR-unsafe init; the docs show a top-level `initApp(...).then(render)` which works +in Vite but breaks in Next.js. -After being prompted to enter the password you've previously set for the wallet the new `.pem` file will be created. +**`UnlockPanelManager` replaces v4 login button components.** In v4 you imported +``, ``, and so on, one component +per provider. In v5 you call `UnlockPanelManager.init({ loginHandler })` and then +`.openUnlockPanel()` from any button. The panel itself is a web component rendered +by `@multiversx/sdk-dapp-ui`. -The command arguments can be found [here](https://github.com/multiversx/mx-sdk-py-cli/blob/main/CLI.md#walletconvert) or by typing: +**Hooks read from a Zustand store.** `useGetAccount()`, `useGetIsLoggedIn()`, and +`useGetNetworkConfig()` are thin wrappers over `useSelector` against a Zustand +store initialized inside `initApp()`. That is why they return empty/default values +until init completes, and why `` gates rendering on the `ready` flag. -```sh -mxpy wallet convert --help -``` +## Pitfalls +:::warning[Pitfall 1: UnlockPanelManager.init's onClose must be async] +The docs example uses sync callbacks. That fails under TypeScript strict mode with +`error TS2322: Type '() => void' is not assignable to type 'OnCloseUnlockPanelType'`. +The `OnCloseUnlockPanelType` requires `Promise`. Use `async () => { ... }` +or `() => Promise.resolve(...)`. The sample code in `app/providers.tsx` does this +correctly. +::: -## Building a smart contract +:::warning[Pitfall 2: wallet providers require HTTPS, even in dev] +`npm run dev` defaults to `http://localhost:3000`. The DeFi extension and Ledger +providers refuse to connect over HTTP. WalletConnect (xPortal) **does** work over +HTTP because the connection happens over the WalletConnect relay, not the dApp +origin. See +[Local HTTPS for dApp dev](/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup) +for the mkcert workflow that makes `https://localhost:3000` work. +::: -In order to deploy a smart contract on the network, you need to build it first. For this purpose, [sc-meta](/developers/meta/sc-build-reference#how-to-basic-build) should be used. To learn more about `sc-meta`, please check out [this page](/developers/meta/sc-meta). +:::warning[Pitfall 3: initApp() must run client-side only] +`initApp()` reads from `sessionStorage` during rehydration. On the server (during +Next.js SSR or build), `sessionStorage` is undefined and the call throws. The +`"use client"` plus `useEffect` pattern in `app/providers.tsx` is mandatory; you +cannot put `initApp()` at module scope. +::: -The contract we will be using for this examples can be found [here](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/adder). +:::note[Pitfall 4: expect two build warnings] +Building this recipe surfaces two non-blocking warnings: +`Attempted import error: 'firstValueFrom' is not exported from 'rxjs'` and +`Critical dependency: the request of a dependency is an expression`. The first is +the `@ledgerhq/hw-transport-web-ble` package pulling an older `rxjs`; the second is +`protobufjs/inquire` doing a runtime `require` lookup. Neither breaks the build. +::: -Build a contract as follows: +## See also -```sh -sc-meta all build --path -``` +- [Minimal sdk-dapp v5 in Vite + React](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) + is the same recipe, Vite path, no SSR concerns. +- [Local HTTPS for dApp dev](/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup) + is how to make `https://localhost:3000` work for both Next.js and Vite. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the next thing you will do once login works. +- [Migrate v4 to v5](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration) + if you have an existing v4 Next.js codebase. -If our working directory is already the contract's directory we can skip the `--path` argument as by default the contract's directory is the _current working directory_. +--- -The generated `.wasm` file will be created in a directory called `output` inside the contract's directory. +### Minimal sdk-dapp v5 app in Vite + React +This recipe gives you the smallest working sdk-dapp v5 setup in a fresh Vite + +React + TypeScript app. Use this if you are scaffolding a new dApp on Vite: clone +the `recipes/vite-react-minimal` directory and rename it. -## Deploying a smart contract +This recipe is **not** the right starting point if you are on Next.js (use +[Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) +instead, the App Router has SSR concerns this recipe deliberately avoids) or if +you have an existing v4 codebase to upgrade (use +[Migrate v4 to v5](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration)). -After you've built your smart contract, it can be deployed on the network. +The Vite path is simpler than the Next.js path because there is no SSR. +`initApp()` could run at module scope; we keep it in a `useEffect` only so the +React Strict Mode double-mount does not trigger redundant work in dev. -For deploying a smart contract the following command can be used: +## Prerequisites -```sh -mxpy contract deploy -``` +- Node.js >= 20.13.1 (sdk-dapp's stated minimum). +- pnpm or npm. +- A devnet wallet, either the DeFi browser extension or xPortal (mobile via + WalletConnect). -To deploy a smart contract you have to send a transaction to the **Smart Contract Deploy Address** and that address is `erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu`, but you don't have to worry about setting the receiver of the transaction because the above command takes care of it. +## Install -The `--bytecode` argument specifies the path to your previously-built contract. If you've built the smart contract using `mxpy`, the generated `.wasm` file will be in a folder called `output`. +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/vite-react-minimal +npm install +npm run dev +# open https://localhost:5173 (note the s, see Pitfall 2) +``` -For example, if your contract is in `~/contracts/adder`, the generated bytecode file `adder.wasm` will be in `~/contracts/adder/output`. So, when providing the `--bytecode` argument the path should be `~/contracts/adder/output/adder.wasm`. +## package.json: pin the SDK and peer deps -The `mxpy contract deploy` command needs a multitude of other parameters that can be checked out [here](https://github.com/multiversx/mx-sdk-py-cli/blob/main/CLI.md#contractdeploy) or by simply typing the following: +`bignumber.js` and `protobufjs` are peer dependencies of sdk-core and must be +installed explicitly. The `@vitejs/plugin-basic-ssl` plugin is the simplest way +to get HTTPS on the Vite dev server. -```sh -mxpy contract deploy --help +```json +{ + "name": "cookbook-recipe-vite-react-minimal", + "version": "0.1.0", + "private": true, + "description": "Cookbook recipe — minimal sdk-dapp v5 in Vite + React + TypeScript. Compiles strict.", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit --strict", + "lint": "eslint . --max-warnings=0" + }, + "dependencies": { + "@multiversx/sdk-core": "^15.4.0", + "@multiversx/sdk-dapp": "^5.6.0", + "bignumber.js": "^9.1.2", + "protobufjs": "^7.4.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-basic-ssl": "^1.2.0", + "@vitejs/plugin-react": "^4.3.3", + "eslint": "^8.57.1", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.14", + "typescript": "^5.6.3", + "typescript-eslint": "^8.13.0", + "vite": "^5.4.10" + }, + "engines": { + "node": ">=20.13.1" + } +} ``` -We will use a `.pem` file for the sake of simplicity but you can easily use any wallet type. +## vite.config.ts: HTTPS and CommonJS interop -Let's see a simple example: +Two things matter: `basicSsl()` for HTTPS dev, and `optimizeDeps` for sdk-dapp's +CommonJS transitive packages. This exact config was arrived at by actually +running `npm run dev` and `npm run build` and fixing two real failures, not by +reasoning from the docs alone. See Pitfalls 5 and 6 before you trim this file +down for your own project. -```sh -mxpy contract deploy --bytecode ~/contracts/adder/output/adder.wasm \ - --proxy=https://devnet-gateway.multiversx.com \ - --arguments 0 --gas-limit 5000000 \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --send +```ts +// vite.config.ts — sdk-dapp v5 + Vite + React. +// +// Two things matter for sdk-dapp to work cleanly under Vite: +// +// 1. HTTPS for the dev server. Most wallet providers (DeFi extension, +// Ledger, Passkey) refuse to talk to http://localhost. We use +// @vitejs/plugin-basic-ssl as a zero-config solution; if you want a +// fully trusted cert (no browser warning every session), see the +// "Local HTTPS for dApp dev" recipe for the mkcert path. +// +// 2. The CommonJS interop plumbing for sdk-dapp's transitive WalletConnect +// and Ledger deps. Unlike Next.js, Vite's default resolver is +// ESM-clean for sdk-dapp 5.x and we don't need the `externals` array +// that recipes/nextjs-minimal/next.config.js carries. If you hit +// "Unexpected token 'export'" or "Cannot use import statement outside +// a module" errors at runtime, narrow the offending package and add +// it to optimizeDeps.include — but see the comment on optimizeDeps +// below before adding '@multiversx/sdk-dapp' itself; that one breaks +// the dev server rather than fixing it. + +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import basicSsl from '@vitejs/plugin-basic-ssl'; + +export default defineConfig({ + plugins: [react(), basicSsl()], + server: { + // Do NOT set `https` here yourself. basicSsl() already injects the + // generated cert into server.https via its own Vite `config` hook — see + // node_modules/@vitejs/plugin-basic-ssl/README.md. It's also not a + // boolean option: Vite's type is `https.ServerOptions | undefined` + // (node_modules/vite/dist/node/index.d.ts), so a literal `https: true` + // fails `tsc --strict` ("no overload matches this call") the moment + // anything actually typechecks this file. See the "Local HTTPS for + // dApp dev" recipe's basic-ssl snippet for the same fix in context. + port: 5173, + }, + // sdk-dapp pulls in a small number of CJS deps that Vite needs to + // pre-bundle. The list is conservative — add to it only if you see + // module-format errors in the browser console. + // + // Do NOT list '@multiversx/sdk-dapp' (the bare package) here. Verified + // against the installed @multiversx/sdk-dapp@5.6.23: its package.json has + // no "main", "module", or "exports" field at all — by design, every import + // is a deep path like '@multiversx/sdk-dapp/out/react/account/useGetAccount' + // Telling Vite to eagerly pre-bundle the bare specifier makes it try to + // resolve a package entry point that doesn't exist, and the dev server + // fails to start outright: + // "Error: Failed to resolve entry for package '@multiversx/sdk-dapp'. + // The package may have incorrect main/module/exports specified in its + // package.json." (reproduced with `npm run dev` before this fix). + // '@multiversx/sdk-core' is a different case — it does ship a real + // `main: "out/index.js"` barrel, so including it here is safe and correct. + optimizeDeps: { + include: ['@multiversx/sdk-core', 'bignumber.js', 'protobufjs'], + // These three deep paths are unresolvable in the actually-installed tree + // (npm install on 2026-07-07 pulled @multiversx/sdk-dapp@5.6.23): three + // of sdk-dapp's Ledger transport strategies + // (hw-transport-webusb, hw-transport-webhid, hw-transport-web-ble) each + // bundle their OWN nested copy of @ledgerhq/devices@8.16.0, and that + // nested copy is missing the hid-framing.js / ble/sendAPDU.js / + // ble/receiveAPDU.js files its sibling packages import — genuinely + // absent from that package version on disk, not just an exports-map + // resolution quirk (the top-level, separately-hoisted + // @ledgerhq/devices@8.0.3 does have them). This is an upstream Ledger + // packaging bug, not a Vite or sdk-dapp config issue; the fix here only + // silences the *bundler* error the same way `vite build`'s own + // suggestion does, not the underlying missing files. + exclude: [ + '@ledgerhq/devices/hid-framing', + '@ledgerhq/devices/ble/sendAPDU', + '@ledgerhq/devices/ble/receiveAPDU', + ], + }, + build: { + rollupOptions: { + // Same root cause as optimizeDeps.exclude above, mirrored for the + // production build (`vite build` uses Rollup, not esbuild, and hits + // the identical unresolvable-import error there independently). + external: [ + '@ledgerhq/devices/hid-framing', + '@ledgerhq/devices/ble/sendAPDU', + '@ledgerhq/devices/ble/receiveAPDU', + ], + }, + }, +}); ``` -The `--proxy` is used to specify the url of the proxy and the `--chain` is used to select the network the contract will be deployed to. The chain ID and the proxy need to match for our transaction to be executed. We can't prepare a transaction for the Devnet (using `--chain D`) and send it using the mainnet proxy (https://gateway.multiversx.com). +## index.html: the Vite entry -The `--arguments` is used in case our contract needs any arguments for the initialization. We know our `adder` needs a value to start adding from, so we set that to `0`. +Single root div plus an ESM script tag. Vite injects the bundle. -The `--gas-limit` is used to set the gas we are willing to pay so our transaction will be executed. 5 million gas is a bit too much because our contract is very small and simple, but better to be sure. In case our transaction doesn't have enough gas the network will not execute it, saying something like `Insufficient gas limit`. +```html + + + + + + Cookbook recipe — Minimal sdk-dapp v5 in Vite + + +
+ + + +``` -The `--pem` argument is used to provide the sender of the transaction, the payer of the fee. The sender will also be the owner of the contract. The nonce of the sender is fetched from the network if the `--proxy` argument is provided. The nonce can also be explicitly set using the `--nonce` argument. If the nonce is explicitly set, mxpy will not fetch the nonce from the network. +## src/main.tsx: bootstrap +Classic React 18 `createRoot` plus `StrictMode`. `` wraps the app, the +same pattern as Next.js, just without the layout/page split. -### Deploying a smart contract providing the ABI file +```tsx title="src/main.tsx" +// src/main.tsx — Vite entry point. +// +// The classic React 18 createRoot + StrictMode setup. wraps +// the entire app — same pattern as Next.js, just without the layout/page +// split that the App Router imposes. +// +// Why StrictMode? It double-mounts components in dev to flush out unsafe +// side effects. The `initStarted` module-scope flag in providers.tsx +// handles the double-mount cleanly; if you remove that flag, you'll see +// initApp() invoked twice in dev. -For functions that have complex arguments, we can use the ABI file generated when building the contract. The ABI can be provided using the `--abi` argument. When using the ABI, and only when using the ABI, the arguments should be written in a `json` file and should be provided via the `--arguments-file` argument. +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; +import { Providers } from './providers'; -For this example, we'll use the [multisig contract](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/multisig). +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error('No #root element found in index.html'); +} -First, we'll prepare the file containing the constructors arguments. We'll refer to this file as `deploy_multisig_arguments.json`. The constructor requires two arguments, the first is of type `u32` and the second one is of type `variadic
`. All the arguments in this file **should** be placed inside a list. The arguments file should look like this: +createRoot(rootElement).render( + + + + + , +); +``` -```json -[ - 2, - [ - { - "bech32": "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" - }, - { - "hex": "8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8" +## src/providers.tsx: initApp wrapper + +The body is identical to the Next.js recipe's `app/providers.tsx`, minus the +`"use client"` directive. That is the whole story: same code, no SSR concern. + +```tsx title="src/providers.tsx" +// src/providers.tsx — sdk-dapp v5 init wrapper for Vite. +// +// Same conceptual job as recipes/nextjs-minimal/app/providers.tsx, simpler +// implementation: +// +// - No "use client" needed. Vite has no SSR; the entire app is a CSR +// bundle that hydrates in the browser. sessionStorage is always +// defined by the time React mounts. +// - We can call initApp() at module scope if we want, but we keep it in +// a useEffect so the React-Strict-Mode double-mount in dev doesn't +// throw a warning. The module-scope `initStarted` flag matches the +// Next.js recipe exactly. +// +// IMPORTANT: the OnCloseUnlockPanelType requires Promise. The docs +// example uses sync `() => navigate(...)` +// which fails strict-mode compile. We use async callbacks. + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { dappConfig } from './lib/multiversx'; + +// Module-scope flag prevents double-init under React Strict Mode (which +// deliberately mounts useEffect twice in dev to surface unsafe side +// effects). initApp is idempotent in v5 but skipping the second call +// avoids redundant network round-trips. +let initStarted = false; + +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; } - ] -] -``` + initStarted = true; + + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + + // UnlockPanelManager.init must be called AFTER initApp resolves. + // Both callbacks return Promise — see Pitfall 1. + UnlockPanelManager.init({ + loginHandler: async (): Promise => { + // Replace with your post-login navigation (e.g. a router push). + // For this minimal recipe we just leave the user on the home + // page — useGetIsLoggedIn() will flip and the UI re-renders. + // eslint-disable-next-line no-console + console.log('Logged in.'); + }, + onClose: async (): Promise => { + // eslint-disable-next-line no-console + console.log('Unlock panel closed.'); + }, + }); -Let's go a bit through our file and see why it looks like this. First, as mentioned above, we have to place all the arguments inside a list. Then, the value `2` corresponds to the type `u32`. After that, we have another list that corresponds to the type `variadic`. Inside this list, we need to insert our addresses. For `mxpy`to encode addresses properly, we need to provide the address values inside a dictionary that can contain two keys: we can provide the address as the `bech32` representation or as the `hex encoded` public key. + setReady(true); + }); -After finishing the arguments file, we can run the following command to deploy the contract: + return () => { + cancelled = true; + }; + }, []); -```sh -mxpy contract deploy --bytecode ~/contracts/multisig/output/multisig.wasm \ - --proxy=https://devnet-gateway.multiversx.com \ - --abi ~/contracts/multisig/output/multisig.abi.json \ - --arguments-file deploy_multisig_arguments.json \ - --gas-limit 500000000 \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --send + if (!ready) { + return ( +
+ Initializing wallet SDK… +
+ ); + } + + return <>{children}; +} ``` +## src/App.tsx: login and account display -## Calling the Smart Contract +`useGetIsLoggedIn()`, `useGetAccount()`, and `useGetNetworkConfig()` are +sdk-dapp's React hooks. They read from the Zustand store that `initApp()` populates. -After deploying our smart contract we can start interacting with it. The contract has a function called `add()` that we can call and it will increase the value stored in the contract with the value we provide. +```tsx title="src/App.tsx" +// src/App.tsx — landing component. +// +// Two states: +// 1. Logged out → "Connect wallet" button (opens UnlockPanelManager). +// 2. Logged in → bech32 address + EGLD balance. +// +// All blockchain state comes from sdk-dapp React hooks. These read from the Zustand store that +// initApp populates, so they MUST run inside — they return +// empty defaults until init has resolved. -To call a function we use the `mxpy contract call` command. Here's an example of how we can do that: +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import { formatAmount } from '@multiversx/sdk-dapp/out/lib/sdkDappUtils'; + +export function App(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const account = useGetAccount(); + const { network } = useGetNetworkConfig(); + + const handleConnect = (): void => { + // openUnlockPanel() returns Promise (see + // node_modules/@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager.d.ts). + // This handler is a sync onClick, so we explicitly discard the promise — + // matching the eslint no-floating-promises gate. + void UnlockPanelManager.getInstance().openUnlockPanel(); + }; -```sh -mxpy contract call erd1qqqqqqqqqqqqqpgq3zrpqj3sulnc9xq95sljetxhf9s07pqtd8ssfkxjv4 \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --proxy=https://devnet-gateway.multiversx.com --chain D \ - --function add --arguments 5 --gas-limit 1000000 \ - --send -``` + const handleLogout = async (): Promise => { + const provider = getAccountProvider(); + await provider.logout(); + }; -The positional argument is the contract address that we want to interact with. The `--pem`, `--proxy` and `--chain` arguments are used the same as above in the deploy transaction. + return ( +
+

Minimal sdk-dapp v5 in Vite + React

+

+ Network: {network.chainId} · API:{' '} + {network.apiAddress} +

+ + {!isLoggedIn ? ( + + ) : ( +
+

Connected

+

+ Address: {account.address} +

+

+ Balance:{' '} + + {formatAmount({ + input: account.balance, + decimals: 18, + digits: 4, + showLastNonZeroDecimal: true, + })}{' '} + {network.egldLabel} + +

+ +
+ )} +
+ ); +} +``` -Using the `--function` argument we specify the function we want to call and with the `--arguments` argument we specify the value we want to add. We set the gas we are willing to pay for the transaction and finally we send the transaction. +## src/lib/multiversx.ts: environment config +Environment variables under Vite come from `import.meta.env`, not `process.env`. +The `VITE_` prefix is required to expose a variable to the client bundle (see +Pitfall 4). -### Calling the smart contract providing the ABI file +```ts title="src/lib/multiversx.ts" +// src/lib/multiversx.ts — environment configuration for sdk-dapp's initApp. +// +// In Vite, environment variables come from import.meta.env (NOT +// process.env). The VITE_ prefix exposes them to the client bundle; vars +// without the prefix are server-only and never leak. +// +// The full dAppConfig shape accepts more keys than this. +// We only set the keys we actually use; sdk-dapp fills the rest with +// defaults. -Same as we did for deploying the contract, we can call functions by providing the ABI file and the arguments file. +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +// Read from import.meta.env (Vite's runtime environment object). +// Fallback is the demo project ID — works in dev but rate-limited; +// register your own at https://cloud.walletconnect.com. +const WALLET_CONNECT_PROJECT_ID = + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; + +export const dappConfig = { + storage: { + // Vite always runs in the browser at dev time and in the static-export + // bundle at runtime — sessionStorage is always defined. We don't need + // the SSR-safety tap-dance the Next.js recipe does. + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + nativeAuth: true, + // See node_modules/@multiversx/sdk-dapp/out/methods/initApp/initApp.types.d.ts — + // `theme` is a ThemesEnum member, not a bare string literal. + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + }, +}; -Since we deployed the [multisig contract](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/multisig), we'll call the `proposeTransferExecute` endpoint. +export { EnvironmentsEnum }; +``` -First, we'll prepare the file containing the endpoints arguments. We'll refer to this file as `call_multisig_arguments.json`. The `proposeTransferExecute` endpoint requires four arguments, the first is of type `Address`, the second one is of type `BigUInt`, the third is of type `Option` and the fourth is of type `variadic`. All the arguments in this file **should** be placed inside a list. The arguments file should look like this: +## Run it -```json -[ - { - "bech32": "erd1qqqqqqqqqqqqqpgqs63rcpahnwtjnedj5y6uuqh096nzf75gczpsc4fgtu" - }, - 1000000000000000000, - 5000000, - [ - { - "hex": "616464403037" - } - ] -] +```bash +npm run dev ``` -Let's go a bit through our file and see why it looks like this. First, as mentioned above, we have to place all the arguments inside a list. Then, the contract expects an address, so we provide the `bech32` representation. After that, we have a `BigUInt` value that we can provide as a number. The third value is `Option`, so we provide it as a number, as well. In case we wanted to skip this value, we could've simply used `0`. The last parameter is of type `variadic`. Because it's a variadic value, we have to place the arguments inside a list. Since we can't write bytes, we `hex encode` the value and place it in a dictionary containing the key-value pair `"hex": ""`, same as we did above for the address. +Then open `https://localhost:5173`. You should see a "Connect wallet" button plus +the active chain ID and API URL; after clicking, the sdk-dapp unlock panel; after +connecting, the bech32 address and EGLD balance. -After finishing the arguments file, we can run the following command to call the endpoint: +## How it works -```sh -mxpy contract call erd1qqqqqqqqqqqqqpgqjsg84gq5e79rrc2rm5ervval3jrrfvvfd8sswc6xjy \ - --proxy=https://devnet-gateway.multiversx.com \ - --abi ~/contracts/multisig/output/multisig.abi.json \ - --arguments-file call_multisig_arguments.json \ - --function proposeTransferExecute - --gas-limit 500000000 \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --send -``` +This recipe is the Vite parallel to the +[Next.js minimal recipe](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal). +Three observations on the differences: +**No `"use client"` needed.** Vite has no SSR; the entire app is CSR. +`sessionStorage` is always defined by the time React mounts, so we do not have to +defer `initApp()` to `useEffect` for SSR-safety reasons. We *do* still keep it in +`useEffect`, but only so the module-scope `initStarted` guard can protect against +React Strict Mode's double-mount in dev. -## Querying the Smart Contract +**HTTPS via plugin, not flag.** Next.js 14 has `next dev --experimental-https`. +Vite uses a plugin (`@vitejs/plugin-basic-ssl`). Both produce the same outcome, a +self-signed cert with the browser warning the first time. For a fully trusted +cert with no warning, see +[Local HTTPS for dApp dev](/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup). -Querying a contract is done by calling a so called `view function`. We can get data from a contract without sending a transaction to the contract, basically without spending money. +**Same hooks, same store.** All of sdk-dapp's React hooks work identically across +Vite and Next.js. The hook layer is framework-agnostic; only the bootstrap differs. -As you know, our contract has a function called `add()` that we previously called, and a `view function` called `getSum()`. Using this `getSum()` function we can see the value that is currently stored in the contract. +## Pitfalls -If you remember, when we deployed the contract we passed the value `0` as a contract argument, this means the contract started adding from `0`. When calling the `add()` function we used the value `5`. This means that now if we call `getSum()` we should get the value `5`. To do that, we use the `mxpy contract query` command. Let's try it! +:::warning[Pitfall 1: UnlockPanelManager.init's onClose must be async] +The docs example uses sync callbacks. That fails under TypeScript strict mode +with `error TS2322: Type '() => void' is not assignable to type 'OnCloseUnlockPanelType'`. +The `OnCloseUnlockPanelType` requires `Promise`. Use `async () => { ... }` +or `() => Promise.resolve(...)`. The sample code in `src/providers.tsx` does this +correctly. +::: -```sh -mxpy contract query erd1qqqqqqqqqqqqqpgq3zrpqj3sulnc9xq95sljetxhf9s07pqtd8ssfkxjv4 \ - --proxy https://devnet-gateway.multiversx.com \ - --function getSum -``` +:::warning[Pitfall 2: wallet providers require HTTPS, even in dev] +`@vitejs/plugin-basic-ssl` flips the dev server to HTTPS automatically; you should +see `https://localhost:5173` in the dev-server output. The DeFi extension and +Ledger refuse to connect over HTTP. WalletConnect (xPortal) works over HTTP +because the auth happens off-origin. +::: -We see that `mxpy` returns our value as a base64 string, as a hex number and as a integer. Indee, we see the expected value. +:::warning[Pitfall 3: HMR can re-run initApp] +Vite's hot-module-replacement re-runs modules without a full page reload. Without +the `initStarted` module-scope flag in `src/providers.tsx`, every save triggers +another `initApp()` invocation. The flag in this recipe matches the Next.js one, +keep it. +::: +:::warning[Pitfall 4: environment variables are import.meta.env, NOT process.env] +Vite injects env vars via `import.meta.env`. The `VITE_` prefix is required for +client-bundled values; `process.env` does not exist in the browser bundle. The +recipe reads `import.meta.env.VITE_WALLETCONNECT_PROJECT_ID`. If you copy the +Next.js recipe's `process.env.NEXT_PUBLIC_*` reads verbatim, you get `undefined` +everywhere. +::: -### Querying the smart contract providing the ABI file +:::danger[Pitfall 5: never add '@multiversx/sdk-dapp' itself to optimizeDeps.include] +This breaks the dev server outright. Confirmed by actually running `npm run dev` +against the installed `@multiversx/sdk-dapp@5.6.23`: its `package.json` declares +no `main`, `module`, or `exports` field at all. Every public import is a deep path +like `@multiversx/sdk-dapp/out/react/account/useGetAccount`. Asking Vite to +eagerly pre-bundle the bare package name makes it try to resolve an entry point +that does not exist (`Failed to resolve entry for package "@multiversx/sdk-dapp"`). +`@multiversx/sdk-core` is safe to include, it ships a real `main: "out/index.js"` +barrel. +::: -We'll call the `signed` readonly endpoint of the [multisig contract](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/multisig). This endpoint accepts two arguments: the first is the address, and the second is the proposal ID, which will be used to verify if the address has signed the proposal. The endpoint returns a `boolean` value, `true` if the address has signed the proposal and `false` otherwise. +:::warning[Pitfall 6: a nested @ledgerhq/devices copy is missing files (upstream bug)] +Also found by actually running `npm run dev` / `npm run build`: three of +sdk-dapp's Ledger transport strategies each bundle their own nested +`@ledgerhq/devices@8.16.0`, and that copy is missing `hid-framing.js` and two +`ble/*` files on disk (the separately hoisted top-level `@ledgerhq/devices@8.0.3` +has them). Left alone, this breaks `npm run dev` and hard-fails `npm run build`. +`vite.config.ts` works around it by externalizing the three specific deep paths in +both `optimizeDeps.exclude` and `build.rollupOptions.external`. This is an upstream +Ledger packaging bug, not a mistake in this config. The DeFi extension, +xPortal/WalletConnect, and web-wallet login paths this recipe demonstrates are +unaffected. +::: -Let's prepare the arguments file. The first argument is of type `Address` and the second one is of type `u32`, so our file looks like this: +## See also -```json -[ - { - "bech32": "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx" - }, - 1 -] -``` +- [Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) + is the App Router parallel; choose this if you need SSR. +- [Local HTTPS for dApp dev](/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup) + is the mkcert plus Vite plugin walkthrough for fully trusted certs. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the next thing you will do once login works. +- [Migrate v4 to v5](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration) + if you have an existing v4 Vite codebase. -As above, we encapsulate the address in a dictionary and the `u32` value is simply a number. We'll refer to this file as `query_multisig_arguments.json`. +--- -After preparing the file, we can run the following command: +### Multi-token transfer in one tx -```sh -mxpy contract query erd1qqqqqqqqqqqqqpgqjsg84gq5e79rrc2rm5ervval3jrrfvvfd8sswc6xjy \ - --proxy https://devnet-gateway.multiversx.com \ - --function signed \ - --abi ~/contracts/multisig/output/multisig.abi.json \ - --arguments-file query_multisig_arguments.json -``` +Send native EGLD plus two (or more) ESDT/NFT/SFT tokens in a **single** +transaction, via sdk-core's `TransfersController.createTransactionForTransfer`. +Sibling recipe to +[Send an ESDT](/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt) (one +fungible token) and +[Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld) +(native only), this one combines native EGLD with 2+ token transfers in the exact +same call. -## Upgrading a Smart Contract +If instead you are building a browser dApp where the end user's own wallet should +sign, see +[Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send). +This recipe is for when **your own code holds the private key**. -In case there's a new release of your Smart Contract, or perhaps you've patched a possible vulnerability you can upgrade the code of the Smart Contract deployed on the network. +## Prerequisites -We've modified our adder contract to add `1` to every value added to the contract. Now every time the `add()` function is called will add the value provided with `1`. In order to do that we access the source code and navigate to the `add()` endpoint. We can see that `value` is added to `sum` each time the endpoint is called. Modify the line to look something like this `self.sum().update(|sum| *sum += value + 1u32);` +- Node.js >= 20.13.1. +- A devnet wallet with some devnet EGLD (for gas), see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites for how to generate a throwaway one. -Before deploying the contract we need to build it again to make sure we are using the latest version. We then deploy the newly built contract, then we call it and query it. +## Install -First we build the contract: +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/multi-token-transfer +npm install +npm run build +npm start -- ./wallet.pem erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th +``` + +`TEST-abcdef` and `TESTNFT-abcdef` are illustrative placeholder identifiers, same +convention as +[Send an ESDT](/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt)'s +`TEST-abcdef` example. + +## Sending + +```ts title="src/multiTransfer.ts" +// src/multiTransfer.ts — the actual subject of this recipe: sending +// native EGLD plus two (or more) ESDT/NFT/SFT tokens in a SINGLE +// transaction, via `TransfersController.createTransactionForTransfer`. +// +// Confirmed from node_modules/@multiversx/sdk-core/out/transfers/resources.d.ts: +// +// export declare type CreateTransferTransactionInput = { +// receiver: Address; +// nativeAmount?: bigint; +// tokenTransfers?: TokenTransfer[]; +// data?: Uint8Array; +// }; +// +// Unlike the ESDT-transfer casing trap this Cookbook's `send-esdt` recipe +// flags (`createTransactionForEsdtTokenTransfer` vs +// `createTransactionForESDTTokenTransfer`), `createTransactionForTransfer` +// is spelled identically on both `TransfersController` and +// `TransferTransactionsFactory` — no casing trap here, confirmed by +// reading both .d.ts files. +// +// Sibling recipe to `send-esdt` (a single fungible token) and `send-egld` +// (native only) — this one combines native EGLD with 2+ token transfers +// in the exact same call. + +import { Address, Token, TokenTransfer } from '@multiversx/sdk-core'; +import type { Account, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export interface TokenTransferInput { + /** Token identifier, e.g. "TICKER-abcdef" (fungible) or "TICKER-abcdef-0a" broken apart into identifier + nonce. */ + identifier: string; + /** 0 for a fungible token; > 0 for an NFT/SFT. */ + nonce: bigint; + /** Amount in the token's own smallest denomination. */ + amount: bigint; +} + +export interface MultiTransferInput { + receiver: string; + /** Optional native EGLD to send alongside the token transfers, in smallest denomination. */ + nativeAmountInSmallestDenomination?: bigint; + tokenTransfers: TokenTransferInput[]; +} + +export interface MultiTransferOutput { + txHash: string; +} + +/** + * Sends native EGLD (optional) plus 2+ token transfers in a single + * transaction. `sender.nonce` must already reflect the network's current + * nonce — see src/account.ts's loadDevnetAccount. + */ +export async function sendMultiTokenTransfer( + entrypoint: DevnetEntrypoint, + sender: Account, + input: MultiTransferInput, +): Promise { + const controller = entrypoint.createTransfersController(); + + const tokenTransfers = input.tokenTransfers.map( + (t) => + new TokenTransfer({ + token: new Token({ identifier: t.identifier, nonce: t.nonce }), + amount: t.amount, + }), + ); -```sh -sc-meta all build + const transaction = await controller.createTransactionForTransfer( + sender, + sender.getNonceThenIncrement(), + { + receiver: Address.newFromBech32(input.receiver), + ...(input.nativeAmountInSmallestDenomination !== undefined + ? { nativeAmount: input.nativeAmountInSmallestDenomination } + : {}), + tokenTransfers, + }, + ); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash }; +} ``` -Then we upgrade the contract by running: +## Run it -```sh -mxpy contract upgrade erd1qqqqqqqqqqqqqpgq3zrpqj3sulnc9xq95sljetxhf9s07pqtd8ssfkxjv4 \ - --bytecode ~/contracts/adder/output/adder.wasm \ - --proxy=https://devnet-gateway.multiversx.com --chain D \ - --arguments 0 --gas-limit 5000000 \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --send +```bash +npm start -- ``` -We provide as a positional argument the contract's address that we want to upgrade, in our case the previously deployed adder contract. The `--bytecode` is used to provide the new code that will replace the old code. We also set the `--arguments` to `0` as we didn't change the constructor and the contract will start counting from `0` again. The rest of the arguments you know from all the previous operations we've done. +Expected output: -As shown above, we can also upgrade the contract by providing the ABI file and the arguments file: +```text +Sender: erd1... (nonce 0) +Receiver: erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th +Sending in ONE transaction: + - 0.001 EGLD (native) + - 2.5 TEST-abcdef (fungible, 6 decimals) + - 1 TESTNFT-abcdef-01 (NFT, nonce 1) + +Sent. Transaction hash: <64-char hex hash> +``` + +## How it works + +**`createTransactionForTransfer` is spelled identically on both API levels**, +confirmed from `resources.d.ts` and both +`TransfersController`/`TransferTransactionsFactory` `.d.ts` files. Unlike the +ESDT-transfer casing trap that `send-esdt` documents, there is no +Controller-vs-Factory naming mismatch here. + +**Two genuinely surprising, hand-verified facts about the transaction this +produces:** + +1. **The transaction's outer `receiver` is the SENDER'S OWN address, not the real + destination.** A request built for receiver `erd1qyu5...` logged + `"receiver":"erd1ayggn..."`, the sender's own address. The real destination is + instead the **first argument** inside `data`, hex-encoded, decoding it back to + bech32 gives exactly the intended receiver. If you inspect a built + transaction's `.receiver` or `.value` directly expecting the real recipient or + EGLD amount, you will be looking at the wrong fields for this transaction type. +2. **Combining `nativeAmount` with `tokenTransfers` folds the EGLD into a THIRD + synthetic token-transfer entry**, using the special identifier `EGLD-000000`, + not carried in `value` (which is `"0"` here). 2 real token transfers plus 1 + native amount produced a decoded data field starting + `MultiESDTNFTTransfer@@03@...`, count `03`, not `02`. + +**The full decoded payload, field by field** (from a real run of this recipe): -```sh -mxpy contract upgrade erd1qqqqqqqqqqqqqpgq3zrpqj3sulnc9xq95sljetxhf9s07pqtd8ssfkxjv4 \ - --bytecode ~/contracts/adder/output/adder.wasm \ - --proxy=https://devnet-gateway.multiversx.com --chain D \ - --gas-limit 5000000 \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --abi=~/contracts/multisig/output/multisig.abi.json, - --arguments-file=upgrade_arguments.json - --send -``` +```text +MultiESDTNFTTransfer +@0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1 (destination address) +@03 (3 transfers: 2 real tokens + 1 synthetic EGLD) +@544553542d616263646566 ("TEST-abcdef") +@ (nonce 0 — fungible, encoded as an EMPTY argument) +@2625a0 (2,500,000 = 2.5 units at 6 decimals) +@544553544e46542d616263646566 ("TESTNFT-abcdef") +@01 (nonce 1 — NFT) +@01 (amount 1) +@45474c442d303030303030 ("EGLD-000000" — the synthetic native-EGLD entry) +@ (nonce 0, empty again) +@038d7ea4c68000 (1,000,000,000,000,000 = 0.001 EGLD) +``` + +`2625a0` = 2,500,000 matches the same hex `send-esdt` independently verified for +"2.5 units of a 6-decimal token", confirming consistent encoding across both +recipes. + +## Pitfalls + +:::danger[Pitfall 1: do not read tx.receiver or tx.value expecting the real destination/amount] +Both point at the sender's own address / zero, by protocol design, for this +transaction shape. The real information is encoded in `tx.data`. +::: -Now let's add `5` to the contract one more time. We do so by running the following: +:::warning[Pitfall 2: a zero nonce encodes as an empty argument, not a literal 00 byte] +This recipe's own decoded output shows an empty string between the two `@` +separators for both zero-nonce entries, worth knowing if you are hand-parsing a +`MultiESDTNFTTransfer` payload instead of trusting the SDK's decoder. +::: -```sh -mxpy contract call erd1qqqqqqqqqqqqqpgq3zrpqj3sulnc9xq95sljetxhf9s07pqtd8ssfkxjv4 \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --proxy=https://devnet-gateway.multiversx.com --chain D \ - --function add --arguments 5 --gas-limit 1000000 \ - --send -``` +:::note[Pitfall 3: this recipe deliberately never succeeds against a real balance or token holdings] +It proves the payload is well-formed via a clean "insufficient funds" rejection +(gas is always paid in EGLD, independent of which tokens move), not a confirmed +on-chain transfer. +::: -Now, if we query the contract we should see the value `6`. We added `5` in the contract but modified the contract code to add `1` to every value. Let's see! +## See also -```sh -mxpy contract query erd1qqqqqqqqqqqqqpgq3zrpqj3sulnc9xq95sljetxhf9s07pqtd8ssfkxjv4 --proxy https://devnet-gateway.multiversx.com --function getSum -``` +- [Send an ESDT](/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt) + is the single-token case this recipe generalizes. +- [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld) + is the native-only case. +- [Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) + covers the fetch-then-increment pattern this recipe relies on. -We see that we indeed got the value `6`. Our upgrade was successful. +--- +### Multi-Values -## Verifying a smart contract +## Single values vs. multi-values -Verifying a smart contract means ensuring that the contract deployed on the network matches a specific version of the original source code. That is done by an external service that, under the hood, performs a reproducible build of the given contract and compares the resulting bytecode with the one deployed on the network. +To recap, we have discussed about data being represented either in a: +- nested encoding, as part of the byte representation of a larger object; +- top encoding, the full byte representation of an object. -To learn more about reproducible builds, please follow [**this page**](/developers/reproducible-contract-builds). If you'd like to set up a Github Workflow that performs a reproducible build of your smart contract, follow the examples in [**this repository**](https://github.com/multiversx/mx-contracts-rs). +But even the top encoding only refers to a _single_ object, being represented as a _single_ array of bytes. This encoding, no matter how simple or complex, is the representation for a _single_ argument, result, log topic, log event, NFT attribute, etc. -The command used for verifying contracts is: +However, we sometimes want to work with _multiple_, variadic arguments, or an arbitrary number of results. An elegant solution is modelling them as special collections of top-encodable objects that each represent an individual item. For instance, we could have a list of separate arguments, of arbitrary length. -```sh -mxpy contract verify -``` +Multi-values work similarly to varargs in other languages, such as C, where you can write `void f(int arg, ...) { ... }`. In the smart contract framework they do not need specialized syntax, we use the type system to define their behavior. -Let's see an example: +:::info Note +In the framework, single values are treated as a special case of multi-value, one that consumes exactly one argument, or returns exactly one value. -```sh -export CONTRACT_ADDRESS="erd1qqqqqqqqqqqqqpgq6eynj8xra5v87qqzpjhc5fnzzh0fqqzld8ssqrez2g" +In effect, all serializable types implement the multi-value traits. +::: -mxpy --verbose contract verify ${CONTRACT_ADDRESS} \ - --packaged-src=adder-0.0.0.source.json \ - --verifier-url="https://devnet-play-api.multiversx.com" \ - --docker-image="multiversx/sdk-rust-contract-builder:v8.0.1" \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem -``` -:::info -The account that triggers the code verification process must be the owner of the contract. -::: +## Parsing and limitations -:::info -The _packaged source_ passed as `--packaged-src` can be obtained either from [the Github Workflows for reproducible builds](https://github.com/multiversx/mx-contracts-rs/tree/main/.github/workflows) set up on your own repository, or from locally invoking a reproducible build, as depicted [here](https://docs.multiversx.com/developers/reproducible-contract-builds/#reproducible-build-using-mxpy). -::: +It is important to understand that arguments get read one by one from left to right, so there are some limitations as to how var-args can be positioned. Argument types also define how the arguments are consumed, so, for instance, if a type specifies that all remaining arguments will be consumed, it doesn't really make sense to have any other argument after that. +For instance, let's consider the behavior of `MultiValueEncoded`, which consumes all subsequent arguments. Hence, it's advisable to place it as the last argument in the function, like so: -## Creating and sending transactions +```rust +#[endpoint(myEndpoint)] +fn my_endpoint(&self, first_arg: ManagedBuffer, second_arg: TokenIdentifier, last_arg: MultiValueEncoded) +``` +Placing any argument after `MultiValueEncoded` will not initialize that argument, because `MultiValueEncoded` will consume all arguments following it. An important rule to remember is that an endpoint can have only one `MultiValueEncoded` argument, and it should always occupy the last position in order to achieve the desired outcome. -To create a new transaction we use the `mxpy tx new` command. Let's see how that works: +Another scenario to consider involves the use of multiple `Option` arguments. Take, for instance, the following endpoint: -```sh -mxpy tx new --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --receiver erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx \ - --gas-limit 50000 --value 1000000000000000000 \ - --proxy https://devnet-gateway.multiversx.com --chain D \ - --send +```rust +#[endpoint(myOptionalEndpoint)] +fn my_optional_endpoint(&self, first_arg: OptionalValue, second_arg: OptionalValue) ``` +In this context, both arguments (or none) should be provided at the same time in order to get the desired effect. Since arguments are processed sequentially from left to right, supplying a single value will automatically assign it to the first argument, making it impossible to determine which argument should receive that value. -That's it! As easy as that. We sent a transaction from Alice to Bob. We choose the receiver of our transaction using the `--receiver` argument and set the gas limit to `50000` because that is the gas cost of a simple move balance transaction. Notice we used the `--value` argument to pass the value that we want to transfer but we passed in the denomintated value. We transferred 1 eGLD (1 \* 10^18). We then specify the proxy and the chain ID for the network we want to send our transaction to and use the `--send` argument to broadcast it. +The same rule applies when any regular argument is placed after a var-arg, thus, a strong restriction regarding arguments' order has been enforced. Regular arguments `must not` be placed after var-args. -In case you want to save the transaction you can also provide the `--outfile` argument and a `json` file containing the transaction will be saved at the specified location. If you just want to prepare the transaction without broadcasting it simply remove the `--send` argument. +To further enhance clarity and minimize potential errors related to var-args, starting from framework version `v0.44.0`, it is no longer allowed by default to have multiple var-args. This restriction can be lifted by using the #[allow_multiple_var_args] annotation. +:::info Note +`#[allow_multiple_var_args]` is required when using more than one var-arg in an endpoint and is placed at the endpoint level, alongside the `#[endpoint]` annotation. Utilizing `#[allow_multiple_var_args]` in any other manner will not work. -## Guarded transactions +Considering this, our optional endpoint from the example before becomes: +```rust +#[allow_multiple_var_args] +#[endpoint(myOptionalEndpoint)] +fn my_optional_endpoint(&self, first_arg: OptionalValue, second_arg: OptionalValue) +``` +::: -If your address is guarded, you'll have to provide some additional arguments because your transaction needs to be co-signed. +The absence of #[allow_multiple_var_args] as an endpoint attribute, along with the use of multiple var-args and/or the placement of regular arguments after var-args, leads to build failure, as the parsing validations now consider the count and positions of var-args. -The first extra argument we'll need is the `--guardian` argument. This specifies the guardian address of our address. Then, if our account is guarded by a service like our trusted co-signer service we have to provide the `--guardian-service-url` which specifies where the transaction is sent to be co-signed. +However, when `#[allow_multiple_var_args]` is used, there is no other parsing validation (except the ones from above) to enforce the var-args rules mentioned before. In simpler terms, using the annotation implies that the developer is assuming responsibility for handling multiple var-args and anticipating the outcomes, effectively placing trust in their ability to manage the situation. -Keep in mind that **mxpy** always calls the `/sign-transaction` endpoint of the `--guardian-service-url` you have provided. Another argument we'll need is `--guardian-2fa-code` which is the code generated by an external authenticator. -Each guarded transaction needs an additional `50000` gas for the `gasLimit`. The `version` field needs to be set to `2`. The `options` field needs to have the second least significant bit set to "1". +## Standard multi-values -:::note -Here are the urls to our hosted co-signer services: +These are the common multi-values provided by the framework: +- Straightforward var-args, an arbitrary number of arguments of the same type. + - Can be defined in code as: + - `MultiValueVec` - the unmanaged version. To be used outside of contracts. + - `MultiValueManagedVec` - the equivalent managed version. The only limitation is that `T` must implement `ManagedVecItem`, because we are working with an underlying `ManagedVec`. Values are deserialized eagerly, the endpoint receives them already prepared. + - `MultiValueEncoded` - the lazy version of the above. Arguments are only deserialized when iterating over this structure. This means that `T` does not need to implement `ManagedVecItem`, since it never gets stored in a `ManagedVec`. + - In all these 3 cases, `T` can be any serializable type, either single or multi-value. + - Such a var-arg will always consume all the remaining arguments, so it doesn't make sense to place any other arguments after it, single or multi-value. The framework doesn't forbid it, but single values will crash at runtime, since they always need a value, and multi-values will always be empty. +- Multi-value tuples. + - Defined as `MultiValueN`, where N is a number between 2 and 16, and `T1`, `T2`, ..., `TN` can be any serializable types, either single or multi-value; e.g. `MultiValue3`. + - It doesn't make much sense to use them as arguments on their own (it is easier and equivalent to just have separate named arguments), but they do have the following uses: + - They can be embedded in a regular var-arg to obtain groups of arguments. For example `MultiValueVec>` defines pairs of numbers. There is no more need to check in code that an even number of arguments was passed, the deserializer resolves this on its own. + - Rust does not allow returning more than one result, but by returning a multi-value tuple we can have an endpoint return several values, of different types. +- Optional arguments. + - Defined as `OptionalValue`, where `T` can be any serializable type, either single or multi-value. + - At most one argument will be consumed. For this reason it sometimes makes sense to have several optional arguments one after the other, or optional arguments followed by var-args. + - Do note, however, that an optional argument cannot be missing if there is anything else coming after it. For example if an endpoint has arguments `a: OptionalValue, b: OptionalValue`, `b` can be missing, or both can be missing, but there is no way to have `a` missing and `b` to be there, because passing any argument will automatically get it assigned to `a`. +- Counted var-args. + - Suppose we actually do want two sets of var-args in an endpoint. One solution would be to explicitly state how many arguments each of them contain (or at least the first one). Counted var-args make this simple. + - Defined as `MultiValueManagedVecCounted`, where `T` can be any serializable type, either single or multi-value. + - Always takes a number argument first, which represents how many arguments follow. Then it consumes exactly that many arguments. + - Can be followed by other arguments, single or multi-value. +- Async call result. + - Asynchronous call callbacks also need to know whether or not the call failed, so they have a special format for transmitting arguments. The first argument is always the error code. If the error code is `0`, then the call result follows. Otherwise, we get one additional argument, which is the error message. To easily deserialize this, we use a special type. + - Defined as `ManagedAsyncCallResult`. + - There is also an unmanaged version, `AsyncCallResult`, but it is no longer used nowadays. + - They are both enums, the managed part only refers to the error message. +- Ignored arguments. + - Sometimes, for backwards compatibility or other reasons it can happen to have (optional) arguments that are never used and not of interest. To avoid any useless deserialization, it is possible to define an argument of type `IgnoreValue` at the end. + - By doing so, any number of arguments are allowed at the end, all of which will be completely ignored. -- Mainnet: [https://tools.multiversx.com/guardian](https://tools.multiversx.com/guardian) -- Devnet: [https://devnet-tools.multiversx.com/guardian](https://devnet-tools.multiversx.com/guardian) -- Testnet: [https://testnet-tools.multiversx.com/guardian](https://testnet-tools.multiversx.com/guardian) +So, to recap: -::: +| Managed Version | Unmanaged version | What it represents | Similar single value | +| ---------------------------------- | -------------------- | ------------------------------- | ------------------------------ | +| `MultiValueN` | | A fixed number of arguments | Tuple `(T1, T2, ..., TN)` | +| `OptionalValue` | | An optional argument | `Option` | +| `MultiValueEncoded` | `MultiValueVec` | Variable number of arguments | `Vec` | +| `MultiValueManagedVec` | `MultiValueVec` | Variable number of arguments | `Vec` | +| `MultiValueManagedVecCounted` | | Counted number of arguments | `(usize, Vec)` | +| `ManagedAsyncCallResult` | `AsyncCallResult` | Async call result in callback | `Result` | +| `IgnoreValue` | | Any number of ignored arguments | Unit `()` | -```sh -mxpy tx new --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --receiver erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx \ - --gas-limit 200000 --value 1000000000000000000 \ - --proxy https://devnet-gateway.multiversx.com --chain D \ - --guardian erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8 \ - --guardian-service-url https://devnet-tools.multiversx.com/guardian \ - --guardian-2fa-code 123456 --version 2 --options 2 - --send -``` -If your address is guarded by another wallet, you'll still need to provide the `--guardian` argument and the guardian's wallet that will co-sign the transaction, but you don't need to provide the 2fa code and the service url. You can provide the guardian's wallet using one of the following arguments: `--guardian-pem`, `--guardian-keyfile`, or `--guardian-ledger`. +## Storage mapper contents as multi-values +The storage mapper declaration is a method that can normally also be made into a public view endpoint. If so, when calling them, the entire contents of the mapper will be read from storage and serialized as multi-value. Only recommended when there is little data, or in tests. -## Relayed transactions V3 +These storage mappers are, in no particular order: +- BiDiMapper +- LinkedListMapper +- MapMapper +- QueueMapper +- SetMapper +- SingleValueMapper +- UniqueIdMapper +- UnorderedSetMapper +- UserMapper +- VecMapper +- FungibleTokenMapper +- NonFungibleTokenMapper -Relayed transactions are transactions with the fee paid by a so-called relayer. In other words, if a relayer is willing to pay for a transaction, it is not mandatory for the sender to have any EGLD for fees. To learn more about relayed transactions check out [this page](/developers/relayed-transactions/). -In this section we'll see how we can send `Relayed V3` transactions using `mxpy`. For a more detailed look on `Relayed V3` transactions, take a look [here](/developers/relayed-transactions/#relayed-transactions-version-3). For these kind of transactions two new transaction fields were introduced, `relayer` and `relayerSignature`. In this example we'll see how we can create the relayed transaction. +## Multi-values in action -For this, a new command `mxpy tx relay` has been added. The command can be used to relay a previously signed transaction. The saved transaction can be loaded from a file using the `--infile` argument. +To clarify the way multi-values work in real life, let's provide some examples of how one would go avout calling an endpoint with variadic arguments. -There are two options when creating the relayed transaction: -1. Create the relayed transaction separately. (Sender signature and relayer signature are added by different entities.) -2. Create the complete relayed transaction. (Both signatures are added by the same entity.) -### Creating the inner transaction +### Option vs. OptionalValue -The inner transaction is any regular transaction, with the following notes: -- relayer address must be added -- extra 50000 (base cost) gas must be added for the relayed operation. For more details on how the gas is computed, check out [this page](/developers/relayed-transactions/#relayed-transactions-version-3). +Assume we want to have an endpoint that takes a token identifier, and, optionally, a token nonce. There are two ways of doing this: -This can be generated through `mxpy tx new` command. A new argument `--relayer` has been added for this feature. +```rust +#[endpoint(myOptArgEndpoint1)] +fn my_opt_arg_endpoint_1(&self, token_id: TokenIdentifier, opt_nonce: Option) {} +``` -```sh -mxpy tx new --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --receiver erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx \ - --gas-limit 100000 --value 1000000000000000000 \ - --proxy https://devnet-gateway.multiversx.com --chain D \ - --relayer erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8 \ - --outfile inner_tx.json +```rust +#[endpoint(myOptArgEndpoint2)] +fn my_opt_arg_endpoint_2(&self, token_id: TokenIdentifier, opt_nonce: OptionalValue) {} ``` -After creating the inner transaction, we are ready to create the relayed transaction. +We want to call these endpoints with arguments: `TOKEN-123456` (`0x544f4b454e2d313233343536`) and `5`. To contrast for the two endpoints: +- Endpoint 1: `myOptArgEndpoint1@544f4b454e2d313233343536@010000000000000005` +- Endpoint 2: `myOptArgEndpoint2@544f4b454e2d313233343536@05` -### Creating the relayed transaction +:::info Note +In the first case, we are dealing with an [Option](/developers/data/composite-values#options), whose first encoded byte needs to be `0x01`, to signal `Some`. In the second case there is no need for `Option`, `Some` is signalled simply by the fact that the argument was provided. -We can create the relayed transaction by running the following command: +Also note that the nonce itself is nested-encoded in the first case (being _nested_ in an `Option`), whereas in the second case it can be top-encoded directly. +::: -```sh -mxpy tx relay --relayer-pem ~/multiversx-sdk/testwallets/latest/users/carol.pem \ - --proxy https://devnet-gateway.multiversx.com --chain D \ - --infile inner_tx.json \ - --send -``` +Now let's do the same exercise for the case where we want to omit the nonce altogether: +- Endpoint 1: + - `myOptArgEndpoint1@544f4b454e2d313233343536@`, or + - `myOptArgEndpoint1@544f4b454e2d313233343536@00` - also accepted +- Endpoint 2: + - `myOptArgEndpoint2@544f4b454e2d313233343536` -### Creating the relayed transaction in one step +:::info Note +The difference is less striking in this case. -This can be done through `mxpy tx new` command, as follows: -```sh -mxpy tx new --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --receiver erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx \ - --relayer erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8 \ - --relayer-pem ~/multiversx-sdk/testwallets/latest/users/carol.pem \ - --gas-limit 100000 --value 1000000000000000000 \ - --proxy https://devnet-gateway.multiversx.com --chain D \ - --send -``` +In the first case, we encoded `None` as an empty byte array (encoding it as `0x00` is also accepted). In any case, we do need to pass it as an explicit argument. +In the second case, the last argument is omitted altogether. +::: -## Using the Ledger hardware wallet +We also want to point out that the multi-value implementation is more efficient in terms of gas. It is more easier for the smart contract to count the number of arguments and top-decode, than parse a composite type, like `Option`. -You can sign any transaction (regular transfers, smart contract deployments and calls) using a Ledger hardware wallet by leveraging the `--ledger` command-line argument. -First, connect your device to the computer, unlock it and open the MultiversX Ledger app. +### ManagedVec vs. MultiValueEncoded -Then, you can perform a trivial connectivity check by running: +In this example, let's assume we want to receive any number of triples of the form (token ID, nonce, amount). This can be implemented in two ways: -```sh -mxpy ledger version +```rust +#[endpoint(myVarArgsEndpoint1)] +fn my_var_args_endpoint_1(&self, args: ManagedVec<(TokenIdentifier, u64, BigUint)>) {} ``` -The output should look like this: - -```sh -MultiversX App version: ... +```rust +#[endpoint(myVarArgsEndpoint2)] +fn my_var_args_endpoint_2(&self, args: MultiValueManagedVec) {} ``` -Another trivial check is to ask the device for the (first 10) MultiversX addresses it manages: +The first approach seems a little simpler from the perspective of the smart contract implementation, since we only have a `ManagedVec` of tuples. But when we try to encode this argument, to call the endpoint, we are struck with a format that is quite devastating, both for performance and usability. -```sh -mxpy ledger addresses -``` +Let's call these endpoints with triples: `(TOKEN-123456, 5, 100)` and `(TOKEN-123456, 10, 500)`. The call data would have to look like this: +`myVarArgsEndpoint1@0000000c_544f4b454e2d313233343536_0000000000000005_00000001_64_0000000c_544f4b454e2d313233343536_000000000000000a_00000002_01f4`. -The output should look like this: +:::info Note +Above, we've separated the parts with `_` for readability purposes only. On the real blockchain, there would be no underscores, everything would be concatenated. -```sh -account index = 0 | address index = 0 | address: erd1... -account index = 0 | address index = 1 | address: erd1... -account index = 0 | address index = 2 | address: erd1... -... -``` +Every single value in this call data needs to be nested-encoded. We need to lay out the length or each token identifier, nonces are spelled out in full 8 bytes, and we also need the length of each `BigUint` value. +::: -Now let's sign and broadcast a transaction (EGLD transfer): +As you can see, that endpoint is very hard to work with. All arguments are concatenated into one big chunk, and every single value needs to be nested-encoded. This is why we need to lay out the length for each `TokenIdentifier` (e.g. the 0000000c in front, which is length 12) as well as for each `BigUint` (e.g. the `00000001` before `64`). The nonces are spelled out in their full 8 bytes. -```sh -mxpy tx new --proxy https://devnet-gateway.multiversx.com \ - --receiver erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th \ - --gas-limit 50000 --value 1000000000000000000 \ - --ledger \ - --send -``` +The second endpoint is a lot easier to use. For the same arguments, the call data looks like this: +`myVarArgsEndpoint2@544f4b454e2d313233343536@05@64@544f4b454e2d313233343536@0a@01f4`. -By default, the first MultiversX address managed by the device is used as the sender (signer) of the transaction. In order to select a different address, you can use the `--ledger-address-index` CLI parameter: +It is a lot more readable, for several reasons: +- We have 6 arguments instead of 1; +- The argument separator makes it much easier for both us and the smart contract to distinguish where each value ends and where the next one begins; +- All values are top-encoded, so there is no more need for lengths; the nonces can be expressed in a more compact form. -```sh -mxpy tx new --proxy https://devnet-gateway.multiversx.com \ - --receiver erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th \ - --gas-limit 50000 --value 1000000000000000000 \ - --ledger --ledger-address-index=42 \ - --send -``` +Once again, the multi-value implementation is more efficient in terms of gas. All the contract needs to do is to make sure that the number of arguments is a multiple of 3, and then top-decode each value. Conversely, in the first example, a lot more memory needs to be moved around when splitting the large argument into pieces. -:::info -For MultiversX, **the account index should always be `0`**, while the address index is allowed to vary. Therefore, you should not use the `--ledger-account-index` CLI parameter (it will be removed in a future release). -::: -Now let's deploy a smart contract using the Ledger: +## Implementation details -```sh -mxpy contract deploy --proxy=https://devnet-gateway.multiversx.com \ - --bytecode=adder.wasm --gas-limit=5000000 \ - --ledger --ledger-address-index=42 \ - --send -``` +All serializable types will implement traits `TopEncodeMulti` and `TopDecodeMulti`. -Then, perform a contract call: +The components that do argument parsing, returning results, or handling of event logs all work with these two traits. -```sh -mxpy contract call erd1qqqqqqqqqqqqqpgqwwef37kmegph97egvvrxh3nccx7xuygez8ns682zz0 \ - --proxy=https://devnet-gateway.multiversx.com \ - --function add --arguments 42 --gas-limit 5000000 \ - --ledger --ledger-address-index=42 \ - --send +All serializable types (the ones that implement `TopEncode`) are explicitly declared to also be multi-value in this declaration: + +```rust +/// All single top encode types also work as multi-value encode types. +impl TopEncodeMulti for T +where + T: TopEncode, +{ + fn multi_encode_or_handle_err(&self, output: &mut O, h: H) -> Result<(), H::HandledErr> + where + O: TopEncodeMultiOutput, + H: EncodeErrorHandler, + { + output.push_single_value(self, h) + } +} ``` -:::note -As of October 2023, on Windows (or WSL), you might encounter some issues when trying to use Ledger in `mxpy`. -::: +To create a custom multi-value type, one needs to manually implement these two traits for the type. Unlike for single values, there is no [equivalent derive syntax](/developers/data/custom-types). -## Interacting with the Multisig Smart Contract +--- -As of `mxpy v11`, interacting with Multisig contracts has become a lot easier because a dedicated command group called `mxpy multisig` has been added. We can deploy a multisig contract, create proposals and query the contract. For a full list of all the possible actions, run the following command: +### MultiversX API -```sh -mxpy multisig -h -``` +## About MultiversX API -#### Deploying a multisig contract +`api.multiversx.com` is the public instance of MultiversX API and is a wrapper over `gateway.multiversx.com` that brings a robust caching mechanism, alongside Elasticsearch +historical queries support, tokens media support, delegation & staking data, and many others. -```sh -mxpy multisig deploy \ - --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --bytecode path/to/multisig.wasm \ - --abi path/to/multisig.abi.json - --quorum 2 - --board-members erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx - --proxy=https://devnet-gateway.multiversx.com \ - --gas-limit 100000000 \ - --send -``` -#### Creating a new proposal +## Public URLs -```sh -mxpy multisig add-board-member --contract erd1qqqqqqqqqqqqqpgq2ukrsg73nwgu3uz6sp8vequuyrhtv2akd8ssyrg7wj \ - --abi path/to/multisig.abi.json - --board-member erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8 \ - --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --proxy=https://devnet-gateway.multiversx.com \ - --gas-limit 10000000 \ - --send -``` +Mainnet: [https://api.multiversx.com](https://api.multiversx.com). -#### Querying the contract +Testnet: [https://testnet-api.multiversx.com](https://testnet-api.multiversx.com). -```sh -mxpy multisig get-proposers --contract erd1qqqqqqqqqqqqqpgq2ukrsg73nwgu3uz6sp8vequuyrhtv2akd8ssyrg7wj \ - --abi path/to/multisig.abi.json - --proxy=https://devnet-gateway.multiversx.com -``` +Devnet: [https://devnet-api.multiversx.com](https://devnet-api.multiversx.com). -## Interacting with the Governance Smart Contract -As of `mxpy v11`, mxpy allows for easier interaction with the Governance smart contract. We can create a new governance proposal, vote for a proposal and query the contract. For a full list of the available commands, run the following command: +## External Providers -```sh -mxpy governance -h -``` +**Blastapi** -#### Creating a new proposal +Mainnet: [https://multiversx-api.public.blastapi.io](https://multiversx-api.public.blastapi.io). -```sh -mxpy governance propose \ - --commit-hash 30118901102b0bef11d675f4327565ae5246eeb5 \ - --start-vote-epoch 1000 \ - --end-vote-epoch 1010 \ - --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --proxy=https://devnet-gateway.multiversx.com \ - --gas-limit 100000000 \ - --send -``` +Devnet: [https://multiversx-api-devnet.public.blastapi.io](https://multiversx-api-devnet.public.blastapi.io). -#### Voting for a proposal +Checkout information about [pricing](https://blastapi.io/pricing) and API [limitations per plan](https://docs.blastapi.io/blast-documentation/apis-documentation/core-api/multiversx). -```sh -mxpy governance vote \ - --proposal-nonce 1 \ - --vote yes \ - --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --proxy=https://devnet-gateway.multiversx.com \ - --gas-limit 100000000 \ - --send -``` +More details on how to get your private endpoint can be found [here](https://docs.blastapi.io/blast-documentation/tutorials-and-guides/using-blast-to-get-a-blockchain-endpoint-1). -#### Querying the contract +**Kepler** -```sh -mxpy governance get-proposal-info \ - --proposal-nonce 1 \ - --proxy=https://devnet-gateway.multiversx.com -``` +High-performance infrastructure layer purpose-built for the MultiversX ecosystem. +Affordable plans, high limits for API, Gateway, Elastic Search, Event Notifier, and many more! -## Token Management Operations +[https://kepler.projectx.mx](https://kepler.projectx.mx) -User can now perform token management operations, such as issuing fungible tokens, issuing semi-fungible tokens, creating NFTs and more, directly via `mxpy`. For a full list of available commands type: -```sh -mxpy token -h -``` +## Dependencies -#### Issue a fungible token -```sh -mxpy token issue-fungible \ - --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --token-name test --token-ticker TEST \ - --initial-supply 1000000000000 \ - --num-decimals 6 \ - --proxy=https://devnet-gateway.multiversx.com \ - --send -``` +### Core dependencies -#### Pause a token +For its basic functionality (without including caching or storage improvements), api.multiversx.com depends on the following external systems: -```sh -mxpy token pause \ - --token-identifier TEST-123456 \ - --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --proxy=https://devnet-gateway.multiversx.com \ - --send -``` +- `gateway`: also referred as Proxy, provides access to node information, such as network settings, account balance, sending transactions, etc + docs: [Proxy](/sdk-and-tools/proxy). +- `index`: a database that indexes data that can be queries, such as transactions, blocks, nfts, etc. + docs: [Elasticsearch](/sdk-and-tools/elastic-search). +- `delegation`: a microservice used to fetch providers list from the delegation API. Not currently open for public access. -#### Set special roles on fungible tokens -```sh -mxpy token set-special-role-fungible \ - --token-identifier TEST-123456 \ - --user erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx \ - --local-mint \ - --local-burn \ - --esdt-transfer-role \ - --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --proxy=https://devnet-gateway.multiversx.com \ - --send -``` +### Other dependencies ---- +It uses on the following internal systems: -### NestJS SDK +- redis: used to cache various data, for performance purposes +- rabbitmq: pub/sub for sending mainly NFT process information from the transaction processor instance to the queue worker instance -MultiversX NestJS Microservice Utilities +It depends on the following optional external systems: -**sdk-nestjs** contains a set of utilities commonly used in the MultiversX microservices ecosystem. +- events notifier rabbitmq: queue that pushes logs & events which are handled internally e.g. to trigger NFT media fetch +- data: provides EGLD price information for transactions +- xexchange: provides price information regarding various tokens listed on xExchange +- ipfs: ipfs gateway for fetching mainly NFT metadata & media files +- media: ipfs gateway which will be used as prefix for NFT media & metadata returned in the NFT details +- media internal: caching layer for ipfs data to fetch from a centralized system such as S3 for performance reasons +- AWS S3: used to process newly minted NFTs & uploads their thumbnails +- github: used to fetch provider profile & node information from github -| Package | Source code | Description | -|--------------------------------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------| -| [sdk-nestjs](https://www.npmjs.com/package/@multiversx/sdk-nestjs) | [Github](https://github.com/multiversx/mx-sdk-nestjs) | A set of utilities commonly used in the MultiversX Microservice ecosystem. | +It uses the following optional internal systems: -:::tip -When developing microservices, we recommend starting from the **microservice-template** as it integrates off-the-shelf features like: public & private endpoints, cache warmer, transactions processor, queue worker -::: +- mysql database: used to store mainly NFT media & metadata information +- mongo database: used to store mainly NFT media & metadata information -| Source code | Description | -|----------------------------------------------------------------------------|------------------------------------------------------------------------------------------| -| [microservice-template](https://github.com/multiversx/mx-template-service) | REST API facade template for microservices that interact with the MultiversX blockchain. | +## Ways to start MultiversX API -## Packages +An API instance can be started with the following behavior: -The following table contains the NPM packages that are included inside the NestJS SDK: +- public API: provides REST API for the consumers +- private API: used to report prometheus metrics & health checks +- transaction processor & completed: fetches real-time transactions & logs from the blockchain; takes action based on various events, such as clearing cache values and publishing events on a queue +- cache warmer: used to proactively fetch data & pushes it to cache, to improve performance & scalability +- elastic updater: used to attach various extra information to items in the elasticsearch, for not having to fetch associated data from other external systems when performing listing requests +- events notifier: perform various decisions based on incoming logs & events +- subscription: used to manage subscriptions, fetch and broadcast data to subscribers -| Package | NPM | Description + additional docs | -|-----------------------|------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------| -| sdk-nestjs-common | [@multiversx/sdk-nestjs-common](https://www.npmjs.com/package/@multiversx/sdk-nestjs-common) | Common functionalities to be used in MultiversX microsevices. | -| sdk-nestjs-auth | [@multiversx/sdk-nestjs-auth](https://www.npmjs.com/package/@multiversx/sdk-nestjs-auth) | Native Auth functionalities to be used for securily handle sessions. | -| sdk-nestjs-http | [@multiversx/sdk-nestjs-http](https://www.npmjs.com/package/@multiversx/sdk-nestjs-http) | HTTP requests handling utilities | -| sdk-nestjs-monitoring | [@multiversx/sdk-nestjs-monitoring](https://www.npmjs.com/package/@multiversx/sdk-nestjs-monitoring) | Microservices monitoring helpers. | -| sdk-nestjs-elastic | [@multiversx/sdk-nestjs-elastic](https://www.npmjs.com/package/@multiversx/sdk-nestjs-elastic) | Elasticsearch interactions helpers. | -| sdk-nestjs-redis | [@multiversx/sdk-nestjs-redis](https://www.npmjs.com/package/@multiversx/sdk-nestjs-redis) | Redis interactions helpers. | -| sdk-nestjs-rabbitmq | [@multiversx/sdk-nestjs-rabbitmq](https://www.npmjs.com/package/@multiversx/sdk-nestjs-rabbitmq) | RabbitMQ interactions helpers. | -| sdk-nestjs-cache | [@multiversx/sdk-nestjs-cache](https://www.npmjs.com/package/@multiversx/sdk-nestjs-cache) | Common cache operations utilities. [docs](/sdk-and-tools/sdk-nestjs/sdk-nestjs-cache) | +### Rate Limits + +Public MultiversX APIs utilize a protective rate-limiting mechanism to ensure network stability. The following limitations apply: + +#### HTTP Requests (REST API) +* **api.multiversx.com (_Mainnet_):** Maximum of **2 requests / IP / second**. +* **devnet-api.multiversx.com (_Devnet_):** Maximum of **5 requests / IP / second**. --- -### NestJS SDK Auth utilities +#### WebSocket Subscriptions +* **Subscription Limit:** A single client can create a maximum of **10 different subscription variants** (active parallel subscriptions). -NPM Version +## Rest API documentation -## MultiversX NestJS Microservice Native Authentication Utilities +Rest API documentation of `api.multiversx.com` can be found on the [Swagger docs](https://api.multiversx.com). -This package contains a set of utilities commonly used for authentication purposes in the MultiversX Microservice ecosystem. - The package relies on [@multiversx/sdk-native-auth-server](https://www.npmjs.com/package/@multiversx/sdk-native-auth-server) for validating access tokens signed by MultiversX wallets. +## WebSocket Subscription Documentation -## Installation +Real-time blockchain streaming is supported through the MultiversX WebSocket Subscription API. +A dedicated guide is available here [WebSocket Subscription Guide](./ws-subscriptions.md) - `sdk-nestjs-auth` is delivered via **npm** and it can be installed as follows: -```bash - npm install @multiversx/sdk-nestjs-auth - ``` +## References -## Obtaining a Native Auth token +- Github repository: [https://github.com/multiversx/mx-api-service](https://github.com/multiversx/mx-api-service) +- Swagger docs: [https://api.multiversx.com](https://api.multiversx.com) +- Raw JSON Swagger OpenAPI definitions: [https://api.multiversx.com/-json](https://api.multiversx.com/-json) +- MultiversX blog: [https://multiversx.com/blog/elrond-api-internet-scale-defi](https://multiversx.com/blog/elrond-api-internet-scale-defi) -This package validates a payload signed by a MultiversX wallet. You can use the MultiversX Utils website to get a token you can use for testing. +--- - ![image](https://github.com/multiversx/mx-sdk-nestjs/assets/6889483/374a4e3a-f7d3-4bd9-a212-a8615c89ab53) +### MultiversX API WebSocket -1. Navigate to [https://utils.multiversx.com/auth](https://utils.multiversx.com/auth) and choose the desired network from the select located in the upper right corner of the page -2. Click the **Generate** button -3. Select a wallet you prefer to use from the modal dialog, and give it access to the page -4. **Done!** You can now copy the token and use it as a Bearer token in your requests. +## MultiversX WebSocket Subscription API -To use it in your requests, you need to have an `Authorization` header with the value : - `Bearer `. +Starting with the release [v1.17.0](https://github.com/multiversx/mx-api-service/releases/tag/v1.17.0) we introduced WebSocket Subscription functionality. -You also need to add an `origin` header with the value `https://utils.multiversx.com`. -*Note: these steps are only needed while testing. In production, a frontend application will handle token generation* +It is useful for subscribing to new events in real-time, rather than performing polling (requesting latest events with a given refresh period). -## Utility +## Update Frequency and Stream Modes -The package provides a series of [NestJS Guards](https://docs.nestjs.com/guards) that can be used for easy authorization on endpoints in your application. - It also provides some [NestJS Decorators](https://docs.nestjs.com/custom-decorators) that expose the decoded information found in the access token. +The WebSocket API supports two primary modes of data consumption: **Pulse Stream** and **Filtered Stream**. -## Configuration +### 1. Pulse Stream (Snapshot & Loop) +Subscribers receive the most recent events for a specific timeframe at regular intervals defined by the API. +* **Behavior:** You receive an update every round (or configured interval). +* **Content:** Each update contains the latest events for the requested buffer (e.g., latest 25 blocks). +* **Duplicates:** Because of the repeating interval, **duplicate events may appear across batches**. It is the user’s responsibility to filter these duplicates. +* **Available Streams:** Transactions, Blocks, Pool, Events, Stats. -The authentication guards need 2 parameters on instantiation. - The fist parameter needs to be an instance of a class implementing the `ErdnestConfigService` interface. -The second one, needs to be an instance of a [Caching service](https://www.npmjs.com/package/@multiversx/sdk-nestjs-cache) +### 2. Filtered Stream (Custom Real-time Streams) +Subscribers receive events strictly as they occur on the blockchain, filtered by specific criteria. +* **Behavior:** You are notified immediately when a new event matches your filter. +* **Content:** Data flows in real-time from the moment of subscription. +* **Duplicates:** **No duplicate events are sent.** You receive each item exactly once. +* **Available Streams:** `Transactions`, `Events`, and `Transfers` are supported in this mode. -```typescript - import { Injectable } from "@nestjs/common"; - import { ApiConfigService } from "./api.config.service"; - import { ErdnestConfigService } from "@multiversx/sdk-nestjs-common"; +## Rest API models compatibility +The MultiversX WebSocket Subscription API provides real-time blockchain data identical in structure to REST API responses: - @Injectable() - export class SdkNestjsConfigServiceImpl implements ErdnestConfigService { - constructor( - private readonly apiConfigService: ApiConfigService, - ) { } - getSecurityAdmins(): string[] { - return this.apiConfigService.getSecurityAdmins(); - } +[https://api.multiversx.com](https://api.multiversx.com) +[https://devnet-api.multiversx.com](https://devnet-api.multiversx.com) +[https://testnet-api.multiversx.com](https://testnet-api.multiversx.com) - getJwtSecret(): string { - return this.apiConfigService.getJwtSecret(); - } - getApiUrl(): string { - return this.apiConfigService.getApiUrl(); - } +All updates mirror REST responses and include a `Count` field representing **the total number of existing items at the moment the update was delivered**. - getNativeAuthMaxExpirySeconds(): number { - return this.apiConfigService.getNativeAuthMaxExpirySeconds(); - } +## Selecting the WebSocket Endpoint - getNativeAuthAcceptedOrigins(): string[] { - return this.apiConfigService.getNativeAuthAcceptedOrigins(); - } - } - ``` +Before connecting, fetch the WebSocket cluster: -You can register it as a provider, and the DI mechanism of NestJS will handle instantiation for you. +### Mainnet +```text +https://api.multiversx.com/websocket/config +``` -```typescript - import { Module } from '@nestjs/common'; - import { ERDNEST_CONFIG_SERVICE } from "@multiversx/sdk-nestjs-common"; +### Testnet +```text +https://testnet-api.multiversx.com/websocket/config +``` - @Module({ - providers: [ - { - provide: ERDNEST_CONFIG_SERVICE, - useClass: SdkNestjsConfigServiceImpl, - }, - ], - }) - export class AppModule {} - ``` +### Devnet +```text +https://devnet-api.multiversx.com/websocket/config +``` -## Using the Auth Guards +### Response example +```json +{ + "url": "socket-api-xxxx.multiversx.com" +} +``` -NestJS guards can be controller-scoped, method-scoped, or global-scoped. Setting up a guard from the package is done through the `@UseGuards` decorator from the `@nestjs/common` package. +### WebSocket endpoint +```text +https:///ws/subscription +``` -### Native Auth Guard +--- -`NativeAuthGuard` performs validation of the block hash, verifies its validity, as well as origin verification on the access token. +## Subscription Events Overview -```typescript - import { NativeAuthGuard } from "@multiversx/sdk-nestjs-auth"; +| Stream Type | Stream Name | Subscribe Event | Update Event | Description | +|---|---|---|---|---| +| **Pulse** | Transactions | `subscribeTransactions` | `transactionUpdate` | Recurring latest buffer | +| **Pulse** | Blocks | `subscribeBlocks` | `blocksUpdate` | Recurring latest buffer | +| **Pulse** | Pool | `subscribePool` | `poolUpdate` | Recurring mempool dump | +| **Pulse** | Events | `subscribeEvents` | `eventsUpdate` | Recurring latest events | +| **Pulse** | Stats | `subscribeStats` | `statsUpdate` | Recurring chain stats | +| **Filtered** | Custom Txs | `subscribeCustomTransactions` | `customTransactionUpdate` | Real-time filtered Txs | +| **Filtered** | Custom Transfers | `subscribeCustomTransfers` | `customTransferUpdate` | Real-time filtered Transfers | +| **Filtered** | Custom Events| `subscribeCustomEvents` | `customEventUpdate` | Real-time filtered Events | - @Controller('projects') - @UseGuards(NativeAuthGuard) - export class ProjectsController { - // your methods... - } - ``` +--- -In the example above the `NativeAuthGuard` is controller-scoped. This means that all of the methods from `ProjectsController` will be protected by the guard. +## Pulse Stream Subscriptions -```typescript - import { NativeAuthGuard } from "@multiversx/sdk-nestjs-auth"; +**Note:** This mode pushes the latest buffer of data repeatedly. **Duplicate events may appear across batches**, and it is the user’s responsibility to filter or handle those duplicates on their side. - @Controller('projects') - export class ProjectsController { - @Get() - async getAll() { - return this.projectsService.getAll(); - } +### Transactions (Pulse) - @Post() - @UseGuards(NativeAuthGuard) - async createProject(@Body() createProjectDto: CreateProjectDto) { - return this.projectsService.create(createProjectDto); - } - } - ``` +#### Payload (DTO) -In this case, the guard is method-scoped. Only `createProject` benefits from the native auth checks. +| Field | Type | Required | +|-------------------------|----------------------------------------------------|----------| +| from | number | YES | +| size | number (1–50) | YES | +| status | `"success" \| "pending" \| "invalid" \| "fail"` | NO | +| order | `"asc" \| "desc"` | NO | +| isRelayed | boolean | NO | +| isScCall | boolean | NO | +| withScResults | boolean | NO | +| withRelayedScresults | boolean | NO | +| withOperations | boolean | NO | +| withLogs | boolean | NO | +| withScamInfo | boolean | NO | +| withUsername | boolean | NO | +| withBlockInfo | boolean | NO | +| withActionTransferValue | boolean | NO | +| fields | string[] | NO | -### Native Auth Admin Guard +#### Example usage - `NativeAuthAdminGuard` allows only specific addresses to be authenticated. The addresses are defined in the [config](#configuration) file and are passed to the guard via the ErdnestConfigService. +```js +import { io } from "socket.io-client"; -*This guard cannot be used by itself. It always has to be paired with a `NativeAuthGuard`* +async function main() { + const { url } = await fetch("https://api.multiversx.com/websocket/config") + .then((r) => r.json()); -```typescript - import { NativeAuthGuard, NativeAuthAdminGuard } from "@multiversx/sdk-nestjs-auth"; + const socket = io(`https://${url}`, { path: "/ws/subscription" }); - @Controller('admin') - @UseGuards(NativeAuthGuard, NativeAuthAdminGuard) - export class AdminController { - // your methods... - } - ``` + const payload = { from: 0, size: 25 }; -### JWT Authenticate Guard + socket.emit("subscribeTransactions", payload); - `JwtAuthenticateGuard` performs validation of a traditional [JSON web token](https://datatracker.ietf.org/doc/html/rfc7519). The usage is exactly the same as for the native auth guards. + socket.on("transactionUpdate", (data) => { + console.log("Transactions update:", data); + }); +} -```typescript - import { JwtAuthenticateGuard } from "@multiversx/sdk-nestjs-auth"; +main().catch(console.error); +``` - @Controller('users') - @UseGuards(JwtAuthenticateGuard) - export class UsersController { - // your methods... - } - ``` +#### Update Example +```json +{ + "transactions": [ + { + "txHash": "7f172e468e61210805815f33af8500d827aff36df6196cc96783c6d592a5fc76", + "sender": "erd1srdxd75cg7nkaxxy3llz4hmwqqkmcej0jelv8ults8m86g29aj3sxjkc45", + "receiver": "erd19waq9tlhj32ane9duhkv6jusm58ca5ylnthhg9h8fcumtp8srh4qrl3hjj", + "nonce": 211883, + "status": "pending", + "timestamp": 1763718888 + } + ], + "transactionsCount": 1234567 +} +``` -### JWT Admin Guard +--- - `JwtAdminGuard` relies on the same mechanism, only specific addresses can be authenticated. The addresses are defined in the [config](#configuration) file and are passed to the guard via the ErdnestConfigService. +### Blocks (Pulse) -*There is one caveat: when creating the JWT, the client must include an `address` field in the payload, before signing it.* +#### Payload (DTO) -```typescript - import { JwtAuthenticateGuard, JwtAdminGuard } from "@multiversx/sdk-nestjs-auth"; +| Field | Type | Required | +|-----------------------|---------------------|----------| +| from | number | YES | +| size | number (1–50) | YES | +| shard | number | NO | +| order | `"asc" \| "desc"` | NO | +| withProposerIdentity | boolean | NO | - @Controller('admin') - @UseGuards(JwtAuthenticateGuard, JwtAdminGuard) - export class AdminController { - // your methods... - } - ``` +#### Example usage -### Jwt Or Native Auth Guard +```js +import { io } from "socket.io-client"; - `JwtOrNativeAuthGuard` guard will authorize requests containing either JWT or a native auth access token. The package will first look for a valid JWT. If that fails, it will look for a valid native auth access token. +async function main() { + const { url } = await fetch("https://api.multiversx.com/websocket/config") + .then((r) => r.json()); -### Global-scoped guards + const socket = io(`https://${url}`, { path: "/ws/subscription" }); - ```typescript - const app = await NestFactory.create(AppModule); - app.useGlobalGuards(new NativeAuthGuard(new SdkNestjsConfigServiceImpl(apiConfigService), cachingService)); - ``` + const payload = { from: 0, size: 25 }; -## Using the Auth Decorators + socket.emit("subscribeBlocks", payload); -The package exposes 3 decorators : `NativeAuth`, `Jwt` and `NoAuth` + socket.on("blocksUpdate", (data) => { + console.log("Blocks update:", data); + }); +} -### NativeAuth Decorator +main().catch(console.error); +``` -The `NativeAuth` decorator accepts a single parameter. In can be one of the following values : +#### Update Example -- `issued` - block timestamp -- `expires` - expiration time -- `address` - address that signed the access token -- `origin` - URL of the page that generated the token -- `extraInfo` - optional arbitrary data +```json +{ + "blocks": [ + { + "hash": "8576bb346bc95680f1ab0eb1fb8c43bbd03ef6e6ac8fd24a3c6e85d4c81be16b", + "epoch": 1939, + "nonce": 27918028, + "round": 27933551, + "shard": 0, + "timestamp": 1763718906 + } + ], + "blocksCount": 111636242 +} +``` -Below is an example showing how to use the decorator to extract the signers address : +--- - ```typescript - import { NativeAuthGuard, NativeAuth } from "@multiversx/sdk-nestjs-auth"; - import { Controller, Get, UseGuards } from "@nestjs/common"; +### Pool (Pulse) - @Controller() - export class AuthController { - @Get("/auth") - @UseGuards(NativeAuthGuard) - authorize(@NativeAuth('address') address: string): string { - console.log(`Access granted for address ${address}`); - return address; - } - } - ``` +#### Payload (DTO) -### Jwt Decorator +| Field | Type | Required | +|--------|-----------------------------------------------------|----------| +| from | number | YES | +| size | number (1–50) | YES | +| type | `"Transaction" \| "SmartContractResult" \| "Reward"`| NO | -The `Jwt` decorator works just like `NativeAuth`. The fields accessible inside it are dependent on the client that created the token, and are out of scope for this documentation. +#### Example usage -### No Auth Decorator +```js +import { io } from "socket.io-client"; -The `NoAuth` decorator can be used to skip authorization on a specific method. This is useful when a guard is scoped globally or at the controller level. +async function main() { + const { url } = await fetch("https://api.multiversx.com/websocket/config") + .then((r) => r.json()); - ```typescript - import { NoAuth } from "@multiversx/sdk-nestjs-auth"; - import { Controller, Get, UseGuards } from "@nestjs/common"; + const socket = io(`https://${url}`, { path: "/ws/subscription" }); - @Controller() - export class PublicController { - @NoAuth() - @Get("/public-posts") - listPosts() { - // .... - } - } - ``` + const payload = { from: 0, size: 25, type: "Transaction" }; + + socket.emit("subscribePool", payload); + + socket.on("poolUpdate", (data) => { + console.log("Pool update:", data); + }); +} + +main().catch(console.error); +``` + +#### Update Example + +```json +{ + "pool": [ + { + "txHash": "0b0cd3932689c6853e50ccc0f49feeb9c5f2a68858cbd213fd0825dd4bc0632b", + "sender": "erd1jfwjg6tl87rhe73zmd5ygm8xmc9u3ys80mjvakdc7ca3kknr2kjq7s98h3", + "receiver": "erd1qqqqqqqqqqqqqpgq0dsmyccxtlkrjvv0czyv2p4kcy72xvt3nzgq8j2q3y", + "nonce": 1166, + "function": "claim", + "type": "Transaction" + } + ], + "poolCount": 1902 +} +``` --- -### NestJS SDK Cache utilities +### Events (Pulse) -NPM Version +#### Payload (DTO) +| Field | Type | Required | +|-------|---------------|----------| +| from | number | YES | +| size | number (1–50) | YES | +| shard | number | NO | -## MultiversX NestJS Microservice Cache Utilities +#### Example usage -This package contains a set of utilities commonly used for caching purposes in the MultiversX Microservice ecosystem. +```js +import { io } from "socket.io-client"; +async function main() { + const { url } = await fetch("https://api.multiversx.com/websocket/config") + .then((r) => r.json()); -## Installation + const socket = io(`https://${url}`, { path: "/ws/subscription" }); -`sdk-nestjs-cache` is delivered via **npm** and it can be installed as follows: + const payload = { from: 0, size: 25 }; -```bash -npm install @multiversx/sdk-nestjs-cache -``` + socket.emit("subscribeEvents", payload); + socket.on("eventsUpdate", (data) => { + console.log("Events update:", data); + }); +} -## Utility +main().catch(console.error); +``` -The package exports two services: an **in-memory cache service** and **remote cache service**. +#### Update Example +```json +{ + "events": [ + { + "txHash": "b5bde891df72e26fb36e7ab3acc14b74044bd9aa82b4852692f5b9a767e0391f-1-0", + "identifier": "signalError", + "address": "erd1jv5m4v3yr0wy6g2jtz2v344sfx572rw6aclum9c6r7rd4ej4l6csjej2wh", + "timestamp": 1763718864, + "topics": [ + "9329bab2241bdc4d21525894c8d6b049a9e50ddaee3fcd971a1f86dae655feb1", + "4865616c7468206e6f74206c6f7720656e6f75676820666f72206c69717569646174696f6e2e" + ], + "shardID": 1 + } + ], + "eventsCount": 109432 +} +``` -## Table of contents +--- -- [In Memory Cache](#in-memory-cache) - super fast in-memory caching system based on [LRU cache](https://www.npmjs.com/package/lru-cache) -- [Redis Cache](#redis-cache) - Caching system based on [Redis](https://www.npmjs.com/package/@multiversx/sdk-nestjs-redis) -- [Cache Service](#cache-service) - MultiversX caching system which combines in-memory and Redis cache, forming a two-layer caching system +### Stats (Pulse) +#### Payload (DTO) -## In memory cache +Stats subscription does not accept any payload. -In memory cache, available through `InMemoryCacheService`, is used to read and write data from and into the memory storage of your Node.js instance. +#### Example usage -*Note that if you have multiple instances of your application you must sync the local cache across all your instances.* +```js +import { io } from "socket.io-client"; -Let's take as an example a `ConfigService` that loads some non-crucial configuration from the database and can be cached for 10 seconds. +async function main() { + const { url } = await fetch("https://api.multiversx.com/websocket/config") + .then((r) => r.json()); -Usage example: + const socket = io(`https://${url}`, { path: "/ws/subscription" }); -```typescript -import { Injectable } from '@nestjs/common'; -import { InMemoryCacheService } from '@multiversx/sdk-nestjs-cache'; + socket.emit("subscribeStats"); -@Injectable() -export class ConfigService { - constructor( - private readonly inMemoryCacheService: InMemoryCacheService - ){} + socket.on("statsUpdate", (data) => { + console.log("Stats update:", data); + }); +} - async loadConfiguration(){ - return await this.inMemoryCacheService.getOrSet( - 'configurationKey', - () => this.getConfigurationFromDb(), - 10, // 10 seconds - ) - } +main().catch(console.error); +``` - private async getConfigurationFromDb(){ - // fetch configuration from db - } +#### Update Example + +```json +{ + "shards": 3, + "blocks": 111636242, + "accounts": 9126654, + "transactions": 569773975, + "scResults": 402596990, + "epoch": 1939, + "roundsPassed": 9478, + "roundsPerEpoch": 14400, + "refreshRate": 6000 } ``` -When the `.loadConfiguration()` method is called for the first time, the `.getConfigurationFromDb()` method will be executed and the value returned from it will be set in cache with `configurationKey` key. If another `.loadConfiguration()` call comes in 10 seconds interval, the data will be returned from cache and `.getConfigurationFromDb()` will not be executed again. +--- +## Filtered Stream Subscriptions (Custom Streams) -### In memory cache methods +**Note:** These streams provide real-time data (with a small delay after the actions are committed on-chain) for specific criteria. You must provide at least one filter criteria when subscribing. +### Custom Transactions (Filtered) -#### `.get(key: string): Promise` +Subscribes to transactions matching specific criteria (Sender, Receiver, or Function) as they happen. -- **Parameters:** - - `key`: The key of the item to retrieve from the cache. -- **Returns:** A `Promise` that resolves to the cached value or `undefined` if the key is not present. +#### Subscribe Event +`subscribeCustomTransactions` +#### Payload (DTO) -#### `.getMany(keys: string[]): Promise<(T | undefined)[]>` +| Field | Type | Required | Description | +|---|---|---|---| +| sender | string | NO* | Filter by sender address (bech32) | +| receiver | string | NO* | Filter by receiver address (bech32) | +| function | string | NO* | Filter by smart contract function name | -- **Parameters:** - - `keys`: An array of keys to retrieve from the cache. -- **Returns:** A `Promise` that resolves to an array of cached values corresponding to the provided keys. +*\*At least one field must be provided.* +#### Example usage -#### `.set(key: string, value: T, ttl: number, cacheNullable?: boolean): void` +```js +import { io } from "socket.io-client"; -- **Parameters:** - - `key`: The key under which to store the value. - - `value`: The value to store in the cache. - - `ttl`: Time-to-live for the cached item in seconds. - - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` +async function main() { + const { url } = await fetch("https://api.multiversx.com/websocket/config") + .then((r) => r.json()); + const socket = io(`https://${url}`, { path: "/ws/subscription" }); -#### `.setMany(keys: string[], values: T[], ttl: number, cacheNullable?: boolean): Promise` + // Subscribe to all transactions sent by a specific address + const payload = { + sender: "erd1..." + }; -- **Parameters:** - - `keys`: An array of keys under which to store the values. - - `values`: An array of values to store in the cache. - - `ttl`: Time-to-live for the cached items in seconds. - - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` + socket.emit("subscribeCustomTransactions", payload); + socket.on("customTransactionUpdate", (data) => { + // data.transactions: Transaction[] + // data.timestampMs: number + console.log("New Custom Transaction:", data); + }); +} +``` -#### `.delete(key: string): Promise` +#### Update Example -- **Parameters:** - - `key`: The key of the item to delete from the cache. -- **Returns:** A `Promise` that resolves when the item is successfully deleted. +```json +{ + "transactions": [ + { + "txHash": "7f172e468e61210805815f33af8500d827aff36df6196cc96783c6d592a5fc76", + "sender": "erd1srdxd75cg7nkaxxy3llz4hmwqqkmcej0jelv8ults8m86g29aj3sxjkc45", + "receiver": "erd19waq9tlhj32ane9duhkv6jusm58ca5ylnthhg9h8fcumtp8srh4qrl3hjj", + "nonce": 211883, + "status": "pending", + "timestamp": 1763718888 + } + ], + "timestampMs": 1763718888000 +} +``` +--- -#### `.getOrSet(key: string, createValueFunc: () => Promise, ttl: number, cacheNullable?: boolean): Promise` +### Custom Transfers (Filtered) -- **Parameters:** - - `key`: The key of the item to retrieve or create. - - `createValueFunc`: A function that creates the value if the key is not present in the cache. - - `ttl`: Time-to-live for the cached item in seconds. - - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` -- **Returns:** A `Promise` that resolves to the cached or newly created value. +Subscribes to the complete stream of actions associated with a specific Address or Token. This includes not only standard transactions but all blockchain operations: Transactions, Smart Contract Results (SCRs), Rewards, ... . It is the preferred mode for tracking the full real-time activity of an account or the global movement of a specific token. +#### Subscribe Event +`subscribeCustomTransfers` -#### `.setOrUpdate(key: string, createValueFunc: () => Promise, ttl: number, cacheNullable?: boolean): Promise` +#### Payload (DTO) -- **Parameters:** - - `key`: The key of the item to update or create. - - `createValueFunc`: A function that creates the new value for the key. - - `ttl`: Time-to-live for the cached item in seconds. - - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` -- **Returns:** A `Promise` that resolves to the updated or newly created value. +| Field | Type | Required | Description | +|---|---|---|---| +| sender | string | NO* | Filter by sender address (bech32) | +| receiver | string | NO* | Filter by receiver address (bech32) | +| relayer | string | NO* | Filter by the relayer address (for meta-transactions) | +| function | string | NO* | Filter by smart contract function name | +| token | string | NO* | Filter by Token Identifier (e.g., `USDC-c76f1f` or `EGLD` for native egld) | +| address | string | NO** | **Universal Filter:** Matches if the address is the Sender **OR** Receiver **OR** Relayer. | +*\*At least one field from the list above must be provided.* +*\*\*The `address` field cannot be combined with `sender`, `receiver`, or `relayer` in the same payload.* -## Redis Cache +#### Example usage -Redis cache, available through `RedisCacheService`, is a caching system build on top of Redis. It is used as a shared cache among multiple microservices. +**Scenario: Listen for specific Token transfers (e.g., USDC)** +Useful for tracking volume on a specific token. -Let's build the same config loader class but with data shared across multiple clusters using Redis. The implementation is almost identical since both `InMemoryCache` and `RedisCache` have similar class structure. +```js +import { io } from "socket.io-client"; -Usage example: +async function main() { + const { url } = await fetch("https://api.multiversx.com/websocket/config") + .then((r) => r.json()); -```typescript -import { Injectable } from '@nestjs/common'; -import { RedisCacheService } from '@multiversx/sdk-nestjs-cache'; + const socket = io(`https://${url}`, { path: "/ws/subscription" }); -@Injectable() -export class ConfigService { - constructor( - private readonly redisCacheService: RedisCacheService, - ){} + const payload = { + token: "USDC-c76f1f" + }; - async loadConfiguration(){ - return await this.redisCacheService.getOrSet( - 'configurationKey', - () => this.getConfigurationFromDb(), - 10, - ); - } + socket.emit("subscribeCustomTransfers", payload); - private async getConfigurationFromDb(){ - // fetch configuration from db - } + socket.on("customTransferUpdate", (data) => { + // data.transfers: Transaction[] (filtered) + // data.timestampMs: number + console.log("New USDC Transfer:", data); + }); } - ``` +**Scenario: Listen for ANY activity related to an address** +Using the `address` field is a shorthand to avoid creating 3 separate subscriptions (sender, receiver, relayer). -### Redis cache methods +```js + const payload = { + address: "erd1..." // Will capture incoming, outgoing, and relayed transfers + }; + socket.emit("subscribeCustomTransfers", payload); +``` -#### `get(key: string): Promise` +#### Update Example -- **Parameters:** - - `key`: The key of the item to retrieve. -- **Returns:** A `Promise` that resolves to the cached value or `undefined` if the key is not found. +```json +{ + "transfers": [ + { + "txHash": "cb5d0644ef40943db8035ff50913c4a974a469c2479a73c3cd3ab8de9027be0f", + "receiver": "erd1qqqqqqqqqqqqqpgqxn6hj5m9x33zuq0xynjkusd8tsz3u6a94fvsn2m2ry", + "receiverShard": 1, + "sender": "erd1qqqqqqqqqqqqqpgqcc69ts8409p3h77q5chsaqz57y6hugvc4fvs64k74v", + "senderAssets": { + ... + }, + "senderShard": 1, + "status": "success", + "value": "0", + "timestamp": 1765963650, + "function": "exchange", + "action": { + "category": "esdtNft", + "name": "transfer", + "description": "Transfer", + "arguments": { + "transfers": [ + { + "type": "FungibleESDT", + "ticker": "USDC", + "svgUrl": "https://tools.multiversx.com/assets-cdn/tokens/USDC-c76f1f/icon.svg", + "token": "USDC-c76f1f", + "decimals": 6, + "value": "4087442" + } + ], + "receiver": "erd1qqqqqqqqqqqqqpgqxn6hj5m9x33zuq0xynjkusd8tsz3u6a94fvsn2m2ry", + "functionName": "exchange", + "functionArgs": [ + "01" + ] + } + }, + "type": "SmartContractResult", + "originalTxHash": "46bb841a087c5ce95ca28d0e95c860c661b4d32514bb2970137536036bf591b3" + }, + ], + "timestampMs": 1763718888000 +} +``` +--- -#### `getMany(keys: string[]): Promise<(T | undefined | null)[]>` +### Custom Events (Filtered) -- **Parameters:** - - `keys`: An array of keys to retrieve. -- **Returns:** A `Promise` that resolves to an array of cached values corresponding to the input keys. Values may be `undefined` or `null` if not found. +Subscribes to smart contract events matching specific criteria as they happen. +#### Subscribe Event +`subscribeCustomEvents` -#### `set(key: string, value: T, ttl: number, cacheNullable?: boolean): void` +#### Payload (DTO) -- **Parameters:** - - `key`: The key under which to store the value. - - `value`: The value to store in the cache. - - `ttl`: Time-to-live for the cached item in seconds. - - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` +| Field | Type | Required | Description | +|---|---|---|---| +| address | string | NO* | Filter by the address associated with the event | +| identifier | string | NO* | Filter by event identifier (name) | +| logAddress | string | NO* | Filter by the contract address that emitted the log | +*\*At least one field must be provided.* -#### `setMany(keys: string[], values: T[], ttl: number, cacheNullable?: boolean): void` +#### Example usage -- **Parameters:** - - `keys`: An array of keys for the items to update or create. - - `values`: An array of values to set in the cache. - - `ttl`: Time-to-live for the cached items in seconds. - - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` +```js +import { io } from "socket.io-client"; +async function main() { + const { url } = await fetch("https://api.multiversx.com/websocket/config") + .then((r) => r.json()); -#### `delete(key: string): void` + const socket = io(`https://${url}`, { path: "/ws/subscription" }); -- **Parameters:** - - `key`: The key of the item to delete. -- **Returns:** A `Promise` that resolves when the item is successfully deleted from the cache. + // Subscribe to a specific event identifier + const payload = { + identifier: "swap" + }; + socket.emit("subscribeCustomEvents", payload); -#### `getOrSet(key: string, createValueFunc: () => Promise, ttl: number, cacheNullable?: boolean): Promise` + socket.on("customEventUpdate", (data) => { + // data.events: Events[] + // data.timestampMs: number + console.log("New Custom Event:", data); + }); +} +``` -- **Parameters:** - - `key`: The key of the item to retrieve or create. - - `createValueFunc`: A function that creates the new value for the key. - - `ttl`: Time-to-live for the cached item in seconds. - - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` +#### Update Example -**Note:** These are just some of the methods available in the `RedisCacheService` class. +```json +{ + "events": [ + { + "txHash": "b5bde891df72e26fb36e7ab3acc14b74044bd9aa82b4852692f5b9a767e0391f-1-0", + "identifier": "signalError", + "address": "erd1jv5m4v3yr0wy6g2jtz2v344sfx572rw6aclum9c6r7rd4ej4l6csjej2wh", + "timestamp": 1763718864, + "topics": [ + "9329bab2241bdc4d21525894c8d6b049a9e50ddaee3fcd971a1f86dae655feb1", + "4865616c7468206e6f74206c6f7720656e6f75676820666f72206c69717569646174696f6e2e" + ], + "shardID": 1 + } + ], + "timestampMs": 1763718864000 +} +``` +--- -## Cache Service +## Unsubscribing -Cache service is using both [In Memory Cache](#in-memory-cache) and [Redis Cache](#redis-cache) to form a two-layer caching system. +To stop receiving updates for any stream, you must emit the corresponding unsubscribe event. -Usage example: +**The Rule:** +1. Add the prefix `un` to the subscription event name (e.g., `subscribeTransactions` → `unsubscribeTransactions`, `subscribeCustomTransactions` → `unsubscribeCustomTransactions`). +2. Send the **exact same payload** used for the subscription. -```typescript -import { Injectable } from '@nestjs/common'; -import { CacheService } from '@multiversx/sdk-nestjs-cache'; +### Example: Unsubscribe from Custom Transactions -@Injectable() -export class ConfigService { - constructor( - private readonly cacheService: CacheService, - ){} +If you subscribed with: +```js +const payload = { sender: "erd1..." }; +socket.emit("subscribeCustomTransactions", payload); +``` - async loadConfiguration(){ - return await this.cacheService.getOrSet( - 'configurationKey', - () => this.getConfigurationFromDb(), - 5, // in memory TTL - 10, // redis TTL - ); - } +You must unsubscribe with: +```js +socket.emit("unsubscribeCustomTransactions", payload); +``` - private async getConfigurationFromDb(){ - // fetch configuration from db - } -} +### Example: Unsubscribe from Custom Transfers +If you subscribed with: +```js +const payload = { token: "USDC-c76f1f" }; +socket.emit("subscribeCustomTransfers", payload); ``` -Whenever `.loadConfigurationMethod()` is called, the service will first look into the in memory cache if there is a value stored for the specified key and return it. If the value is not found in the in memory cache it will look for the same key in Redis cache and return it if found. If the value is not found in Redis, the `.getConfigurationFromDb()` method is called and the returned value is stored in memory for 5 seconds (the TTL provided in the third parameter) and in Redis for 10 seconds (the value provided in the fourth parameter). +You must unsubscribe with: +```js +socket.emit("unsubscribeCustomTransfers", payload); +``` -*Note: we usually use smaller TTL for in memory cache because when it comes to in memory cache it takes longer to synchronize all instances and it is better to fall back to Redis and lose a bit of reading speed than to have inconsistent data.* +### Example: Unsubscribe from Blocks +If you subscribed with: +```js +const payload = { from: 0, size: 25 }; +socket.emit("subscribeBlocks", payload); +``` -All methods from `CacheService` use the two layer caching system except the ones that contains `local` and `remote` in their name. Those methods refer strictly to in memory cache and Redis cache. +You must unsubscribe with: +```js +socket.emit("unsubscribeBlocks", payload); +``` -Examples: +--- -- `.getLocal()`, `.setLocal()`, `.getOrSetLocal()` are the same methods as [In Memory Cache](#in-memory-cache) -- `.getRemote()`, `.setRemove()`, `.getOrSetRemove()` are the same methods as [Redis Cache](#redis-cache) +## Error Handling +Unexpected behaviors, such as sending an invalid payload or exceeding the server's subscription limits, will trigger an `error` event emitted by the server. -### Cache service methods +You should listen to this event to handle failures gracefully. +### Payload (DTO) -#### `get(key: string): Promise` +The error object contains context about which subscription failed and why. -- **Parameters:** - - `key`: The key of the item to retrieve. The method first checks the local in-memory cache, and if not found, it retrieves the value from the Redis cache. -- **Returns:** A `Promise` that resolves to the cached value or `undefined` if the key is not found. +| Field | Type | Description | +|---------|--------|------------------------------------------------------------------------------------| +| pattern | string | The subscription topic (event name) that was requested (e.g., `subscribePool`). | +| data | object | The original payload sent by the client that caused the error. | +| error | object | The specific error returned by the server. | +### Example usage -#### `getMany(keys: string[]): Promise<(T | undefined)[]>` +```js +import { io } from "socket.io-client"; -- **Parameters:** - - `keys`: An array of keys to retrieve. The method first checks the local in-memory cache, and for missing keys, it retrieves values from the Redis cache. -- **Returns:** A `Promise` that resolves to an array of cached values corresponding to the input keys. Values may be `undefined` if not found. +// ... setup socket connection ... +// Listen for generic errors from the server +socket.on("error", (errorData) => { + console.error("Received error from server:"); + console.dir(errorData, { depth: null }); +}); +``` -#### `set(key: string, value: T, ttl: number, cacheNullable?: boolean): Promise` +### Error Example -- **Parameters:** - - `key`: The key under which to store the value. The method sets the value in both the local in-memory cache and the Redis cache. - - `value`: The value to store in the cache. - - `ttl`: Time-to-live for the cached item in seconds. - - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` +**Scenario:** The client attempts to open more subscriptions than the server allows (e.g., limit of X). +```json +{ + "pattern": "subscribePool", + "data": { + "from": 0, + "size": 25, + "type": "badInput" + }, + "error": [ + { + "target": { + "from": 0, + "size": 25, + "type": "badInput" + }, + "value": "badInput", + "property": "type", + "children": [], + "constraints": { + "isEnum": "type must be one of the following values: Transaction, SmartContractResult, Reward" + } + } + ] +} +``` -#### `setMany(keys: string[], values: T[], ttl: number, cacheNullable?: boolean): Promise` +--- -- **Parameters:** - - `keys`: An array of keys for the items to update or create. The method sets values in both the local in-memory cache and the Redis cache. - - `values`: An array of values to set in the cache. - - `ttl`: Time-to-live for the cached items in seconds. - - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` +## Summary +- WebSocket endpoint is dynamically obtained via `/websocket/config`. +- **Pulse Stream Subscriptions:** periodic updates with possible duplicates (Transactions, Blocks, Pool, Events, Stats). +- **Filtered Stream Subscriptions:** real-time updates with only new data (CustomTransactions, **CustomTransfers**, CustomEvents). +- **Unsubscribing:** Use `un` prefix + same payload. +- Payload DTOs define allowed fields and required/optional rules. +- Update messages mirror REST API and include `Count` fields. +- Errors are emitted via the standard `error` event. +- Uses `socket.io-client`. -#### `delete(key: string): Promise` +This document contains everything required to use MultiversX WebSocket Subscriptions effectively. -- **Parameters:** - - `key`: The key of the item to delete. The method deletes the item from both the local in-memory cache and the Redis cache. -- **Returns:** A `Promise` that resolves when the item is successfully deleted from both caches. +--- +### MultiversX SDK cookbook -#### `deleteMany(keys: string[]): Promise` +import CookbookIndex from "@site/src/components/cookbook/CookbookIndex"; +import manifest from "@site/src/data/cookbook-manifest.json"; -- **Parameters:** - - `keys`: An array of keys for the items to delete. The method deletes items from both the local in-memory cache and the Redis cache. -- **Returns:** A `Promise` that resolves when all items are successfully deleted from both caches. +The cookbook is a set of **atomic, copy-paste recipes** for building on +MultiversX with the JavaScript and TypeScript SDKs, plus the Rust contract +framework where that is the right tool. Each recipe solves one concrete task, +shows every file you need and nothing you do not, and is written to be lifted +straight into your project. +Every recipe's code is **extracted from the page and type-checked in CI**. The +teal **Verified** badge on a card means that check passed on the date shown. If a +scheduled recheck ever fails, the badge degrades to a neutral **Needs recheck** +state instead, so the teal only ever means "trustworthy right now". What you copy +is what compiles. -#### `getOrSet(key: string, createValueFunc: () => Promise, ttl: number, cacheNullable?: boolean): Promise` +:::tip[Building with a coding agent?] +Point your agent at the docs and let it read the recipes directly. +[Agent or agentic engineer? Start here](./agents.mdx) covers the machine-readable +surface (`llms.txt`) and how to wire the cookbook into an agentic workflow. +::: -- **Parameters:** - - `key`: The key of the item to retrieve or create. The method first checks the local in-memory cache, and if not found, it retrieves the value from the Redis cache or creates it using the provided function. - - `createValueFunc`: A function that creates the new value for the key. - - `ttl`: Time-to-live for the cached item in seconds. - - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` + -... (and many more) +--- -**Note:** These are just some of the methods available in the `CacheService` class. For a comprehensive list of methods and their descriptions, refer to the class implementation. +### MultiversX Smart Contracts ---- +## MultiversX Smart Contracts -### NestJS SDK Monitoring utilities +The MultiversX Blockchain's virtual machine (VM) executes [WebAssembly](https://en.wikipedia.org/wiki/WebAssembly). This means that it can execute smart contracts +written in _any programming language_ that can be compiled to WASM bytecode. **Though, we only provide support for Rust.** -NPM Version +Developers are encouraged to use Rust for their smart contracts, however. MultiversX provides a [Rust framework](https://github.com/multiversx/mx-sdk-rs) +which allows for unusually clean and efficient code in smart contracts, a rarity in the blockchain field. +A declarative testing framework is bundled as well. +Read more about the MultiversX VM [here](/learn/space-vm). -## MultiversX NestJS Microservice Monitoring Utilities +Navigate through the sidebar to see some tutorials to get you started, as well as the Rust framework documentation. -This package contains a set of utilities commonly used for monitoring purposes in the MultiversX Microservice ecosystem. -The package relies on Prometheus to aggregate the metrics, and it is using [prom-client](https://www.npmjs.com/package/prom-client) as a client for it. +--- +### MultiversX Smart Contracts API limits -## Installation +## MultiversX Smart Contracts API limits -`sdk-nestjs-monitoring` is delivered via **npm,** and it can be installed as follows: +Starting with the Polaris release (February 2023), we have added blockchain data call limits for Smart Contracts. This means that in a single transaction, a Smart Contract cannot perform more than a protocol-level configured number of transfers, trie reads, or built-in function calls. -```bash -npm install @multiversx/sdk-nestjs-monitoring +This approach comes as a better alternative than increasing the gas costs of those operations since the limits are still very high, so most probably only the most expensive contracts' functions will suffer from these limitations. + +These limits are set in the gas schedule files in the `MaxPerTransaction` section. For example, this +gas schedule file https://github.com/multiversx/mx-chain-mainnet-config/blob/master/gasSchedules/gasScheduleV7.toml has the following limits: + +```toml +[MaxPerTransaction] + MaxBuiltInCallsPerTx = 100 + MaxNumberOfTransfersPerTx = 250 + MaxNumberOfTrieReadsPerTx = 1500 ``` +which translates to: +* each transaction can make a maximum 100 built-in functions calls, such as "get last nonce", "get last randomness", "ESDT unpause", "ESDT NFT create" and so on; +* each transaction can create maximum 250 transfers (as in produced smart contract results). For example, a call to "ESDT NFT transfer" will create 1 +smart contract results and a call to "multi ESDT NFT transfer" will consume the number of transfers defined in the function call; +* each transaction can access its data trie for a maximum 1500 get operations (can read a maximum 1500 values stored in the contract). -## Utility +These limits are subject to change, a new release can activate a new gas schedule at a defined epoch. -The package exports **performance profilers**, **interceptors** and **metrics**. +--- +### MultiversX tools on multiple platforms -### Performance profiler +Generally speaking, the MultiversX tools should work on all platforms. However, platform-specific issues can occur. This page aims to be an entry point for troubleshooting platform-specific issues, in regards to the MultiversX toolset. -`PerformanceProfiler` is a class exported by the package that allows you to measure the execution time of your code. +:::note +If you discover a platform-specific issue, please let us known, on the [corresponding GitHub repository](/sdk-and-tools/overview). -```typescript -import { PerformanceProfiler } from '@multiversx/sdk-nestjs-monitoring'; +If you are blocked by a platform-specific issue, please consider using a **devcontainer**, as described [here](/sdk-and-tools/devcontainers). +::: -const profiler = new PerformanceProfiler(); -await doSomething(); -const profilerDurationInMs = profiler.stop(); +## Linux -console.log(`doSomething() method execution time lasted ${profilerDurationInMs} ms`); -``` +All tools are expected to work on Linux. They are generally tested on Ubuntu-based distributions. -The `.stop()` method can receive two optional parameters: +## MacOS -- `description` - text used for default logging. Default: `undefined` -- `log` - boolean to determine if log should be printed. If `log` is set to true, the logging class used to print will be `Logger` from `"@nestjs/common"`.``Default: `false` +All tools are expected to work on MacOS. Though, even if the tests within the continuous integration flows cover MacOS, some inconveniences might still occur. -```typescript -import { PerformanceProfiler } from '@multiversx/sdk-nestjs-monitoring'; +### Apple Silicon (M1, M2) -const profiler = new PerformanceProfiler(); -await doSomething(); -profiler.stop(`doSomething() execution time`, true); +As of February 2024, the Node can only be compiled using the AMD64 version of Go. Thus, dependent tools, such as [localnets](/developers/setup-local-testnet), the [Chain Simulator](/sdk-and-tools/chain-simulator) etc. will rely on the [Apple Rosetta binary translator](https://en.wikipedia.org/wiki/Rosetta_(software)). + +:::note +As of February 2024, a native ARM64 version of the Node is in the works. This will allow the dependent tools to run natively on Apple Silicon. +::: + +If you'd like to manually build a Go tool that only works on AMD64 (for now), download & extract the Go toolchain for AMD64. For example: + +```sh +wget https://go.dev/dl/go1.20.7.darwin-amd64.tar.gz +tar -xf go1.20.7.darwin-amd64.tar.gz ``` -The output of the code above will be "`doSomething() execution time: 1.532ms`" +Then, export `GOPATH` and `GOENV` variables into your shell: ---- +```sh +export GOPATH=/(path to extracted toolchain)/go +export GOENV=/(path to extracted toolchain)/go/env +``` +Afterwards, build the tools, as needed. The obtained binaries will be AMD64, and they will run on your ARM64 system. -### Cpu Profiler +## Windows -`CpuProfiler` is a class exported by the package that allows you to measure the CPU execution time of your code. Given that JavaScript is a single-threaded language, it's important to be mindful of the amount of CPU time allocated to certain operations, as excessive consumption can lead to slowdowns or even blockages in your process. +Some tools can be difficult to install or run **directly on Windows**. For example, when building smart contracts, the encountered issues might be harder to tackle, especially for beginners. -```typescript -import { CpuProfiler } from '@multiversx/sdk-nestjs-monitoring'; +Therefore, we recommend using the [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/en-us/windows/wsl/install), instead of running the tools directly on Windows. -const profiler = new CpuProfiler(); -await doHttpRequest() -const profilerDurationInMs = profiler.stop(); +--- -console.log(`doHttpRequest() method execution time lasted ${profilerDurationInMs} ms`); -``` +### mxpy CLI cookbook -The `.stop()` method can receive two optional parameters: +## mxpy (Command Line Interface) -- `description` - text used for default logging. Setting the description automatically triggers the printing of the `PerformanceProfiler` value. Default: `undefined` +**mxpy**, as a command-line tool, can be used to simplify and automate the interaction with the MultiversX network - it can be easily used in shell scripts, as well. It implements a set of **commands**, organized within **groups**. -```typescript -import { CpuProfiler } from '@multiversx/sdk-nestjs-monitoring'; +:::important +In order to migrate to the newer `mxpy`, please follow [the migration guide](https://github.com/multiversx/mx-sdk-py-cli/issues?q=label:migration). +::: -const httpReqCpuProfiler = new CpuProfiler(); -await doHttpRequest(); -httpReqCpuProfiler.stop(`doHttpRequest() execution time`); +The complete Command Line Interface is listed [**here**](https://github.com/multiversx/mx-sdk-py-cli/blob/main/CLI.md). Command usage and description are available through the `--help` or `-h` flags. -const cpuProfiler = new CpuProfiler(); -await doSomethingCpuIntensive(); -cpuProfiler.stop(`doSomethingCpuIntensive() execution time`); +For example: + +```sh +mxpy --help +mxpy tx --help +mxpy tx new --help ``` -The output of the code above will be
+This page will guide you through the process of handling common tasks using **mxpy**. -`doHttpRequest() execution time: 100ms, CPU time: 1ms` -`doSomethingCpuIntensive() execution time: 20ms, CPU time 18ms` -*Note that a big execution time does not necessarily have an impact on the CPU load of the application. That means that, for example, while waiting for an HTTP request, the JavaScript thread can process other things. That is not the case for CPU time. When a method consumes a lot of CPU time, Javascript will not be able to process other tasks, potentially causing a freeze until the CPU-intensive task is complete.* +## Managing dependencies ---- +Using `mxpy` you can either check if a dependency is installed or install a new dependency. +To check if a dependency is installed you can use: -## Interceptors +```sh +mxpy deps check +``` -The package provides a series of [Nestjs Interceptors](https://docs.nestjs.com/interceptors) which will automatically log and set the CPU and overall duration for each request in a [Prometheus](https://prometheus.io) histogram ready to be scrapped by Prometheus. +To install a new dependency you can use: -`LoggingInterceptor` interceptor will set the execution time of each request in a Prometheus histogram using [performance profilers](#performance-profiler). +```sh +mxpy deps install +``` -`RequestCpuTimeInterceptor` interceptor will set the CPU execution time of each request in a Prometheus histogram using [cpu profiler](#cpu-profiler). +Both `mxpy deps check ` and `mxpy deps install ` use the `` as a positional argument. -*Both interceptors expect an instance of `metricsService` class as an argument.* +To find out which dependencies can be managed using `mxpy`, you can type one of the following commands to see the positional arguments it accepts: -```typescript -import { MetricsService, RequestCpuTimeInterceptor, LoggingInterceptor } from '@multiversx/sdk-nestjs-monitoring'; +```sh +mxpy deps check -h +mxpy deps install -h +``` -async function bootstrap() { - // AppModule imports MetricsModule - const publicApp = await NestFactory.create(AppModule); - const metricsService = publicApp.get(MetricsService); +For example, in order to check if the `testwallets` dependency is installed, you would type: - const globalInterceptors = []; - globalInterceptors.push(new RequestCpuTimeInterceptor(metricsService)); - globalInterceptors.push(new LoggingInterceptor(metricsService)); +```sh +mxpy deps check testwallets +``` - publicApp.useGlobalInterceptors(...globalInterceptors); -} +For example, to install the `testwallets` dependency, you can simply type the command: + +```sh +mxpy deps install testwallets ``` +When installing dependencies, the `--overwrite` argument can be used to overwrite an existing version. -## MetricsModule and MetricsService +For example, to overwrite the installation of a dependency, you can simply type the command: -`MetricsModule` is a [Nestjs Module](https://docs.nestjs.com/modules) responsible for aggregating metrics data through `MetricsService` and exposing them to be consumed by Prometheus. `MetricsService` is extensible, you can define and aggregate your own metrics and expose them. By default it exposes a set of metrics created by the interceptors specified [here](#interceptors). Most of the MultiversX packages expose metrics by default through this service. For example [@multiversx/sdk-nestjs-redis](https://www.npmjs.com/package/@multiversx/sdk-nestjs-redis) automatically tracks the execution time of each redis query, overall redis health and much more, by leveraging the `MetricsService`. +```sh +mxpy deps install testwallets --overwrite +``` +## Configuring mxpy -### How to instantiate the MetricsModule and expose metrics endpoints for Prometheus +The configuration can be altered using the `mxpy config` command. -In our example we will showcase how to expose response time and CPU time of HTTP requests. Make sure you have the interceptors in place as shown [here](#interceptors). After the interceptors are in place, as requests comes through your application, the metrics are being populated into `MetricsService` class and we just have to expose the output of the `.getMetrics()` method on `MetricsService` through a controller. +:::tip +mxpy's configuration is stored in the file `~/multiversx-sdk/mxpy.json`. +::: -```typescript -import { Controller, Get } from '@nestjs/common'; -import { MetricsService } from '@multiversx/sdk-nestjs-monitoring'; -@Controller('metrics') -export class MetricsController { - constructor( - private readonly metricsService: MetricsService - ){} +### Viewing the current mxpy configuration - @Get() - getMetrics(): string { - return this.metricsService.getMetrics(); - } +As of `mxpy v11`, we've introduced more configuration options, such as `environments` and `wallets`. + +In order to view the current configuration, one can issue the command `mxpy config dump`. Output example: + +```json +{ + "dependencies.testwallets.tag": "" } ``` +For viewing the default configuration the following command can be used: -### How to add custom metrics +```sh +mxpy config dump --defaults +``` -Adding custom metrics is just a matter of creating another class which uses `MetricsService`. -We can create a new class called `ApiMetricsService` which will have a new custom metric `heartbeatsHistogram`. +### Updating the mxpy configuration -```typescript -import { Injectable } from '@nestjs/common'; -import { MetricsService } from '@multiversx/sdk-nestjs-monitoring'; -import { register, Histogram } from 'prom-client'; +One can alter the current configuration using the command `mxpy config set`. -@Injectable() -export class ApiMetricsService { - private static heartbeatsHistogram: Histogram; +The default config contains the **log level** of the CLI. The default log level is set to `info`, but can be changed. The available values are: [debug, info, warning, error]. To set the log level, we can use the following command: +```sh +mxpy config set log_level debug +``` - constructor(private readonly metricsService: MetricsService) { - if (!ApiMetricsService.heartbeatsHistogram) { - ApiMetricsService.heartbeatsHistogram = new Histogram({ - name: 'heartbeats', - help: 'Heartbeats', - labelNames: ['app'], - buckets: [], - }); - } - } +:::note +Previously, the `default_address_hrp` was also stored in the config. As of `mxpy v11` it has been moved to the `env` config, which we'll talk about in the next section. +::: - async getMetrics(): Promise { - const baseMetrics = await this.metricsService.getMetrics(); - const currentMetrics = await register.metrics(); +### Configuring environments - return baseMetrics + '\n' + currentMetrics; - } +An `env config` is a named environment configuration that stores commonly used settings (like proxy url, hrp, and flags) for use with the **mxpy CLI**. Environments can be useful when switching between networks, such as Mainnet and Devnet. - setHeartbeatDuration(app: string, duration: number) { - ApiMetricsService.heartbeatsHistogram.labels(app).observe(duration); - } +The values that are available for configuration and their default values are the following: +```json +{ + "default_address_hrp": "erd", + "proxy_url": "", + "explorer_url": "", + "ask_confirmation": "false", } ``` -The only change we have to do is that we need to instantiate this class and call `.getMetrics()` method on it to return to us both default and our new custom metrics. - -The `.setHeartbeatDuration()` method will be used in our business logic whenever we want to add a new value to that histogram. - ---- +#### Creating a new env config -### NFT & SFT tokens +To create a new env config, we use the following command: -```mdx-code-block -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; -import TableWrapper from "@site/src/components/TableWrapper"; +```sh +mxpy config-env new ``` +Additionally, `--template` can be used to create a config from an existing env config. After a new env config is created, it becomes the active one. You can then set its values using the `mxpy config-env set` command. -## **Introduction** +#### Setting the default hrp -MultiversX NFTs(non-fungible tokens) are a breed of digital assets that are revolutionizing the world of art, collectibles, and more. These NFTs are unique, one-of-a-kind tokens that are built on blockchain technology, allowing for secure ownership and transfer of these assets. With MultiversX NFTs, every token is assigned a unique identification code(ticker) and metadata that distinguishes it from every other token, making each NFT truly one-of-a-kind. Read the full page for a comprehensive guide on how to brand, issue, transfer, assign roles and many other features, for both NFTs and SFTs. +The `default_address_hrp` might need to be changed depending on the network you plan on using (e.g Sovereign Chain). Most of the commands that might need the `address hrp` already provide a parameter called `--hrp` or `--address-hrp`, that can be explicitly set, but there are system smart contract addresses that cannot be changed by providing the parameter. If those addresses need to be changed, we can use the following command to set the `default hrp` that will be used throughout mxpy. Here we set the default hrp to `test`: +```sh +mxpy config-env set default_address_hrp test --env test-env +``` +:::note +Explicitly providing `--hrp` will **always** be used over the one fetched from the network or the `hrp` set in the config. +::: -### NFT and SFT +#### Setting the proxy url -The MultiversX protocol introduces native NFT support by adding metadata and attributes on top of the already existing [Fungible tokens](/tokens/fungible-tokens). -This way, one can issue a semi-fungible token or a non-fungible token which is quite similar to an ESDT, but has a few more attributes, as well as an assignable URI. -Once owning a quantity of a NFT/SFT, users will have their data store directly under their account, inside the trie. All the fields available inside a NFT/SFT token can be found [here](/tokens/nft-tokens#nftsft-fields). +If `proxy_url` is set in the active environment, the `--proxy` argument is no longer required for the commands that need this argument. -**The flow of issuing and transferring non-fungible or semi-fungible tokens is:** +To set the proxy url, use the following command: -- register/issue the token -- set roles to the address that will create the NFT/SFTs -- create the NFT/SFT -- transfer quantity(es) +```sh +mxpy config-env set proxy_url https://devnet-api.multiversx.com --env devnet +``` +#### Setting the explorer url -### Meta ESDT +**mxpy** already knows the explorer urls for all three networks (Mainnet, Devnet, Testnet). This is particularly useful when running the CLI on custom networks where an explorer is also available. This key is not required to be present in the `env config` for the config to be valid. To set the explorer url use the following command: -In addition to NFTs and SFTs, MultiversX introduced Meta ESDTs. -Meta ESDTs are a special case of semi-fungible-tokens. They can be seen as regular ESDT fungible tokens that also have properties. -In a particular example, LKMEX or XMEX are MetaESDTs and their properties help implement the release schedule. +```sh +mxpy config-env set explorer_url https://url-to-explorer.com --env test-env +``` +#### Setting the ask for confirmation flag -## **Branding** - -Anyone can create NFTs and SFTs tokens on MultiversX Network. There are also no limits in tokens names or tickers. For example, -one issues an `AliceToken` with the ticker `ALC`. Anyone else is free to create a new token with the same token name and -the same token ticker. The only difference will be the random sequence of the token identifier. So the "original" token -could have received the random sequence `1q2w3e` resulting in the `ALC-1q2w3e` identifier, while the second token could -have received the sequence `3e4r5t` resulting in `ALC-3e4r5t`. +If set to `true`, whenever sending a transaction, mxpy will display the transaction and will ask for your confirmation. To set the flag, use the following command: -In order to differentiate between an original token and other tokens with the same name or ticker, we have introduced a -branding mechanism that allows tokens owners to provide a logo, a description, a website, as well as social link for their tokens. MultiversX products such as Explorer, Wallet and so on -will display tokens in accordance to their branding, if any. +```sh +mxpy config-env set ask_confirmation true --env mainnet +``` -A token owner can submit a branding request by opening a Pull Request on https://github.com/multiversx/mx-assets. +#### Dumping the active env config +We can see the values set in our active env config. To do so, we use the following command: -### **Submitting a branding request** +```sh +mxpy config-env dump +``` -Token owners can create a PR to the https://github.com/multiversx/mx-assets with the logo in .png and .svg format, as well as a .json file containing all the relevant information. +#### Dumping all the available env configs -Here’s a prefilled template for the .json file to get you started: +We may have multiple env configs, maybe one for each network. To dump all the available env configs, we use the following command: -```json -{ - "website": "https://www.multiversxtoken.com", - "description": "MultiversX Token is a collection of 10.000 unique and randomly generated tokens.", - "social": { - "email": "mxt-token@multiversxtoken.com", - "blog": "https://www.multiversxtoken.com/MXT-token-blog", - "twitter": "https://twitter.com/MXT-token-twitter" - }, - "status": "active" -} +```sh +mxpy config-env list ``` +#### Deleting a value from the active env config -## **Issuance of Non-Fungible Tokens** - -One has to perform an issuance transaction in order to register a non-fungible token. -Non-Fungible Tokens are issued via a request to the Metachain, which is a transaction submitted by the Account which will manage the tokens. When issuing a token, one must provide a token name, a ticker and optionally additional properties. This transaction has the form: +We can also delete the key-value pairs saved in the env config. For example, let's say we want to delete the explorer url, so we use the following command: -```rust -IssuanceTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 50000000000000000 # (0.05 EGLD) - GasLimit: 60000000 - Data: "issueNonFungible" + - "@" + + - "@" + -} +```sh +mxpy config-env delete explorer_url --env test-env ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +#### Getting a value from the active env config -Optionally, the properties can be set when issuing a token. Example: +If we want to see just the value of a env config key from a specific environment, we can use the following command: -```rust -IssuanceTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 50000000000000000 # (0.05 EGLD) - GasLimit: 60000000 - Data: "issueNonFungible" + - "@" + + - "@" + + - "@" + <"canFreeze" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canWipe" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canPause" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canTransferNFTCreateRole" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canChangeOwner" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canUpgrade" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canAddSpecialRoles" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - ... -} +```sh +mxpy config-env get --env mainnet ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +#### Deleting an env config -The receiver address `erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u` is a built-in system smart contract (not a VM-executable contract), which only handles token issuance and other token management operations, and does not handle any transfers. -The contract will add a random string to the ticker thus creating the **token identifier**. The random string starts with “-” and has 6 more random characters. For example, a token identifier could look like _ALC-6258d2_. +To delete an env config, we use the following command: +```sh +mxpy config-env remove +``` -## **Issuance of Semi-Fungible Tokens** +#### Switching to a different env config -One has to perform an issuance transaction in order to register a semi-fungible token. -Semi-Fungible Tokens are issued via a request to the Metachain, which is a transaction submitted by the Account which will manage the tokens. When issuing a semi-fungible token, one must provide a token name, a ticker and optionally additional properties. This transaction has the form: +To switch to a new env config, we use the following command: -```rust -IssuanceTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 50000000000000000 # (0.05 EGLD) - GasLimit: 60000000 - Data: "issueSemiFungible" + - "@" + + - "@" + -} +```sh +mxpy config-env switch --env ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +You can manage multiple environment configurations with ease using `mxpy config-env`. This feature helps streamline workflows when working with multiple networks or projects. Use `mxpy config-env list` to see all available configs, and `switch` to quickly toggle between them. -Optionally, the properties can be set when issuing a token. Example: +### Configuring wallets -```rust -IssuanceTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 50000000000000000 # (0.05 EGLD) - GasLimit: 60000000 - Data: "issueSemiFungible" + - "@" + + - "@" + + - "@" + <"canFreeze" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canWipe" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canPause" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canTransferNFTCreateRole" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canChangeOwner" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canUpgrade" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canAddSpecialRoles" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - ... +Wallets can be configured in the wallet config. Among all configured wallets, one must be set as the active wallet. This active wallet will be used by default in all mxpy commands, unless another wallet is explicitly provided using `--pem`, `--keystore`, or `--ledger`. Alternatively, the `--sender` argument can be used to specify a particular sender address from the address config (e.g. --sender alice). + +The values that are available for configuring wallets are the following: +```json +{ + "path": "", + "index": "", } ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +Supported wallet types include PEM files and keystores. The CLI will determine the type based on the given path and act accordingly. If the wallet is of type `keystore`, you'll be prompted to enter the wallet's password. -The receiver address `erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u` is a built-in system smart contract (not a VM-executable contract), which only handles token issuance and other token management operations, and does not handle any transfers. -The contract will add a random string to the ticker thus creating the **token identifier**. The random string starts with “-” and has 6 more random characters. For example, a token identifier could look like _ALC-6258d2_. +The `path` field represents the absolute path to the wallet. +The `index` field represents the index that will be used when deriving the wallet from the secret key. This field is optional, the default index is `0`. -## **Issuance of Meta-ESDT Tokens** +#### Creating a new wallet config -One has to perform an issuance transaction in order to register a Meta-ESDT token. -Meta-ESDT Tokens are issued via a request to the Metachain, which is a transaction submitted by the Account which will manage the tokens. When issuing a semi-fungible token, one must provide a token name, a ticker and optionally additional properties. This transaction has the form: +When configuring a new wallet we need to give it an alias. An alias is a user-defined name that identifies a configured wallet (e.g. alice, bob, dev-wallet). To create a new wallet config, we use the following command: -```rust -IssuanceTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 50000000000000000 # (0.05 EGLD) - GasLimit: 60000000 - Data: "registerMetaESDT" + - "@" + + - "@" + + - "@" + -} +```sh +mxpy config-wallet new ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +This command accepts the `--path` argument, so the path to the wallet can be set directly, without needing to call `mxpy config-wallet set` afterwards. -Optionally, the properties can be set when issuing a token. Example: +#### Setting the wallet config fields -```rust -IssuanceTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 50000000000000000 # (0.05 EGLD) - GasLimit: 60000000 - Data: "registerMetaESDT" + - "@" + + - "@" + + - "@" + - "@" + <"canFreeze" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canWipe" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canPause" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canTransferNFTCreateRole" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canChangeOwner" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canUpgrade" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - "@" + <"canAddSpecialRoles" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + - ... -} +For a config to be valid, we need to set at least the `path` field for an already created wallet alias. To do so, we use the following command: + +```sh +mxpy config-wallet set path absolute/path/to/pem/wallet.pem --alias alice ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +#### Getting the value of a field -The receiver address `erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u` is a built-in system smart contract (not a VM-executable contract), which only handles token issuance and other token management operations, and does not handle any transfers. -The contract will add a random string to the ticker thus creating the **token identifier**. The random string starts with “-” and has 6 more random characters. For example, a token identifier could look like _ALC-6258d2_. +We can get the value of a field from an alias using the following command: +```sh +mxpy config-wallet get path --alias alice +``` -### **Converting an SFT into Meta-ESDT** +#### Dumping the active wallet config -An already existing _semi-fungible token_ can be converted into a Meta-ESDT token if the owner sends the following transaction: +To view all the properties of the active address, use the following command: -```rust -ConvertSftToMetaESDTTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 0 - GasLimit: 60000000 - Data: "changeSFTToMetaESDT" + - "@" + + - "@" + -} +```sh +mxpy config-wallet dump ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - - -## **Parameters format** +#### Dumping all configured wallets -Token Name: +To view all the wallets configured, use the following command: -- length between 3 and 50 characters -- alphanumeric characters only +```sh +mxpy config-wallet list +``` -Token Ticker: +#### Switching to a different wallet -- length between 3 and 10 characters -- alphanumeric UPPERCASE only +We may have multiple wallets configured, so to switch between them, we use the following command: +```sh +mxpy config-wallet switch --alias alice +``` -## **Issuance examples** +#### Removing an address from the config -For example, a user named Alice wants to issue an ESDT called "AliceTokens" with the ticker "ALC". The issuance transaction would be: +We can remove an address from the config using the alias of the address and the following command: -```rust -IssuanceTransaction { - Sender: erd1sg4u62lzvgkeu4grnlwn7h2s92rqf8a64z48pl9c7us37ajv9u8qj9w8xg - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 50000000000000000 # (0.05 EGLD) - GasLimit: 60000000 - Data: "issueSemiFungible" + - "@416c696365546f6b656e73" + - "@414c43" + -} +```sh +mxpy config-wallet remove --alias alice ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - -Once this transaction is processed by the Metachain, Alice becomes the designated **manager of AliceTokens**. She can add quantity later using `ESDTNFTCreate`. For more operations available to ESDT token managers, see [Token management](/tokens/fungible-tokens#token-management). +## Estimating the Gas Limit for transactions -In that smart contract result, the `data` field will contain a transfer syntax which is explained below. What is important to note is that the token identifier can be fetched from -here in order to use it for transfers. Alternatively, the token identifier can be fetched from the API (explained also in section [REST API - Get NFT data](/tokens/nft-tokens#get-nft-data-for-an-address) ). +mxpy (version 11.1.0 and later) can automatically estimate the required gas limit for transactions when a proxy URL is provided. The estimation works by simulating the transaction before sending it. +While the estimation is generally accurate, it's recommended to add a safety margin to account for potential state changes. This can be done in two ways: -## **Roles** +1. Per transaction, using the `--gas-limit-multiplier` flag: -In order to be able to perform actions over a token, one needs to have roles assigned. -The existing roles are: +```sh +mxpy tx new --gas-limit-multiplier 1.1 ... +``` -For NFT: +2. As a global default setting in the config: -- ESDTRoleNFTCreate : this role allows one to create a new NFT -- ESDTRoleNFTBurn : this role allows one to burn a specific NFT -- ESDTRoleNFTUpdateAttributes : this role allows one to change the attributes of a specific NFT -- ESDTRoleNFTAddURI : this role allows one add URIs for a specific NFT -- ESDTTransferRole : this role enables transfer only to specified addresses. The addresses with the transfer role can transfer anywhere. -- ESDTRoleNFTUpdate : this role allows one to update meta data attributes of a specific NFT -- ESDTRoleModifyRoyalties : this role allows one to modify royalities of a specific NFT -- ESDTRoleSetNewURI : this role allows one to set new uris of a specific NFT -- ESDTRoleModifyCreator : this role allows one to rewrite the creator of a specific token -- ESDTRoleNFTRecreate : this role allows one to recreate the whole NFT with new attributes +```sh +mxpy config set gas_limit_multiplier 1.1 +``` -For SFT: +A multiplier of 1.1 (10% increase) is typically sufficient for most transactions. -- ESDTRoleNFTCreate : this role allows one to create a new SFT -- ESDTRoleNFTBurn : this role allows one to burn quantity of a specific SFT -- ESDTRoleNFTAddQuantity : this role allows one to add quantity of a specific SFT -- ESDTTransferRole : this role enables transfer only to specified addresses. The addresses with the transfer role can transfer anywhere. -- ESDTRoleNFTUpdate : this role allows one to update meta data attributes of a specific SFT -- ESDTRoleModifyRoyalties : this role allows one to modify royalities of a specific SFT -- ESDTRoleSetNewURI : this role allows one to set new uris of a specific SFT -- ESDTRoleModifyCreator : this role allows one to rewrite the creator of a specific token -- ESDTRoleNFTRecreate : this role allows one to recreate the whole NFT with new attributes -To see how roles can be assigned, please refer to [this](/tokens/nft-tokens#assigning-roles) section. +## Creating wallets +There are a couple available wallet formats: -## **Assigning roles** +- `raw-mnemonic` - secret phrase in plain text +- `keystore-mnemonic` - secret phrase, as a password-encrypted JSON keystore file +- `keystore-secret-key` - secret key (irreversibly derived from the secret phrase), as a password-encrypted JSON keystore file +- `pem` - secret key (irreversibly derived from the secret phrase), as a PEM file -Roles can be assigned by sending a transaction to the Metachain from the ESDT manager. +For this example, we are going to create a `keystore-mnemonic` wallet. -Within a transaction of this kind, any number of roles can be assigned (minimum 1). +Let's create a keystore wallet: -```rust -RolesAssigningTransaction { - Sender:
- Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 0 - GasLimit: 60000000 - Data: "setSpecialRole" + - "@" + + - "@" +
+ - "@" + + - "@" + + - ... -} +```sh +mxpy wallet new --format keystore-mnemonic --outfile test_wallet.json ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - -For example, `ESDTRoleNFTCreate` = `45534454526f6c654e4654437265617465` - -Unset transactions are very similar. You can find an example [here](/tokens/fungible-tokens#unset-special-role). +The wallet's mnemonic will appear, followed by a prompt to set a password for the file. Once you input the password and press "Enter", the file will be generated at the location specified by the `--outfile` argument. -## **NFT/SFT fields** +## Converting a wallet -Below you can find the fields involved when creating an NFT. +As you have read above, there are multiple ways in which you can store your secret keys. -**NFT Name** +To convert a wallet from a type to another you can use: -- The name of the NFT or SFT +```sh +mxpy wallet convert +``` -**Quantity** +:::info +Keep in mind that the conversion isn't always possible (due to irreversible derivations of the secret phrase): -- The quantity of the token. If NFT, it must be `1` +- `raw-mnemonic` can be converted to any other format +- `keystore-mnemonic` can be converted to any other format +- `keystore-secret-key` can only be converted to `pem` +- `pem` can only be converted to `keystore-secret-key` -**Royalties** +It's mandatory that you keep a backup of your secret phrase somewhere safe. +::: -- Allows the creator to receive royalties for any transaction involving their NFT -- Base format is a numeric value between 0 an 10000 (0 meaning 0% and 10000 meaning 100%) +Let's convert the previously created `keystore-mnemonic` to a `PEM` wallet. We discourage the use of PEM wallets for storing cryptocurrencies due to their lower security level. However, they prove to be highly convenient and user-friendly for application testing purposes. -**Hash** +To convert the wallet we type the following command: -- Arbitrary field that should contain the hash of the NFT metadata -- Optional filed, should be left `null` when building the transaction to create the NFT +```sh +mxpy wallet convert --infile test_wallet.json --in-format keystore-mnemonic --outfile converted_wallet.pem --out-format pem +``` -**Attributes** +After being prompted to enter the password you've previously set for the wallet the new `.pem` file will be created. -- Represents additional information about the NFT or SFT, like picture traits or tags for your NFT/collection -- The field should follow a `metadata:ipfsCID/fileName.json;tags:tag1,tag2,tag3` format -- Below you can find a sample for the extra metadata format that should be stored on IPFS: +The command arguments can be found [here](https://github.com/multiversx/mx-sdk-py-cli/blob/main/CLI.md#walletconvert) or by typing: -```json -{ - "description": "This is a sample description", - "attributes": [ - { - "trait_type": "Background", - "value": "Yellow", - "{key}": "{value}", - "{...}": "{...}", - "{key}": "{value}" - }, - { - "trait_type": "Headwear", - "value": "BlackBeanie" - }, - { - "trait_type": "SampleTrait3", - "value": "SampleValue3" - } - ], - "collection": "ipfsCID/fileName.json" -} +```sh +mxpy wallet convert --help ``` -**URI(s)** -- Mandatory field that represents the URL to a [supported](#supported-media-types) media file ending with the file extension as described in the [example](#example) below -- Field should contain the `Uniform Resource Identifier` +## Building a smart contract -Note: As a best practice, we recommend storing the files for media & extra metadata(from attributes field) within same folder on your storage provider, ideally IPFS. Also, in order to have a thumbnail generated for the uploaded file the size of the file should be less or equal to 64MB. +In order to deploy a smart contract on the network, you need to build it first. For this purpose, [sc-meta](/developers/meta/sc-build-reference#how-to-basic-build) should be used. To learn more about `sc-meta`, please check out [this page](/developers/meta/sc-meta). -:::important -Please note that each argument must be encoded in hexadecimal format with an even number of characters. -::: +The contract we will be using for this examples can be found [here](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/adder). +Build a contract as follows: -### **Supported Media Types** +```sh +sc-meta all build --path +``` -Below you can find a table with the supported media types for NFTs available on MultiversX network. +If our working directory is already the contract's directory we can skip the `--path` argument as by default the contract's directory is the _current working directory_. -| Media Extension | Media Type | -|-----------------|-----------------| -| .png | image/png | -| .jpeg | image/jpeg | -| .jpg | image/jpg | -| .gif | image/gif | -| .webp | image/webp | -| .svg | image/svg | -| .svg | image/svg+xml | -| .acc | audio/acc | -| .flac | audio/flac | -| .m4a | audio/m4a | -| .mp3 | audio/mp3 | -| .wav | audio/wav | -| .mov | video/mov | -| .quicktime | video/quicktime | -| .mp4 | video/mp4 | -| .webm | video/webm | +The generated `.wasm` file will be created in a directory called `output` inside the contract's directory. -### **Example** +## Deploying a smart contract -Below you can find a table representing an example of the fields for a non-fungible token that resembles a song. +After you've built your smart contract, it can be deployed on the network. - -| Property | Plain value | Encoded value | -|----------------|--------------------------------------------------------|----------------------------------------------------------------------------------------------------------| -| **NFT Name** | Beautiful song | 42656175746966756c20736f6e67 | -| **Quantity** | 1 | 01 | -| **Royalties** | 7500 _=75%_ | 1d4c | -| **Hash** | 00 | 00 | -| **Attributes** | metadata:_ipfsCID/song.json_;tags:song,beautiful,music | 6d657461646174613a697066734349442f736f6e672e6a736f6e3b746167733a736f6e672c62656175746966756c2c6d75736963 | -| **URI** | _URL_to_decentralized_storage/song.mp3_ | 55524c5f746f5f646563656e7472616c697a65645f73746f726167652f736f6e672e6d7033 | - +For deploying a smart contract the following command can be used: -In this example we are creating a NFT representing a song. Hash is left null, we are sharing media location URL and we are also providing the location of the extra metadata within the attributes field. +```sh +mxpy contract deploy +``` +To deploy a smart contract you have to send a transaction to the **Smart Contract Deploy Address** and that address is `erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu`, but you don't have to worry about setting the receiver of the transaction because the above command takes care of it. -## **Creation of an NFT** +The `--bytecode` argument specifies the path to your previously-built contract. If you've built the smart contract using `mxpy`, the generated `.wasm` file will be in a folder called `output`. -A single address can own the role of creating an NFT for an ESDT token. This role can be transferred by using the `ESDTNFTCreateRoleTransfer` function. +For example, if your contract is in `~/contracts/adder`, the generated bytecode file `adder.wasm` will be in `~/contracts/adder/output`. So, when providing the `--bytecode` argument the path should be `~/contracts/adder/output/adder.wasm`. -An NFT can be created on top of an existing ESDT by sending a transaction to self that contains the function call that triggers the creation. -Any number of URIs can be assigned (minimum 1) +The `mxpy contract deploy` command needs a multitude of other parameters that can be checked out [here](https://github.com/multiversx/mx-sdk-py-cli/blob/main/CLI.md#contractdeploy) or by simply typing the following: -```rust -NFTCreationTransaction { - Sender:
- Receiver: - Value: 0 - GasLimit: 3000000 + Additional gas (see below) - Data: "ESDTNFTCreate" + - "@" + + - "@" + + - "@" + + - "@" + + - "@" + + - "@" + + - "@" + + - "@" + + - ... -} +```sh +mxpy contract deploy --help ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +We will use a `.pem` file for the sake of simplicity but you can easily use any wallet type. -Additional gas refers to: +Let's see a simple example: -- Transaction payload cost: Data field length \* 1500 (GasPerDataByte = 1500) -- Storage cost: Size of NFT data \* 50000 (StorePerByte = 50000) +```sh +mxpy contract deploy --bytecode ~/contracts/adder/output/adder.wasm \ + --proxy=https://devnet-gateway.multiversx.com \ + --arguments 0 --gas-limit 5000000 \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --send +``` -To see more about the required fields, please refer to [this](/tokens/nft-tokens#nftsft-fields) section. +The `--proxy` is used to specify the url of the proxy and the `--chain` is used to select the network the contract will be deployed to. The chain ID and the proxy need to match for our transaction to be executed. We can't prepare a transaction for the Devnet (using `--chain D`) and send it using the mainnet proxy (https://gateway.multiversx.com). -:::tip -Note that because NFTs are stored in accounts trie, every transaction involving the NFT will require a gas limit depending on NFT data size. -::: +The `--arguments` is used in case our contract needs any arguments for the initialization. We know our `adder` needs a value to start adding from, so we set that to `0`. -Most of the times you will be able to create the NFTs by issuing one single transaction. -This assumes that the metadata file as well as the NFT media is already uploaded to IPFS. +The `--gas-limit` is used to set the gas we are willing to pay so our transaction will be executed. 5 million gas is a bit too much because our contract is very small and simple, but better to be sure. In case our transaction doesn't have enough gas the network will not execute it, saying something like `Insufficient gas limit`. -There are times, however, when uploading the metadata file before issuing the NFT is not possible (eg. when issued from a smart contract) -In these cases it is possible to update an NFT with the metadata file after it was issued by sending an additional transaction. You can find more information [here](/tokens/nft-tokens#change-nft-attributes) about how to update the attributes +The `--pem` argument is used to provide the sender of the transaction, the payer of the fee. The sender will also be the owner of the contract. The nonce of the sender is fetched from the network if the `--proxy` argument is provided. The nonce can also be explicitly set using the `--nonce` argument. If the nonce is explicitly set, mxpy will not fetch the nonce from the network. -## **Other management operations** +### Deploying a smart contract providing the ABI file -Managing non-fungible tokens (NFTs) and semi-fungible tokens (SFTs) presents a greater degree of complexity compared to the management of simple fungible tokens. These unique tokens require specialized transactions for proper identification, ownership, and transfer, making the process of managing them more intricate than that of fungible tokens. +For functions that have complex arguments, we can use the ABI file generated when building the contract. The ABI can be provided using the `--abi` argument. When using the ABI, and only when using the ABI, the arguments should be written in a `json` file and should be provided via the `--arguments-file` argument. +For this example, we'll use the [multisig contract](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/multisig). -### **Transfer NFT Creation Role** +First, we'll prepare the file containing the constructors arguments. We'll refer to this file as `deploy_multisig_arguments.json`. The constructor requires two arguments, the first is of type `u32` and the second one is of type `variadic
`. All the arguments in this file **should** be placed inside a list. The arguments file should look like this: -:::tip -This role can be transferred only if the `canTransferNFTCreateRole` property of the token is set to `true`. -::: +```json +[ + 2, + [ + { + "bech32": "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" + }, + { + "hex": "8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8" + } + ] +] +``` -The role of creating an NFT can be transferred by a Transaction like this: +Let's go a bit through our file and see why it looks like this. First, as mentioned above, we have to place all the arguments inside a list. Then, the value `2` corresponds to the type `u32`. After that, we have another list that corresponds to the type `variadic`. Inside this list, we need to insert our addresses. For `mxpy`to encode addresses properly, we need to provide the address values inside a dictionary that can contain two keys: we can provide the address as the `bech32` representation or as the `hex encoded` public key. -```rust -TransferCreationRoleTransaction { - Sender:
- Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 0 - GasLimit: 60000000 + length of Data field in bytes * 1500 - Data: "transferNFTCreateRole" + - "@" + + - "@" + + - "@" + -} +After finishing the arguments file, we can run the following command to deploy the contract: + +```sh +mxpy contract deploy --bytecode ~/contracts/multisig/output/multisig.wasm \ + --proxy=https://devnet-gateway.multiversx.com \ + --abi ~/contracts/multisig/output/multisig.abi.json \ + --arguments-file deploy_multisig_arguments.json \ + --gas-limit 500000000 \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --send ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +## Calling the Smart Contract -### **Stop NFT creation** +After deploying our smart contract we can start interacting with it. The contract has a function called `add()` that we can call and it will increase the value stored in the contract with the value we provide. -The ESDT manager can stop the creation of an NFT for the given ESDT forever by removing the only `ESDTRoleNFTCreate` role available. -This is done by performing a transaction like this: +To call a function we use the `mxpy contract call` command. Here's an example of how we can do that: -```rust -StopNFTCreationTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 0 - GasLimit: 60000000 - Data: "stopNFTCreate" + - "@" + + -} +```sh +mxpy contract call erd1qqqqqqqqqqqqqpgq3zrpqj3sulnc9xq95sljetxhf9s07pqtd8ssfkxjv4 \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --proxy=https://devnet-gateway.multiversx.com --chain D \ + --function add --arguments 5 --gas-limit 1000000 \ + --send ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - +The positional argument is the contract address that we want to interact with. The `--pem`, `--proxy` and `--chain` arguments are used the same as above in the deploy transaction. -### **Change NFT Attributes** +Using the `--function` argument we specify the function we want to call and with the `--arguments` argument we specify the value we want to add. We set the gas we are willing to pay for the transaction and finally we send the transaction. -An user that has the `ESDTRoleNFTUpdateAttributes` role set for a given ESDT, can change the attributes of a given NFT/SFT. -:::tip -`ESDTNFTUpdateAttributes` will remove the old attributes and add the new ones. Therefore, if you want to keep the old attributes you will have to pass them along with the new ones. -::: -This is done by performing a transaction like this: +### Calling the smart contract providing the ABI file -```rust -ESDTNFTUpdateAttributesTransaction { - Sender:
- Receiver: - Value: 0 - GasLimit: 10000000 - Data: "ESDTNFTUpdateAttributes" + - "@" + + - "@" + + - "@" + -} -``` +Same as we did for deploying the contract, we can call functions by providing the ABI file and the arguments file. -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +Since we deployed the [multisig contract](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/multisig), we'll call the `proposeTransferExecute` endpoint. -To see how you can assign this role in case it is not set, please refer to [this](/tokens/nft-tokens#assigning-roles) section. +First, we'll prepare the file containing the endpoints arguments. We'll refer to this file as `call_multisig_arguments.json`. The `proposeTransferExecute` endpoint requires four arguments, the first is of type `Address`, the second one is of type `BigUInt`, the third is of type `Option` and the fourth is of type `variadic`. All the arguments in this file **should** be placed inside a list. The arguments file should look like this: +```json +[ + { + "bech32": "erd1qqqqqqqqqqqqqpgqs63rcpahnwtjnedj5y6uuqh096nzf75gczpsc4fgtu" + }, + 1000000000000000000, + 5000000, + [ + { + "hex": "616464403037" + } + ] +] +``` -### **Add URIs to NFT** +Let's go a bit through our file and see why it looks like this. First, as mentioned above, we have to place all the arguments inside a list. Then, the contract expects an address, so we provide the `bech32` representation. After that, we have a `BigUInt` value that we can provide as a number. The third value is `Option`, so we provide it as a number, as well. In case we wanted to skip this value, we could've simply used `0`. The last parameter is of type `variadic`. Because it's a variadic value, we have to place the arguments inside a list. Since we can't write bytes, we `hex encode` the value and place it in a dictionary containing the key-value pair `"hex": ""`, same as we did above for the address. -An user that has the `ESDTRoleNFTAddURI` role set for a given ESDT, can add uris to a given NFT/SFT. -This is done by performing a transaction like this: +After finishing the arguments file, we can run the following command to call the endpoint: -```rust -ESDTNFTAddURITransaction { - Sender:
- Receiver: - Value: 0 - GasLimit: 10000000 - Data: "ESDTNFTAddURI" + - "@" + + - "@" + + - "@" + + - "@" + + - ... -} +```sh +mxpy contract call erd1qqqqqqqqqqqqqpgqjsg84gq5e79rrc2rm5ervval3jrrfvvfd8sswc6xjy \ + --proxy=https://devnet-gateway.multiversx.com \ + --abi ~/contracts/multisig/output/multisig.abi.json \ + --arguments-file call_multisig_arguments.json \ + --function proposeTransferExecute + --gas-limit 500000000 \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --send ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -To see how you can assign this role in case it is not set, please refer to [this](/tokens/nft-tokens#assigning-roles) section. +## Querying the Smart Contract +Querying a contract is done by calling a so called `view function`. We can get data from a contract without sending a transaction to the contract, basically without spending money. -### **Add quantity (SFT only)** +As you know, our contract has a function called `add()` that we previously called, and a `view function` called `getSum()`. Using this `getSum()` function we can see the value that is currently stored in the contract. -A user that has the `ESDTRoleNFTAddQuantity` role set for a given Semi-Fungible Token, can increase its quantity. This function will not work for NFTs, because in that case the quantity cannot be higher than 1. +If you remember, when we deployed the contract we passed the value `0` as a contract argument, this means the contract started adding from `0`. When calling the `add()` function we used the value `5`. This means that now if we call `getSum()` we should get the value `5`. To do that, we use the `mxpy contract query` command. Let's try it! -```rust -AddQuantityTransaction { - Sender:
- Receiver: - Value: 0 - GasLimit: 10000000 - Data: "ESDTNFTAddQuantity" + - "@" + + - "@" + - "@" + -} +```sh +mxpy contract query erd1qqqqqqqqqqqqqpgq3zrpqj3sulnc9xq95sljetxhf9s07pqtd8ssfkxjv4 \ + --proxy https://devnet-gateway.multiversx.com \ + --function getSum ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +We see that `mxpy` returns our value as a base64 string, as a hex number and as a integer. Indee, we see the expected value. -If successful, the balance of the address for the given SFT will be increased with the number specified in the argument. +### Querying the smart contract providing the ABI file -### **Burn quantity** +We'll call the `signed` readonly endpoint of the [multisig contract](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/multisig). This endpoint accepts two arguments: the first is the address, and the second is the proposal ID, which will be used to verify if the address has signed the proposal. The endpoint returns a `boolean` value, `true` if the address has signed the proposal and `false` otherwise. -A user that has the `ESDTRoleNFTBurn` role set for a given semi-fungible Token, can burn some (or all) of the quantity. +Let's prepare the arguments file. The first argument is of type `Address` and the second one is of type `u32`, so our file looks like this: -```rust -BurnQuantityTransaction { - Sender:
- Receiver: - Value: 0 - GasLimit: 10000000 - Data: "ESDTNFTBurn" + - "@" + + - "@" + - "@" + -} +```json +[ + { + "bech32": "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx" + }, + 1 +] ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +As above, we encapsulate the address in a dictionary and the `u32` value is simply a number. We'll refer to this file as `query_multisig_arguments.json`. -If successful, the quantity from the argument will be decreased from the balance of the address for that given token. +After preparing the file, we can run the following command: +```sh +mxpy contract query erd1qqqqqqqqqqqqqpgqjsg84gq5e79rrc2rm5ervval3jrrfvvfd8sswc6xjy \ + --proxy https://devnet-gateway.multiversx.com \ + --function signed \ + --abi ~/contracts/multisig/output/multisig.abi.json \ + --arguments-file query_multisig_arguments.json +``` -### **Freezing and Unfreezing a single NFT** -The manager of an ESDT token may freeze the NFT held by a specific Account. As a consequence, no NFT can be transferred to or from the frozen Account. Freezing and unfreezing a single NFT of an Account are operations designed to help token managers to comply with regulations. The transaction that freezes a single NFT of an Account has the form: +## Upgrading a Smart Contract -```rust -FreezeTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 0 - GasLimit: 60000000 - Data: "freezeSingleNFT" + - "@" + + - "@" + - "@" + -} -``` +In case there's a new release of your Smart Contract, or perhaps you've patched a possible vulnerability you can upgrade the code of the Smart Contract deployed on the network. -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +We've modified our adder contract to add `1` to every value added to the contract. Now every time the `add()` function is called will add the value provided with `1`. In order to do that we access the source code and navigate to the `add()` endpoint. We can see that `value` is added to `sum` each time the endpoint is called. Modify the line to look something like this `self.sum().update(|sum| *sum += value + 1u32);` -The reverse operation, unfreezing, will allow further transfers to and from the Account: +Before deploying the contract we need to build it again to make sure we are using the latest version. We then deploy the newly built contract, then we call it and query it. -```rust -UnfreezeTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 0 - GasLimit: 60000000 - Data: "unFreezeSingleNFT" + - "@" + + - "@" + + - "@" + -} +First we build the contract: + +```sh +sc-meta all build ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +Then we upgrade the contract by running: +```sh +mxpy contract upgrade erd1qqqqqqqqqqqqqpgq3zrpqj3sulnc9xq95sljetxhf9s07pqtd8ssfkxjv4 \ + --bytecode ~/contracts/adder/output/adder.wasm \ + --proxy=https://devnet-gateway.multiversx.com --chain D \ + --arguments 0 --gas-limit 5000000 \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --send +``` -### **Wiping a single NFT** +We provide as a positional argument the contract's address that we want to upgrade, in our case the previously deployed adder contract. The `--bytecode` is used to provide the new code that will replace the old code. We also set the `--arguments` to `0` as we didn't change the constructor and the contract will start counting from `0` again. The rest of the arguments you know from all the previous operations we've done. -The manager of an ESDT token may wipe out a single NFT held by a frozen Account. This operation is similar to burning the quantity, but the Account must have been frozen beforehand, and it must be done by the token manager. Wiping the tokens of an Account is an operation designed to help token managers to comply with regulations. Such a transaction has the form: +As shown above, we can also upgrade the contract by providing the ABI file and the arguments file: -```rust -WipeTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 0 - GasLimit: 60000000 - Data: "wipeSingleNFT" + - "@" + + - "@" + + - "@" + -} +```sh +mxpy contract upgrade erd1qqqqqqqqqqqqqpgq3zrpqj3sulnc9xq95sljetxhf9s07pqtd8ssfkxjv4 \ + --bytecode ~/contracts/adder/output/adder.wasm \ + --proxy=https://devnet-gateway.multiversx.com --chain D \ + --gas-limit 5000000 \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --abi=~/contracts/multisig/output/multisig.abi.json, + --arguments-file=upgrade_arguments.json + --send ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - +Now let's add `5` to the contract one more time. We do so by running the following: -### **Modify Royalties** +```sh +mxpy contract call erd1qqqqqqqqqqqqqpgq3zrpqj3sulnc9xq95sljetxhf9s07pqtd8ssfkxjv4 \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --proxy=https://devnet-gateway.multiversx.com --chain D \ + --function add --arguments 5 --gas-limit 1000000 \ + --send +``` -The manager of an ESDT token may want to set new royalities. This operation will rewrite the royalities on the specified token ID. -It requires `ESDTRoleNFTModifyRoyalties` role. -This is done by performing a transaction like this: +Now, if we query the contract we should see the value `6`. We added `5` in the contract but modified the contract code to add `1` to every value. Let's see! -```rust -ModifyRoyalitiesTransaction { - Sender: - Receiver: - Value: 0 - GasLimit: 60000000 - Data: "ESDTModifyRoyalties" + - "@" + + - "@" + + - "@" + -} +```sh +mxpy contract query erd1qqqqqqqqqqqqqpgq3zrpqj3sulnc9xq95sljetxhf9s07pqtd8ssfkxjv4 --proxy https://devnet-gateway.multiversx.com --function getSum ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - +We see that we indeed got the value `6`. Our upgrade was successful. -### **Set new URIs** -The manager of an ESDT token may want to set new URIs. This operation will rewrite the URIs on the specified token ID. -It requires `ESDTRoleNFTSetNewURIs` role. -This is done by performing a transaction like this: +## Verifying a smart contract -```rust -SetNewURIsTransaction { - Sender: - Receiver: - Value: 0 - GasLimit: 60000000 - Data: "ESDTSetNewURIs" + - "@" + + - "@" + + - "@" + + - "@" + + - ... -} -``` +Verifying a smart contract means ensuring that the contract deployed on the network matches a specific version of the original source code. That is done by an external service that, under the hood, performs a reproducible build of the given contract and compares the resulting bytecode with the one deployed on the network. -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +To learn more about reproducible builds, please follow [**this page**](/developers/reproducible-contract-builds). If you'd like to set up a Github Workflow that performs a reproducible build of your smart contract, follow the examples in [**this repository**](https://github.com/multiversx/mx-contracts-rs). +The command used for verifying contracts is: -### **Modify Creator** +```sh +mxpy contract verify +``` -The creator of a token can be changed. For this, the token has to be moved to the new creator account. The new creator -account requires `ESDTRoleModifyCreator` role. Also, the token has to be of dynamic type in order for this to work. +Let's see an example: -The creator can be modified using a transaction like this: +```sh +export CONTRACT_ADDRESS="erd1qqqqqqqqqqqqqpgq6eynj8xra5v87qqzpjhc5fnzzh0fqqzld8ssqrez2g" -```rust -ModifyCreatorTransaction { - Sender: - Receiver: - Value: 0 - GasLimit: 60000000 - Data: "ESDTModifyCreator" + - "@" + + - "@" + -} +mxpy --verbose contract verify ${CONTRACT_ADDRESS} \ + --packaged-src=adder-0.0.0.source.json \ + --verifier-url="https://devnet-play-api.multiversx.com" \ + --docker-image="multiversx/sdk-rust-contract-builder:v8.0.1" \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +:::info +The account that triggers the code verification process must be the owner of the contract. +::: +:::info +The _packaged source_ passed as `--packaged-src` can be obtained either from [the Github Workflows for reproducible builds](https://github.com/multiversx/mx-contracts-rs/tree/main/.github/workflows) set up on your own repository, or from locally invoking a reproducible build, as depicted [here](https://docs.multiversx.com/developers/reproducible-contract-builds/#reproducible-build-using-mxpy). +::: -### **MetaData Update** -The manager of an ESDT token may want to update token metadata. This operation will update token metadata on the specified token ID. -It requires `ESDTRoleNFTUpdate` role. If nothing is received for a given attribute, the old version of that attribute will be kept. +## Creating and sending transactions -This is done by performing a transaction like this: +To create a new transaction we use the `mxpy tx new` command. Let's see how that works: -```rust -MetaDataUpdateTransaction { - Sender: - Receiver: - Value: 0 - GasLimit: 60000000 - Data: "ESDTMetaDataUpdate" + - "@" + + - "@" + + - "@" + + - "@" + + - "@" + + - "@" + + - "@" + -} +```sh +mxpy tx new --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --receiver erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx \ + --gas-limit 50000 --value 1000000000000000000 \ + --proxy https://devnet-gateway.multiversx.com --chain D \ + --send ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +That's it! As easy as that. We sent a transaction from Alice to Bob. We choose the receiver of our transaction using the `--receiver` argument and set the gas limit to `50000` because that is the gas cost of a simple move balance transaction. Notice we used the `--value` argument to pass the value that we want to transfer but we passed in the denomintated value. We transferred 1 eGLD (1 \* 10^18). We then specify the proxy and the chain ID for the network we want to send our transaction to and use the `--send` argument to broadcast it. +In case you want to save the transaction you can also provide the `--outfile` argument and a `json` file containing the transaction will be saved at the specified location. If you just want to prepare the transaction without broadcasting it simply remove the `--send` argument. -### **MetaData Recreate** -The whole NFT can be recreated with new attributes using a transaction like this: +## Guarded transactions -The manager of an ESDT token may want to recreate the whole token with new attributes. This operation will recreate token attributes on the specified token ID. -It requires `ESDTRoleNFTRecreate` role. If an argument is not being set, that field is set to zero. +If your address is guarded, you'll have to provide some additional arguments because your transaction needs to be co-signed. -This is done by performing a transaction like this: +The first extra argument we'll need is the `--guardian` argument. This specifies the guardian address of our address. Then, if our account is guarded by a service like our trusted co-signer service we have to provide the `--guardian-service-url` which specifies where the transaction is sent to be co-signed. -```rust -MetaDataRecreateTransaction { - Sender: - Receiver: - Value: 0 - GasLimit: 60000000 - Data: "ESDTMetaDataRecreate" + - "@" + + - "@" + + - "@" + + - "@" + + - "@" + + - "@" + + - "@" + -} -``` +Keep in mind that **mxpy** always calls the `/sign-transaction` endpoint of the `--guardian-service-url` you have provided. Another argument we'll need is `--guardian-2fa-code` which is the code generated by an external authenticator. -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +Each guarded transaction needs an additional `50000` gas for the `gasLimit`. The `version` field needs to be set to `2`. The `options` field needs to have the second least significant bit set to "1". +:::note +Here are the urls to our hosted co-signer services: -### **Make token dynamic** +- Mainnet: [https://tools.multiversx.com/guardian](https://tools.multiversx.com/guardian) +- Devnet: [https://devnet-tools.multiversx.com/guardian](https://devnet-tools.multiversx.com/guardian) +- Testnet: [https://testnet-tools.multiversx.com/guardian](https://testnet-tools.multiversx.com/guardian) -The ESDT manager can change token type to dynamic using a transaction like this: +::: -```rust -ChangeToDynamicTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 0 - GasLimit: 60000000 - Data: "changeToDynamic" + - "@" + -} +```sh +mxpy tx new --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --receiver erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx \ + --gas-limit 200000 --value 1000000000000000000 \ + --proxy https://devnet-gateway.multiversx.com --chain D \ + --guardian erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8 \ + --guardian-service-url https://devnet-tools.multiversx.com/guardian \ + --guardian-2fa-code 123456 --version 2 --options 2 + --send ``` -The following token types cannot be changed to dynamic: `FungibleESDT`, `NonFungibleESDT`, `NonFungibleESDTv2` - -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - +If your address is guarded by another wallet, you'll still need to provide the `--guardian` argument and the guardian's wallet that will co-sign the transaction, but you don't need to provide the 2fa code and the service url. You can provide the guardian's wallet using one of the following arguments: `--guardian-pem`, `--guardian-keyfile`, or `--guardian-ledger`. -### **Update token** -The token type can be updated to the latest version, which will update token type and propagate it to shard's -system account. Currently, token type is correctly saved only on metachain and there is no type related information -on shard level, the shard only knows if the token is non fungible with implicit type `NonFungibleESDT`, but it -does not know specifically if it's `NonFungibleESDT`, `MetaESDT` or `SemiFungibleESDT`. So, the update operation will -proceed in the following way: -- if token type is `NonFungibleESDT` it will be set to `NonFungibleESDTv2`, and it will be propagated to shard's system account -- if token type is `MetaESDT` or `SemiFungibleESDT` it will be propagated to shard's system account +## Relayed transactions V3 -This can be done using a transaction like this: +Relayed transactions are transactions with the fee paid by a so-called relayer. In other words, if a relayer is willing to pay for a transaction, it is not mandatory for the sender to have any EGLD for fees. To learn more about relayed transactions check out [this page](/developers/relayed-transactions/). -```rust -UpdateTokenIDTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 0 - GasLimit: 60000000 - Data: "updateTokenID" + - "@" + -} -``` +In this section we'll see how we can send `Relayed V3` transactions using `mxpy`. For a more detailed look on `Relayed V3` transactions, take a look [here](/developers/relayed-transactions/#relayed-transactions-version-3). For these kind of transactions two new transaction fields were introduced, `relayer` and `relayerSignature`. In this example we'll see how we can create the relayed transaction. -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +For this, a new command `mxpy tx relay` has been added. The command can be used to relay a previously signed transaction. The saved transaction can be loaded from a file using the `--infile` argument. +There are two options when creating the relayed transaction: +1. Create the relayed transaction separately. (Sender signature and relayer signature are added by different entities.) +2. Create the complete relayed transaction. (Both signatures are added by the same entity.) -### **Register dynamic token** +### Creating the inner transaction -A token can be registered directly as dynamic. +The inner transaction is any regular transaction, with the following notes: +- relayer address must be added +- extra 50000 (base cost) gas must be added for the relayed operation. For more details on how the gas is computed, check out [this page](/developers/relayed-transactions/#relayed-transactions-version-3). -This can be done using a transaction like this: +This can be generated through `mxpy tx new` command. A new argument `--relayer` has been added for this feature. -```rust -RegisterDynamicTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 50000000000000000 # (0.05 EGLD) - GasLimit: 60000000 - Data: "registerDynamic" + - "@" + + - "@" + + - "@" + - # For META token type only - "@" + - # (number of decimals) -} +```sh +mxpy tx new --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --receiver erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx \ + --gas-limit 100000 --value 1000000000000000000 \ + --proxy https://devnet-gateway.multiversx.com --chain D \ + --relayer erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8 \ + --outfile inner_tx.json ``` -The following token types cannot be registered as dynamic: `FungibleESDT` +After creating the inner transaction, we are ready to create the relayed transaction. +### Creating the relayed transaction -### **Register and set all roles to dynamic** +We can create the relayed transaction by running the following command: -A token can be registered directly as dynamic together will all roles set for the specific type. +```sh +mxpy tx relay --relayer-pem ~/multiversx-sdk/testwallets/latest/users/carol.pem \ + --proxy https://devnet-gateway.multiversx.com --chain D \ + --infile inner_tx.json \ + --send +``` -This can be done using a transaction like this: +### Creating the relayed transaction in one step -```rust -RegisterAndSetAllRolesDynamicTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 50000000000000000 # (0.05 EGLD) - GasLimit: 60000000 - Data: "registerAndSetAllRolesDynamic" + - "@" + + - "@" + + - "@" + + - # For META token type only - "@" + - # (number of decimals) -} +This can be done through `mxpy tx new` command, as follows: +```sh +mxpy tx new --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --receiver erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx \ + --relayer erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8 \ + --relayer-pem ~/multiversx-sdk/testwallets/latest/users/carol.pem \ + --gas-limit 100000 --value 1000000000000000000 \ + --proxy https://devnet-gateway.multiversx.com --chain D \ + --send ``` -The following token types cannot be registered as dynamic: `FungibleESDT` +## Using the Ledger hardware wallet -### **Transferring token management rights** +You can sign any transaction (regular transfers, smart contract deployments and calls) using a Ledger hardware wallet by leveraging the `--ledger` command-line argument. -The manager of an ESDT token can transfer the ownership if the ESDT was created as upgradable. Check the [ESDT - Upgrading (changing properties)](/tokens/fungible-tokens#upgrading-changing-properties) section for more details. +First, connect your device to the computer, unlock it and open the MultiversX Ledger app. +Then, you can perform a trivial connectivity check by running: -### **Upgrading (changing properties)** +```sh +mxpy ledger version +``` -The manager of an ESDT token may individually change any of the properties of the token, or multiple properties at once, only if the ESDT was created as upgradable. -Check the [ESDT - Transferring token management rights](/tokens/fungible-tokens#transferring-token-management-rights) section for more details. +The output should look like this: +```sh +MultiversX App version: ... +``` -## **Transfers** +Another trivial check is to ask the device for the (first 10) MultiversX addresses it manages: -Performing an ESDT NFT transfer is done by specifying the receiver's address inside the `Data` field, alongside other details. An ESDT NFT transfer transaction has the following form: +```sh +mxpy ledger addresses +``` -```rust -TransferTransaction { - Sender: - Receiver: - Value: 0 - GasLimit: 1000000 + length of Data field in bytes * 1500 - Data: "ESDTNFTTransfer" + - "@" + + - "@" + + - "@" + + - "@" + -} +The output should look like this: + +```sh +account index = 0 | address index = 0 | address: erd1... +account index = 0 | address index = 1 | address: erd1... +account index = 0 | address index = 2 | address: erd1... +... ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +Now let's sign and broadcast a transaction (EGLD transfer): -:::tip -Here is an example of an NFT identifier: `ABC-1a9c7d-05dc` +```sh +mxpy tx new --proxy https://devnet-gateway.multiversx.com \ + --receiver erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th \ + --gas-limit 50000 --value 1000000000000000000 \ + --ledger \ + --send +``` -The collection identifier is `ABC-1a9c7d` and the NFT nonce is `05dc`. Note that the `05dc` is hexadecimal encoded, it represents decimal 1500. +By default, the first MultiversX address managed by the device is used as the sender (signer) of the transaction. In order to select a different address, you can use the `--ledger-address-index` CLI parameter: -Also note that a MultiversX address is in bech32, so you will need to convert the address from bech32 to hexadecimal. This can be done with the `hex()` method of mx-sdk-js-core for address (all the methods for addresses can be found [here](https://github.com/multiversx/mx-sdk-js-core/blob/main/src/address.ts)) or manually with an external converter which you can find [here.](https://utils.multiversx.com/converters#addresses-bech32-to-hexadecimal) +```sh +mxpy tx new --proxy https://devnet-gateway.multiversx.com \ + --receiver erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th \ + --gas-limit 50000 --value 1000000000000000000 \ + --ledger --ledger-address-index=42 \ + --send +``` + +:::info +For MultiversX, **the account index should always be `0`**, while the address index is allowed to vary. Therefore, you should not use the `--ledger-account-index` CLI parameter (it will be removed in a future release). ::: +Now let's deploy a smart contract using the Ledger: -## **Transfers to a Smart Contract** +```sh +mxpy contract deploy --proxy=https://devnet-gateway.multiversx.com \ + --bytecode=adder.wasm --gas-limit=5000000 \ + --ledger --ledger-address-index=42 \ + --send +``` -To perform the transfer from your account to the smart contract, you have to use the following transaction format: +Then, perform a contract call: -```rust -TransferTransaction { - Sender: - Receiver: - Value: 0 - GasLimit: 1000000 + extra for smart contract call - Data: "ESDTNFTTransfer" + - "@" + + - "@" + + - "@" + + - "@" + + - "@" + + - "@" + + - "@" + + - <...> -} +```sh +mxpy contract call erd1qqqqqqqqqqqqqpgqwwef37kmegph97egvvrxh3nccx7xuygez8ns682zz0 \ + --proxy=https://devnet-gateway.multiversx.com \ + --function add --arguments 42 --gas-limit 5000000 \ + --ledger --ledger-address-index=42 \ + --send ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +:::note +As of October 2023, on Windows (or WSL), you might encounter some issues when trying to use Ledger in `mxpy`. +::: +## Interacting with the Multisig Smart Contract -## **Multiple tokens transfer** +As of `mxpy v11`, interacting with Multisig contracts has become a lot easier because a dedicated command group called `mxpy multisig` has been added. We can deploy a multisig contract, create proposals and query the contract. For a full list of all the possible actions, run the following command: -Multiple semi-fungible and/or non-fungible tokens can be transferred in a single transaction to a single receiver. +```sh +mxpy multisig -h +``` -More details can be found [here](/tokens/fungible-tokens#multiple-tokens-transfer). +#### Deploying a multisig contract +```sh +mxpy multisig deploy \ + --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --bytecode path/to/multisig.wasm \ + --abi path/to/multisig.abi.json + --quorum 2 + --board-members erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx + --proxy=https://devnet-gateway.multiversx.com \ + --gas-limit 100000000 \ + --send +``` -## **Example flow** +#### Creating a new proposal -Let's see a complete flow of creating and transferring a Semi-Fungible Token. +```sh +mxpy multisig add-board-member --contract erd1qqqqqqqqqqqqqpgq2ukrsg73nwgu3uz6sp8vequuyrhtv2akd8ssyrg7wj \ + --abi path/to/multisig.abi.json + --board-member erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8 \ + --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --proxy=https://devnet-gateway.multiversx.com \ + --gas-limit 10000000 \ + --send +``` -**Step 1: Issue/Register a Semi-Fungible Token** +#### Querying the contract -```rust -{ - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 50000000000000000 # 0.05 EGLD - GasLimit: 60000000 - Data: "issueSemiFungible" + - "@416c696365546f6b656e73" + # AliceTokens - "@414c43" + # ALC -} +```sh +mxpy multisig get-proposers --contract erd1qqqqqqqqqqqqqpgq2ukrsg73nwgu3uz6sp8vequuyrhtv2akd8ssyrg7wj \ + --abi path/to/multisig.abi.json + --proxy=https://devnet-gateway.multiversx.com ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - -**Step 2: Fetch the token identifier** +## Interacting with the Governance Smart Contract -For doing this, one has to check the previously sent transaction and see the Smart Contract Result of it. -It will look similar to `@ok@414c432d317132773365`. The `414c432d317132773365` represents the token identifier in hexadecimal encoding. +As of `mxpy v11`, mxpy allows for easier interaction with the Governance smart contract. We can create a new governance proposal, vote for a proposal and query the contract. For a full list of the available commands, run the following command: -**Step 3: Set roles** +```sh +mxpy governance -h +``` -Assign `ESDTRoleNFTCreate` and `ESDTRoleNFTAddQuantity` roles to an address. You can set these roles to your very own address. +#### Creating a new proposal -```rust -{ - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 0 - GasLimit: 60000000 - Data: "setSpecialRole" + - "@414c432d317132773365" + # previously fetched token identifier - "@" +
+ - "@45534454526f6c654e4654437265617465" + # ESDTRoleNFTCreate - "@45534454526f6c654e46544164645175616e74697479" # ESDTRoleNFTAddQuantity - ... -} +```sh +mxpy governance propose \ + --commit-hash 30118901102b0bef11d675f4327565ae5246eeb5 \ + --start-vote-epoch 1000 \ + --end-vote-epoch 1010 \ + --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --proxy=https://devnet-gateway.multiversx.com \ + --gas-limit 100000000 \ + --send ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +#### Voting for a proposal -**Step 4: Create NFT** +```sh +mxpy governance vote \ + --proposal-nonce 1 \ + --vote yes \ + --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --proxy=https://devnet-gateway.multiversx.com \ + --gas-limit 100000000 \ + --send +``` -Now, the NFT creation transaction for the example case defined [here](/tokens/nft-tokens#creation-of-an-nft) looks like this: +#### Querying the contract -```rust -{ - Sender:
- Receiver: - Value: 0 - GasLimit: 3000000 - Data: "ESDTNFTCreate" + - "@414c432d317132773365" + # previously fetched token identifier - "@01" + # quantity: 1 - "@42656175746966756c20736f6e67" + # NFT name: 'Beautiful song' in hexadecimal encoding - "@1d4c" + # Royalties: 7500 =75%c in hexadecimal encoding - "@00" + # Hash: 00 in hexadecimal encoding - "@6d657461646174613a697066734349442f736f6e672e6a736f6e3b746167733a736f6e672c62656175746966756c2c6d75736963" + # Attributes: metadata:ipfsCID/song.json;tags:song,beautiful,music in hexadecimal encoding> + - "@55524c5f746f5f646563656e7472616c697a65645f73746f726167652f736f6e672e6d7033" + # URI: URL_to_decentralized_storage/song.mp3 in hexadecimal encoding> + - "@" + + -} +```sh +mxpy governance get-proposal-info \ + --proposal-nonce 1 \ + --proxy=https://devnet-gateway.multiversx.com ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -:::tip -Note that the nonce is very important when creating an NFT. You must save the nonce after NFT creation because you will need it for further actions. +## Token Management Operations -The `NFT nonce` is different from the creator's nonce. +User can now perform token management operations, such as issuing fungible tokens, issuing semi-fungible tokens, creating NFTs and more, directly via `mxpy`. For a full list of available commands type: -It can be fetched by viewing all the tokens for the address via API. -::: +```sh +mxpy token -h +``` -**Step 5: Transfer** +#### Issue a fungible token -```rust -{ - Sender: - Receiver: - Value: 0 - GasLimit: 1000000 + length of Data field in bytes * 1500 - Data: "ESDTNFTTransfer" + - "@414c432d317132773365" + # previously fetched token identifier - "@" + + - "@" + + - "@" + -} +```sh +mxpy token issue-fungible \ + --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --token-name test --token-ticker TEST \ + --initial-supply 1000000000000 \ + --num-decimals 6 \ + --proxy=https://devnet-gateway.multiversx.com \ + --send ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +#### Pause a token +```sh +mxpy token pause \ + --token-identifier TEST-123456 \ + --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --proxy=https://devnet-gateway.multiversx.com \ + --send +``` -## **REST API** +#### Set special roles on fungible tokens -There are a number of API endpoints that one can use to interact with ESDT NFT data. These are: +```sh +mxpy token set-special-role-fungible \ + --token-identifier TEST-123456 \ + --user erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx \ + --local-mint \ + --local-burn \ + --esdt-transfer-role \ + --pem ~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --proxy=https://devnet-gateway.multiversx.com \ + --send +``` +--- -### GET **Get NFT data for an address** {#get-nft-data-for-an-address} +### Native auth, token issuance, expiry, auto-logout - - +What `nativeAuth` actually gives you once configured: a bearer token issued +automatically on login, an automatic warning toast before it expires, and an +automatic logout when it does, all scheduled by `LogoutManager`, none of it wired +up by hand anywhere in this recipe. -Returns the balance of an address for specific ESDT Tokens. +Use this recipe once you already have login working (see +[Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button)) +and need to understand what to do with the resulting token, or need to control +the auto-logout behavior. Do not use it if you have not enabled `nativeAuth` at +all; the config itself is a one-line `initApp()` option, covered in "Configuring +native auth" below. -```bash -https://gateway.multiversx.com/address//nft//nonce/ -``` +## Prerequisites -| Param | Required | Type | Description | -| --------------- | ----------------------------------------- | --------- | -------------------------------------- | -| bech32Address | REQUIRED | `string` | The Address to query in bech32 format. | -| tokenIdentifier | REQUIRED | `string` | The token identifier. | -| nonce | REQUIRED | `numeric` | The nonce after the NFT creation. | +- A working sdk-dapp v5 setup (Next.js or Vite, see the start-here recipes). +- Any wallet provider to log in with. This recipe uses the generic + `UnlockPanelManager` picker. - - +## Install -```json -{ - "data": { - "tokenData": { - "attributes": "YXR0cmlidXRl", - "balance": "2", - "creator": "erd1ukn0tukrdhuv0zzxn0zlr53g7h0fr68dz9dd56mkksev59nwuvnswnlyuy", - "hash": "aGFzaA==", - "name": "H", - "nonce": 1, - "properties": "", - "royalties": "9000", - "tokenIdentifier": "4W97C-32b5ce", - "uris": ["bmZ0IHVyaQ=="] - } - }, - "error": "", - "code": "successful" -} -``` +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/native-auth +npm install +npm run dev +# open https://localhost:5173 +``` + +## Configuring native auth + +```ts title="src/lib/multiversx.ts" +// src/lib/multiversx.ts — environment configuration for sdk-dapp's initApp, +// with an explicit (not just `nativeAuth: true`) native auth config so every +// field is visible and documented in one place. +// +// NativeAuthConfigType (node_modules/@multiversx/sdk-dapp/out/services/nativeAuth/nativeAuth.types.d.ts): +// every field is optional; omitted fields fall back to +// getDefaultNativeAuthConfig() (.../services/nativeAuth/methods/getDefaultNativeAuthConfig.cjs): +// origin -> window.location.origin +// apiAddress -> the configured network's API address +// expirySeconds -> 86400 (24h) +// tokenExpirationToastWarningSeconds -> 300 (5 min) +// +// This recipe deliberately overrides both time values to a couple of +// minutes so the auto-logout warning toast and the actual auto-logout are +// both observable within one `npm run dev` session, instead of requiring a +// 24-hour wait. A real dApp should use the defaults (or its own security +// policy's value) — do not ship NATIVE_AUTH_EXPIRY_SECONDS this short. +export const NATIVE_AUTH_EXPIRY_SECONDS = 120; +export const NATIVE_AUTH_WARNING_SECONDS = 30; - - +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +// This recipe uses UnlockPanelManager's generic picker (see providers.tsx), +// which offers every registered provider including WalletConnect — so the +// project ID is still needed here even though the recipe's own point is +// native auth, not any one specific provider. Same demo ID used across the +// rest of the corpus; register your own at https://cloud.walletconnect.com. +const WALLET_CONNECT_PROJECT_ID = + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; + +export const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + nativeAuth: { + expirySeconds: NATIVE_AUTH_EXPIRY_SECONDS, + tokenExpirationToastWarningSeconds: NATIVE_AUTH_WARNING_SECONDS, + }, + // `theme` is a ThemesEnum member (runtime value 'mvx:dark-theme'), not a + // bare string — see node_modules/@multiversx/sdk-dapp/out/methods/initApp/initApp.types.d.ts. + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + }, +}; +export { EnvironmentsEnum }; +``` + +## Reading token and expiry state + +```tsx title="src/NativeAuthPanel.tsx" +// src/NativeAuthPanel.tsx — the actual subject of this recipe: reading +// native-auth token state, and the automatic expiry-warning/auto-logout +// behavior that comes with `nativeAuth` for free. +// +// Three things this component demonstrates, all verified against the +// actually-installed @multiversx/sdk-dapp source (not just its .d.ts files): +// +// 1. TOKEN ISSUANCE. Once nativeAuth is configured (src/lib/multiversx.ts) +// and the user logs in, `useGetLoginInfo().tokenLogin.nativeAuthToken` +// is populated automatically — no extra call needed. `TokenLoginType` +// (node_modules/@multiversx/sdk-dapp/out/types/login.types.d.ts) shows +// the field name; it's a bearer token, safe to attach to `Authorization: +// Bearer ` headers on your own backend's API calls. +// +// 2. EXPIRY IS AUTOMATIC, NOT SOMETHING YOU SCHEDULE YOURSELF. +// `DappProvider.login()` calls `LogoutManager.getInstance().init()` as +// its last step (.../providers/DappProvider/DappProvider.cjs). That +// schedules, purely from the values you passed as `nativeAuth` config: +// - a warning toast (`toastId: 'native-auth-expired'`) at +// `tokenExpirationToastWarningSeconds` before real expiry +// - a "Logging out" toast (`toastId: 'native-auth-logout'`) 3 seconds +// before the actual logout +// - the actual logout — `getAccountProvider().logout()` — at +// `expirySeconds` after login +// (.../managers/LogoutManager/LogoutManager.cjs). None of this is wired +// up in this component; it happens because nativeAuth was configured. +// +// 3. `LogoutManager.getInstance().stop()` is the ONLY public knob. It +// clears all three scheduled timers. There's no public "extend the +// session" call — re-arming means calling `.init()` again, which +// re-reads the CURRENT token's real expiry from the store and +// reschedules from there (it does not mint a new token or change +// `expirySeconds` itself). + +import { useGetLoginInfo } from '@multiversx/sdk-dapp/out/react/loginInfo/useGetLoginInfo'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { LogoutManager } from '@multiversx/sdk-dapp/out/managers/LogoutManager/LogoutManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import { NATIVE_AUTH_EXPIRY_SECONDS, NATIVE_AUTH_WARNING_SECONDS } from './lib/multiversx'; -### GET **Get NFTs/SFTs registered by an address** {#get-nftssfts-registered-by-an-address} +export function NativeAuthPanel(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const { tokenLogin, providerType, loginExpiresAt } = useGetLoginInfo(); - - + if (!isLoggedIn) { + return

Log in to see the issued native-auth token and its expiry state.

; + } -Returns the identifiers of the tokens that have been registered by the provided address. + const token = tokenLogin?.nativeAuthToken; -```bash -https://gateway.multiversx.com/address//registered-nfts -``` + const handleLogoutNow = async (): Promise => { + await getAccountProvider().logout(); + }; -| Param | Required | Type | Description | -| ------------- | ----------------------------------------- | -------- | -------------------------------------- | -| bech32Address | REQUIRED | `string` | The Address to query in bech32 format. | + const handleStopAutoLogout = (): void => { + LogoutManager.getInstance().stop(); + }; -
- + const handleRearmAutoLogout = (): void => { + void LogoutManager.getInstance().init(); + }; -```json -{ - "data": { - "tokens": ["ABC-36tg72"] - }, - "error": "", - "code": "successful" + return ( +
+

Native auth state

+
+
providerType
+
+ {providerType ?? 'none'} +
+ +
nativeAuthToken
+
{token ? {truncate(token)} : not issued}
+ +
configured expirySeconds
+
+ {NATIVE_AUTH_EXPIRY_SECONDS} — real per-token TTL, + baked into the token before the wallet signs it + (`services/nativeAuth/nativeAuth.cjs`'s `initialize()` embeds it as + the token string's 3rd dot-separated segment). +
+ +
configured tokenExpirationToastWarningSeconds
+
+ {NATIVE_AUTH_WARNING_SECONDS} — how long before real + expiry the automatic warning toast fires. +
+ +
loginExpiresAt (from useGetLoginInfo)
+
+ {loginExpiresAt ?? 'null'} + {loginExpiresAt !== null && ( + <> + {' '} + ({new Date(loginExpiresAt).toISOString()}) + + )} +
+ Not the same thing as the token's real expiry above.{' '} + This is a separate, generic rolling session ceiling (defaults to + "24 hours from now", already in milliseconds — no ×1000 needed) + maintained by an internal store middleware, unrelated to the + `expirySeconds` value configured for nativeAuth. Confirmed from + `node_modules/@multiversx/sdk-dapp/out/store/middleware/logoutMiddleware.cjs`. + Don't use this field to display "your session expires at X" — it + will disagree with the LogoutManager-driven toasts above whenever + `expirySeconds` isn't ~24h. +
+
+ +
+ + + +
+
+ ); } -``` -
-
+function truncate(value: string): string { + return value.length > 24 ? `${value.slice(0, 12)}…${value.slice(-8)}` : value; +} +const buttonStyle: React.CSSProperties = { + padding: '0.5rem 1rem', + fontSize: '0.9rem', + cursor: 'pointer', +}; -### GET **Get tokens where an address has a given role** {#get-tokens-where-an-address-has-a-given-role} +const dlStyle: React.CSSProperties = { + display: 'grid', + gridTemplateColumns: 'minmax(140px, max-content) 1fr', + columnGap: '1rem', + rowGap: '0.75rem', + fontSize: '0.9rem', +}; +``` - - +## The demo page -Returns the identifiers of the tokens where the given address has the given role. +```tsx title="src/App.tsx" +// src/App.tsx — demo page. Login UI here is the generic picker (same as +// "Adding a wallet login button") — the point of this recipe is what +// NativeAuthPanel shows once you're in, not how you got there. -```bash -https://gateway.multiversx.com/address//esdts-with-role/ -``` +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import { NativeAuthPanel } from './NativeAuthPanel'; -| Param | Required | Type | Description | -| ------------- | ----------------------------------------- | -------- | -------------------------------------- | -| bech32Address | REQUIRED | `string` | The Address to query in bech32 format. | -| role | REQUIRED | `string` | The role to query for. | +export function App(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const account = useGetAccount(); + const { network } = useGetNetworkConfig(); -The role can be one of the roles specified in the documentation (for example: ESDTRoleNFTCreate) + const handleConnect = (): void => { + void UnlockPanelManager.getInstance().openUnlockPanel(); + }; - - + const handleDisconnect = async (): Promise => { + await getAccountProvider().logout(); + }; -```json -{ - "data": { - "tokens": ["ABC-36tg72"] - }, - "error": "", - "code": "successful" + return ( +
+

Native auth: token issuance, expiry, auto-logout

+

+ Network: {network.chainId} +

+ + {isLoggedIn ? ( + <> +

+ Connected as {account.address}. +

+ + + ) : ( + + )} + + +
+ ); } ``` -
-
+## Provider bootstrap +`providers.tsx` calls `initApp()` (with the native-auth config above) once and +gates rendering until the store is ready. -### GET **Parse non/semi fungible tokens transfer logs** {#parse-nonsemi-fungible-tokens-transfer-logs} +```tsx title="src/providers.tsx" +// src/providers.tsx — sdk-dapp v5 init wrapper + UnlockPanelManager setup. +// +// Same shape as the "Adding a wallet login button" recipe — this recipe is +// about what happens AFTER login (the native-auth token and its automatic +// expiry handling), not about which login UI you use, so it reuses the +// generic picker rather than a dedicated single-provider button. +// +// initApp(dappConfig) is what actually turns on native auth — see +// src/lib/multiversx.ts for the `nativeAuth` config object. Nothing in this +// file configures LogoutManager directly: DappProvider.login() calls +// `LogoutManager.getInstance().init()` for you as its last step +// (node_modules/@multiversx/sdk-dapp/out/providers/DappProvider/DappProvider.cjs) +// — confirmed from source, not assumed. See src/NativeAuthPanel.tsx for the +// consumer-facing half of this recipe. -Each **successful** nft/sft transfer generates logs and events that can be used to parse all the details about a transfer -(token identifier, sent amount and receiver). -In order to get the logs and events generated by the transfer, one should know the transaction's hash. +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { dappConfig } from './lib/multiversx'; + +let initStarted = false; + +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; - - + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; -| Param | Required | Type | Description | -| ------ | ----------------------------------------- | -------- | --------------------------- | -| txHash | REQUIRED | `string` | The hash of the transaction | + UnlockPanelManager.init({ + loginHandler: async (): Promise => { + // eslint-disable-next-line no-console + console.log('Logged in — native auth token issued.'); + }, + onClose: async (): Promise => { + // eslint-disable-next-line no-console + console.log('Unlock panel closed without logging in.'); + }, + }); -```bash -https://gateway.multiversx.com/transaction/*txHash*?withResults=true -``` + setReady(true); + }); - - + return () => { + cancelled = true; + }; + }, []); -```rust -{ - "data": { - "transaction": { - ... - "logs": { - "address": "...", - "events": [ - { - "address": "...", - "identifier": "ESDTNFTTransfer", - "topics": [ - "VFNGVC1jODY3ZzM=", // TSFT-c867g3 - "CEI=", // 2114 - "Ag==", // 2 - "givNK+JiLZ5VA5/dP11QKoYEn7qoqnD8uPchH3ZMLw4=" // erd1sg4u62lzvgkeu4grnlwn7h2s92rqf8a64z48pl9c7us37ajv9u8qj9w8xg - ], - "data": null - }, - } - } - "error": "", - "code": "successful" + if (!ready) { + return ( +
+ Initializing wallet SDK… +
+ ); + } + + return <>{children}; } ``` -The event with the identifier `ESDTNFTTransfer` will have the following topics: +## How it works -- 1st topic: token identifier (decoding: base64 to string) -- 2nd topic: token nonce (decoding: base64 to hex string + hex string to big number / integer) -- 3rd topic: the amount to be sent (decoding: base64 to hex string + hex string to big number) -- 4th topic: the recipient of the tokens (decoding: base64 to hex string + hex string to bech32 address) +**`nativeAuth` accepts `true` (all defaults) or a full config object.** This +recipe passes an explicit object so both timing fields are visible: +`expirySeconds` (default 86400 / 24h) and `tokenExpirationToastWarningSeconds` +(default 300 / 5 min), both confirmed from +`node_modules/@multiversx/sdk-dapp/out/services/nativeAuth/methods/getDefaultNativeAuthConfig.cjs`. +This recipe deliberately overrides both to a couple of minutes so the whole +lifecycle is observable in one dev session. Ship the defaults, or your own +policy's values, not these. -In this example, `erd1sg4u62lzvgkeu4grnlwn7h2s92rqf8a64z48pl9c7us37ajv9u8qj9w8xg` received 2 tokens of the collection -`TSFT-c867g3` with nonce `2114`. +**`expirySeconds` is a real, per-token TTL, signed by the wallet, not a +client-only timer.** The login-token string sdk-dapp builds before the wallet +signs it is `${origin}.${blockHash}.${expirySeconds}.${extraInfo}` +(`node_modules/@multiversx/sdk-dapp/out/services/nativeAuth/nativeAuth.cjs`'s +`initialize()`). The API validates the signed token against that same embedded +value, so a short `expirySeconds` really does produce a short-lived token. -
-
+**Auto-logout is wired up inside `provider.login()` itself, you never call it.** +`DappProvider.login()`'s last step is `LogoutManager.getInstance().init()`. +`LogoutManager.init()` reads the real token's remaining TTL and schedules a +warning toast, a "Logging out" toast 3 seconds before expiry, and the actual +`getAccountProvider().logout()` call. The only public methods this recipe calls +directly are `.stop()` (cancel all three timers) and `.init()` again (re-arm from +the token's current remaining TTL). +## Pitfalls -### GET **Get all ESDT tokens for an address** {#get-all-esdt-tokens-for-an-address} +:::warning[Pitfall 1: the 120-second expirySeconds in this recipe is for demonstration only] +It exists purely so the warning-toast then logout-toast then real-logout sequence +is observable in one sitting. Ship a real value, the 86400-second default, or +whatever your own security policy requires. +::: -One can use [get all esdt tokens for an address endpoint](/tokens/fungible-tokens#get-all-esdt-tokens-for-an-address) used for ESDT. +:::danger[Pitfall 2: loginExpiresAt is NOT your native-auth token's expiry] +`useGetLoginInfo().loginExpiresAt` is a separate, generic rolling ~24-hour +session ceiling (already in milliseconds) maintained by an unrelated store +middleware, not the `expirySeconds` you configure for nativeAuth. Displaying it +as "your session expires at X" would be wrong whenever `expirySeconds` is not +~24h. There is currently no public field that returns the real per-token TTL +directly; this recipe shows the *configured* value instead of decoding the token +client-side. +::: +:::warning[Pitfall 3: there is no public "extend my session" call] +`LogoutManager` only exposes `.stop()` and `.init()`. Calling `.init()` again +reschedules from the *current* token's remaining TTL. It does not mint a new token +or push the expiry further out. Extending a session for real means logging in again. +::: -### GET **Get all issued ESDT tokens** {#get-all-issued-esdt-tokens} +:::note[Pitfall 4: .stop() only cancels the client-side timers] +The token still expires server-side at its real TTL. Any authenticated API call +made after that point fails regardless of whether `LogoutManager` is running. +`.stop()` just means the SDK will not proactively force a client-side logout for you. +::: -One can use [get all issued esdt tokens endpoint](/tokens/fungible-tokens#get-all-issued-esdt-tokens) used for ESDT. +## See also +- [Add a wallet login button](/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button) + is the generic picker this recipe's login UI reuses. +- [Read the connected account](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + is the account-level counterpart to this recipe's login-info focus. +- [Login via the DeFi extension](/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login) + is a single-provider login flow that also issues a native-auth token the same way. -### POST **Get ESDT properties** {#get-esdt-properties} +--- -Properties can be queried via the [getTokenProperties function](/tokens/fungible-tokens#get-esdt-token-properties) provided by ESDT. +### NestJS SDK +MultiversX NestJS Microservice Utilities -### POST **Get special roles** {#get-special-roles} +**sdk-nestjs** contains a set of utilities commonly used in the MultiversX microservices ecosystem. -Special roles can be queried via the [getSpecialRoles function](/tokens/fungible-tokens#get-special-roles-for-a-token) provided by ESDT. +| Package | Source code | Description | +|--------------------------------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------| +| [sdk-nestjs](https://www.npmjs.com/package/@multiversx/sdk-nestjs) | [Github](https://github.com/multiversx/mx-sdk-nestjs) | A set of utilities commonly used in the MultiversX Microservice ecosystem. | + +:::tip +When developing microservices, we recommend starting from the **microservice-template** as it integrates off-the-shelf features like: public & private endpoints, cache warmer, transactions processor, queue worker +::: + +| Source code | Description | +|----------------------------------------------------------------------------|------------------------------------------------------------------------------------------| +| [microservice-template](https://github.com/multiversx/mx-template-service) | REST API facade template for microservices that interact with the MultiversX blockchain. | + + +## Packages + +The following table contains the NPM packages that are included inside the NestJS SDK: + +| Package | NPM | Description + additional docs | +|-----------------------|------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------| +| sdk-nestjs-common | [@multiversx/sdk-nestjs-common](https://www.npmjs.com/package/@multiversx/sdk-nestjs-common) | Common functionalities to be used in MultiversX microsevices. | +| sdk-nestjs-auth | [@multiversx/sdk-nestjs-auth](https://www.npmjs.com/package/@multiversx/sdk-nestjs-auth) | Native Auth functionalities to be used for securily handle sessions. | +| sdk-nestjs-http | [@multiversx/sdk-nestjs-http](https://www.npmjs.com/package/@multiversx/sdk-nestjs-http) | HTTP requests handling utilities | +| sdk-nestjs-monitoring | [@multiversx/sdk-nestjs-monitoring](https://www.npmjs.com/package/@multiversx/sdk-nestjs-monitoring) | Microservices monitoring helpers. | +| sdk-nestjs-elastic | [@multiversx/sdk-nestjs-elastic](https://www.npmjs.com/package/@multiversx/sdk-nestjs-elastic) | Elasticsearch interactions helpers. | +| sdk-nestjs-redis | [@multiversx/sdk-nestjs-redis](https://www.npmjs.com/package/@multiversx/sdk-nestjs-redis) | Redis interactions helpers. | +| sdk-nestjs-rabbitmq | [@multiversx/sdk-nestjs-rabbitmq](https://www.npmjs.com/package/@multiversx/sdk-nestjs-rabbitmq) | RabbitMQ interactions helpers. | +| sdk-nestjs-cache | [@multiversx/sdk-nestjs-cache](https://www.npmjs.com/package/@multiversx/sdk-nestjs-cache) | Common cache operations utilities. [docs](/sdk-and-tools/sdk-nestjs/sdk-nestjs-cache) | --- -### operations +### NestJS SDK Auth utilities -This page describes the structure of the `operations` index (Elasticsearch), and also depicts a few examples of how to query it. +NPM Version -## _id +## MultiversX NestJS Microservice Native Authentication Utilities -The _id field of this index is represented by the transactions OR smart contract result hash, in a hexadecimal encoding. +This package contains a set of utilities commonly used for authentication purposes in the MultiversX Microservice ecosystem. + The package relies on [@multiversx/sdk-native-auth-server](https://www.npmjs.com/package/@multiversx/sdk-native-auth-server) for validating access tokens signed by MultiversX wallets. -## Fields +## Installation -| Field | Description | -|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| miniBlockHash | The miniBlockHash field represents the hash of the miniblock in which the transaction was included. | -| nonce | The nonce field represents the transaction sequence number of the sender address. | -| round | The round field represents the round of the block when the transaction was executed. | -| value | The value field represents the amount of EGLD to be sent from the sender to the receiver. | -| receiver | The receiver field represents the destination address of the transaction. | -| sender | The sender field represents the address of the transaction sender. | -| receiverShard | The receiverShard field represents the shard ID of the receiver address. | -| senderShard | The senderShard field represents the shard ID of the sender address. | -| gasPrice | The gasPrice field represents the amount to be paid for each gas unit. | -| gasLimit | The gasLimit field represents the maximum gas units the sender is willing to pay for. | -| gasUsed | The gasUsed field represents the amount of gas used by the transaction. | -| fee | The fee field represents the amount of EGLD the sender paid for the transaction. | -| initialPaidFee | The initialPaidFee field represents the initial amount of EGLD the sender paid for the transaction, before the refund. | -| data | The data field holds additional information for a transaction. It can contain a simple message, a function call, an ESDT transfer payload, and so on. | -| signature | The signature of the transaction, hex-encoded. | -| timestamp | The timestamp field represents the timestamp of the block in which the transaction was executed. | -| status | The status field represents the status of the transaction. | -| senderUserName | The senderUserName field represents the username of the sender address. | -| receiverUserName | The receiverUserName field represents the username of the receiver address. | -| hasScResults | The hasScResults field is true if the transaction has smart contract results. | -| isScCall | The isScCall field is true if the transaction is a smart contract call. | -| hasOperations | The hasOperations field is true if the transaction has smart contract results. | -| tokens | The tokens field contains a list of ESDT tokens that are transferred based on the data field. The indices from the `tokens` list are linked with the indices from `esdtValues` list. | -| esdtValues | The esdtValues field contains a list of ESDT values that are transferred based on the data field. | -| receivers | The receivers field contains a list of receiver addresses in case of ESDTNFTTransfer or MultiESDTTransfer. | -| receiversShardIDs | The receiversShardIDs field contains a list of receiver addresses' shard IDs. | -| type | The type field represents the type of the transaction based on the data field. | -| operation | The operation field represents the operation of the transaction based on the data field. | -| function | The function field holds the name of the function that is called in case of a smart contract call. | -| isRelayed | The isRelayed field is true if the transaction is a relayed transaction. | -| version | The version field represents the version of the transaction. | -| hasLogs | The hasLogs field is true if the transaction has logs. | + `sdk-nestjs-auth` is delivered via **npm** and it can be installed as follows: +```bash + npm install @multiversx/sdk-nestjs-auth + ``` -This index contains both transactions and smart contract results. This is useful because one can query both of them in a single request. +## Obtaining a Native Auth token -The unified structure will contain an extra field in order to be able to differentiate between them. +This package validates a payload signed by a MultiversX wallet. You can use the MultiversX Utils website to get a token you can use for testing. + ![image](https://github.com/multiversx/mx-sdk-nestjs/assets/6889483/374a4e3a-f7d3-4bd9-a212-a8615c89ab53) -| Field | Description | -|-------|------------------------------------------------------------------------------------------------| -| type | It can be `normal` in case of a transaction and `unsigned` in case of a smart contract result. | +1. Navigate to [https://utils.multiversx.com/auth](https://utils.multiversx.com/auth) and choose the desired network from the select located in the upper right corner of the page +2. Click the **Generate** button +3. Select a wallet you prefer to use from the modal dialog, and give it access to the page +4. **Done!** You can now copy the token and use it as a Bearer token in your requests. +To use it in your requests, you need to have an `Authorization` header with the value : + `Bearer `. -## Query examples +You also need to add an `origin` header with the value `https://utils.multiversx.com`. +*Note: these steps are only needed while testing. In production, a frontend application will handle token generation* +## Utility -### Fetch the latest transactions of an address +The package provides a series of [NestJS Guards](https://docs.nestjs.com/guards) that can be used for easy authorization on endpoints in your application. + It also provides some [NestJS Decorators](https://docs.nestjs.com/custom-decorators) that expose the decoded information found in the access token. -``` -curl --request GET \ - --url ${ES_URL}/operations/_search \ - --header 'Content-Type: application/json' \ - --data '{ - "query": { - "bool": { - "must": [ - { - "match": { - "type": "normal" - } - } - ], - "should": [ - { - "match": { - "sender": "erd..." - } - }, - { - "match": { - "receiver": "erd..." - } - }, - { - "match": { - "receivers": "erd..." - } - } - ] - } - }, - "sort": [ - { - "timestamp": { - "order": "desc" - } - } - ] -}' -``` +## Configuration -### Fetch the latest operations of an address +The authentication guards need 2 parameters on instantiation. + The fist parameter needs to be an instance of a class implementing the `ErdnestConfigService` interface. +The second one, needs to be an instance of a [Caching service](https://www.npmjs.com/package/@multiversx/sdk-nestjs-cache) -``` -ADDRESS="erd1..." +```typescript + import { Injectable } from "@nestjs/common"; + import { ApiConfigService } from "./api.config.service"; + import { ErdnestConfigService } from "@multiversx/sdk-nestjs-common"; -curl --request GET \ - --url ${ES_URL}/operations/_search \ - --header 'Content-Type: application/json' \ - --data '{ - "query": { - "bool": { - "should": [ - { - "match": { - "sender": "${ADDRESS}" - } - }, - { - "match": { - "receiver": "${ADDRESS}" - } - }, - { - "match": { - "receivers": "${ADDRESS}" - } - } - ] - } - }, - "sort": [ - { - "timestamp": { - "order": "desc" - } - } - ] -}' -``` + @Injectable() + export class SdkNestjsConfigServiceImpl implements ErdnestConfigService { + constructor( + private readonly apiConfigService: ApiConfigService, + ) { } + getSecurityAdmins(): string[] { + return this.apiConfigService.getSecurityAdmins(); + } -### Fetch all the smart contract results generated by a transaction + getJwtSecret(): string { + return this.apiConfigService.getJwtSecret(); + } -``` -curl --request GET \ - --url ${ES_URL}/operations/_search \ - --header 'Content-Type: application/json' \ - --data '{ - "query": { - "bool": { - "must": [ - { - "match": { - "originalTxHash": "d6.." - } - }, - { - "match": { - "type": "unsigned" - } - } - ] - } - } -}' -``` + getApiUrl(): string { + return this.apiConfigService.getApiUrl(); + } ---- + getNativeAuthMaxExpirySeconds(): number { + return this.apiConfigService.getNativeAuthMaxExpirySeconds(); + } -### Overview + getNativeAuthAcceptedOrigins(): string[] { + return this.apiConfigService.getNativeAuthAcceptedOrigins(); + } + } + ``` -[comment]: # "mx-abstract" +You can register it as a provider, and the DI mechanism of NestJS will handle instantiation for you. -## Overview +```typescript + import { Module } from '@nestjs/common'; + import { ERDNEST_CONFIG_SERVICE } from "@multiversx/sdk-nestjs-common"; -There are several ways to write smart contract tests in Rust directly. This is for the largest part possible because of the Rust VM and debugger that can act as an execution backend. + @Module({ + providers: [ + { + provide: ERDNEST_CONFIG_SERVICE, + useClass: SdkNestjsConfigServiceImpl, + }, + ], + }) + export class AppModule {} + ``` -This is a simplified diagram of what a Rust test will do during its execution: +## Using the Auth Guards -```mermaid -graph TD - test[Test] - vm[Rust VM] --> bcm[BlockchainMock] - test -->|"register contract"| bcm - test -->|"set accounts"| bcm - test -->|"check accounts"| bcm - test -->|"call
deploy
query"| scexec[SC code] - scexec --> vm -``` +NestJS guards can be controller-scoped, method-scoped, or global-scoped. Setting up a guard from the package is done through the `@UseGuards` decorator from the `@nestjs/common` package. -Of course, a local test environment is not a blockchain, so many things need to be mocked. The test state is held on a `BlockchainMock`, which needs to be initialized with user accounts, tokens, smart contracts, etc. +### Native Auth Guard +`NativeAuthGuard` performs validation of the block hash, verifies its validity, as well as origin verification on the access token. +```typescript + import { NativeAuthGuard } from "@multiversx/sdk-nestjs-auth"; -### ScenarioWorld (facade) + @Controller('projects') + @UseGuards(NativeAuthGuard) + export class ProjectsController { + // your methods... + } + ``` -In order to simplify interactions with the system, all tests use a unique facade for all operations. It is called `ScenarioWorld`, and it gets created at the beginning of each test in a `world()` function. +In the example above the `NativeAuthGuard` is controller-scoped. This means that all of the methods from `ProjectsController` will be protected by the guard. +```typescript + import { NativeAuthGuard } from "@multiversx/sdk-nestjs-auth"; + @Controller('projects') + export class ProjectsController { + @Get() + async getAll() { + return this.projectsService.getAll(); + } -### Registering contracts + @Post() + @UseGuards(NativeAuthGuard) + async createProject(@Body() createProjectDto: CreateProjectDto) { + return this.projectsService.create(createProjectDto); + } + } + ``` -Since we don't have native execution in the Rust backend yet, the only way to run contracts is to register the contract implementation for the given contract code identifier. In simpler words, we tell the environment "whenever you encounter this contract code, run this code that I've written instead". +In this case, the guard is method-scoped. Only `createProject` benefits from the native auth checks. -Since this operation is specific to only the Rust debugger, it doesn't go through the mandos pipeline. +### Native Auth Admin Guard + `NativeAuthAdminGuard` allows only specific addresses to be authenticated. The addresses are defined in the [config](#configuration) file and are passed to the guard via the ErdnestConfigService. +*This guard cannot be used by itself. It always has to be paired with a `NativeAuthGuard`* -### Calling contract code +```typescript + import { NativeAuthGuard, NativeAuthAdminGuard } from "@multiversx/sdk-nestjs-auth"; -There are many ways to call contract code, but the one we recommend is [black-box style](/developers/testing/rust/sc-blackbox-calls) using the [unified transaction syntax](/developers/transactions/tx-overview). + @Controller('admin') + @UseGuards(NativeAuthGuard, NativeAuthAdminGuard) + export class AdminController { + // your methods... + } + ``` -The call styles are: -- unified transaction syntax - - [**black-box**](/developers/testing/rust/sc-blackbox-calls) (recommended) - - white-box (coming soon) -- Mandos steps in Rust (no longer recommended) -- [Whitebox framework (legacy)](whitebox-legacy) +### JWT Authenticate Guard + `JwtAuthenticateGuard` performs validation of a traditional [JSON web token](https://datatracker.ietf.org/doc/html/rfc7519). The usage is exactly the same as for the native auth guards. +```typescript + import { JwtAuthenticateGuard } from "@multiversx/sdk-nestjs-auth"; -## Rust testing architecture + @Controller('users') + @UseGuards(JwtAuthenticateGuard) + export class UsersController { + // your methods... + } + ``` -We saw the simplified diagram in the introduction, now let's go in a little more depth. -```mermaid -graph TD - scenworld["ScenarioWorld (facade)"] - bb[Blackbox test] --> scenworld - wb[Whitebox test] --> scenworld - unit[Unit test] --> scenworld - scenrunner["ScenarioRunner (interface)"] - scenworld -->|"set step
check step
tx step"| scenrunner - vm[Rust VM] --> bcm[BlockchainMock] - scenworld -->|"register contract"| bcm - scenrunner -->|"set step"| bcm - scenrunner -->|"check step"| bcm - scenrunner -->|"tx step"| scexec[SC code] - scexec --> vm - scenrunner -->|"save trace"| trace["trace.scen.json"] -``` +### JWT Admin Guard -The ScenarioWorld and its builders are in fact constructing mandos steps in the background and are sending them to the backends. + `JwtAdminGuard` relies on the same mechanism, only specific addresses can be authenticated. The addresses are defined in the [config](#configuration) file and are passed to the guard via the ErdnestConfigService. -The tests are also allowed to construct these mandos steps themselves, but this is currently discouraged, because mandos syntax is very weakly typed and prone to error. +*There is one caveat: when creating the JWT, the client must include an `address` field in the payload, before signing it.* +```typescript + import { JwtAuthenticateGuard, JwtAdminGuard } from "@multiversx/sdk-nestjs-auth"; + @Controller('admin') + @UseGuards(JwtAuthenticateGuard, JwtAdminGuard) + export class AdminController { + // your methods... + } + ``` -### ScenarioExecutor (Mandos) +### Jwt Or Native Auth Guard -All our tests run through a scenario (Mandos) execution layer, which is the reason why we are able to export mandos traces out of almost any test. + `JwtOrNativeAuthGuard` guard will authorize requests containing either JWT or a native auth access token. The package will first look for a valid JWT. If that fails, it will look for a valid native auth access token. -All test actions are converted to mandos steps before execution. This mechanism is designed to decouple the high-level code from the backend it is run on. In principle, these mandos steps could be executed on other backends too. +### Global-scoped guards -However, there are only two mandos executors right now: -- Rust VM backend -- save to file (trace). + ```typescript + const app = await NestFactory.create(AppModule); + app.useGlobalGuards(new NativeAuthGuard(new SdkNestjsConfigServiceImpl(apiConfigService), cachingService)); + ``` -The system can also load mandos scenarios from files and execute them as such, on the Rust VM backend. +## Using the Auth Decorators ---- +The package exposes 3 decorators : `NativeAuth`, `Jwt` and `NoAuth` -### Overview +### NativeAuth Decorator -Here you can find some common issues and their solutions, in the context of [MultiversX SDKs and Tools](/sdk-and-tools/overview). +The `NativeAuth` decorator accepts a single parameter. In can be one of the following values : -1. [Fix Rust installation](/sdk-and-tools/troubleshooting/fix-rust-setup) -2. [Fix IDEs configuration](/sdk-and-tools/troubleshooting/ide-setup) +- `issued` - block timestamp +- `expires` - expiration time +- `address` - address that signed the access token +- `origin` - URL of the page that generated the token +- `extraInfo` - optional arbitrary data ---- +Below is an example showing how to use the decorator to extract the signers address : -### Overview + ```typescript + import { NativeAuthGuard, NativeAuth } from "@multiversx/sdk-nestjs-auth"; + import { Controller, Get, UseGuards } from "@nestjs/common"; -The processing cost of a MultiversX transaction is determined by its gas limit, the actual gas consumption, and the gas price per gas unit, leading to a processing fee in EGLD that may be subject to a gas refund in certain cases. + @Controller() + export class AuthController { + @Get("/auth") + @UseGuards(NativeAuthGuard) + authorize(@NativeAuth('address') address: string): string { + console.log(`Access granted for address ${address}`); + return address; + } + } + ``` +### Jwt Decorator -## Cost of processing (gas units) +The `Jwt` decorator works just like `NativeAuth`. The fields accessible inside it are dependent on the client that created the token, and are out of scope for this documentation. -Each MultiversX transaction has a **processing cost**, expressed as **an amount of _gas units_**. At broadcast time, each transaction must be provided a **gas limit** (`gasLimit`), which acts as an _upper limit_ of the processing cost. +### No Auth Decorator +The `NoAuth` decorator can be used to skip authorization on a specific method. This is useful when a guard is scoped globally or at the controller level. -### Constraints + ```typescript + import { NoAuth } from "@multiversx/sdk-nestjs-auth"; + import { Controller, Get, UseGuards } from "@nestjs/common"; -For any transaction, the `gasLimit` must be greater or equal to `erd_min_gas_limit` but smaller or equal to `erd_max_gas_per_transaction`, these two being [parameters of the Network](/sdk-and-tools/rest-api/network#get-network-configuration): + @Controller() + export class PublicController { + @NoAuth() + @Get("/public-posts") + listPosts() { + // .... + } + } + ``` -``` -networkConfig.erd_min_gas_limit <= tx.gasLimit <= networkConfig.erd_max_gas_per_transaction -``` +--- +### NestJS SDK Cache utilities -### Cost components +NPM Version -The **actual gas consumption** - also known as **used gas** - is the consumed amount from the provided **gas limit** - the amount of gas units actually required by the Network in order to process the transaction. The unconsumed amount is called **remaining gas**. -At processing time, the Network breaks the **used gas** down into two components: +## MultiversX NestJS Microservice Cache Utilities -- gas used by **value movement and data handling** -- gas used by **contract execution** (for executing System or User-Defined Smart Contract) +This package contains a set of utilities commonly used for caching purposes in the MultiversX Microservice ecosystem. -:::note -Simple transfers of value (EGLD transfers) only require the _value movement and data handling_ component of the gas usage (that is, no _execution_ gas), while Smart Contract calls require both components of the gas consumption. This includes ESDT and NFT transfers as well, because they are in fact calls to a System Smart Contract. -::: -The **value movement and data handling** cost component is easily computable, using on the following formula: +## Installation -``` -tx.gasLimit = - networkConfig.erd_min_gas_limit + - networkConfig.erd_gas_per_data_byte * lengthOf(tx.data) +`sdk-nestjs-cache` is delivered via **npm** and it can be installed as follows: + +```bash +npm install @multiversx/sdk-nestjs-cache ``` -The **contract execution** cost component is easily computable for System Smart Contract calls (based on formulas specific to each contract), but harder to determine _a priori_ for user-defined Smart Contracts. This is where _simulations_ and _estimations_ are employed. +## Utility -## Processing fee (EGLD) +The package exports two services: an **in-memory cache service** and **remote cache service**. -The **processing fee**, measured in EGLD, is computed with respect to the **actual gas cost** - broken down into its components - and the **gas price per gas unit**, which differs between the components. -The **gas price per gas unit** for the **value movement and data handling** must be specified by the transaction, and it must be equal or greater than a Network parameter called `erd_min_gas_price`. +## Table of contents -While the price of a gas unit for the **value movement and data handling** component equals the **gas price** provided in the transaction, the price of a gas unit for the **contract execution** component is computed with respect to another Network parameter called `erd_gas_price_modifier`: +- [In Memory Cache](#in-memory-cache) - super fast in-memory caching system based on [LRU cache](https://www.npmjs.com/package/lru-cache) +- [Redis Cache](#redis-cache) - Caching system based on [Redis](https://www.npmjs.com/package/@multiversx/sdk-nestjs-redis) +- [Cache Service](#cache-service) - MultiversX caching system which combines in-memory and Redis cache, forming a two-layer caching system -``` -value_movement_and_data_handling_price_per_unit = tx.GasPrice -contract_execution_price_per_unit = tx.GasPrice * networkConfig.erd_gas_price_modifier -``` -:::note -Generally speaking, the price of a gas unit for **contract execution** is lower than the price of a gas unit for **value movement and data handling**, due to the gas price modifier for contracts (`erd_gas_price_modifier`). -::: +## In memory cache -The **processing fee** formula looks like this: +In memory cache, available through `InMemoryCacheService`, is used to read and write data from and into the memory storage of your Node.js instance. -``` -processing_fee = - value_movement_and_data_handling_cost * value_movement_and_data_handling_price_per_unit + - contract_execution_cost * contract_execution_price_per_unit -``` +*Note that if you have multiple instances of your application you must sync the local cache across all your instances.* -After processing the transaction, the Network will send a value called **gas refund** back to the sender of the transaction, computed with respect to the unconsumed (component of the) gas, if applicable (if the **paid fee** is higher than the **necessary fee**). +Let's take as an example a `ConfigService` that loads some non-crucial configuration from the database and can be cached for 10 seconds. ---- +Usage example: -### Payload (data) +```typescript +import { Injectable } from '@nestjs/common'; +import { InMemoryCacheService } from '@multiversx/sdk-nestjs-cache'; -## Overview +@Injectable() +export class ConfigService { + constructor( + private readonly inMemoryCacheService: InMemoryCacheService + ){} -The data field can hold arbitrary data, but for practical purposes, it is normally one of three: + async loadConfiguration(){ + return await this.inMemoryCacheService.getOrSet( + 'configurationKey', + () => this.getConfigurationFromDb(), + 10, // 10 seconds + ) + } -- a function call, -- deploy data, or -- an upgrade call. + private async getConfigurationFromDb(){ + // fetch configuration from db + } +} +``` -We can always give this data in raw form, however, we usually prefer using a proper type system, for safety. +When the `.loadConfiguration()` method is called for the first time, the `.getConfigurationFromDb()` method will be executed and the value returned from it will be set in cache with `configurationKey` key. If another `.loadConfiguration()` call comes in 10 seconds interval, the data will be returned from cache and `.getConfigurationFromDb()` will not be executed again. -:::caution -Always use [proxies](./tx-proxies.md) when the target contract ABI is known. A contract proxy is a Rust equivalent of its ABI, and using adds invaluable type safety to your calls. -Using raw data is acceptable only when we are forwarding calls to unknown contracts, for instance in contracts like the multisig, governance of other forwarders. -::: +### In memory cache methods -## Diagram +#### `.get(key: string): Promise` -The basic transition diagram for constructing the data field is the one below. It shows the allowed data types and how to get from one to the other. +- **Parameters:** + - `key`: The key of the item to retrieve from the cache. +- **Returns:** A `Promise` that resolves to the cached value or `undefined` if the key is not present. -Notice how the deploy and upgrade calls are further specified by the code source: either explicit code, or the code of another deployed contract. -```mermaid -graph LR - subgraph "Data: raw" - data-unit["()"] - deploy["DeployCall<()>"] - data-unit -->|raw_deploy| deploy - deploy -->|from_source| deploy-src["DeployCall<FromSource<_>>"] - deploy -->|code| deploy-code["DeployCall<Code<_>>"] - data-unit -->|raw_upgrade| upgrade["UpgradeCall<()>"] - upgrade -->|from_source| upgrade-src["UpgradeCall<CodeSource<_>>"] - upgrade -->|code| upgrade-code["UpgradeCall<Code<_>>"] - data-unit -->|"raw_call(endpoint_name)"| fc[FunctionCall] - end -``` +#### `.getMany(keys: string[]): Promise<(T | undefined)[]>` -What the first diagram does **not** contain are some additional methods that change the values, but not the type. They are: +- **Parameters:** + - `keys`: An array of keys to retrieve from the cache. +- **Returns:** A `Promise` that resolves to an array of cached values corresponding to the provided keys. -- `code_metadata` (deploy & upgrade only), -- `argument` -- `argument_raw`. -If we also add them to the diagram we get a more complex version of it: +#### `.set(key: string, value: T, ttl: number, cacheNullable?: boolean): void` -```mermaid -graph LR - subgraph "Data: raw, detailed" - data-unit["()"] - deploy["DeployCall<()>"] - data-unit -->|raw_deploy| deploy - deploy -->|from_source| deploy-src["DeployCall<FromSource<_>>"] - deploy -->|code| deploy-code["DeployCall<Code<_>>"] - data-unit -->|raw_upgrade| upgrade["UpgradeCall<()>"] - upgrade -->|from_source| upgrade-src["UpgradeCall<CodeSource<_>>"] - upgrade -->|code| upgrade-code["UpgradeCall<Code<_>>"] - data-unit -->|"raw_call(endpoint_name)"| fc[FunctionCall] - - deploy -->|"code_metadata
argument
argument_raw"| deploy - deploy-code -->|"code_metadata
argument
argument_raw"| deploy-code - deploy-src -->|"code_metadata
argument
argument_raw"| deploy-src - upgrade -->|"code_metadata
argument
argument_raw"| upgrade - upgrade-code -->|"code_metadata
argument
argument_raw"| upgrade-code - upgrade-src -->|"code_metadata
argument
argument_raw"| upgrade-src - fc -->|"argument
argument_raw"| fc - end -``` +- **Parameters:** + - `key`: The key under which to store the value. + - `value`: The value to store in the cache. + - `ttl`: Time-to-live for the cached item in seconds. + - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` -:::info -These are diagrams for the raw calls, without proxies. You can find the one involving proxies [here](./tx-proxies.md#diagram). -::: +#### `.setMany(keys: string[], values: T[], ttl: number, cacheNullable?: boolean): Promise` -## No data +- **Parameters:** + - `keys`: An array of keys under which to store the values. + - `values`: An array of values to store in the cache. + - `ttl`: Time-to-live for the cached items in seconds. + - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` -Transactions with no data are classified as simple transfers. These simple transactions can be transferred using: -- **`.transfer()`**: executes simple transfers with a zero gas limit. -```rust title=lib.rs - self.tx() - .to(&caller) - .egld_or_single_esdt(&token_identifier, 0, &balance) - .transfer(); -``` -- **`.transfer_if_not_empty()`**: it facilitates the transfer of funds with a zero gas limit only if the amount exceeds zero; otherwise, no action is taken. -```rust title=lib.rs - self.tx() - .to(ToCaller) - .payment(&token_payment) - .transfer_if_not_empty(); -``` +#### `.delete(key: string): Promise` +- **Parameters:** + - `key`: The key of the item to delete from the cache. +- **Returns:** A `Promise` that resolves when the item is successfully deleted. -## Untyped function call +#### `.getOrSet(key: string, createValueFunc: () => Promise, ttl: number, cacheNullable?: boolean): Promise` -**`.raw_call(...)`** starts a contract call serialised by hand. It is used in proxy functions. It is safe to use [proxies](./tx-proxies.md) instead since manual serialisation is not type-safe. +- **Parameters:** + - `key`: The key of the item to retrieve or create. + - `createValueFunc`: A function that creates the value if the key is not present in the cache. + - `ttl`: Time-to-live for the cached item in seconds. + - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` +- **Returns:** A `Promise` that resolves to the cached or newly created value. -### Argument +#### `.setOrUpdate(key: string, createValueFunc: () => Promise, ttl: number, cacheNullable?: boolean): Promise` -**`.argument(...)`** serializes the value, but does not enforce type safety. It adds one argument to a function call. +- **Parameters:** + - `key`: The key of the item to update or create. + - `createValueFunc`: A function that creates the new value for the key. + - `ttl`: Time-to-live for the cached item in seconds. + - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` +- **Returns:** A `Promise` that resolves to the updated or newly created value. -Can be called multiple times, once for each argument. -```rust -tx().raw_call("example").argument(&arg1).argument(&arg2) -``` +## Redis Cache -It is safe to user [proxies](./tx-proxies.md) instead, whenever possible. +Redis cache, available through `RedisCacheService`, is a caching system build on top of Redis. It is used as a shared cache among multiple microservices. +Let's build the same config loader class but with data shared across multiple clusters using Redis. The implementation is almost identical since both `InMemoryCache` and `RedisCache` have similar class structure. -### Raw arguments +Usage example: -**`arguments_raw(...)`** overrides the entire argument buffer. It takes one argument of type `ManagedArgBuffer`. The arguments need to have been serialized beforehand. +```typescript +import { Injectable } from '@nestjs/common'; +import { RedisCacheService } from '@multiversx/sdk-nestjs-cache'; -```rust -tx().raw_call("example").arguments_raw(&arguments) -``` +@Injectable() +export class ConfigService { + constructor( + private readonly redisCacheService: RedisCacheService, + ){} + async loadConfiguration(){ + return await this.redisCacheService.getOrSet( + 'configurationKey', + () => this.getConfigurationFromDb(), + 10, + ); + } -### Code metadata + private async getConfigurationFromDb(){ + // fetch configuration from db + } +} -**`.code_metadata()`** explicitly sets code metadata. -```rust -tx().raw_call("example").code_metadata(code_metadata) ``` -## Untyped deploy +### Redis cache methods -**`.raw_deploy()`** starts a contract deploy call serialised by hand. It is used in proxy deployment functions. It is safe to use [proxies](./tx-proxies.md) instead since manual serialisation is not type-safe. -Deployment calls needs to set: +#### `get(key: string): Promise` +- **Parameters:** + - `key`: The key of the item to retrieve. +- **Returns:** A `Promise` that resolves to the cached value or `undefined` if the key is not found. -### Argument -Same as for [function call arguments](#argument). +#### `getMany(keys: string[]): Promise<(T | undefined | null)[]>` -```rust -tx().raw_deploy().argument(&argument) -``` +- **Parameters:** + - `keys`: An array of keys to retrieve. +- **Returns:** A `Promise` that resolves to an array of cached values corresponding to the input keys. Values may be `undefined` or `null` if not found. -### Raw arguments +#### `set(key: string, value: T, ttl: number, cacheNullable?: boolean): void` -Same as for [function call raw arguments](#raw-arguments). +- **Parameters:** + - `key`: The key under which to store the value. + - `value`: The value to store in the cache. + - `ttl`: Time-to-live for the cached item in seconds. + - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` -```rust -tx().raw_deploy().arguments_raw(&arguments) -``` +#### `setMany(keys: string[], values: T[], ttl: number, cacheNullable?: boolean): void` -### Code +- **Parameters:** + - `keys`: An array of keys for the items to update or create. + - `values`: An array of values to set in the cache. + - `ttl`: Time-to-live for the cached items in seconds. + - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` -**`.code(...)`** explicitly sets the deployment code source as bytes. -Argument will normally be a `ManagedBuffer`, but can be any type that implements trait `CodeValue`. +#### `delete(key: string): void` -```rust -tx().raw_deploy().code(code_bytes) -``` +- **Parameters:** + - `key`: The key of the item to delete. +- **Returns:** A `Promise` that resolves when the item is successfully deleted from the cache. +#### `getOrSet(key: string, createValueFunc: () => Promise, ttl: number, cacheNullable?: boolean): Promise` -### From source +- **Parameters:** + - `key`: The key of the item to retrieve or create. + - `createValueFunc`: A function that creates the new value for the key. + - `ttl`: Time-to-live for the cached item in seconds. + - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` -**`.from_source(...)`** will instruct the VM to copy the code from another previously deployed contract. +**Note:** These are just some of the methods available in the `RedisCacheService` class. -Argument will normally be a `ManagedAddress`, but can be any type that implements trait `FromSourceValue`. -```rust -tx().raw_deploy().from_source(other_address) -``` +## Cache Service +Cache service is using both [In Memory Cache](#in-memory-cache) and [Redis Cache](#redis-cache) to form a two-layer caching system. -### New address +Usage example: -**`.new_address(...)`** defines a mock address for the deployed contract (allowed only in testing environments). -```rust -tx().raw_deploy().new_address(address) -``` +```typescript +import { Injectable } from '@nestjs/common'; +import { CacheService } from '@multiversx/sdk-nestjs-cache'; -The example below is a blackbox test for deploy functionality. This call encapsulates a raw_deploy that explicitly sets the deployment code source with *"adder.mxsc.json"* and the returned address of the deploy with *"sc: adder"*. +@Injectable() +export class ConfigService { + constructor( + private readonly cacheService: CacheService, + ){} -```rust title=adder_blackbox_test.rs -fn deploy(&mut self) { - self.world - .tx() - .from(OWNER_ADDRESS) - .raw_deploy() - .argument(5u32) - .code(CODE_PATH) - .new_address(ADDER_ADDRESS) - .run(); + async loadConfiguration(){ + return await this.cacheService.getOrSet( + 'configurationKey', + () => this.getConfigurationFromDb(), + 5, // in memory TTL + 10, // redis TTL + ); + } + + private async getConfigurationFromDb(){ + // fetch configuration from db + } } ``` +Whenever `.loadConfigurationMethod()` is called, the service will first look into the in memory cache if there is a value stored for the specified key and return it. If the value is not found in the in memory cache it will look for the same key in Redis cache and return it if found. If the value is not found in Redis, the `.getConfigurationFromDb()` method is called and the returned value is stored in memory for 5 seconds (the TTL provided in the third parameter) and in Redis for 10 seconds (the value provided in the fourth parameter). -## Untyped upgrade - -`.raw_upgrade()` starts a contract deployment upgrade serialised by hand. It is used in a proxy upgrade call. It is safe to use [proxies](./tx-proxies.md) instead since manual serialisation is not type-safe. All upgrade calls require: - - -### Argument - -Same as for [function call arguments](#argument). +*Note: we usually use smaller TTL for in memory cache because when it comes to in memory cache it takes longer to synchronize all instances and it is better to fall back to Redis and lose a bit of reading speed than to have inconsistent data.* -```rust -tx().raw_upgrade().argument(&argument) -``` +All methods from `CacheService` use the two layer caching system except the ones that contains `local` and `remote` in their name. Those methods refer strictly to in memory cache and Redis cache. +Examples: -### Raw arguments +- `.getLocal()`, `.setLocal()`, `.getOrSetLocal()` are the same methods as [In Memory Cache](#in-memory-cache) +- `.getRemote()`, `.setRemove()`, `.getOrSetRemove()` are the same methods as [Redis Cache](#redis-cache) -Same as for [function call raw arguments](#raw-arguments). -```rust -tx().raw_upgrade().arguments_raw(&arguments) -``` +### Cache service methods -### Code metadata +#### `get(key: string): Promise` -Same as for [function call raw arguments](#code-metadata). -```rust -tx().raw_upgrade().code_metadata(code_metadata) -``` +- **Parameters:** + - `key`: The key of the item to retrieve. The method first checks the local in-memory cache, and if not found, it retrieves the value from the Redis cache. +- **Returns:** A `Promise` that resolves to the cached value or `undefined` if the key is not found. -### Code +#### `getMany(keys: string[]): Promise<(T | undefined)[]>` -Same as for [deploy code](#code). +- **Parameters:** + - `keys`: An array of keys to retrieve. The method first checks the local in-memory cache, and for missing keys, it retrieves values from the Redis cache. +- **Returns:** A `Promise` that resolves to an array of cached values corresponding to the input keys. Values may be `undefined` if not found. -Argument will normally be a `ManagedBuffer`, but can be any type that implements trait `CodeValue`. -```rust -tx().raw_upgrade().code(code_bytes) -``` +#### `set(key: string, value: T, ttl: number, cacheNullable?: boolean): Promise` +- **Parameters:** + - `key`: The key under which to store the value. The method sets the value in both the local in-memory cache and the Redis cache. + - `value`: The value to store in the cache. + - `ttl`: Time-to-live for the cached item in seconds. + - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` -### From source -Same as for [deploy from source](#from-source). +#### `setMany(keys: string[], values: T[], ttl: number, cacheNullable?: boolean): Promise` -**`.from_source(...)`** will instruct the VM to copy the code from another previously deployed contract. +- **Parameters:** + - `keys`: An array of keys for the items to update or create. The method sets values in both the local in-memory cache and the Redis cache. + - `values`: An array of values to set in the cache. + - `ttl`: Time-to-live for the cached items in seconds. + - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` -Argument will normally be a `ManagedAddress`, but can be any type that implements trait `FromSourceValue`. -```rust -tx().raw_upgrade().from_source(other_address) -``` +#### `delete(key: string): Promise` -The example below is an endpoint that contains upgrade functionality. This call encapsulates a raw_upgrade that explicitly sets the upgrade call source with a specific ManagedAddress and *upgradeable* code metadata. +- **Parameters:** + - `key`: The key of the item to delete. The method deletes the item from both the local in-memory cache and the Redis cache. +- **Returns:** A `Promise` that resolves when the item is successfully deleted from both caches. -```rust title=contract.rs -#[endpoint] -fn upgrade_from_source( - &self, - child_sc_address: ManagedAddress, - source_address: ManagedAddress, - opt_arg: OptionalValue, -) { - self.tx() - .to(child_sc_address) - .typed(contract_proxy::ContractProxy) - .upgrade(opt_arg) - .code_metadata(CodeMetadata::UPGRADEABLE) - .from_source(source_address) - .upgrade_async_call_and_exit(); -} -``` ---- +#### `deleteMany(keys: string[]): Promise` -### Payments +- **Parameters:** + - `keys`: An array of keys for the items to delete. The method deletes items from both the local in-memory cache and the Redis cache. +- **Returns:** A `Promise` that resolves when all items are successfully deleted from both caches. -## Overview -Payments can be easily attached to the transaction with the new syntax through the `Payment` generic. In order to specialize the generic, the framework provides the `.payment(...)` method, which accepts all legal types (all types that `can` be payment) such as: `EsdtTokenPayment`, `(TokenIdentifier, u64, BigUint)`, `ManagedVec`, etc. The framework also provides other various helper methods, basically wrappers around `.payment(...)` for accessibility. +#### `getOrSet(key: string, createValueFunc: () => Promise, ttl: number, cacheNullable?: boolean): Promise` +- **Parameters:** + - `key`: The key of the item to retrieve or create. The method first checks the local in-memory cache, and if not found, it retrieves the value from the Redis cache or creates it using the provided function. + - `createValueFunc`: A function that creates the new value for the key. + - `ttl`: Time-to-live for the cached item in seconds. + - `cacheNullable` (optional): If set to `false`, the method will not cache null or undefined values. Default: `true` -## Diagram +... (and many more) -The payment is a little more complex than the previous fields. The `.payment(...)` method is sufficient to set any kind of acceptable payment object. However, we have several more functions to help setup the payment field: +**Note:** These are just some of the methods available in the `CacheService` class. For a comprehensive list of methods and their descriptions, refer to the class implementation. -```mermaid -graph LR - payment-unit["()"] - payment-unit --->|"<proxy>"| not-payable["NotPayable"] - payment-unit --->|egld| egld-biguint["Egld(BigUint)"] - payment-unit --->|egld| egld-u64["Egld(u64)"] - payment-unit --->|egld| egld-num["Egld(NumExpr)"] - payment-unit --->|"payment
esdt"| EsdtTokenPayment - payment-unit --->|"payment
single_esdt"| EsdtTokenPaymentRefs - EsdtTokenPayment -->|esdt| MultiEsdtPayment - MultiEsdtPayment -->|esdt| MultiEsdtPayment - payment-unit --->|"payment
multi_esdt"| MultiEsdtPayment - payment-unit --->|"payment"| EgldOrEsdtTokenPayment - payment-unit --->|"payment
egld_or_single_esdt"| EgldOrEsdtTokenPaymentRefs - payment-unit --->|"payment
egld_or_multi_esdt"| EgldOrMultiEsdtPayment -``` +--- +### NestJS SDK Monitoring utilities -## No payments +NPM Version -When no payments are added, the `Payment` fields remain of type `()`. This makes sense when dealing with contract or built-in function calls that don't involve payment. -```rust title=contract.rs - self.tx() // tx with sc environment - .to(&to) - .raw_call(endpoint_name) // raw endpoint call - .sync_call() // synchronous call -``` +## MultiversX NestJS Microservice Monitoring Utilities -In this example, the transaction is a smart contract call to a non payable endpoint with no arguments. +This package contains a set of utilities commonly used for monitoring purposes in the MultiversX Microservice ecosystem. +The package relies on Prometheus to aggregate the metrics, and it is using [prom-client](https://www.npmjs.com/package/prom-client) as a client for it. +## Installation -### `NotPayable` +`sdk-nestjs-monitoring` is delivered via **npm,** and it can be installed as follows: -The `NotPayable` object is a variation of the no-payment indicator `()`. It acts the same way, with only one difference: no payment can be added on top of it. +```bash +npm install @multiversx/sdk-nestjs-monitoring +``` -[Proxies](tx-proxies#notpayable-protection) will add the `NotPayable` flag to transactions to non-payable endpoints, to provide an additional layer of security. +## Utility +The package exports **performance profilers**, **interceptors** and **metrics**. -## EGLD payment -We represent EGLD payments with the wrapper type `Egld`. This wrapper makes sure that there is no ambiguity as to what the given amount represents. +### Performance profiler -The `Egld` contains a single field, holding the amount. This amount will eventually be resolved as a `BigUint`, but the developer can also provide other types, if convenient, such as `u64`, `i32`, or `NumExpr`. In general, any type that implements trait `EgldValue` can be used. +`PerformanceProfiler` is a class exported by the package that allows you to measure the execution time of your code. -EGLD payments can be specified using `.payment(Egld(amount))`, but for brevity it is common to use method `.egld(amount)`, which does the same. +```typescript +import { PerformanceProfiler } from '@multiversx/sdk-nestjs-monitoring'; -:::caution -The amounts are expressed in indivisible atto-EGLD (10^-18) units. You must always take the denomination into account. -::: +const profiler = new PerformanceProfiler(); +await doSomething(); +const profilerDurationInMs = profiler.stop(); -Some examples of specifying EGLD payments: +console.log(`doSomething() method execution time lasted ${profilerDurationInMs} ms`); +``` +The `.stop()` method can receive two optional parameters: -### `BigUint` +- `description` - text used for default logging. Default: `undefined` +- `log` - boolean to determine if log should be printed. If `log` is set to true, the logging class used to print will be `Logger` from `"@nestjs/common"`.``Default: `false` -The most straightforward type to encode amounts. +```typescript +import { PerformanceProfiler } from '@multiversx/sdk-nestjs-monitoring'; -```rust title=contract.rs - #[endpoint] - #[payable("EGLD")] - fn send_egld( - &self, - to: ManagedAddress, - egld_amount: BigUint - ) { - self - .tx() // tx with sc environment - .to(to) - .egld(egld_amount) // BigUint value - .transfer() - } +const profiler = new PerformanceProfiler(); +await doSomething(); +profiler.stop(`doSomething() execution time`, true); ``` +The output of the code above will be "`doSomething() execution time: 1.532ms`" -### `&BigUint` , `ManagedRef` - +--- -References are also allowed. A slightly less common variation is the `ManagedRef` type, which is used in certain places of the framework. It is semantically equivalent to a readonly reference (`&...`). You can see it in this example: -```rust title=contract.rs - #[endpoint] - #[payable("EGLD")] - fn forward_egld( - &self, - to: ManagedAddress, - endpoint_name: ManagedBuffer, - args: MultiValueEncoded, - ) { - let payment = self.call_value().egld(); // readonly BigUint managed reference - self - .tx() // tx with sc environment - .to(to) - .egld(payment) // BigUint value - .raw_call(endpoint_name) // endpoint call - .argument(&args) - .sync_call() - } -``` +### Cpu Profiler +`CpuProfiler` is a class exported by the package that allows you to measure the CPU execution time of your code. Given that JavaScript is a single-threaded language, it's important to be mindful of the amount of CPU time allocated to certain operations, as excessive consumption can lead to slowdowns or even blockages in your process. -### `u64` +```typescript +import { CpuProfiler } from '@multiversx/sdk-nestjs-monitoring'; -In tests it might be unwieldy to keep creating `BigUint` instances, and the amounts might not be so large, so it can be more comfortable to work with `u64` values. +const profiler = new CpuProfiler(); +await doHttpRequest() +const profilerDurationInMs = profiler.stop(); -```rust title=blackbox_test.rs - const STAKE_AMOUNT: u64 = 20; - self.world - .tx() // tx with test exec environment - .from(&address.to_address()) - .to(PRICE_AGGREGATOR_ADDRESS) - .typed(price_aggregator_proxy::PriceAggregatorProxy) // typed call - .stake() // endpoint call - .egld(STAKE_AMOUNT) // u64 value - .run(); +console.log(`doHttpRequest() method execution time lasted ${profilerDurationInMs} ms`); ``` +The `.stop()` method can receive two optional parameters: -### `NumExpr` +- `description` - text used for default logging. Setting the description automatically triggers the printing of the `PerformanceProfiler` value. Default: `undefined` -This type helps us control how the values will be exported in the mandos trace. Its contents will be a numeric mandos expression. Use if you are insterested in a readable mandos output. +```typescript +import { CpuProfiler } from '@multiversx/sdk-nestjs-monitoring'; -Alternatively, it can visually convey certain information, in this case the mandos separators are (mis-)used to indicate the EGLD decimals. +const httpReqCpuProfiler = new CpuProfiler(); +await doHttpRequest(); +httpReqCpuProfiler.stop(`doHttpRequest() execution time`); -```rust title=interact.rs - self.interactor - .tx() // tx with interactor exec environment - .from(&self.wallet_address) - .to(self.state.current_adder_address()) - .egld(NumExpr("0,050000000000000000")) // 0,05 EGLD => 5 * 10^16 - .prepare_async() - .run() - .await; +const cpuProfiler = new CpuProfiler(); +await doSomethingCpuIntensive(); +cpuProfiler.stop(`doSomethingCpuIntensive() execution time`); ``` -In this example, the value put inside `NumExpr` is equal to 0,05 EGLD. The `0,` component is just stylistic and can be ignored, and there are 16 digits after `5` from a total of 18, marking the decimal point. +The output of the code above will be
+`doHttpRequest() execution time: 100ms, CPU time: 1ms` +`doSomethingCpuIntensive() execution time: 20ms, CPU time 18ms` -### `i32` +*Note that a big execution time does not necessarily have an impact on the CPU load of the application. That means that, for example, while waiting for an HTTP request, the JavaScript thread can process other things. That is not the case for CPU time. When a method consumes a lot of CPU time, Javascript will not be able to process other tasks, potentially causing a freeze until the CPU-intensive task is complete.* -`i32` is only supported because it is the default Rust numeric type and unannotated numeric constants are of this type. +--- -Make sure you don't pass a negative value! -```rust title=blackbox_test.rs - state - .world - .tx() // tx with test exec environment - .from(FIRST_USER_ADDRESS) - .to(CROWDFUNDING_ADDRESS) - .typed(crowdfunding_esdt_proxy::CrowdfundingProxy) - .fund() - .egld(1000) // i32 value - .with_result(ExpectError(4, "wrong token")) - .run(); -``` +## Interceptors +The package provides a series of [Nestjs Interceptors](https://docs.nestjs.com/interceptors) which will automatically log and set the CPU and overall duration for each request in a [Prometheus](https://prometheus.io) histogram ready to be scrapped by Prometheus. +`LoggingInterceptor` interceptor will set the execution time of each request in a Prometheus histogram using [performance profilers](#performance-profiler). -## General ESDT payment +`RequestCpuTimeInterceptor` interceptor will set the CPU execution time of each request in a Prometheus histogram using [cpu profiler](#cpu-profiler). -Any ESDT payment can be easily attached to a transaction through the `.esdt(...)` method. This method accepts as argument any type that can be converted into an `EsdtTokenPayment` and can be called multiple times on the same call. +*Both interceptors expect an instance of `metricsService` class as an argument.* +```typescript +import { MetricsService, RequestCpuTimeInterceptor, LoggingInterceptor } from '@multiversx/sdk-nestjs-monitoring'; -```mermaid -graph LR - payment-unit["()"] - payment-unit -->|esdt| EsdtTokenPayment - EsdtTokenPayment -->|esdt| MultiEsdtPayment - MultiEsdtPayment -->|esdt| MultiEsdtPayment -``` +async function bootstrap() { + // AppModule imports MetricsModule + const publicApp = await NestFactory.create(AppModule); + const metricsService = publicApp.get(MetricsService); -```rust title=contract.rs -self.tx() // tx with sc environment - .to(caller) - .esdt((esdt_token_id.clone(), nonce, amount.clone())) // a tuple with three values - .esdt(EsdtTokenPayment::new(esdt_token_id, nonce, amount)) // an EsdtTokenPayment - .transfer(); + const globalInterceptors = []; + globalInterceptors.push(new RequestCpuTimeInterceptor(metricsService)); + globalInterceptors.push(new LoggingInterceptor(metricsService)); + + publicApp.useGlobalInterceptors(...globalInterceptors); +} ``` -In this example, calling `.esdt(...)` will attach an ESDT payment load to the transaction. When adding subsequent `.esdt(...)` calls, the payload automatically converts into a `multi payment`. +## MetricsModule and MetricsService +`MetricsModule` is a [Nestjs Module](https://docs.nestjs.com/modules) responsible for aggregating metrics data through `MetricsService` and exposing them to be consumed by Prometheus. `MetricsService` is extensible, you can define and aggregate your own metrics and expose them. By default it exposes a set of metrics created by the interceptors specified [here](#interceptors). Most of the MultiversX packages expose metrics by default through this service. For example [@multiversx/sdk-nestjs-redis](https://www.npmjs.com/package/@multiversx/sdk-nestjs-redis) automatically tracks the execution time of each redis query, overall redis health and much more, by leveraging the `MetricsService`. -## Single ESDT payment with references -Sometimes we don't have ownership of the token identifier object, or amount, and we would like to avoid unnecessary clones. For this reason, we have created the `EsdtTokenPaymentRefs`, which contains references and can be used as the payment object. +### How to instantiate the MetricsModule and expose metrics endpoints for Prometheus -For brevity, instead of `payment(EsdtTokenPaymentRefs::new(&token_identifier, token_nonce, &amount))`, we can use `.single_esdt(&token_identifier, token_nonce, &amount)`. +In our example we will showcase how to expose response time and CPU time of HTTP requests. Make sure you have the interceptors in place as shown [here](#interceptors). After the interceptors are in place, as requests comes through your application, the metrics are being populated into `MetricsService` class and we just have to expose the output of the `.getMetrics()` method on `MetricsService` through a controller. -```rust title=contract.rs - #[payable] - #[endpoint] - fn send_esdt(&self, to: ManagedAddress) { - let payment = self.call_value().single(); - let half_payment = &payment.amount / 2u32; +```typescript +import { Controller, Get } from '@nestjs/common'; +import { MetricsService } from '@multiversx/sdk-nestjs-monitoring'; - self.tx() - .to(&to) - .payment(PaymentRefs::new( - &payment.token_identifier, - 0, - &half_payment, - )) - .transfer(); +@Controller('metrics') +export class MetricsController { + constructor( + private readonly metricsService: MetricsService + ){} - self.tx() - .to(&self.blockchain().get_caller()) - .payment(PaymentRefs::new( - &payment.token_identifier, - 0, - &half_payment, - )) - .transfer(); - } + @Get() + getMetrics(): string { + return this.metricsService.getMetrics(); + } +} ``` -In this case, adding the ESDT token as payment through `.single_esdt(...)` gives the developer the possibility to keep using the referenced values afterwards without cloning. - +### How to add custom metrics -## Multi ESDT payment - -The framework defines the alias `type MultiEsdtPayment = ManagedVec>;`, which is how multi-esdt payments are held in memory. If we have an object of this type, we can pass it directly as payment. +Adding custom metrics is just a matter of creating another class which uses `MetricsService`. -```rust -let tokens_to_claim = MultiEsdtPayment::::new(); // multiple tokens -self.tx().to(&caller).payment(tokens_to_claim).transfer(); // multi token payment -``` +We can create a new class called `ApiMetricsService` which will have a new custom metric `heartbeatsHistogram`. -It is also possible to pass a reference or `ManagedRef` as payment, e.g.: +```typescript +import { Injectable } from '@nestjs/common'; +import { MetricsService } from '@multiversx/sdk-nestjs-monitoring'; +import { register, Histogram } from 'prom-client'; -```rust -// type annotation added for clarity, normally inferred -let payments: ManagedRef<'static, MultiEsdtPayment> = self.call_value().all_esdt_transfers(); -self.tx().to(&caller).payments(payments).transfer(); -``` +@Injectable() +export class ApiMetricsService { + private static heartbeatsHistogram: Histogram; -The input type is enough for the `payment` method. We also have `.multi_esdt(...)`, which does the same as `payment`, but will additionally convert the argument to `MultiEsdtPayment`. For instance, sending an `EsdtTokenPayment` to `multi_esdt` will set the payment to `MultiEsdtPayment` instead of `EsdtTokenPayment`. + constructor(private readonly metricsService: MetricsService) { + if (!ApiMetricsService.heartbeatsHistogram) { + ApiMetricsService.heartbeatsHistogram = new Histogram({ + name: 'heartbeats', + help: 'Heartbeats', + labelNames: ['app'], + buckets: [], + }); + } + } -It is also possible to construct a `MultiEsdtPayment` by calling `.esdt(...)` at least twice, as seen [earlier](#general-esdt-payment). + async getMetrics(): Promise { + const baseMetrics = await this.metricsService.getMetrics(); + const currentMetrics = await register.metrics(); + return baseMetrics + '\n' + currentMetrics; + } + setHeartbeatDuration(app: string, duration: number) { + ApiMetricsService.heartbeatsHistogram.labels(app).observe(duration); + } +} +``` -## Mixed transfers +The only change we have to do is that we need to instantiate this class and call `.getMetrics()` method on it to return to us both default and our new custom metrics. -Sometimes we don't know at compile time what kind of transfers we are going to perform. For this reason, we also provide contract call types that work with both EGLD and ESDT tokens. +The `.setHeartbeatDuration()` method will be used in our business logic whenever we want to add a new value to that histogram. +--- -### EGLD or single ESDT +### New contract from sc-meta new --template empty -`EgldOrEsdtTokenPayment` allows us to decide at runtime for either EGLD or single ESDT payments. The object can be used as payment. +The real, unedited output of `sc-meta new --template empty`, then the smallest +real customization on top (one storage mapper, one endpoint, one view), because a +genuinely empty contract does not prove the workflow actually works end to end. +Every command in this recipe was run for real with `rustc 1.93.0`, +`cargo 1.93.0`, and `multiversx-sc-meta 0.64.1`. -```rust -// type annotation added for clarity, normally inferred -let payment: EgldOrEsdtTokenPayment = self.call_value().egld_or_single_esdt(); -self.tx().to(to).payment(payment).transfer(); -``` +## Prerequisites +- Rust via `rustup`, with the `wasm32v1-none` target installed (see "A version + note" below for why not `wasm32-unknown-unknown`). +- `sc-meta` (`cargo install multiversx-sc-meta`). +- Optional: `wasm-opt`, for size-optimized release builds (this recipe was + verified without it). -### EGLD or single ESDT references +## Step 1: the bare scaffold -We also have the type `EgldOrEsdtTokenPaymentRefs`, which contains references to the token identifier and amount. +```bash +sc-meta new --template empty --name greeter +``` -For brevity, instead of `payment(EgldOrEsdtTokenPaymentRefs::new(&egld_or_esdt_token_identifier, token_nonce, &amount))`, we can use `.single_esdt(&egld_or_esdt_token_identifier, token_nonce, &amount)`. +This does more than copy files: it renames the trait, the crate names, the module +paths, and the scenario references throughout, all in one pass. Two things worth +noting immediately, both confirmed by actually running the command rather than +reading about it: +- **There is no `sc-config.toml`, no `wasm/`, and no `output/` yet.** A bare + `sc-meta new` output does not have these; project-anatomy overviews often show + them as if every contract project always does. +- **The scenario test files' function names do not get renamed.** The *file* is + renamed and the scenario path inside is updated, but the test function itself is + still `fn empty_go()`, not `fn greeter_go()`. Not a bug, just something to + expect when you diff a freshly generated project. +## Step 2: add a storage mapper, an endpoint, and a proxy -### EGLD or multi-ESDT +```rust title="greeter/src/greeter.rs" +#![no_std] -`EgldOrMultiEsdtPayment` allows us to decide at runtime for either EGLD or multiple ESDT payments. The object can be used as payment. +use multiversx_sc::imports::*; -```rust -// type annotation added for clarity, normally inferred -let payments: EgldOrMultiEsdtPayment = self.call_value().any_payment(); -self.tx().to(to).payment(payment).transfer(); -``` +pub mod greeter_proxy; +/// A minimal greeter contract. Starts from `sc-meta new --template empty +/// --name greeter` (the exact, unmodified output of that command) with one +/// endpoint and one storage mapper added on top — the smallest real next step +/// after scaffolding, and the natural bridge into the +/// storage-mapper-decision-table recipe. +#[multiversx_sc::contract] +pub trait Greeter { + #[init] + fn init(&self) {} -## Normalization + #[upgrade] + fn upgrade(&self) {} -We call normalization the logic of converting transactions with an ESDT payments fields into ESDT built-in function calls. Depending on the type of payment, the generated call be to either one of: -- ESDTTransfer, -- ESDTNFTTransfer, -- MultiESDTNFTTransfer. + /// Stores a greeting message for the calling address. Overwrites any + /// previous greeting for the same caller. + #[endpoint(setGreeting)] + fn set_greeting(&self, message: ManagedBuffer) { + let caller = self.blockchain().get_caller(); + self.greeting(&caller).set(message); + } -For ESDTNFTTransfer and MultiESDTNFTTransfer, the recipient field also needs to be changed into the sender, and the real recipient added to the arguments. + /// Reads back the greeting message stored for `address`. Returns an + /// empty ManagedBuffer if that address never called setGreeting. + #[view(getGreeting)] + #[storage_mapper("greeting")] + fn greeting(&self, address: &ManagedAddress) -> SingleValueMapper; +} +``` -This operation is done automatically by the framework before sending transactions, so the developer should normally not worry about it. +**Generating the proxy has a real chicken-and-egg order to it.** Adding +`pub mod greeter_proxy;` before the file exists fails the build +(`error[E0583]: file not found for module`). The working order: ---- +1. Add `sc-config.toml`: `[[proxy]]` / `path = "src/greeter_proxy.rs"`. +2. Write the contract logic WITHOUT the `pub mod greeter_proxy;` line yet. +3. Run `sc-meta all proxy`, which compiles the contract (which does not yet + reference the proxy module) and generates `src/greeter_proxy.rs` from the + resulting ABI. +4. Add `pub mod greeter_proxy;` now that the file exists. -### Preparing SCs for Supernova +## Step 3: build -The MultiversX Supernova upgrade reduces block time from **6 seconds to 0.6 seconds**, enabling sub-second blocks. While this is a major improvement, it can impact existing smart contracts, especially those relying on assumptions about timestamp behavior. +```bash +sc-meta all build +``` -This guide explains how to prepare your contracts for Supernova safely. +Real output, captured from a build: +```text +Building greeter.wasm in .../greeter/wasm ... +RUSTFLAGS="-C link-arg=-s -C link-arg=-zstack-size=131072" cargo +1.93-aarch64-apple-darwin build --target=wasm32v1-none --release ... + Finished `release` profile [optimized] target(s) in 6.78s +Copying .../target/wasm32v1-none/release/greeter_wasm.wasm to ../output/greeter.wasm ... +Warning: wasm-opt not installed. +Packing ../output/greeter.mxsc.json ... +Contract size: 996 bytes. +``` +`sc-meta all build` also regenerates `wasm/src/lib.rs`'s endpoint list on every +run, confirmed by diffing it before and after adding `setGreeting` / `getGreeting`: +it picked both up automatically with no manual edit. +## Step 4: test -## Understand What Changes — and What Doesn’t +```rust title="greeter/tests/greeter_blackbox_test.rs" +// tests/greeter_blackbox_test.rs — a hand-written blackbox test for the +// endpoint added on top of the bare `empty` scaffold. The two +// `greeter_scenario_*_test.rs` files (unmodified from `sc-meta new +// --template empty`) only exercise deploy; this test exercises the actual +// custom logic, following the recommended blackbox-test pattern. -All existing timestamp APIs continue returning **seconds** unless you explicitly call the millisecond versions. Nothing changes silently in the VM, the framework, or deployed contracts. +use multiversx_sc_scenario::imports::*; -Obviously, contracts that have the 6 seconds between blocks hardcoded will have to change. But more importantly, block times expressed in seconds no longer uniquely identify a block, which leads to potential problems. +use greeter::greeter_proxy; -We are going to go through the most important patterns to look out for. +const OWNER: TestAddress = TestAddress::new("owner"); +const CONTRACT: TestSCAddress = TestSCAddress::new("greeter-contract"); +const CODE_PATH: MxscPath = MxscPath::new("output/greeter.mxsc.json"); +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + blockchain.register_contract(CODE_PATH, greeter::ContractBuilder); + blockchain +} +#[test] +fn set_and_get_greeting() { + let mut world = world(); + world.account(OWNER).nonce(1); + // Deploy. + world + .tx() + .from(OWNER) + .typed(greeter_proxy::GreeterProxy) + .init() + .code(CODE_PATH) + .new_address(CONTRACT) + .run(); -## Potential problems to look out for + // Before any call, the greeting for OWNER is empty. + world + .query() + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .greeting(OWNER.to_address()) + .returns(ExpectValue(ManagedBuffer::::new())) + .run(); + // Call setGreeting as OWNER. + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .set_greeting(ManagedBuffer::::from(b"hello devnet")) + .run(); -### Replace Hardcoded Block Timing + // getGreeting now returns what we stored, keyed by the caller address. + world + .query() + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .greeting(OWNER.to_address()) + .returns(ExpectValue(ManagedBuffer::::from(b"hello devnet"))) + .run(); +} -If your contract uses hard-coded constants (like `6 seconds` or `6000 milliseconds`) to estimate the time between blocks, this logic will need to be changed. +#[test] +fn greeting_is_keyed_per_caller() { + let mut world = world(); + world.account(OWNER).nonce(1); + let other: TestAddress = TestAddress::new("other-caller"); + world.account(other).nonce(1); -It might estimate durations from nonce deltas, or vice-versa, it might estimate number of blocks by timestamps. + world + .tx() + .from(OWNER) + .typed(greeter_proxy::GreeterProxy) + .init() + .code(CODE_PATH) + .new_address(CONTRACT) + .run(); -**Fix:** Use the API instead: `self.blockchain().get_block_round_time_millis()`. This returns `6000` (as `DurationMillis`) today and `600` after Supernova. + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .set_greeting(ManagedBuffer::::from(b"from owner")) + .run(); + // A different caller who never called setGreeting still reads back + // empty — this is what a parameterized storage mapper gives you: the key + // includes the parameter automatically, so each address gets its own + // storage slot, not a shared one. + world + .query() + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .greeting(other.to_address()) + .returns(ExpectValue(ManagedBuffer::::new())) + .run(); + world + .query() + .to(CONTRACT) + .typed(greeter_proxy::GreeterProxy) + .greeting(OWNER.to_address()) + .returns(ExpectValue(ManagedBuffer::::from(b"from owner"))) + .run(); +} +``` +```bash +cargo test +``` -### Avoid Timestamp-Based Monotonicity +```text +running 2 tests +test greeting_is_keyed_per_caller ... ok +test set_and_get_greeting ... ok +test result: ok. 2 passed; 0 failed; ... -Logic such as: +running 1 test +test empty_go ... ok -``` -require!(ts_now > last_ts) +running 1 test +test empty_rs ... ok ``` -may break because multiple blocks can share the same timestamp. +All 4 tests pass: the 2 new blackbox tests plus the 2 scaffold-provided scenario +tests. -**Fix:** Use **block nonces** for guaranteed monotonicity. +## A version note +Three different version numbers showed up while working through this recipe, and +they disagree: +| Source | `multiversx-sc` version | +| --- | --- | +| Commonly documented "current" version | 0.65.0 | +| `mx-sdk-rs` GitHub `master`, `contracts/examples/empty/Cargo.toml` | 0.66.2 | +| This machine's installed `sc-meta` (0.64.1), and what it actually generated | 0.64.1 | +`sc-meta new` bundles its own template, versioned to match whatever +`multiversx-sc-meta` release you have installed; it does not fetch the latest +framework version from anywhere. Run `sc-meta upgrade` after scaffolding for the +latest framework version, or pass `sc-meta new --tag ` to pin one +explicitly. -### Prevent Rate-Limit Bypasses +Also worth noting: the actual build command targets `wasm32v1-none`, not +`wasm32-unknown-unknown` as some older references list, confirmed directly from +the real `sc-meta all build` invocation logged above. -If your contract allows one action “per block” but checks the difference in **seconds**, multiple blocks in the same second can bypass restrictions. +## Pitfalls -**Fix:** Use block nonces or switch to millisecond timestamps. +:::danger[Pitfall 1: declaring the proxy module before generating it breaks the build] +See Step 2's ordering: generate first, declare the module second. +::: +:::note[Pitfall 2: a bare sc-meta new output has no sc-config.toml] +Project-anatomy diagrams often show one unconditionally; you only need it once +you want proxy generation or a multi-contract build. +::: +:::tip[Pitfall 3: wasm-opt not being installed does not fail the build] +It is a warning, and `sc-meta all build` still produces a valid, deployable +`.wasm` / `.mxsc.json`, just without size optimization. Install it before +shipping to mainnet if contract size matters. +::: +:::note[Pitfall 4: scenario test function names surviving a rename are not a sign something went wrong] +See Step 1's second bullet. +::: -### Revisit Expiration Logic +:::warning[Pitfall 5: sc-meta new's bundled template version may lag the framework's documented current version] +Check what actually got generated (`cat Cargo.toml`) rather than assuming it +matches the newest release. +::: -Expiration logic written assuming a fixed block interval may accidentally allow: +## See also -* extra blocks before expiration -* longer-than-intended windows for execution +- [Storage mappers: which to pick, when](/sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/storage-mapper-decision-table) + is the natural next recipe: this one introduces exactly one mapper; that one + covers the rest. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the dApp side that would eventually call an endpoint like `setGreeting`. -If your expiration logic uses seconds, double-check assumptions. +--- +### NFT & SFT tokens +```mdx-code-block +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import TableWrapper from "@site/src/components/TableWrapper"; +``` -### Stop Using Timestamps as Block Identifiers +## **Introduction** -Before Supernova, a timestamp could uniquely identify a block. -After Supernova, multiple blocks may share the same timestamp. +MultiversX NFTs(non-fungible tokens) are a breed of digital assets that are revolutionizing the world of art, collectibles, and more. These NFTs are unique, one-of-a-kind tokens that are built on blockchain technology, allowing for secure ownership and transfer of these assets. With MultiversX NFTs, every token is assigned a unique identification code(ticker) and metadata that distinguishes it from every other token, making each NFT truly one-of-a-kind. Read the full page for a comprehensive guide on how to brand, issue, transfer, assign roles and many other features, for both NFTs and SFTs. -If you use timestamps as map keys or identifiers: -* collisions will occur -* data may be overwritten or skipped -**Fix:** Use block **nonces**. +### NFT and SFT +The MultiversX protocol introduces native NFT support by adding metadata and attributes on top of the already existing [Fungible tokens](/tokens/fungible-tokens). +This way, one can issue a semi-fungible token or a non-fungible token which is quite similar to an ESDT, but has a few more attributes, as well as an assignable URI. +Once owning a quantity of a NFT/SFT, users will have their data store directly under their account, inside the trie. All the fields available inside a NFT/SFT token can be found [here](/tokens/nft-tokens#nftsft-fields). +**The flow of issuing and transferring non-fungible or semi-fungible tokens is:** +- register/issue the token +- set roles to the address that will create the NFT/SFTs +- create the NFT/SFT +- transfer quantity(es) -### Review Reward and Accumulation Calculations -Reward logic that uses: +### Meta ESDT -``` -delta = ts_now - last_ts -``` +In addition to NFTs and SFTs, MultiversX introduced Meta ESDTs. +Meta ESDTs are a special case of semi-fungible-tokens. They can be seen as regular ESDT fungible tokens that also have properties. +In a particular example, LKMEX or XMEX are MetaESDTs and their properties help implement the release schedule. -may behave unexpectedly: -* delta may be zero over multiple blocks -* rewards might accumulate slower or unevenly -* divisions by delta may cause division-by-zero errors +## **Branding** -Consider switching to milliseconds for finer granularity. +Anyone can create NFTs and SFTs tokens on MultiversX Network. There are also no limits in tokens names or tickers. For example, +one issues an `AliceToken` with the ticker `ALC`. Anyone else is free to create a new token with the same token name and +the same token ticker. The only difference will be the random sequence of the token identifier. So the "original" token +could have received the random sequence `1q2w3e` resulting in the `ALC-1q2w3e` identifier, while the second token could +have received the sequence `3e4r5t` resulting in `ALC-3e4r5t`. +In order to differentiate between an original token and other tokens with the same name or ticker, we have introduced a +branding mechanism that allows tokens owners to provide a logo, a description, a website, as well as social link for their tokens. MultiversX products such as Explorer, Wallet and so on +will display tokens in accordance to their branding, if any. +A token owner can submit a branding request by opening a Pull Request on https://github.com/multiversx/mx-assets. -## Migration Guidance +### **Submitting a branding request** -Just as important as fixing potential issues with Supernova, it is essential not to introduce new bugs in the process. +Token owners can create a PR to the https://github.com/multiversx/mx-assets with the logo in .png and .svg format, as well as a .json file containing all the relevant information. -Make sure to take the following into consideration when migrating: +Here’s a prefilled template for the .json file to get you started: +```json +{ + "website": "https://www.multiversxtoken.com", + "description": "MultiversX Token is a collection of 10.000 unique and randomly generated tokens.", + "social": { + "email": "mxt-token@multiversxtoken.com", + "blog": "https://www.multiversxtoken.com/MXT-token-blog", + "twitter": "https://twitter.com/MXT-token-twitter" + }, + "status": "active" +} +``` +## **Issuance of Non-Fungible Tokens** -### Seconds vs. Milliseconds +One has to perform an issuance transaction in order to register a non-fungible token. +Non-Fungible Tokens are issued via a request to the Metachain, which is a transaction submitted by the Account which will manage the tokens. When issuing a token, one must provide a token name, a ticker and optionally additional properties. This transaction has the form: -Switching from second to millisecond timestamp can be error-prone. +```rust +IssuanceTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 50000000000000000 # (0.05 EGLD) + GasLimit: 60000000 + Data: "issueNonFungible" + + "@" + + + "@" + +} +``` -The most dangerous bug is accidentally mixing second and millisecond values, causing storage and logic corruption. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -To prevent this issue, use the [strongly typed timestamp and duration objects](time-types). +Optionally, the properties can be set when issuing a token. Example: -Starting in **multiversx-sc v0.63.0**, all timestamp APIs have typed replacements: +```rust +IssuanceTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 50000000000000000 # (0.05 EGLD) + GasLimit: 60000000 + Data: "issueNonFungible" + + "@" + + + "@" + + + "@" + <"canFreeze" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canWipe" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canPause" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canTransferNFTCreateRole" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canChangeOwner" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canUpgrade" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canAddSpecialRoles" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + ... +} +``` -* `get_block_timestamp_seconds()` -* `get_block_timestamp_millis()` -* `get_prev_block_timestamp_seconds()` -* `get_prev_block_timestamp_millis()` -* `get_block_round_time_millis()` -* `epoch_start_block_timestamp_millis()` +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -And avoid using raw `u64` for time values. +The receiver address `erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u` is a built-in system smart contract (not a VM-executable contract), which only handles token issuance and other token management operations, and does not handle any transfers. +The contract will add a random string to the ticker thus creating the **token identifier**. The random string starts with “-” and has 6 more random characters. For example, a token identifier could look like _ALC-6258d2_. -Make sure to convert form `u64` to type timestamps **before** doing any other refactoring. It is much safer this way. +## **Issuance of Semi-Fungible Tokens** +One has to perform an issuance transaction in order to register a semi-fungible token. +Semi-Fungible Tokens are issued via a request to the Metachain, which is a transaction submitted by the Account which will manage the tokens. When issuing a semi-fungible token, one must provide a token name, a ticker and optionally additional properties. This transaction has the form: +```rust +IssuanceTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 50000000000000000 # (0.05 EGLD) + GasLimit: 60000000 + Data: "issueSemiFungible" + + "@" + + + "@" + +} +``` -### Backwards compatibility +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -When upgrading an existing contract from second to millisecond timestamps, it is essential to: +Optionally, the properties can be set when issuing a token. Example: -* Ensure storage remains consistent -* Update ESDT metadata carefully -* Add compatibility logic if needed +```rust +IssuanceTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 50000000000000000 # (0.05 EGLD) + GasLimit: 60000000 + Data: "issueSemiFungible" + + "@" + + + "@" + + + "@" + <"canFreeze" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canWipe" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canPause" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canTransferNFTCreateRole" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canChangeOwner" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canUpgrade" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canAddSpecialRoles" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + ... +} +``` -If you are unsure that this can be done safely, it might be safer to keep the contract running on second timestamps. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +The receiver address `erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u` is a built-in system smart contract (not a VM-executable contract), which only handles token issuance and other token management operations, and does not handle any transfers. +The contract will add a random string to the ticker thus creating the **token identifier**. The random string starts with “-” and has 6 more random characters. For example, a token identifier could look like _ALC-6258d2_. +## **Issuance of Meta-ESDT Tokens** -## Summary Checklist +One has to perform an issuance transaction in order to register a Meta-ESDT token. +Meta-ESDT Tokens are issued via a request to the Metachain, which is a transaction submitted by the Account which will manage the tokens. When issuing a semi-fungible token, one must provide a token name, a ticker and optionally additional properties. This transaction has the form: -To prepare for Supernova: +```rust +IssuanceTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 50000000000000000 # (0.05 EGLD) + GasLimit: 60000000 + Data: "registerMetaESDT" + + "@" + + + "@" + + + "@" + +} +``` -* [ ] Upgrade to `multiversx-sc v0.63.0` -* [ ] Use typed timestamp/duration APIs -* [ ] Remove all hardcoded 6-second or 6000-millisecond assumptions -* [ ] Avoid using timestamps as block identifiers -* [ ] Review expiration, reward, and accumulation logic -* [ ] Switch to millisecond timestamps where appropriate -* [ ] Ensure storage/metadata compatibility -* [ ] Update tests to use for millisecond block timestamps, where needed +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ ---- +Optionally, the properties can be set when issuing a token. Example: -### Proxies +```rust +IssuanceTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 50000000000000000 # (0.05 EGLD) + GasLimit: 60000000 + Data: "registerMetaESDT" + + "@" + + + "@" + + + "@" + + "@" + <"canFreeze" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canWipe" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canPause" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canTransferNFTCreateRole" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canChangeOwner" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canUpgrade" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + "@" + <"canAddSpecialRoles" hexadecimal encoded> + "@" + <"true" or "false" hexadecimal encoded> + + ... +} +``` -## Overview +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -Proxies are objects that mimic the contract, they provide methods with the same names and the same argument types. When called, they will format a transaction for the contract. So they act as translators: you can call them just like regular functions, and they will translate it for the blockchain and pass on the call. They also tell you what type the function is expected to return. +The receiver address `erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u` is a built-in system smart contract (not a VM-executable contract), which only handles token issuance and other token management operations, and does not handle any transfers. +The contract will add a random string to the ticker thus creating the **token identifier**. The random string starts with “-” and has 6 more random characters. For example, a token identifier could look like _ALC-6258d2_. -New proxies are generated as structures. If you have the proxy, you have all its methods. The generated code is in plain sight and readable, designed to add no overhead to the contract binaries once compiled. -Proxies can be simply copied between crates, so there is no more need for dependencies between contract crates. Proxies do not mind what their source contract code looks like or what framework version it uses. +### **Converting an SFT into Meta-ESDT** +An already existing _semi-fungible token_ can be converted into a Meta-ESDT token if the owner sends the following transaction: -## How to generate +```rust +ConvertSftToMetaESDTTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 0 + GasLimit: 60000000 + Data: "changeSFTToMetaESDT" + + "@" + + + "@" + +} +``` -Proxy generation can be triggered by calling `sc-meta all proxy` in the contract root folder. This command generates the proxy of the main contract in `/output` folder with name `proxy.rs` if it has no configuration. To configure it, you need to modify sc-config.toml. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -This first image represents the structure of the project without calling a proxy generator, while the second one shows how it is called and what files it generates. -![img](/img/tree_before_proxy.png) +## **Parameters format** -![img](/img/tree_after_proxy.png) +Token Name: +- length between 3 and 50 characters +- alphanumeric characters only -## How to set up project to re-generate easily +Token Ticker: -In order to configure the proxy, you need to change configuration file. If this file is absent from the project directory, you have to create it. More details about how to set up a configuration file are available [here](../meta/sc-config/). +- length between 3 and 10 characters +- alphanumeric UPPERCASE only -### Path -First, you need to specify the output path where the generated proxy file will be saved. This ensures that whenever you regenerate the proxy, the configured path will be automatically updated with the latest version. +## **Issuance examples** -```toml title=sc-config.toml -[settings] +For example, a user named Alice wants to issue an ESDT called "AliceTokens" with the ticker "ALC". The issuance transaction would be: -[[proxy]] -path = "src/adder_proxy.rs" +```rust +IssuanceTransaction { + Sender: erd1sg4u62lzvgkeu4grnlwn7h2s92rqf8a64z48pl9c7us37ajv9u8qj9w8xg + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 50000000000000000 # (0.05 EGLD) + GasLimit: 60000000 + Data: "issueSemiFungible" + + "@416c696365546f6b656e73" + + "@414c43" + +} ``` -After the proxy is generated via "sc-meta all proxy" command, you have to import the module in the contract. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -```rust title=adder.rs -#![no_std] +Once this transaction is processed by the Metachain, Alice becomes the designated **manager of AliceTokens**. She can add quantity later using `ESDTNFTCreate`. For more operations available to ESDT token managers, see [Token management](/tokens/fungible-tokens#token-management). -use multiversx_sc::imports::*; +In that smart contract result, the `data` field will contain a transfer syntax which is explained below. What is important to note is that the token identifier can be fetched from +here in order to use it for transfers. Alternatively, the token identifier can be fetched from the API (explained also in section [REST API - Get NFT data](/tokens/nft-tokens#get-nft-data-for-an-address) ). -pub mod adder_proxy; -#[multiversx_sc::contract] -pub trait Adder { - ... -} -``` +## **Roles** -:::note -Changing the settings requires configuring the output path. -::: -### Override imports -If you need to override the proxy imports that are encapsulated in the line `use multiversx_sc::proxy_imports::*;` from proxy, you can achieve this by adding the `override_import`. +In order to be able to perform actions over a token, one needs to have roles assigned. +The existing roles are: +For NFT: -```toml title=sc-config.toml -[settings] +- ESDTRoleNFTCreate : this role allows one to create a new NFT +- ESDTRoleNFTBurn : this role allows one to burn a specific NFT +- ESDTRoleNFTUpdateAttributes : this role allows one to change the attributes of a specific NFT +- ESDTRoleNFTAddURI : this role allows one add URIs for a specific NFT +- ESDTTransferRole : this role enables transfer only to specified addresses. The addresses with the transfer role can transfer anywhere. +- ESDTRoleNFTUpdate : this role allows one to update meta data attributes of a specific NFT +- ESDTRoleModifyRoyalties : this role allows one to modify royalities of a specific NFT +- ESDTRoleSetNewURI : this role allows one to set new uris of a specific NFT +- ESDTRoleModifyCreator : this role allows one to rewrite the creator of a specific token +- ESDTRoleNFTRecreate : this role allows one to recreate the whole NFT with new attributes -[[proxy]] -path = "src/adder_proxy.rs" -override-import = "use multiversx_sc::abi::{ContractAbi, EndpointAbi};" -``` - -### Rename paths -If you want to rename paths from structures and enumerations in a generated proxy, use the `path-rename` setting. Specify both the original path and the new name you want. - -```toml title=sc-config.toml -[[proxy]] -path = "src/adder_proxy.rs" -[[proxy.path-rename]] -from = "adder" -to = "new_path::adder" -``` - -### Generate variant from multi-contract -To generate a proxy for a specific variant within a multi-contract project, use the `variant` setting and specify the desired variant. +For SFT: -```toml title=multicontract.toml -[settings] -main = "multi-contract-main" +- ESDTRoleNFTCreate : this role allows one to create a new SFT +- ESDTRoleNFTBurn : this role allows one to burn quantity of a specific SFT +- ESDTRoleNFTAddQuantity : this role allows one to add quantity of a specific SFT +- ESDTTransferRole : this role enables transfer only to specified addresses. The addresses with the transfer role can transfer anywhere. +- ESDTRoleNFTUpdate : this role allows one to update meta data attributes of a specific SFT +- ESDTRoleModifyRoyalties : this role allows one to modify royalities of a specific SFT +- ESDTRoleSetNewURI : this role allows one to set new uris of a specific SFT +- ESDTRoleModifyCreator : this role allows one to rewrite the creator of a specific token +- ESDTRoleNFTRecreate : this role allows one to recreate the whole NFT with new attributes -[[proxy]] -variant = "multi_contract_example_feature" -path = "src/multi_contract_example_feature_proxy.rs" -``` +To see how roles can be assigned, please refer to [this](/tokens/nft-tokens#assigning-roles) section. -### Generate custom proxy -First, it needs to be specified that all unlabelled endpoints should **not** be added to this contract. This can be configured using: -- `add-unlabelled` - - values: `true` | `false` - - default: `true` +## **Assigning roles** -```toml title=multicontract.toml -[[proxy]] -path = "src/multisig_view_proxy.rs" -add-unlabelled = false # unlabelled endpoints are not included -``` -If you want to generate a proxy for all endpoints labelled with a certain tag, you can use: -- `add-labels` - - values: `list of strings`, e.g. add-labels = ["label1", "label2"] - - default: `[]` +Roles can be assigned by sending a transaction to the Metachain from the ESDT manager. -The example below will create a proxy for all endpoints that are tagged with *"label(proxy_one)"* and *"label(proxy_two)"*. +Within a transaction of this kind, any number of roles can be assigned (minimum 1). -```toml title=multicontract.toml -[[proxy]] -path = "src/multisig_view_proxy.rs" -add-unlabelled = false -add-labels = ["proxy_one", "proxy_two"] +```rust +RolesAssigningTransaction { + Sender:
+ Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 0 + GasLimit: 60000000 + Data: "setSpecialRole" + + "@" + + + "@" +
+ + "@" + + + "@" + + + ... +} ``` -If you want to generate a proxy based on a specific list of endpoints, there is: -- `add-endpoints` - - values: `list of strings`, e.g. add-endpoints = ["endpoint_one", "endpoint_two"] - - default: `[]` +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -The following example will create a proxy only with the functions specified in the list of `add-endpoints`. +For example, `ESDTRoleNFTCreate` = `45534454526f6c654e4654437265617465` -```toml title=multicontract.toml -[[proxy]] -path = "src/multisig_view_proxy.rs" -add-unlabelled = false -add-endpoints = ["init", "user_role"] -``` -The custom proxy can be generated using simultaneously both configurations: specified endpoints and endpoints labelled with a particular tag. -```toml title=multicontract.toml -[[proxy]] -path = "src/multisig_view_proxy.rs" -add-unlabelled = false -add-labels = ["proxy_one"] -add-endpoints = ["user_role"] -``` +Unset transactions are very similar. You can find an example [here](/tokens/fungible-tokens#unset-special-role). -## Adjustments in contracts +## **NFT/SFT fields** -Before generating a proxy, you have to change every structure and enumeration that contains `#[derive(TypeAbi)]` with `#[type_abi]`. If you do not update structures and enumerations, they will not be correctly generated in the proxy. +Below you can find the fields involved when creating an NFT. -Before version 0.49.0: -```rust title=lib.rs -#[derive(TopEncode, TopDecode, PartialEq, Eq, Clone, Copy, Debug, TypeAbi)] -pub enum Status { - FundingPeriod, - Successful, - Failed, -} -``` -After version 0.49.0: -```rust title=lib.rs -#[type_abi] -#[derive(TopEncode, TopDecode, PartialEq, Eq, Clone, Copy, Debug)] -pub enum Status { - FundingPeriod, - Successful, - Failed, -} -``` -If the custom type is in a private module or another crate is better to replace it because the proxy will be useless. +**NFT Name** +- The name of the NFT or SFT +**Quantity** -## Diagram +- The quantity of the token. If NFT, it must be `1` -This is the diagram for how to populate the data field using proxies. For the raw setup, see [here](tx-data#diagram) +**Royalties** -```mermaid -graph LR - subgraph "Data (via proxies)" - data-unit["()"] - data-unit -->|typed| Proxy - Proxy -->|"init(args, ...)"| deploy - Proxy -->|"upgrade(args, ...)"| upgrade - Proxy -->|"«endpoint»(args, ...)"| fc[Function Call] - deploy -->|from_source| deploy-from-source["DeployCall<FromSource<ManagedAddress>>"] - deploy -->|code| deploy-code["DeployCall<Code<ManagedBuffer>>"] - deploy -->|code_metadata| deploy - upgrade -->|from_source| upgrade-from-source["UpgradeCall<CodeSource<ManagedAddress>>"] - upgrade -->|code| upgrade-code["UpgradeCall<Code<ManagedBuffer>>"] - upgrade -->|code_metadata| upgrade - end -``` +- Allows the creator to receive royalties for any transaction involving their NFT +- Base format is a numeric value between 0 an 10000 (0 meaning 0% and 10000 meaning 100%) +**Hash** +- Arbitrary field that should contain the hash of the NFT metadata +- Optional filed, should be left `null` when building the transaction to create the NFT -## Original type +**Attributes** -Proxies have the power to define the **original type** by themselves because, when they are generated, they have access to the ABI. +- Represents additional information about the NFT or SFT, like picture traits or tags for your NFT/collection +- The field should follow a `metadata:ipfsCID/fileName.json;tags:tag1,tag2,tag3` format +- Below you can find a sample for the extra metadata format that should be stored on IPFS: -The proxy will add the type definition by calling `.original_type()`, which places an [`OriginalResultMarker`](tx-result-handlers#original-result-marker) object as the transaction result handler. This is a zero-sized type, it is only there for type inference and safety in result handlers. +```json +{ + "description": "This is a sample description", + "attributes": [ + { + "trait_type": "Background", + "value": "Yellow", + "{key}": "{value}", + "{...}": "{...}", + "{key}": "{value}" + }, + { + "trait_type": "Headwear", + "value": "BlackBeanie" + }, + { + "trait_type": "SampleTrait3", + "value": "SampleValue3" + } + ], + "collection": "ipfsCID/fileName.json" +} +``` +**URI(s)** +- Mandatory field that represents the URL to a [supported](#supported-media-types) media file ending with the file extension as described in the [example](#example) below +- Field should contain the `Uniform Resource Identifier` -## NotPayable protection +Note: As a best practice, we recommend storing the files for media & extra metadata(from attributes field) within same folder on your storage provider, ideally IPFS. Also, in order to have a thumbnail generated for the uploaded file the size of the file should be less or equal to 64MB. -All non-payment endpoints in the generated proxy have a safeguard to prevent accidental payments. This involves automatically setting the payment to [**NotPayable**](tx-payment#notpayable). +:::important +Please note that each argument must be encoded in hexadecimal format with an even number of characters. +::: ---- -### Proxy architecture +### **Supported Media Types** -Overview of the MultiversX Proxy +Below you can find a table with the supported media types for NFTs available on MultiversX network. +| Media Extension | Media Type | +|-----------------|-----------------| +| .png | image/png | +| .jpeg | image/jpeg | +| .jpg | image/jpg | +| .gif | image/gif | +| .webp | image/webp | +| .svg | image/svg | +| .svg | image/svg+xml | +| .acc | audio/acc | +| .flac | audio/flac | +| .m4a | audio/m4a | +| .mp3 | audio/mp3 | +| .wav | audio/wav | +| .mov | video/mov | +| .quicktime | video/quicktime | +| .mp4 | video/mp4 | +| .webm | video/webm | -## **Introduction** -The MultiversX Proxy acts as an entry point into the MultiversX Network, through a set of Observer Nodes, and (partly) abstracts away the particularities and complexity of sharding. +### **Example** -The Proxy is a project written in **go**, and it serves as foundation for *gateway.multiversx.com*. +Below you can find a table representing an example of the fields for a non-fungible token that resembles a song. -The source code of the Proxy can be found here: [mx-chain-proxy-go](https://github.com/multiversx/mx-chain-proxy-go). + +| Property | Plain value | Encoded value | +|----------------|--------------------------------------------------------|----------------------------------------------------------------------------------------------------------| +| **NFT Name** | Beautiful song | 42656175746966756c20736f6e67 | +| **Quantity** | 1 | 01 | +| **Royalties** | 7500 _=75%_ | 1d4c | +| **Hash** | 00 | 00 | +| **Attributes** | metadata:_ipfsCID/song.json_;tags:song,beautiful,music | 6d657461646174613a697066734349442f736f6e672e6a736f6e3b746167733a736f6e672c62656175746966756c2c6d75736963 | +| **URI** | _URL_to_decentralized_storage/song.mp3_ | 55524c5f746f5f646563656e7472616c697a65645f73746f726167652f736f6e672e6d7033 | + +In this example we are creating a NFT representing a song. Hash is left null, we are sharing media location URL and we are also providing the location of the extra metadata within the attributes field. -## **Architectural Overview** -While any Node in the Network can accept Transaction requests, the Transactions are usually submitted to the **Proxy** application, which maintains a list of Nodes - **Observers** - to forward Transaction requests to - these Observers are selected in such manner that any Transaction submitted to them will be processed by the Network **as soon and as efficiently as possible**. +## **Creation of an NFT** -The Proxy will submit a Transaction on behalf of the user to the REST API of one of its listed Observers, selected for **(a)** being _online_ at the moment and **(b)** being located **within the Shard to which the Sender's Account belongs**. After receiving the Transaction on its REST API, that specific Observer will propagate the Transaction throughout the Network, which will lead to its execution. +A single address can own the role of creating an NFT for an ESDT token. This role can be transferred by using the `ESDTNFTCreateRoleTransfer` function. -The Observer Nodes of the Proxy thus act as a **default dedicated entry point into the Network**. +An NFT can be created on top of an existing ESDT by sending a transaction to self that contains the function call that triggers the creation. +Any number of URIs can be assigned (minimum 1) -It is worth repeating here, though, that submitting a Transaction through the Proxy is completely optional - any Node of the Network will accept Transactions to propagate, given it has not disabled its REST API. +```rust +NFTCreationTransaction { + Sender:
+ Receiver: + Value: 0 + GasLimit: 3000000 + Additional gas (see below) + Data: "ESDTNFTCreate" + + "@" + + + "@" + + + "@" + + + "@" + + + "@" + + + "@" + + + "@" + + + "@" + + + ... +} +``` -![img](/technology/proxy-overview.png) +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -Overview of the MultiversX Proxy +Additional gas refers to: -In the figure above: +- Transaction payload cost: Data field length \* 1500 (GasPerDataByte = 1500) +- Storage cost: Size of NFT data \* 50000 (StorePerByte = 50000) -1. The **MultiversX Network** - consisting of Nodes grouped within Shards. Some of these Nodes are **Observers**. -2. One or more instances of the **MultiversX Proxy** - including the official one - connect to Observer Nodes in order to forward incoming user Transactions to the Network and to query state within the Blockchain. -3. The **client applications** connect to the Network through the MultiversX Proxy. It is also possible for a blockchain-powered application to talk directly to an Observer or even to a Validator. +To see more about the required fields, please refer to [this](/tokens/nft-tokens#nftsft-fields) section. +:::tip +Note that because NFTs are stored in accounts trie, every transaction involving the NFT will require a gas limit depending on NFT data size. +::: -## **Official MultiversX Proxy** +Most of the times you will be able to create the NFTs by issuing one single transaction. +This assumes that the metadata file as well as the NFT media is already uploaded to IPFS. -The official instance of the MultiversX Proxy is located at [https://gateway.multiversx.com](https://gateway.multiversx.com/). +There are times, however, when uploading the metadata file before issuing the NFT is not possible (eg. when issued from a smart contract) +In these cases it is possible to update an NFT with the metadata file after it was issued by sending an additional transaction. You can find more information [here](/tokens/nft-tokens#change-nft-attributes) about how to update the attributes -## **Swagger docs** +## **Other management operations** -The Swagger docs of the proxy can be found at the root of the gateway. For example: [https://gateway.multiversx.com](https://gateway.multiversx.com/). +Managing non-fungible tokens (NFTs) and semi-fungible tokens (SFTs) presents a greater degree of complexity compared to the management of simple fungible tokens. These unique tokens require specialized transactions for proper identification, ownership, and transfer, making the process of managing them more intricate than that of fungible tokens. -## **Set up a Proxy Instance** +### **Transfer NFT Creation Role** -:::caution -Documentation for setting up a Proxy is preliminary and subject to change +:::tip +This role can be transferred only if the `canTransferNFTCreateRole` property of the token is set to `true`. ::: -In order to host a Proxy instance on a web server, one has to first clone and build the repository: +The role of creating an NFT can be transferred by a Transaction like this: -```bash -git clone https://github.com/multiversx/mx-chain-proxy-go.git -cd elrond-proxy-go/cmd/proxy -go build . +```rust +TransferCreationRoleTransaction { + Sender:
+ Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 0 + GasLimit: 60000000 + length of Data field in bytes * 1500 + Data: "transferNFTCreateRole" + + "@" + + + "@" + + + "@" + +} ``` +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -### **Configuration** -The Proxy holds its configuration within the `config` folder: +### **Stop NFT creation** -- `config.toml` - this is the main configuration file. It has to be adjusted so that the Proxy points to a list of chosen Observer Nodes. -- `external.toml` - this file holds configuration necessary to Proxy components that interact with external systems. An example of such an external system is **Elasticsearch** - currently, MultiversX Proxy requires an Elasticsearch instance to implement some of its functionality. -- `apiConfig/credentials.toml` - this file holds the configuration needed for enabling secured endpoints - only accessible by using BasicAuth. -- `apiConfig/v1_0.toml` - this file contains all the endpoints with their settings (open, secured and rate limit). +The ESDT manager can stop the creation of an NFT for the given ESDT forever by removing the only `ESDTRoleNFTCreate` role available. +This is done by performing a transaction like this: -:::note -If the provided observers' addresses from `config.toml` are not physical addresses of each observer, but rather addresses of providers that manage multiple observers for each shard, with self handling of health checks, the Proxy must be started with `--no-status-check` flag. -::: +```rust +StopNFTCreationTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 0 + GasLimit: 60000000 + Data: "stopNFTCreate" + + "@" + + +} +``` +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -## **Snapshotless observers support** -Instead of nodes that perform regular trie operations, such as snapshots and so on, one could use snapshotless nodes, which are, as the name suggests, nodes that have a different configuration which allows them to "bypass" certain costly trie operations, with the downside of losing access to anything but real-time. +### **Change NFT Attributes** -A proxy that only needs real-time data (that is, they are not interested in historical data such as "give me block with nonce X from last month") are a very good use-case for snapshotless observers +An user that has the `ESDTRoleNFTUpdateAttributes` role set for a given ESDT, can change the attributes of a given NFT/SFT. +:::tip +`ESDTNFTUpdateAttributes` will remove the old attributes and add the new ones. Therefore, if you want to keep the old attributes you will have to pass them along with the new ones. +::: +This is done by performing a transaction like this: -### **Proxy snapshotless endpoints** +```rust +ESDTNFTUpdateAttributesTransaction { + Sender:
+ Receiver: + Value: 0 + GasLimit: 10000000 + Data: "ESDTNFTUpdateAttributes" + + "@" + + + "@" + + + "@" + +} +``` -Although there are more endpoints that can be used exclusively with snapshotless observers, here's a list with the most common ones: -- `/address/{address}` : returns data about an address (balance, nonce, username, root hash and so on) -- `/address/{address}/balance` : returns the balance of an address -- `/address/{address}/nonce` : returns the nonce of an address -- `/address/{address}/username` : returns the username of an address -- `/address/{address}/esdt` : returns all the ESDT tokens managed by an address -- `/address/{address}/esdt/{tokenIdentifier}` : returns the token data of an address with a specific token identifier -- `/address/{address}/nft/{tokenIdentifier}/nonce/{nonce}` : returns the NFT/SFT/MetaESDT data of an address for a specific token identifier and nonce -- `/address/{address}/guardian-data` : returns guardian data of an address -- `/address/{address}/keys` : returns the storage key-value pairs of an address -- `/address/{address}/key/{key}` : returns the value of a specific key under an address -- `/network/config` : returns the network configuration -- `/network/status/{shard}` : returns the status of the specific shard -- `/node/heartbeatstatus` : return the heartbeat status of the entire network's nodes -- `/transaction/send` : broadcasts a transaction to the network -- `/vm-values/int` : performing SC Queries (gas-free queries) expecting the result as int -- `/vm-values/hex` : performing SC Queries (gas-free queries) expecting the result as hex -- `/vm-values/string` : performing SC Queries (gas-free queries) expecting the result as string -- `/vm-values/query` : performing SC Queries (gas-free queries) expecting the result as raw -- and so on +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -Basically, every endpoint that doesn't require historical data access can be used with snapshotless observers +To see how you can assign this role in case it is not set, please refer to [this](/tokens/nft-tokens#assigning-roles) section. -## **Dependency on Elasticsearch** +### **Add URIs to NFT** -Currently, Proxy uses the dependency to Elasticsearch in order to satisfy the [Get Address Transactions](/sdk-and-tools/rest-api/addresses/#get-address-transactions) endpoint. +An user that has the `ESDTRoleNFTAddURI` role set for a given ESDT, can add uris to a given NFT/SFT. +This is done by performing a transaction like this: -In order to connect a Proxy instance to an Elasticsearch cluster, one must update the `external.toml` file. +```rust +ESDTNFTAddURITransaction { + Sender:
+ Receiver: + Value: 0 + GasLimit: 10000000 + Data: "ESDTNFTAddURI" + + "@" + + + "@" + + + "@" + + + "@" + + + ... +} +``` ---- +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -### Python SDK +To see how you can assign this role in case it is not set, please refer to [this](/tokens/nft-tokens#assigning-roles) section. -## Overview -This page will guide you through the process of handling common tasks using the MultiversX Python SDK (libraries) **v2 (latest, stable version)**. +### **Add quantity (SFT only)** -:::important -This cookbook makes use of `sdk-py v2`. In order to migrate from `sdk-py v1` to `sdk-py v2`, please also follow [the migration guide](https://github.com/multiversx/mx-sdk-py/issues?q=label:migration). -::: +A user that has the `ESDTRoleNFTAddQuantity` role set for a given Semi-Fungible Token, can increase its quantity. This function will not work for NFTs, because in that case the quantity cannot be higher than 1. -:::note -All examples depicted here are captured in **(interactive) [Jupyter notebooks](https://github.com/multiversx/mx-sdk-py/blob/main/examples/Cookbook.ipynb)**. -::: +```rust +AddQuantityTransaction { + Sender:
+ Receiver: + Value: 0 + GasLimit: 10000000 + Data: "ESDTNFTAddQuantity" + + "@" + + + "@" + + "@" + +} +``` -We are going to use the [multiversx-sdk-py](https://github.com/multiversx/mx-sdk-py) package. This package can be installed directly from GitHub or from [**PyPI**](https://pypi.org/project/multiversx-sdk/). +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - +If successful, the balance of the address for the given SFT will be increased with the number specified in the argument. -## Creating an Entrypoint -The Entrypoint represents a network client that makes the most common operations easily accessible. We have an entrypoint for each network: `MainnetEntrypoint`, `DevnetEntrypoint`, `TestnetEntrypoint` and `LocalnetEntrypoint`. For example, this is how to create a Devnet entrypoint: +### **Burn quantity** -```py -from multiversx_sdk import DevnetEntrypoint +A user that has the `ESDTRoleNFTBurn` role set for a given semi-fungible Token, can burn some (or all) of the quantity. -entrypoint = DevnetEntrypoint() +```rust +BurnQuantityTransaction { + Sender:
+ Receiver: + Value: 0 + GasLimit: 10000000 + Data: "ESDTNFTBurn" + + "@" + + + "@" + + "@" + +} ``` -If we want to create an entrypoint that uses a third party API, we can do so as follows: +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -```py -from multiversx_sdk import DevnetEntrypoint +If successful, the quantity from the argument will be decreased from the balance of the address for that given token. -entrypoint = DevnetEntrypoint(url="https://custom-multiversx-devnet-api.com") -``` -By default, an Entrypoint, in our case the `DevnetEntrypoint`, uses the API, but we can also create a custom one that interacts with the proxy. +### **Freezing and Unfreezing a single NFT** -```py -from multiversx_sdk import DevnetEntrypoint +The manager of an ESDT token may freeze the NFT held by a specific Account. As a consequence, no NFT can be transferred to or from the frozen Account. Freezing and unfreezing a single NFT of an Account are operations designed to help token managers to comply with regulations. The transaction that freezes a single NFT of an Account has the form: -custom_entrypoint = DevnetEntrypoint(url="https://devnet-gateway.multiversx.com", kind="proxy") +```rust +FreezeTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 0 + GasLimit: 60000000 + Data: "freezeSingleNFT" + + "@" + + + "@" + + "@" + +} ``` -We can create an entrypoint from a network provider. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -```py -from multiversx_sdk import NetworkEntrypoint, ApiNetworkProvider +The reverse operation, unfreezing, will allow further transfers to and from the Account: -api = ApiNetworkProvider("https://devnet-api.multiversx.com") -entrypoint = NetworkEntrypoint.new_from_network_provider(network_provider=api, chain_id="D") +```rust +UnfreezeTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 0 + GasLimit: 60000000 + Data: "unFreezeSingleNFT" + + "@" + + + "@" + + + "@" + +} ``` -## Creating Accounts +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -We can create an account directly from the entrypoint. Keep in mind that the account you create is network agnostic, it does not matter which entrypoint is used. -The account can be used for signing and for storing the nonce of the account. It can also be saved to a `pem` or `keystore` file. +### **Wiping a single NFT** -```py -from multiversx_sdk import DevnetEntrypoint +The manager of an ESDT token may wipe out a single NFT held by a frozen Account. This operation is similar to burning the quantity, but the Account must have been frozen beforehand, and it must be done by the token manager. Wiping the tokens of an Account is an operation designed to help token managers to comply with regulations. Such a transaction has the form: -entrypoint = DevnetEntrypoint() -account = entrypoint.create_account() +```rust +WipeTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 0 + GasLimit: 60000000 + Data: "wipeSingleNFT" + + "@" + + + "@" + + + "@" + +} ``` -There are also other ways to instantiate an `Account`. - -#### Instantiating an Account using a secret key - -```py -from multiversx_sdk import Account, UserSecretKey +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -secret_key_hex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9" -secret_key = UserSecretKey(bytes.fromhex(secret_key_hex)) -account = Account(secret_key) -``` +### **Modify Royalties** -#### Instantiating an Account from a PEM file +The manager of an ESDT token may want to set new royalities. This operation will rewrite the royalities on the specified token ID. +It requires `ESDTRoleNFTModifyRoyalties` role. +This is done by performing a transaction like this: -```py -from pathlib import Path -from multiversx_sdk import Account - -account = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +```rust +ModifyRoyalitiesTransaction { + Sender: + Receiver: + Value: 0 + GasLimit: 60000000 + Data: "ESDTModifyRoyalties" + + "@" + + + "@" + + + "@" + +} ``` -#### Instantiating an Account from a Keystore file - -```py -from pathlib import Path -from multiversx_sdk import Account +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -account = Account.new_from_keystore( - file_path=Path("../multiversx_sdk/testutils/testwallets/alice.json"), - password="password" -) -``` -#### Instantiating an Account from a mnemonic +### **Set new URIs** -```py -from multiversx_sdk import Account, Mnemonic +The manager of an ESDT token may want to set new URIs. This operation will rewrite the URIs on the specified token ID. +It requires `ESDTRoleNFTSetNewURIs` role. +This is done by performing a transaction like this: -mnemonic = Mnemonic.generate() -account = Account.new_from_mnemonic(mnemonic.get_text()) +```rust +SetNewURIsTransaction { + Sender: + Receiver: + Value: 0 + GasLimit: 60000000 + Data: "ESDTSetNewURIs" + + "@" + + + "@" + + + "@" + + + "@" + + + ... +} ``` -#### Instantiating an Account from a KeyPair +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -```py -from multiversx_sdk import Account, KeyPair -keypair = KeyPair.generate() -account = Account.new_from_keypair(keypair) -``` +### **Modify Creator** -### Managing the Account nonce +The creator of a token can be changed. For this, the token has to be moved to the new creator account. The new creator +account requires `ESDTRoleModifyCreator` role. Also, the token has to be of dynamic type in order for this to work. -The account has a `nonce` property that the user is responsible for keeping up to date. We can fetch the nonce of the account from the network once and then we can increment it with each transaction we create. Each transaction sent **must** have the correct nonce set, otherwise it will not be executed. For more details check out the [Creating Transactions](#creating-transactions) section below. +The creator can be modified using a transaction like this: -```py -from multiversx_sdk import Account, DevnetEntrypoint, UserSecretKey +```rust +ModifyCreatorTransaction { + Sender: + Receiver: + Value: 0 + GasLimit: 60000000 + Data: "ESDTModifyCreator" + + "@" + + + "@" + +} +``` -secret_key_hex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9" -secret_key = UserSecretKey(bytes.fromhex(secret_key_hex)) +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -account = Account(secret_key) -entrypoint = DevnetEntrypoint() -account.nonce = entrypoint.recall_account_nonce(account.address) +### **MetaData Update** -# create any sort of transaction -... +The manager of an ESDT token may want to update token metadata. This operation will update token metadata on the specified token ID. +It requires `ESDTRoleNFTUpdate` role. If nothing is received for a given attribute, the old version of that attribute will be kept. -# When needed, we can get the nonce and increment it -nonce = account.get_nonce_then_increment() +This is done by performing a transaction like this: + +```rust +MetaDataUpdateTransaction { + Sender: + Receiver: + Value: 0 + GasLimit: 60000000 + Data: "ESDTMetaDataUpdate" + + "@" + + + "@" + + + "@" + + + "@" + + + "@" + + + "@" + + + "@" + +} ``` -### Saving the Account to a file +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -We can save the account to either a `pem` file or a `keystore` file. **We discourage the use of PEM wallets for storing cryptocurrencies due to their lower security level.** However, they prove to be highly convenient and user-friendly for application testing purposes. -#### Saving the Account for a PEM file +### **MetaData Recreate** -```py -from pathlib import Path -from multiversx_sdk import Account, UserSecretKey +The whole NFT can be recreated with new attributes using a transaction like this: -secret_key_hex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9" -secret_key = UserSecretKey(bytes.fromhex(secret_key_hex)) +The manager of an ESDT token may want to recreate the whole token with new attributes. This operation will recreate token attributes on the specified token ID. +It requires `ESDTRoleNFTRecreate` role. If an argument is not being set, that field is set to zero. -account = Account(secret_key) -account.save_to_pem(path=Path("wallet.pem")) +This is done by performing a transaction like this: + +```rust +MetaDataRecreateTransaction { + Sender: + Receiver: + Value: 0 + GasLimit: 60000000 + Data: "ESDTMetaDataRecreate" + + "@" + + + "@" + + + "@" + + + "@" + + + "@" + + + "@" + + + "@" + +} ``` -#### Saving the Account to a Keystore file +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -```py -from pathlib import Path -from multiversx_sdk import Account, UserSecretKey -secret_key_hex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9" -secret_key = UserSecretKey(bytes.fromhex(secret_key_hex)) +### **Make token dynamic** -account = Account(secret_key) -account.save_to_keystore(path=Path("keystoreWallet.json"), password="password") -``` +The ESDT manager can change token type to dynamic using a transaction like this: -### Ledger Account +```rust +ChangeToDynamicTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 0 + GasLimit: 60000000 + Data: "changeToDynamic" + + "@" + +} +``` -It is possible to use a Ledger Device to manage your account. The Ledger account allows you to sign both transactions and messages, but it can also store the nonce of the account. +The following token types cannot be changed to dynamic: `FungibleESDT`, `NonFungibleESDT`, `NonFungibleESDTv2` -By default, the package does not include all the dependencies required to communicate with a Ledger device. To enable Ledger support, install the package with the following command: +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -```sh -pip install multiversx-sdk[ledger] -``` -This will install the necessary dependencies for interacting with a Ledger device. +### **Update token** -When instantiating a `LedgerAccount`, the index of the address that will be used should be provided. By default, the index `0` is used. +The token type can be updated to the latest version, which will update token type and propagate it to shard's +system account. Currently, token type is correctly saved only on metachain and there is no type related information +on shard level, the shard only knows if the token is non fungible with implicit type `NonFungibleESDT`, but it +does not know specifically if it's `NonFungibleESDT`, `MetaESDT` or `SemiFungibleESDT`. So, the update operation will +proceed in the following way: +- if token type is `NonFungibleESDT` it will be set to `NonFungibleESDTv2`, and it will be propagated to shard's system account +- if token type is `MetaESDT` or `SemiFungibleESDT` it will be propagated to shard's system account -```py -from multiversx_sdk import LedgerAccount +This can be done using a transaction like this: -account = LedgerAccount() +```rust +UpdateTokenIDTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 0 + GasLimit: 60000000 + Data: "updateTokenID" + + "@" + +} ``` -When signing transactions with a Ledger device, the transaction details will appear on the device, awaiting your confirmation. The same process applies when signing messages. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -Both **Account** and **LedgerAccount** are compatible with the **IAccount** interface and can be used wherever the interface is expected (e.g. in transaction controllers). -## Calling the Faucet +### **Register dynamic token** -This functionality is not yet available through the entrypoint, but we recommend using the faucet available within the Web Wallet. +A token can be registered directly as dynamic. -- [Testnet Wallet](https://testnet-wallet.multiversx.com/) -- [Devnet Wallet](https://devnet-wallet.multiversx.com/) +This can be done using a transaction like this: -## Interacting with the network +```rust +RegisterDynamicTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 50000000000000000 # (0.05 EGLD) + GasLimit: 60000000 + Data: "registerDynamic" + + "@" + + + "@" + + + "@" + + # For META token type only + "@" + + # (number of decimals) +} +``` -The entrypoint exposes a few methods to directly interact with the network, such as: +The following token types cannot be registered as dynamic: `FungibleESDT` -- `recall_account_nonce(address: Address) -> int;` -- `send_transaction(transaction: Transaction) -> bytes;` -- `send_transactions(transactions: list[Transaction]) -> tuple[int, list[bytes]];` -- `get_transaction(tx_hash: str | bytes) -> TransactionOnNetwork;` -- `await_transaction_completed(tx_hash: str | bytes) -> TransactionOnNetwork;` -Some other methods are exposed through a so called network provider. There are two types of network providers: ApiNetworkProvider and ProxyNetworkProvider. The ProxyNetworkProvider interacts directly with the proxy of an observing squad. The ApiNetworkProvider, as the name suggests, interacts with the API, that is a layer over the proxy. It fetches data from the network but also from Elastic Search. +### **Register and set all roles to dynamic** -To get the underlying network provider from our entrypoint, we can do as follows: +A token can be registered directly as dynamic together will all roles set for the specific type. -```py -from multiversx_sdk import DevnetEntrypoint +This can be done using a transaction like this: -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() +```rust +RegisterAndSetAllRolesDynamicTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 50000000000000000 # (0.05 EGLD) + GasLimit: 60000000 + Data: "registerAndSetAllRolesDynamic" + + "@" + + + "@" + + + "@" + + + # For META token type only + "@" + + # (number of decimals) +} ``` -## Creating a network provider +The following token types cannot be registered as dynamic: `FungibleESDT` -Additionally, when manually instantiating a network provider, a config can be provided to specify the client name and set custom request options. -```py -from multiversx_sdk import NetworkProviderConfig, ApiNetworkProvider +### **Transferring token management rights** -config = NetworkProviderConfig( - client_name="hello-multiversx", - requests_options={ - "timeout": 1, - "auth": ("user", "password") - } -) +The manager of an ESDT token can transfer the ownership if the ESDT was created as upgradable. Check the [ESDT - Upgrading (changing properties)](/tokens/fungible-tokens#upgrading-changing-properties) section for more details. -api = ApiNetworkProvider(url="https://devnet-api.multiversx.com", config=config) -``` -The network providers support a retry mechanism for failing requests. If you'd like to change the default values you can do so as follows: +### **Upgrading (changing properties)** -```py -from multiversx_sdk import NetworkProviderConfig, ApiNetworkProvider, RequestsRetryOptions +The manager of an ESDT token may individually change any of the properties of the token, or multiple properties at once, only if the ESDT was created as upgradable. +Check the [ESDT - Transferring token management rights](/tokens/fungible-tokens#transferring-token-management-rights) section for more details. -retry_options = RequestsRetryOptions( - retries=5, - backoff_factor=0.1, - status_forcelist=[500, 502, 503] -) -config = NetworkProviderConfig( - client_name="hello-multiversx", - requests_options={ - "timeout": 1, - "auth": ("user", "password") - }, - requests_retry_options=retry_options, -) +## **Transfers** -api = ApiNetworkProvider(url="https://devnet-api.multiversx.com", config=config) +Performing an ESDT NFT transfer is done by specifying the receiver's address inside the `Data` field, alongside other details. An ESDT NFT transfer transaction has the following form: + +```rust +TransferTransaction { + Sender: + Receiver: + Value: 0 + GasLimit: 1000000 + length of Data field in bytes * 1500 + Data: "ESDTNFTTransfer" + + "@" + + + "@" + + + "@" + + + "@" + +} ``` -A list of all the available methods from the `ApiNetworkProviders` can be found [here](https://multiversx.github.io/mx-sdk-py/multiversx_sdk.network_providers.html#module-multiversx_sdk.network_providers.api_network_provider). +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -Both the `ApiNetworkProvider` and the `ProxyNetworkProvider` implement a common interface, that can be seen [here](https://multiversx.github.io/mx-sdk-py/multiversx_sdk.network_providers.html#multiversx_sdk.network_providers.interface.INetworkProvider). Therefore, the two network providers can be used interchangeably. +:::tip +Here is an example of an NFT identifier: `ABC-1a9c7d-05dc` -The classes returned by the API have the most used fields easily accessible, but each object has a `raw` field where the raw API response is stored in case some other fields are needed. +The collection identifier is `ABC-1a9c7d` and the NFT nonce is `05dc`. Note that the `05dc` is hexadecimal encoded, it represents decimal 1500. -## Fetching data from the network +Also note that a MultiversX address is in bech32, so you will need to convert the address from bech32 to hexadecimal. This can be done with the `hex()` method of mx-sdk-js-core for address (all the methods for addresses can be found [here](https://github.com/multiversx/mx-sdk-js-core/blob/main/src/address.ts)) or manually with an external converter which you can find [here.](https://utils.multiversx.com/converters#addresses-bech32-to-hexadecimal) +::: -### Fetching the network config -```py -from multiversx_sdk import DevnetEntrypoint +## **Transfers to a Smart Contract** -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() +To perform the transfer from your account to the smart contract, you have to use the following transaction format: -network_config = api.get_network_config() +```rust +TransferTransaction { + Sender: + Receiver: + Value: 0 + GasLimit: 1000000 + extra for smart contract call + Data: "ESDTNFTTransfer" + + "@" + + + "@" + + + "@" + + + "@" + + + "@" + + + "@" + + + "@" + + + <...> +} ``` -### Fetching the network status - -The status is fetched by default from the metachain, but a specific shard number can be provided. - -```py -from multiversx_sdk import DevnetEntrypoint - -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() - -network_status = api.get_network_status() # fetches status from metachain -network_status = api.get_network_status(shard=1) # fetches status from shard 1 -``` +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -### Fetching a block from the network -We instantiate the args and we are going to fetch the block using it's hash. The `API` only supports fetching blocks by hash, while the `PROXY` can fetch blocks by hash or by nonce. Keep in mind, that for the `PROXY` the shard should also be specified in the arguments. +## **Multiple tokens transfer** -#### Fetching a block using the API +Multiple semi-fungible and/or non-fungible tokens can be transferred in a single transaction to a single receiver. -```py -from multiversx_sdk import ApiNetworkProvider +More details can be found [here](/tokens/fungible-tokens#multiple-tokens-transfer). -api = ApiNetworkProvider("https://devnet-api.multiversx.com") -block_hash="1147e111ce8dd860ae43a0f0d403da193a940bfd30b7d7f600701dd5e02f347a" -block = api.get_block(block_hash=block_hash) -``` +## **Example flow** -Additionally, we can fetch the latest block from the network: +Let's see a complete flow of creating and transferring a Semi-Fungible Token. -```py -from multiversx_sdk import ApiNetworkProvider +**Step 1: Issue/Register a Semi-Fungible Token** -api = ApiNetworkProvider("https://devnet-api.multiversx.com") -latest_block = api.get_latest_block() +```rust +{ + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 50000000000000000 # 0.05 EGLD + GasLimit: 60000000 + Data: "issueSemiFungible" + + "@416c696365546f6b656e73" + # AliceTokens + "@414c43" + # ALC +} ``` -#### Fetching a block using the PROXY - -When using the proxy, we have to provide the shard, as well. - -```py -from multiversx_sdk import ProxyNetworkProvider +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -proxy = ProxyNetworkProvider("https://devnet-gateway.multiversx.com") +**Step 2: Fetch the token identifier** -block_hash="1147e111ce8dd860ae43a0f0d403da193a940bfd30b7d7f600701dd5e02f347a" -block = proxy.get_block(shard=1, block_hash=block_hash) -``` +For doing this, one has to check the previously sent transaction and see the Smart Contract Result of it. +It will look similar to `@ok@414c432d317132773365`. The `414c432d317132773365` represents the token identifier in hexadecimal encoding. -We can also fetch the latest block from the network. The default shard will be the metachain, but we can specify a shard to fetch the latest block from. +**Step 3: Set roles** -```py -from multiversx_sdk import ProxyNetworkProvider +Assign `ESDTRoleNFTCreate` and `ESDTRoleNFTAddQuantity` roles to an address. You can set these roles to your very own address. -proxy = ProxyNetworkProvider("https://devnet-gateway.multiversx.com") -block = proxy.get_latest_block() +```rust +{ + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 0 + GasLimit: 60000000 + Data: "setSpecialRole" + + "@414c432d317132773365" + # previously fetched token identifier + "@" +
+ + "@45534454526f6c654e4654437265617465" + # ESDTRoleNFTCreate + "@45534454526f6c654e46544164645175616e74697479" # ESDTRoleNFTAddQuantity + ... +} ``` -### Fetching an account - -To fetch an account we'll need its address. Once we have the address, we simply create an `Address` object and pass it as an argument to the method. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -```py -from multiversx_sdk import Address, DevnetEntrypoint +**Step 4: Create NFT** -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() +Now, the NFT creation transaction for the example case defined [here](/tokens/nft-tokens#creation-of-an-nft) looks like this: -alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -account = api.get_account(address=alice) +```rust +{ + Sender:
+ Receiver: + Value: 0 + GasLimit: 3000000 + Data: "ESDTNFTCreate" + + "@414c432d317132773365" + # previously fetched token identifier + "@01" + # quantity: 1 + "@42656175746966756c20736f6e67" + # NFT name: 'Beautiful song' in hexadecimal encoding + "@1d4c" + # Royalties: 7500 =75%c in hexadecimal encoding + "@00" + # Hash: 00 in hexadecimal encoding + "@6d657461646174613a697066734349442f736f6e672e6a736f6e3b746167733a736f6e672c62656175746966756c2c6d75736963" + # Attributes: metadata:ipfsCID/song.json;tags:song,beautiful,music in hexadecimal encoding> + + "@55524c5f746f5f646563656e7472616c697a65645f73746f726167652f736f6e672e6d7033" + # URI: URL_to_decentralized_storage/song.mp3 in hexadecimal encoding> + + "@" + + +} ``` -### Fetching an account's storage - -We can also fetch an account's storage, which means we can get all the key-value pairs saved for an account. - -```py -from multiversx_sdk import Address, DevnetEntrypoint - -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -address = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -account = api.get_account_storage(address=address) -``` +:::tip +Note that the nonce is very important when creating an NFT. You must save the nonce after NFT creation because you will need it for further actions. -If we only want a specific key, we can fetch it as follows: +The `NFT nonce` is different from the creator's nonce. -```py -from multiversx_sdk import Address, DevnetEntrypoint +It can be fetched by viewing all the tokens for the address via API. +::: -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() +**Step 5: Transfer** -address = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -account = api.get_account_storage_entry(address=address, entry_key="testKey") +```rust +{ + Sender: + Receiver: + Value: 0 + GasLimit: 1000000 + length of Data field in bytes * 1500 + Data: "ESDTNFTTransfer" + + "@414c432d317132773365" + # previously fetched token identifier + "@" + + + "@" + + + "@" + +} ``` -### Waiting for an account to meet a condition - -There are times when we need to wait for a specific condition to be met before proceeding with an action. For example, let's say we want to send 7 EGLD from Alice to Bob, but this can only happen once Alice's balance reaches at least 7 EGLD. This approach is useful in scenarios where you are waiting for external funds to be sent to Alice, allowing her to then transfer the required amount to another recipient. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -We need to define our condition that will be checked each time the account is fetched from the network. For this, we create a function that takes as an argument an `AccountOnNetwork` object and returns a `bool`. -Keep in mind that, this method has a default timeout that can be adjusted using the `AwaitingOptions` class. +## **REST API** -```py -from multiversx_sdk import Address, AccountOnNetwork, DevnetEntrypoint +There are a number of API endpoints that one can use to interact with ESDT NFT data. These are: -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() +### GET **Get NFT data for an address** {#get-nft-data-for-an-address} -def condition_to_be_satisfied(account: AccountOnNetwork) -> bool: - return account.balance >= 7000000000000000000 # 7 EGLD + + +Returns the balance of an address for specific ESDT Tokens. -alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -account = api.await_account_on_condition(address=alice, condition=condition_to_be_satisfied) +```bash +https://gateway.multiversx.com/address//nft//nonce/ ``` -### Sending and simulating transactions +| Param | Required | Type | Description | +| --------------- | ----------------------------------------- | --------- | -------------------------------------- | +| bech32Address | REQUIRED | `string` | The Address to query in bech32 format. | +| tokenIdentifier | REQUIRED | `string` | The token identifier. | +| nonce | REQUIRED | `numeric` | The nonce after the NFT creation. | -In order for our transactions to be executed, we use the network providers to broadcast them to the network. Keep in mind that, in order for transactions to be processed they need to be signed. + + -#### Sending a transaction +```json +{ + "data": { + "tokenData": { + "attributes": "YXR0cmlidXRl", + "balance": "2", + "creator": "erd1ukn0tukrdhuv0zzxn0zlr53g7h0fr68dz9dd56mkksev59nwuvnswnlyuy", + "hash": "aGFzaA==", + "name": "H", + "nonce": 1, + "properties": "", + "royalties": "9000", + "tokenIdentifier": "4W97C-32b5ce", + "uris": ["bmZ0IHVyaQ=="] + } + }, + "error": "", + "code": "successful" +} +``` -```py -from multiversx_sdk import Address, DevnetEntrypoint, Transaction + + -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() -alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -bob = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") +### GET **Get NFTs/SFTs registered by an address** {#get-nftssfts-registered-by-an-address} -transaction = Transaction( - sender=alice, - receiver=bob, - gas_limit=50000, - chain_id="D" -) + + -# set correct nonce and sign the transaction -... +Returns the identifiers of the tokens that have been registered by the provided address. -# broadcast the transaction to the network -transaction_hash = api.send_transaction(transaction) +```bash +https://gateway.multiversx.com/address//registered-nfts ``` -#### Sending multiple transactions - -```py -from multiversx_sdk import Address, DevnetEntrypoint, Transaction +| Param | Required | Type | Description | +| ------------- | ----------------------------------------- | -------- | -------------------------------------- | +| bech32Address | REQUIRED | `string` | The Address to query in bech32 format. | -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() + + -alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -bob = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") +```json +{ + "data": { + "tokens": ["ABC-36tg72"] + }, + "error": "", + "code": "successful" +} +``` -first_transaction = Transaction( - sender=alice, - receiver=bob, - gas_limit=50000, - chain_id="D", - nonce=2 -) -# set correct nonce and sign the transaction -... + + -second_transaction = Transaction( - sender=bob, - receiver=alice, - gas_limit=50000, - chain_id="D", - nonce=1 -) -# set correct nonce and sign the transaction -... -third_transaction = Transaction( - sender=alice, - receiver=alice, - gas_limit=60000, - chain_id="D", - nonce=3, - data=b"hello" -) -# set correct nonce and sign the transaction -... +### GET **Get tokens where an address has a given role** {#get-tokens-where-an-address-has-a-given-role} -# broadcast the transactions to the network -num_of_txs, hashes = api.send_transactions([first_transaction, second_transaction, third_transaction]) -``` + + -#### Simulating transactions +Returns the identifiers of the tokens where the given address has the given role. -A transaction can be simulated before being sent to be processed by the network. It is mostly used for smart contract calls to see what smart contract results are produced. +```bash +https://gateway.multiversx.com/address//esdts-with-role/ +``` -```py -from multiversx_sdk import Address, DevnetEntrypoint, Transaction +| Param | Required | Type | Description | +| ------------- | ----------------------------------------- | -------- | -------------------------------------- | +| bech32Address | REQUIRED | `string` | The Address to query in bech32 format. | +| role | REQUIRED | `string` | The role to query for. | -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() +The role can be one of the roles specified in the documentation (for example: ESDTRoleNFTCreate) -alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgqccmyzj9sade2495w78h42erfrw7qmqxpd8sss6gmgn") + + -transaction = Transaction( - sender=alice, - receiver=contract, - gas_limit=5000000, - chain_id="D", - nonce=entrypoint.recall_account_nonce(alice), # nonce needs to be properly set - data=b"add@07", - signature=b'0' * 64, # signature is not checked by default, but a dummy value must be provided -) -transaction_on_network = api.simulate_transaction(transaction) +```json +{ + "data": { + "tokens": ["ABC-36tg72"] + }, + "error": "", + "code": "successful" +} ``` -#### Estimating the gas cost of a transaction + + -Before sending a transaction to the network to be processed, one can get the estimated gas limit that is required for the transaction to be executed. -```py -from multiversx_sdk import Address, DevnetEntrypoint, Transaction +### GET **Parse non/semi fungible tokens transfer logs** {#parse-nonsemi-fungible-tokens-transfer-logs} -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() +Each **successful** nft/sft transfer generates logs and events that can be used to parse all the details about a transfer +(token identifier, sent amount and receiver). +In order to get the logs and events generated by the transfer, one should know the transaction's hash. -alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgqccmyzj9sade2495w78h42erfrw7qmqxpd8sss6gmgn") + + -nonce = entrypoint.recall_account_nonce(alice) +| Param | Required | Type | Description | +| ------ | ----------------------------------------- | -------- | --------------------------- | +| txHash | REQUIRED | `string` | The hash of the transaction | -transaction = Transaction( - sender=alice, - receiver=contract, - gas_limit=5000000, - chain_id="D", - data=b"add@07", - nonce=nonce -) +```bash +https://gateway.multiversx.com/transaction/*txHash*?withResults=true +``` -transaction_cost_response = api.estimate_transaction_cost(transaction) + + + +```rust +{ + "data": { + "transaction": { + ... + "logs": { + "address": "...", + "events": [ + { + "address": "...", + "identifier": "ESDTNFTTransfer", + "topics": [ + "VFNGVC1jODY3ZzM=", // TSFT-c867g3 + "CEI=", // 2114 + "Ag==", // 2 + "givNK+JiLZ5VA5/dP11QKoYEn7qoqnD8uPchH3ZMLw4=" // erd1sg4u62lzvgkeu4grnlwn7h2s92rqf8a64z48pl9c7us37ajv9u8qj9w8xg + ], + "data": null + }, + } + } + "error": "", + "code": "successful" +} ``` -### Waiting for transaction completion +The event with the identifier `ESDTNFTTransfer` will have the following topics: -After sending a transaction, we may want to wait until the transaction is processed in order to proceed with another action. Keep in mind that, this method has a default timeout that can be adjusted using the `AwaitingOptions` class. +- 1st topic: token identifier (decoding: base64 to string) +- 2nd topic: token nonce (decoding: base64 to hex string + hex string to big number / integer) +- 3rd topic: the amount to be sent (decoding: base64 to hex string + hex string to big number) +- 4th topic: the recipient of the tokens (decoding: base64 to hex string + hex string to bech32 address) -```py -from multiversx_sdk import DevnetEntrypoint +In this example, `erd1sg4u62lzvgkeu4grnlwn7h2s92rqf8a64z48pl9c7us37ajv9u8qj9w8xg` received 2 tokens of the collection +`TSFT-c867g3` with nonce `2114`. -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() + + -tx_hash = "exampletransactionhash" -transaction_on_network = api.await_transaction_completed(transaction_hash=tx_hash) -``` -### Waiting for a transaction to specify a condition +### GET **Get all ESDT tokens for an address** {#get-all-esdt-tokens-for-an-address} -Similar to accounts, we can wait until a transaction satisfies a specific condition. +One can use [get all esdt tokens for an address endpoint](/tokens/fungible-tokens#get-all-esdt-tokens-for-an-address) used for ESDT. -```py -from multiversx_sdk import DevnetEntrypoint, TransactionOnNetwork -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() +### GET **Get all issued ESDT tokens** {#get-all-issued-esdt-tokens} +One can use [get all issued esdt tokens endpoint](/tokens/fungible-tokens#get-all-issued-esdt-tokens) used for ESDT. -def condition_to_be_satisfied(transaction_on_network: TransactionOnNetwork) -> bool: - # can be the creation of an event or something else - ... +### POST **Get ESDT properties** {#get-esdt-properties} -tx_hash = "exampletransactionhash" -transaction_on_network = api.await_transaction_on_condition(transaction_hash=tx_hash, condition=condition_to_be_satisfied) -``` +Properties can be queried via the [getTokenProperties function](/tokens/fungible-tokens#get-esdt-token-properties) provided by ESDT. -### Fetching transactions from the network -After sending transactions, we can fetch the transactions from the network. To do so, we need the transaction hash that we got after broadcasting the transaction. +### POST **Get special roles** {#get-special-roles} -```py -from multiversx_sdk import DevnetEntrypoint +Special roles can be queried via the [getSpecialRoles function](/tokens/fungible-tokens#get-special-roles-for-a-token) provided by ESDT. -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() +--- -tx_hash = "exampletransactionhash" -transaction_on_network = api.get_transaction(tx_hash) -``` +### operations -### Fetching a token from an account +This page describes the structure of the `operations` index (Elasticsearch), and also depicts a few examples of how to query it. -We can fetch a specific token (ESDT, MetaESDT, SFT, NFT) of an account by providing the address and the token. -```py -from multiversx_sdk import Address, DevnetEntrypoint, Token +## _id -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() +The _id field of this index is represented by the transactions OR smart contract result hash, in a hexadecimal encoding. -alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -# these tokens are just for the example, they do not belong to Alice -token = Token(identifier="TEST-123456") # ESDT -token_on_network = api.get_token_of_account(address=alice, token=token) +## Fields -token = Token(identifier="NFT-987654", nonce=11) # NFT -token_on_network = api.get_token_of_account(address=alice, token=token) -``` +| Field | Description | +|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| miniBlockHash | The miniBlockHash field represents the hash of the miniblock in which the transaction was included. | +| nonce | The nonce field represents the transaction sequence number of the sender address. | +| round | The round field represents the round of the block when the transaction was executed. | +| value | The value field represents the amount of EGLD to be sent from the sender to the receiver. | +| receiver | The receiver field represents the destination address of the transaction. | +| sender | The sender field represents the address of the transaction sender. | +| receiverShard | The receiverShard field represents the shard ID of the receiver address. | +| senderShard | The senderShard field represents the shard ID of the sender address. | +| gasPrice | The gasPrice field represents the amount to be paid for each gas unit. | +| gasLimit | The gasLimit field represents the maximum gas units the sender is willing to pay for. | +| gasUsed | The gasUsed field represents the amount of gas used by the transaction. | +| fee | The fee field represents the amount of EGLD the sender paid for the transaction. | +| initialPaidFee | The initialPaidFee field represents the initial amount of EGLD the sender paid for the transaction, before the refund. | +| data | The data field holds additional information for a transaction. It can contain a simple message, a function call, an ESDT transfer payload, and so on. | +| signature | The signature of the transaction, hex-encoded. | +| timestamp | The timestamp field represents the timestamp of the block in which the transaction was executed. | +| status | The status field represents the status of the transaction. | +| senderUserName | The senderUserName field represents the username of the sender address. | +| receiverUserName | The receiverUserName field represents the username of the receiver address. | +| hasScResults | The hasScResults field is true if the transaction has smart contract results. | +| isScCall | The isScCall field is true if the transaction is a smart contract call. | +| hasOperations | The hasOperations field is true if the transaction has smart contract results. | +| tokens | The tokens field contains a list of ESDT tokens that are transferred based on the data field. The indices from the `tokens` list are linked with the indices from `esdtValues` list. | +| esdtValues | The esdtValues field contains a list of ESDT values that are transferred based on the data field. | +| receivers | The receivers field contains a list of receiver addresses in case of ESDTNFTTransfer or MultiESDTTransfer. | +| receiversShardIDs | The receiversShardIDs field contains a list of receiver addresses' shard IDs. | +| type | The type field represents the type of the transaction based on the data field. | +| operation | The operation field represents the operation of the transaction based on the data field. | +| function | The function field holds the name of the function that is called in case of a smart contract call. | +| isRelayed | The isRelayed field is true if the transaction is a relayed transaction. | +| version | The version field represents the version of the transaction. | +| hasLogs | The hasLogs field is true if the transaction has logs. | -### Fetching all fungible tokens of an account -Fetches all fungible tokens held by an account. This method does not handle pagination, that can be achieved by using `do_get_generic`. +This index contains both transactions and smart contract results. This is useful because one can query both of them in a single request. -```py -from multiversx_sdk import Address, DevnetEntrypoint +The unified structure will contain an extra field in order to be able to differentiate between them. -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() -alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -fungible_tokens = api.get_fungible_tokens_of_account(address=alice) -``` +| Field | Description | +|-------|------------------------------------------------------------------------------------------------| +| type | It can be `normal` in case of a transaction and `unsigned` in case of a smart contract result. | -### Fetching all non-fungible tokens of an account -Fetches all non-fungible tokens held by an account. This method does not handle pagination, but can be achieved by using `do_get_generic`. +## Query examples -```py -from multiversx_sdk import Address, DevnetEntrypoint -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() +### Fetch the latest transactions of an address -alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -nfts = api.get_non_fungible_tokens_of_account(address=alice) +``` +curl --request GET \ + --url ${ES_URL}/operations/_search \ + --header 'Content-Type: application/json' \ + --data '{ + "query": { + "bool": { + "must": [ + { + "match": { + "type": "normal" + } + } + ], + "should": [ + { + "match": { + "sender": "erd..." + } + }, + { + "match": { + "receiver": "erd..." + } + }, + { + "match": { + "receivers": "erd..." + } + } + ] + } + }, + "sort": [ + { + "timestamp": { + "order": "desc" + } + } + ] +}' ``` -### Fetching token metadata +### Fetch the latest operations of an address -If we want to fetch the metadata of a token, like `owner`, `decimals` and so on, we can use the following methods: +``` +ADDRESS="erd1..." -```py -from multiversx_sdk import DevnetEntrypoint +curl --request GET \ + --url ${ES_URL}/operations/_search \ + --header 'Content-Type: application/json' \ + --data '{ + "query": { + "bool": { + "should": [ + { + "match": { + "sender": "${ADDRESS}" + } + }, + { + "match": { + "receiver": "${ADDRESS}" + } + }, + { + "match": { + "receivers": "${ADDRESS}" + } + } + ] + } + }, + "sort": [ + { + "timestamp": { + "order": "desc" + } + } + ] +}' +``` -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() -# used for ESDT -fungible_token_definition = api.get_definition_of_fungible_token(token_identifier="TEST-123456") +### Fetch all the smart contract results generated by a transaction -# used for MetaESDT, SFT, NFT -non_fungible_token_definition = api.get_definition_of_tokens_collection( - collection_name="NFT-987654") +``` +curl --request GET \ + --url ${ES_URL}/operations/_search \ + --header 'Content-Type: application/json' \ + --data '{ + "query": { + "bool": { + "must": [ + { + "match": { + "originalTxHash": "d6.." + } + }, + { + "match": { + "type": "unsigned" + } + } + ] + } + } +}' ``` -### Querying Smart Contracts +--- -Smart contract queries or view functions, are endpoints of a contract that only read data from the contract. To send a query to the observer nodes, we can proceed as follows: +### Overview -```py -from multiversx_sdk import Address, DevnetEntrypoint, SmartContractQuery +[comment]: # "mx-abstract" -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() +## Overview -query = SmartContractQuery( - contract=Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen"), - function="getSum", - arguments=[] -) -response = api.query_contract(query=query) -``` +There are several ways to write smart contract tests in Rust directly. This is for the largest part possible because of the Rust VM and debugger that can act as an execution backend. -### Custom Api/Proxy calls +This is a simplified diagram of what a Rust test will do during its execution: -The methods exposed by the `ApiNetworkProvider` or `ProxyNetworkProvider` are the most common and used ones. There might be times when custom API calls are needed. For that we have createad generic methods for both `GET` and `POST` requests. +```mermaid +graph TD + test[Test] + vm[Rust VM] --> bcm[BlockchainMock] + test -->|"register contract"| bcm + test -->|"set accounts"| bcm + test -->|"check accounts"| bcm + test -->|"call
deploy
query"| scexec[SC code] + scexec --> vm +``` -Let's assume we want to get all the transactions that are sent by Alice where the `delegate` function was called. +Of course, a local test environment is not a blockchain, so many things need to be mocked. The test state is held on a `BlockchainMock`, which needs to be initialized with user accounts, tokens, smart contracts, etc. -```py -from multiversx_sdk import Address, DevnetEntrypoint -entrypoint = DevnetEntrypoint() -api = entrypoint.create_network_provider() -alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -url_params = { - "sender": alice.to_bech32(), - "function": "delegate" -} +### ScenarioWorld (facade) -transactions = api.do_get_generic(url="transactions", url_parameters=url_params) -``` +In order to simplify interactions with the system, all tests use a unique facade for all operations. It is called `ScenarioWorld`, and it gets created at the beginning of each test in a `world()` function. -## Creating transactions -In this section, we'll learn how to create different types of transactions. For creating transactions, we can use `controllers` or `factories`. The `controllers` can be used for scripts or quick network interactions, while the `factories` provide a more granular and lower-level approach, usually needed for DApps. Usually, the `controllers` use the same parameters as the `factories` but also take an `Account` and the `nonce` of the sender as arguments. The `controllers` also hold some extra functionality, like waiting for transaction completion and parsing transactions. The same functionality can be obtained for transactions built using the `factories` as well, we'll see how in the sections below. In the following section we'll learn how to create transactions using both. -### Instantiating controllers and factories +### Registering contracts -There are two ways to create controllers and factories: the first one is to get them from the entrypoint and the second one is to manually create them. +Since we don't have native execution in the Rust backend yet, the only way to run contracts is to register the contract implementation for the given contract code identifier. In simpler words, we tell the environment "whenever you encounter this contract code, run this code that I've written instead". -```py -from multiversx_sdk import DevnetEntrypoint, TransfersController, TransferTransactionsFactory, TransactionsFactoryConfig +Since this operation is specific to only the Rust debugger, it doesn't go through the mandos pipeline. -entrypoint = DevnetEntrypoint() -# getting the controller and the factory from the entrypoint -transfers_controller = entrypoint.create_transfers_controller() -transfers_factory = entrypoint.create_transfers_transactions_factory() -# manually instantiating the controller and the factory -controller = TransfersController(chain_id="D") +### Calling contract code -config = TransactionsFactoryConfig(chain_id="D") -factory = TransferTransactionsFactory(config=config) -``` +There are many ways to call contract code, but the one we recommend is [black-box style](/developers/testing/rust/sc-blackbox-calls) using the [unified transaction syntax](/developers/transactions/tx-overview). -### Estimating the Gas Limit for a Transaction +The call styles are: +- unified transaction syntax + - [**black-box**](/developers/testing/rust/sc-blackbox-calls) (recommended) + - white-box (coming soon) +- Mandos steps in Rust (no longer recommended) +- [Whitebox framework (legacy)](whitebox-legacy) -When creating transaction factories or controllers, we can pass an additional argument, a **gas limit estimator**. This gas estimator simulates the transaction before being sent and computes the `gasLimit` that it will require. The `GasLimitEstimator` can be initialized with a multiplier, so that the estimated value will be multiplied by the specified value. It is recommended to use a small multiplier (e.g. 1.1) to cover any possible changes that may occur from the time the transaction is simulated to the time it is actually sent and processed on-chain. The gas limit estimator can be provided to any factory or controller available. Let's see how we can create a `GasLimitEstimator` and use it. -```py -from multiversx_sdk import ApiNetworkProvider, GasLimitEstimator, TransferTransactionsFactory, TransactionsFactoryConfig -api = ApiNetworkProvider("https://devnet-api.multiversx.com") -gas_estimator = GasLimitEstimator(network_provider=api) # create a gas limit estimator with default multiplier of 1.0 -gas_estimator = GasLimitEstimator(network_provider=api, gas_multiplier=1.1) # create a gas limit estimator with a multiplier of 1.1 +## Rust testing architecture -config = TransactionsFactoryConfig(chain_id="D") -transfers_factory = TransferTransactionsFactory(config=config, gas_limit_estimator=gas_estimator) +We saw the simplified diagram in the introduction, now let's go in a little more depth. + +```mermaid +graph TD + scenworld["ScenarioWorld (facade)"] + bb[Blackbox test] --> scenworld + wb[Whitebox test] --> scenworld + unit[Unit test] --> scenworld + scenrunner["ScenarioRunner (interface)"] + scenworld -->|"set step
check step
tx step"| scenrunner + vm[Rust VM] --> bcm[BlockchainMock] + scenworld -->|"register contract"| bcm + scenrunner -->|"set step"| bcm + scenrunner -->|"check step"| bcm + scenrunner -->|"tx step"| scexec[SC code] + scexec --> vm + scenrunner -->|"save trace"| trace["trace.scen.json"] ``` -Also, factories or controllers created through the entrypoints can use the `GasLimitEstimator` as well: +The ScenarioWorld and its builders are in fact constructing mandos steps in the background and are sending them to the backends. -```py -from multiversx_sdk import DevnetEntrypoint +The tests are also allowed to construct these mandos steps themselves, but this is currently discouraged, because mandos syntax is very weakly typed and prone to error. -entrypoint = DevnetEntrypoint(with_gas_limit_estimator=True, gas_limit_multiplier=1.1) -transfers_controller = entrypoint.create_transfers_controller() # will create the controller using the GasLimitEstimator with a 1.1 multiplier -transfers_factory = entrypoint.create_transfers_transactions_factory() # will create the factory using the GasLimitEstimator with a 1.1 multiplier -``` -### Token transfers +### ScenarioExecutor (Mandos) -We can send native tokens (EGLD) and ESDT tokens using both the `controller` and the `factory`. +All our tests run through a scenario (Mandos) execution layer, which is the reason why we are able to export mandos traces out of almost any test. -#### Native token transfers using the controller +All test actions are converted to mandos steps before execution. This mechanism is designed to decouple the high-level code from the backend it is run on. In principle, these mandos steps could be executed on other backends too. -Because we'll use an `Account`, the transaction will be signed. +However, there are only two mandos executors right now: +- Rust VM backend +- save to file (trace). -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint +The system can also load mandos scenarios from files and execute them as such, on the Rust VM backend. -entrypoint = DevnetEntrypoint() +--- -account = Account.new_from_keystore( - file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), - password="password", - address_index=0 -) -# the developer is responsible for managing the nonce -account.nonce = entrypoint.recall_account_nonce(account.address) +### Overview -transfers_controller = entrypoint.create_transfers_controller() -transaction = transfers_controller.create_transaction_for_transfer( - sender=account, - nonce=account.get_nonce_then_increment(), - receiver=Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), - native_transfer_amount=1000000000000000000, # 1 EGLD -) +Here you can find some common issues and their solutions, in the context of [MultiversX SDKs and Tools](/sdk-and-tools/overview). -tx_hash = entrypoint.send_transaction(transaction) -``` +1. [Fix Rust installation](/sdk-and-tools/troubleshooting/fix-rust-setup) +2. [Fix IDEs configuration](/sdk-and-tools/troubleshooting/ide-setup) -If you know you'll only send native tokens, the same transaction can be created using the `create_transaction_for_native_token_transfer` method. +--- -#### Native token transfers using the factory +### Overview -Because we only use the address of the sender, the transactions are not going to be signed or have the nonce field set properly. This should be taken care after the transaction is created. +The processing cost of a MultiversX transaction is determined by its gas limit, the actual gas consumption, and the gas price per gas unit, leading to a processing fee in EGLD that may be subject to a gas refund in certain cases. -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint -entrypoint = DevnetEntrypoint() -factory = entrypoint.create_transfers_transactions_factory() +## Cost of processing (gas units) -alice = Account.new_from_keystore( - file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), - password="password", - address_index=0 -) -# the developer is responsible for managing the nonce -alice.nonce = entrypoint.recall_account_nonce(alice.address) +Each MultiversX transaction has a **processing cost**, expressed as **an amount of _gas units_**. At broadcast time, each transaction must be provided a **gas limit** (`gasLimit`), which acts as an _upper limit_ of the processing cost. -bob = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") -transaction = factory.create_transaction_for_transfer( - sender=alice.address, - receiver=bob, - native_amount=1000000000000000000 # 1 EGLD -) -# set the sender's nonce -transaction.nonce = alice.get_nonce_then_increment() +### Constraints -# sign the transaction using the sender's account -transaction.signature = alice.sign_transaction(transaction) +For any transaction, the `gasLimit` must be greater or equal to `erd_min_gas_limit` but smaller or equal to `erd_max_gas_per_transaction`, these two being [parameters of the Network](/sdk-and-tools/rest-api/network#get-network-configuration): -tx_hash = entrypoint.send_transaction(transaction) +``` +networkConfig.erd_min_gas_limit <= tx.gasLimit <= networkConfig.erd_max_gas_per_transaction ``` -If you know you'll only send native tokens, the same transaction can be created using the `create_transaction_for_native_token_transfer` method. -#### Custom token transfers using the controller +### Cost components -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint, Token, TokenTransfer +The **actual gas consumption** - also known as **used gas** - is the consumed amount from the provided **gas limit** - the amount of gas units actually required by the Network in order to process the transaction. The unconsumed amount is called **remaining gas**. -entrypoint = DevnetEntrypoint() +At processing time, the Network breaks the **used gas** down into two components: -alice = Account.new_from_keystore( - file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), - password="password", - address_index=0 -) -# the developer is responsible for managing the nonce -alice.nonce = entrypoint.recall_account_nonce(alice.address) +- gas used by **value movement and data handling** +- gas used by **contract execution** (for executing System or User-Defined Smart Contract) -esdt = Token(identifier="TEST-123456") -first_transfer = TokenTransfer(token=esdt, amount=1000000000) +:::note +Simple transfers of value (EGLD transfers) only require the _value movement and data handling_ component of the gas usage (that is, no _execution_ gas), while Smart Contract calls require both components of the gas consumption. This includes ESDT and NFT transfers as well, because they are in fact calls to a System Smart Contract. +::: -nft = Token(identifier="NFT-987654", nonce=10) -second_transfer = TokenTransfer(token=nft, amount=1) # when sending NFTs we set the amount to `1` +The **value movement and data handling** cost component is easily computable, using on the following formula: -sft = Token(identifier="SFT-123987", nonce=10) -third_transfer = TokenTransfer(token=nft, amount=7) +``` +tx.gasLimit = + networkConfig.erd_min_gas_limit + + networkConfig.erd_gas_per_data_byte * lengthOf(tx.data) +``` -transfers_controller = entrypoint.create_transfers_controller() -transaction = transfers_controller.create_transaction_for_transfer( - sender=alice, - nonce=alice.get_nonce_then_increment(), - receiver=Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), - token_transfers=[first_transfer, second_transfer, third_transfer] -) +The **contract execution** cost component is easily computable for System Smart Contract calls (based on formulas specific to each contract), but harder to determine _a priori_ for user-defined Smart Contracts. This is where _simulations_ and _estimations_ are employed. -tx_hash = entrypoint.send_transaction(transaction) -``` -If you know you'll only send ESDT tokens, the same transaction can be created using `create_transaction_for_esdt_token_transfer`. +## Processing fee (EGLD) -#### Custom token transafers using the factory +The **processing fee**, measured in EGLD, is computed with respect to the **actual gas cost** - broken down into its components - and the **gas price per gas unit**, which differs between the components. -Because we only use the address of the sender, the transactions are not going to be signed or have the nonce field set properly. This should be taken care after the transaction is created. +The **gas price per gas unit** for the **value movement and data handling** must be specified by the transaction, and it must be equal or greater than a Network parameter called `erd_min_gas_price`. -```py -from pathlib import Path +While the price of a gas unit for the **value movement and data handling** component equals the **gas price** provided in the transaction, the price of a gas unit for the **contract execution** component is computed with respect to another Network parameter called `erd_gas_price_modifier`: -from multiversx_sdk import Account, DevnetEntrypoint, Token, TokenTransfer +``` +value_movement_and_data_handling_price_per_unit = tx.GasPrice +contract_execution_price_per_unit = tx.GasPrice * networkConfig.erd_gas_price_modifier +``` -entrypoint = DevnetEntrypoint() +:::note +Generally speaking, the price of a gas unit for **contract execution** is lower than the price of a gas unit for **value movement and data handling**, due to the gas price modifier for contracts (`erd_gas_price_modifier`). +::: -alice = Account.new_from_keystore( - file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), - password="password", - address_index=0 -) -# the developer is responsible for managing the nonce -alice.nonce = entrypoint.recall_account_nonce(alice.address) +The **processing fee** formula looks like this: -bob = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") +``` +processing_fee = + value_movement_and_data_handling_cost * value_movement_and_data_handling_price_per_unit + + contract_execution_cost * contract_execution_price_per_unit +``` -esdt = Token(identifier="TEST-123456") # fungible tokens don't have nonce -first_transfer = TokenTransfer(token=esdt, amount=1000000000) # we set the desired amount we want to send +After processing the transaction, the Network will send a value called **gas refund** back to the sender of the transaction, computed with respect to the unconsumed (component of the) gas, if applicable (if the **paid fee** is higher than the **necessary fee**). -nft = Token(identifier="NFT-987654", nonce=10) -second_transfer = TokenTransfer(token=nft, amount=1) # when sending NFTs we set the amount to `1` +--- -sft = Token(identifier="SFT-123987", nonce=10) -third_transfer = TokenTransfer(token=nft, amount=7) # for SFTs we set the desired amount we want to send +### Payload (data) -factory = entrypoint.create_transfers_transactions_factory() -transaction = factory.create_transaction_for_transfer( - sender=alice.address, - receiver=bob, - token_transfers=[first_transfer, second_transfer, third_transfer] -) +## Overview -# set the sender's nonce -transaction.nonce = alice.get_nonce_then_increment() +The data field can hold arbitrary data, but for practical purposes, it is normally one of three: -# sign the transaction using the sender's account -transaction.signature = alice.sign_transaction(transaction) +- a function call, +- deploy data, or +- an upgrade call. -tx_hash = entrypoint.send_transaction(transaction) -``` +We can always give this data in raw form, however, we usually prefer using a proper type system, for safety. -If you know you'll only send ESDT tokens, the same transaction can be created using `create_transaction_for_esdt_token_transfer`. +:::caution +Always use [proxies](./tx-proxies.md) when the target contract ABI is known. A contract proxy is a Rust equivalent of its ABI, and using adds invaluable type safety to your calls. -#### Sending native and custom tokens +Using raw data is acceptable only when we are forwarding calls to unknown contracts, for instance in contracts like the multisig, governance of other forwarders. +::: -Also, sending both native and custom tokens is now supported. If a `native_amount` is provided together with `token_transfers`, the native token will also be included in the `MultiESDTNFTTrasfer` built-in function call. -We can send both types of tokens using either the `controller` or the `factory`, but we'll use the controller for the sake of simplicity. +## Diagram -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint, Token, TokenTransfer +The basic transition diagram for constructing the data field is the one below. It shows the allowed data types and how to get from one to the other. -entrypoint = DevnetEntrypoint() +Notice how the deploy and upgrade calls are further specified by the code source: either explicit code, or the code of another deployed contract. -account = Account.new_from_keystore( - file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), - password="password", - address_index=0 -) -# the developer is responsible for managing the nonce -account.nonce = entrypoint.recall_account_nonce(account.address) +```mermaid +graph LR + subgraph "Data: raw" + data-unit["()"] + deploy["DeployCall<()>"] + data-unit -->|raw_deploy| deploy + deploy -->|from_source| deploy-src["DeployCall<FromSource<_>>"] + deploy -->|code| deploy-code["DeployCall<Code<_>>"] + data-unit -->|raw_upgrade| upgrade["UpgradeCall<()>"] + upgrade -->|from_source| upgrade-src["UpgradeCall<CodeSource<_>>"] + upgrade -->|code| upgrade-code["UpgradeCall<Code<_>>"] + data-unit -->|"raw_call(endpoint_name)"| fc[FunctionCall] + end +``` -esdt = Token(identifier="TEST-123456") -first_transfer = TokenTransfer(token=esdt, amount=1000000000) +What the first diagram does **not** contain are some additional methods that change the values, but not the type. They are: -nft = Token(identifier="NFT-987654", nonce=10) -second_transfer = TokenTransfer(token=nft, amount=1) +- `code_metadata` (deploy & upgrade only), +- `argument` +- `argument_raw`. -transfers_controller = entrypoint.create_transfers_controller() -transaction = transfers_controller.create_transaction_for_transfer( - sender=account, - nonce=account.get_nonce_then_increment(), - receiver=Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), - native_transfer_amount=1000000000000000000, # 1 EGLD - token_transfers=[first_transfer, second_transfer] -) +If we also add them to the diagram we get a more complex version of it: -tx_hash = entrypoint.send_transaction(transaction) +```mermaid +graph LR + subgraph "Data: raw, detailed" + data-unit["()"] + deploy["DeployCall<()>"] + data-unit -->|raw_deploy| deploy + deploy -->|from_source| deploy-src["DeployCall<FromSource<_>>"] + deploy -->|code| deploy-code["DeployCall<Code<_>>"] + data-unit -->|raw_upgrade| upgrade["UpgradeCall<()>"] + upgrade -->|from_source| upgrade-src["UpgradeCall<CodeSource<_>>"] + upgrade -->|code| upgrade-code["UpgradeCall<Code<_>>"] + data-unit -->|"raw_call(endpoint_name)"| fc[FunctionCall] + + deploy -->|"code_metadata
argument
argument_raw"| deploy + deploy-code -->|"code_metadata
argument
argument_raw"| deploy-code + deploy-src -->|"code_metadata
argument
argument_raw"| deploy-src + upgrade -->|"code_metadata
argument
argument_raw"| upgrade + upgrade-code -->|"code_metadata
argument
argument_raw"| upgrade-code + upgrade-src -->|"code_metadata
argument
argument_raw"| upgrade-src + fc -->|"argument
argument_raw"| fc + end ``` -### Decoding transaction data +:::info +These are diagrams for the raw calls, without proxies. You can find the one involving proxies [here](./tx-proxies.md#diagram). +::: -For example, when sending multiple ESDT and NFT tokens, the receiver field of the transaction is the same as the sender field and also the value is set to `0` because all the information is encoded in the `data` field of the transaction. -For decoding the data field we have a so called `TransactionDecoder`. We fetch the transaction from the network and then use the decoder. +## No data -```py -from multiversx_sdk import DevnetEntrypoint, TransactionDecoder +Transactions with no data are classified as simple transfers. These simple transactions can be transferred using: -entrypoint = DevnetEntrypoint() -transaction = entrypoint.get_transaction("3e7b39f33f37716186b6ffa8761d066f2139bff65a1075864f612ca05c05c05d") +- **`.transfer()`**: executes simple transfers with a zero gas limit. +```rust title=lib.rs + self.tx() + .to(&caller) + .egld_or_single_esdt(&token_identifier, 0, &balance) + .transfer(); +``` +- **`.transfer_if_not_empty()`**: it facilitates the transfer of funds with a zero gas limit only if the amount exceeds zero; otherwise, no action is taken. +```rust title=lib.rs + self.tx() + .to(ToCaller) + .payment(&token_payment) + .transfer_if_not_empty(); +``` -decoder = TransactionDecoder() -decoded_transaction = decoder.get_transaction_metadata(transaction) -print(decoded_transaction.to_dict()) -``` -### Smart Contracts +## Untyped function call -#### Contract ABIs +**`.raw_call(...)`** starts a contract call serialised by hand. It is used in proxy functions. It is safe to use [proxies](./tx-proxies.md) instead since manual serialisation is not type-safe. -A contract's ABI describes the endpoints, data structure and events that a contract exposes. While contract interactions are possible without the ABI, they are easier to implement when the definitions are available. -##### Loading the ABI from a file +### Argument -```py -from pathlib import Path -from multiversx_sdk.abi import Abi +**`.argument(...)`** serializes the value, but does not enforce type safety. It adds one argument to a function call. -abi = Abi.load(Path("./contracts/adder.abi.json")) +Can be called multiple times, once for each argument. + +```rust +tx().raw_call("example").argument(&arg1).argument(&arg2) ``` -#### Manually construct the ABI +It is safe to user [proxies](./tx-proxies.md) instead, whenever possible. -If an ABI file isn't directly available, but you do have knowledge of the contract's endpoints and types, you can manually construct the ABI. -```py -from multiversx_sdk.abi import Abi, AbiDefinition +### Raw arguments -abi_definition = AbiDefinition.from_dict({ - "endpoints": [{ - "name": "add", - "inputs": [ - { - "name": "value", - "type": "BigUint" - } - ], - "outputs": [] - }] -}) +**`arguments_raw(...)`** overrides the entire argument buffer. It takes one argument of type `ManagedArgBuffer`. The arguments need to have been serialized beforehand. -abi = Abi(definition=abi_definition) +```rust +tx().raw_call("example").arguments_raw(&arguments) ``` -### Smart Contract deployments -For creating smart contract deploy transactions, we have two options, as well: a `controller` and a `factory`. Both of these are similar to the ones presented above for transferring tokens. +### Code metadata -When creating transactions that interact with smart contracts, we should provide the ABI file to the `controller` or `factory` if possible, so we can pass the arguments as native values. If the abi is not provided and we know what types the contract expects, we can pass the arguments as `typed values` (ex: BigUIntValue, ListValue, StructValue, etc.) or `bytes`. +**`.code_metadata()`** explicitly sets code metadata. +```rust +tx().raw_call("example").code_metadata(code_metadata) +``` -#### Deploying a smart contract using the controller -```py -from pathlib import Path +## Untyped deploy -from multiversx_sdk import Account, DevnetEntrypoint -from multiversx_sdk.abi import Abi, BigUIntValue +**`.raw_deploy()`** starts a contract deploy call serialised by hand. It is used in proxy deployment functions. It is safe to use [proxies](./tx-proxies.md) instead since manual serialisation is not type-safe. -# prepare the account -account = Account.new_from_keystore( - file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), - password="password", - address_index=0 -) -# the developer is responsible for managing the nonce -account.nonce = entrypoint.recall_account_nonce(account.address) +Deployment calls needs to set: -# load the abi file -abi = Abi.load(Path("contracts/adder.abi.json")) -# get the smart contracts controller -entrypoint = DevnetEntrypoint() -controller = entrypoint.create_smart_contract_controller(abi=abi) +### Argument -# load the contract bytecode -bytecode = Path("contracts/adder.wasm").read_bytes() +Same as for [function call arguments](#argument). -# For deploy arguments, use typed value objects if you haven't provided an ABI -args = [BigUIntValue(42)] -# Or use simple, plain Python values and objects if you have provided an ABI -args = [42] +```rust +tx().raw_deploy().argument(&argument) +``` -deploy_transaction = controller.create_transaction_for_deploy( - sender=account, - nonce=account.get_nonce_then_increment(), - bytecode=bytecode, - gas_limit=5000000, - arguments=args, - is_upgradeable=True, - is_readable=True, - is_payable=True, - is_payable_by_sc=True -) -# broadcasting the transaction -tx_hash = entrypoint.send_transaction(deploy_transaction) -``` +### Raw arguments -When creating transactions using `SmartContractController` or `SmartContractTransactionsFactory`, even if the ABI is available and provided, you can still use _typed value_ objects as arguments for deployments and interactions. +Same as for [function call raw arguments](#raw-arguments). -Even further, you can use a mix of typed value objects and plain Python values and objects. For example: -```py -args = [U32Value(42), "hello", { "foo": "bar" }, TokenIdentifierValue("TEST-123456")] +```rust +tx().raw_deploy().arguments_raw(&arguments) ``` -#### Parsing contract deployment transactions - -After broadcasting the transaction, we can wait for it's execution to be completed and parse the processed transaction to extract the address of newly deployed smart contract. -```py -# we use the transaction hash we got when broadcasting the transaction -contract_deploy_outcome = controller.await_completed_deploy(tx_hash) # waits for transaction completion and parses the result -contract_address = contract_deploy_outcome.contracts[0].address -print(contract_address.to_bech32()) -``` +### Code -If we want to wait for transaction completion and parse the result in two different steps, we can do as follows: +**`.code(...)`** explicitly sets the deployment code source as bytes. -```py -# we use the transaction hash we got when broadcasting the transaction -# waiting for transaction completion -transaction_on_network = entrypoint.await_transaction_completed(tx_hash) +Argument will normally be a `ManagedBuffer`, but can be any type that implements trait `CodeValue`. -# parsing the transaction -contract_deploy_outcome = controller.parse_deploy(transaction_on_network) +```rust +tx().raw_deploy().code(code_bytes) ``` -#### Computing the smart contract address -Even before broadcasting, at the moment you know the sender's address and the nonce for your deployment transaction, you can (deterministically) compute the (upcoming) address of the smart contract: -```py -from multiversx_sdk import Address, AddressComputer +### From source -# we used Alice for deploying the contract, so we are using her address -alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +**`.from_source(...)`** will instruct the VM to copy the code from another previously deployed contract. -address_computer = AddressComputer() -contract_address = address_computer.compute_contract_address( - deployer=alice, - deployment_nonce=deploy_transaction.nonce # the same nonce we set on the deploy transaction -) +Argument will normally be a `ManagedAddress`, but can be any type that implements trait `FromSourceValue`. -print("Contract address:", contract_address.to_bech32()) +```rust +tx().raw_deploy().from_source(other_address) ``` -#### Deploying a smart contract using the factory -After the transaction is created the `nonce` needs to be properly set and the transaction should be signed before broadcasting it. +### New address -```py -from pathlib import Path +**`.new_address(...)`** defines a mock address for the deployed contract (allowed only in testing environments). +```rust +tx().raw_deploy().new_address(address) +``` -from multiversx_sdk import Address, DevnetEntrypoint, SmartContractTransactionsOutcomeParser -from multiversx_sdk.abi import Abi, BigUIntValue +The example below is a blackbox test for deploy functionality. This call encapsulates a raw_deploy that explicitly sets the deployment code source with *"adder.mxsc.json"* and the returned address of the deploy with *"sc: adder"*. +```rust title=adder_blackbox_test.rs +fn deploy(&mut self) { + self.world + .tx() + .from(OWNER_ADDRESS) + .raw_deploy() + .argument(5u32) + .code(CODE_PATH) + .new_address(ADDER_ADDRESS) + .run(); +} +``` -# load the abi file -abi = Abi.load(Path("contracts/adder.abi.json")) -# get the smart contracts transaction factory -entrypoint = DevnetEntrypoint() -factory = entrypoint.create_smart_contract_transactions_factory(abi=abi) +## Untyped upgrade -# load the contract bytecode -bytecode = Path("contracts/adder.wasm").read_bytes() +`.raw_upgrade()` starts a contract deployment upgrade serialised by hand. It is used in a proxy upgrade call. It is safe to use [proxies](./tx-proxies.md) instead since manual serialisation is not type-safe. All upgrade calls require: -# For deploy arguments, use typed value objects if you haven't provided an ABI to the factory: -args = [BigUIntValue(42)] -# Or use simple, plain Python values and objects if you have provided an ABI to the factory: -args = [42] -alice_address = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +### Argument -deploy_transaction = factory.create_transaction_for_deploy( - sender=alice_address, - bytecode=bytecode, - gas_limit=5000000, - arguments=args, - is_upgradeable=True, - is_readable=True, - is_payable=True, - is_payable_by_sc=True -) +Same as for [function call arguments](#argument). -# load the account -alice = Account.new_from_keystore( - file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), - password="password", - address_index=0 -) -# the developer is responsible for managing the nonce -alice.nonce = entrypoint.recall_account_nonce(alice.address) +```rust +tx().raw_upgrade().argument(&argument) +``` -# set the nonce -deploy_transaction.nonce = alice.nonce -# sign transaction -deploy_transaction.signature = alice.sign_transaction(deploy_transaction) +### Raw arguments -# broadcasting the transaction -tx_hash = entrypoint.send_transaction(deploy_transaction) -print(tx_hash.hex()) +Same as for [function call raw arguments](#raw-arguments). -# waiting for transaction to complete -transaction_on_network = entrypoint.await_transaction_completed(tx_hash) +```rust +tx().raw_upgrade().arguments_raw(&arguments) +``` -# parsing transaction -parser = SmartContractTransactionsOutcomeParser(abi) -contract_deploy_outcome = parser.parse_deploy(transaction_on_network) -contract_address = contract_deploy_outcome.contracts[0].address -print(contract_address.to_bech32()) +### Code metadata + +Same as for [function call raw arguments](#code-metadata). +```rust +tx().raw_upgrade().code_metadata(code_metadata) ``` -### Smart Contract calls -In this section we'll see how we can call an endpoint of our previously deployed smart contract using both approaches with the `controller` and the `factory`. +### Code -#### Calling a smart contract using the controller +Same as for [deploy code](#code). -```py -from pathlib import Path +Argument will normally be a `ManagedBuffer`, but can be any type that implements trait `CodeValue`. -from multiversx_sdk import Account, DevnetEntrypoint -from multiversx_sdk.abi import Abi, BigUIntValue +```rust +tx().raw_upgrade().code(code_bytes) +``` -# prepare the account -account = Account.new_from_keystore( - file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), - password="password", - address_index=0 -) -# the developer is responsible for managing the nonce -account.nonce = entrypoint.recall_account_nonce(account.address) -# load the abi file -abi = Abi.load(Path("contracts/adder.abi.json")) +### From source -# get the smart contracts controller -entrypoint = DevnetEntrypoint() -controller = entrypoint.create_smart_contract_controller(abi=abi) +Same as for [deploy from source](#from-source). -contract_address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug") +**`.from_source(...)`** will instruct the VM to copy the code from another previously deployed contract. -# For deploy arguments, use typed value objects if you haven't provided an ABI -args = [BigUIntValue(42)] -# Or use simple, plain Python values and objects if you have provided an ABI -args = [42] +Argument will normally be a `ManagedAddress`, but can be any type that implements trait `FromSourceValue`. -deploy_transaction = controller.create_transaction_for_execute( - sender=account, - nonce=account.get_nonce_then_increment(), - contract=contract_address, - gas_limit=5000000, - function="add", - arguments=args -) +```rust +tx().raw_upgrade().from_source(other_address) +``` -# broadcasting the transaction -tx_hash = entrypoint.send_transaction(deploy_transaction) -print(tx_hash.hex()) +The example below is an endpoint that contains upgrade functionality. This call encapsulates a raw_upgrade that explicitly sets the upgrade call source with a specific ManagedAddress and *upgradeable* code metadata. + +```rust title=contract.rs +#[endpoint] +fn upgrade_from_source( + &self, + child_sc_address: ManagedAddress, + source_address: ManagedAddress, + opt_arg: OptionalValue, +) { + self.tx() + .to(child_sc_address) + .typed(contract_proxy::ContractProxy) + .upgrade(opt_arg) + .code_metadata(CodeMetadata::UPGRADEABLE) + .from_source(source_address) + .upgrade_async_call_and_exit(); +} ``` -#### Parsing smart contract call transactions +--- -In our case, calling the `add` endpoint does not return anything, but similar to the example above, we could parse this transaction to get the output values of a smart contract call. +### Payments -```py -# waits for transaction completion and parses the result -# we use the transaction hash we got when broadcasting the transaction -contract_call_outcome = controller.await_completed_execute(tx_hash) -values = contract_call_outcome.values -``` +## Overview -#### Calling a smart contract and sending tokens (transfer & execute) +Payments can be easily attached to the transaction with the new syntax through the `Payment` generic. In order to specialize the generic, the framework provides the `.payment(...)` method, which accepts all legal types (all types that `can` be payment) such as: `EsdtTokenPayment`, `(TokenIdentifier, u64, BigUint)`, `ManagedVec`, etc. The framework also provides other various helper methods, basically wrappers around `.payment(...)` for accessibility. -Additionally, if our endpoint requires a payment when called, we can also send tokens to the contract when creating a smart contract call transaction. We can send EGLD, ESDT tokens or both. This is supported both on the `controller` and the `factory`. -```py -from pathlib import Path +## Diagram -from multiversx_sdk import Account, DevnetEntrypoint, Token, TokenTransfer -from multiversx_sdk.abi import Abi, BigUIntValue +The payment is a little more complex than the previous fields. The `.payment(...)` method is sufficient to set any kind of acceptable payment object. However, we have several more functions to help setup the payment field: -# prepare the account -account = Account.new_from_keystore( - file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), - password="password", - address_index=0 -) -# the developer is responsible for managing the nonce -account.nonce = entrypoint.recall_account_nonce(account.address) +```mermaid +graph LR + payment-unit["()"] + payment-unit --->|"<proxy>"| not-payable["NotPayable"] + payment-unit --->|egld| egld-biguint["Egld(BigUint)"] + payment-unit --->|egld| egld-u64["Egld(u64)"] + payment-unit --->|egld| egld-num["Egld(NumExpr)"] + payment-unit --->|"payment
esdt"| EsdtTokenPayment + payment-unit --->|"payment
single_esdt"| EsdtTokenPaymentRefs + EsdtTokenPayment -->|esdt| MultiEsdtPayment + MultiEsdtPayment -->|esdt| MultiEsdtPayment + payment-unit --->|"payment
multi_esdt"| MultiEsdtPayment + payment-unit --->|"payment"| EgldOrEsdtTokenPayment + payment-unit --->|"payment
egld_or_single_esdt"| EgldOrEsdtTokenPaymentRefs + payment-unit --->|"payment
egld_or_multi_esdt"| EgldOrMultiEsdtPayment +``` -# load the abi file -abi = Abi.load(Path("contracts/adder.abi.json")) -# get the smart contracts controller -entrypoint = DevnetEntrypoint() -controller = entrypoint.create_smart_contract_controller(abi=abi) +## No payments -contract_address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug") +When no payments are added, the `Payment` fields remain of type `()`. This makes sense when dealing with contract or built-in function calls that don't involve payment. -# For deploy arguments, use typed value objects if you haven't provided an ABI -args = [BigUIntValue(42)] -# Or use simple, plain Python values and objects if you have provided an ABI -args = [42] +```rust title=contract.rs + self.tx() // tx with sc environment + .to(&to) + .raw_call(endpoint_name) // raw endpoint call + .sync_call() // synchronous call +``` -# creating the transfer -first_token = Token("TEST-38f249", 10) -first_transfer = TokenTransfer(first_token, 1) +In this example, the transaction is a smart contract call to a non payable endpoint with no arguments. -second_token = Token("BAR-c80d29") -second_transfer = TokenTransfer(second_token, 10000000000000000000) -execute_transaction = controller.create_transaction_for_execute( - sender=account, - nonce=account.get_nonce_then_increment(), - contract=contract_address, - gas_limit=5000000, - function="add", - arguments=args, - native_transfer_amount=1000000000000000000, # 1 EGLD, - token_transfers=[first_transfer, second_transfer] -) -# broadcasting the transaction -tx_hash = entrypoint.send_transaction(execute_transaction) -print(tx_hash.hex()) -``` +### `NotPayable` -#### Calling a smart contract using the factory +The `NotPayable` object is a variation of the no-payment indicator `()`. It acts the same way, with only one difference: no payment can be added on top of it. -Let's create the same smart contract call transaction, but using the `factory`. +[Proxies](tx-proxies#notpayable-protection) will add the `NotPayable` flag to transactions to non-payable endpoints, to provide an additional layer of security. -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint, Token, TokenTransfer -from multiversx_sdk.abi import Abi, BigUIntValue -# prepare the account -account = Account.new_from_keystore( - file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), - password="password", - address_index=0 -) -# the developer is responsible for managing the nonce -account.nonce = entrypoint.recall_account_nonce(account.address) +## EGLD payment -# load the abi file -abi = Abi.load(Path("contracts/adder.abi.json")) +We represent EGLD payments with the wrapper type `Egld`. This wrapper makes sure that there is no ambiguity as to what the given amount represents. -# get the smart contracts factory -entrypoint = DevnetEntrypoint() -factory = entrypoint.create_smart_contract_transactions_factory(abi=abi) +The `Egld` contains a single field, holding the amount. This amount will eventually be resolved as a `BigUint`, but the developer can also provide other types, if convenient, such as `u64`, `i32`, or `NumExpr`. In general, any type that implements trait `EgldValue` can be used. -contract_address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug") +EGLD payments can be specified using `.payment(Egld(amount))`, but for brevity it is common to use method `.egld(amount)`, which does the same. -# For deploy arguments, use typed value objects if you haven't provided an ABI to the factory: -args = [BigUIntValue(42)] -# Or use simple, plain Python values and objects if you have provided an ABI to the factory: -args = [42] +:::caution +The amounts are expressed in indivisible atto-EGLD (10^-18) units. You must always take the denomination into account. +::: -# creating the transfer -first_token = Token("TEST-38f249", 10) -first_transfer = TokenTransfer(first_token, 1) +Some examples of specifying EGLD payments: -second_token = Token("BAR-c80d29") -second_transfer = TokenTransfer(second_token, 10000000000000000000) -execute_transaction = factory.create_transaction_for_execute( - sender=account.address, - contract=contract_address, - gas_limit=5000000, - function="add", - arguments=args, - native_transfer_amount=1000000000000000000, # 1 EGLD, - token_transfers=[first_transfer, second_transfer] -) +### `BigUint` -execute_transaction.nonce = account.get_nonce_then_increment() -execute_transaction.signature = account.sign_transaction(execute_transaction) +The most straightforward type to encode amounts. -# broadcasting the transaction -tx_hash = entrypoint.send_transaction(execute_transaction) -print(tx_hash.hex()) +```rust title=contract.rs + #[endpoint] + #[payable("EGLD")] + fn send_egld( + &self, + to: ManagedAddress, + egld_amount: BigUint + ) { + self + .tx() // tx with sc environment + .to(to) + .egld(egld_amount) // BigUint value + .transfer() + } ``` -### Parsing transaction outcome -As said before, the `add` endpoint we called does not return anything, but we could parse the outcome of smart contract call transactions, as follows: +### `&BigUint` , `ManagedRef` -```py -from pathlib import Path -from multiversx_sdk import SmartContractTransactionsOutcomeParser -from multiversx_sdk.abi import Abi +References are also allowed. A slightly less common variation is the `ManagedRef` type, which is used in certain places of the framework. It is semantically equivalent to a readonly reference (`&...`). You can see it in this example: -# load the abi file -abi = Abi.load(Path("contracts/adder.abi.json")) +```rust title=contract.rs + #[endpoint] + #[payable("EGLD")] + fn forward_egld( + &self, + to: ManagedAddress, + endpoint_name: ManagedBuffer, + args: MultiValueEncoded, + ) { + let payment = self.call_value().egld(); // readonly BigUint managed reference + self + .tx() // tx with sc environment + .to(to) + .egld(payment) // BigUint value + .raw_call(endpoint_name) // endpoint call + .argument(&args) + .sync_call() + } +``` -# create the parser -parser = SmartContractTransactionsOutcomeParser(abi=abi) -# fetch the transaction of the network -transaction_on_network = entrypoint.get_transaction(tx_hash) # the tx_hash from the transaction sent above +### `u64` -outcome = parser.parse_execute(transaction=transaction_on_network, function="add") +In tests it might be unwieldy to keep creating `BigUint` instances, and the amounts might not be so large, so it can be more comfortable to work with `u64` values. + +```rust title=blackbox_test.rs + const STAKE_AMOUNT: u64 = 20; + self.world + .tx() // tx with test exec environment + .from(&address.to_address()) + .to(PRICE_AGGREGATOR_ADDRESS) + .typed(price_aggregator_proxy::PriceAggregatorProxy) // typed call + .stake() // endpoint call + .egld(STAKE_AMOUNT) // u64 value + .run(); ``` -### Decoding transaction events -You might be interested into decoding events emitted by a contract. You can do so by using the `TransactionEventsParser`. +### `NumExpr` -Suppose we'd like to decode a `startPerformAction` event emitted by the [multisig](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/multisig) contract. +This type helps us control how the values will be exported in the mandos trace. Its contents will be a numeric mandos expression. Use if you are insterested in a readable mandos output. -First, we load the abi file, then we fetch the transaction, we extract the event from the transaction and then we parse it. +Alternatively, it can visually convey certain information, in this case the mandos separators are (mis-)used to indicate the EGLD decimals. -```py -from pathlib import Path +```rust title=interact.rs + self.interactor + .tx() // tx with interactor exec environment + .from(&self.wallet_address) + .to(self.state.current_adder_address()) + .egld(NumExpr("0,050000000000000000")) // 0,05 EGLD => 5 * 10^16 + .prepare_async() + .run() + .await; +``` -from multiversx_sdk import DevnetEntrypoint, TransactionEventsParser, find_events_by_first_topic -from multiversx_sdk.abi import Abi +In this example, the value put inside `NumExpr` is equal to 0,05 EGLD. The `0,` component is just stylistic and can be ignored, and there are 16 digits after `5` from a total of 18, marking the decimal point. -# load the abi file -abi = Abi.load(Path("contracts/multisig-full.abi.json")) -# fetch the transaction of the network -network_provider = DevnetEntrypoint().create_network_provider() -transaction_on_network = network_provider.get_transaction("exampleTransactionHash") +### `i32` -# extract the event from the transaction -[event] = find_events_by_first_topic(transaction_on_network, "startPerformAction") +`i32` is only supported because it is the default Rust numeric type and unannotated numeric constants are of this type. -# create the parser -events_parser = TransactionEventsParser(abi=abi) +Make sure you don't pass a negative value! -# parse the event -parsed_event = events_parser.parse_event(event) +```rust title=blackbox_test.rs + state + .world + .tx() // tx with test exec environment + .from(FIRST_USER_ADDRESS) + .to(CROWDFUNDING_ADDRESS) + .typed(crowdfunding_esdt_proxy::CrowdfundingProxy) + .fund() + .egld(1000) // i32 value + .with_result(ExpectError(4, "wrong token")) + .run(); ``` -### Encoding/Decoding custom types -Whenever needed, the contract ABI can be used for manually encoding or decoding custom types. -Let's encode a struct called `EsdtTokenPayment` (of [multisig](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/multisig) contract) into binary data. +## General ESDT payment -```py -from pathlib import Path -from multiversx_sdk.abi import Abi +Any ESDT payment can be easily attached to a transaction through the `.esdt(...)` method. This method accepts as argument any type that can be converted into an `EsdtTokenPayment` and can be called multiple times on the same call. -abi = Abi.load(Path("contracts/multisig-full.abi.json")) -encoded = abi.encode_custom_type("EsdtTokenPayment", ["TEST-8b028f", 0, 10000]) -print(encoded) + +```mermaid +graph LR + payment-unit["()"] + payment-unit -->|esdt| EsdtTokenPayment + EsdtTokenPayment -->|esdt| MultiEsdtPayment + MultiEsdtPayment -->|esdt| MultiEsdtPayment ``` -Now, let's decode a struct using the ABI. +```rust title=contract.rs +self.tx() // tx with sc environment + .to(caller) + .esdt((esdt_token_id.clone(), nonce, amount.clone())) // a tuple with three values + .esdt(EsdtTokenPayment::new(esdt_token_id, nonce, amount)) // an EsdtTokenPayment + .transfer(); +``` -```py -from multiversx_sdk.abi import Abi, AbiDefinition +In this example, calling `.esdt(...)` will attach an ESDT payment load to the transaction. When adding subsequent `.esdt(...)` calls, the payload automatically converts into a `multi payment`. -abi_definition = AbiDefinition.from_dict( - { - "endpoints": [], - "events": [], - "types": { - "DepositEvent": { - "type": "struct", - "fields": [ - {"name": "tx_nonce", "type": "u64"}, - {"name": "opt_function", "type": "Option"}, - {"name": "opt_arguments", "type": "Option>"}, - {"name": "opt_gas_limit", "type": "Option"}, - ], - } - }, - } -) -abi = Abi(abi_definition) -decoded_type = abi.decode_custom_type(name="DepositEvent", data=bytes.fromhex("00000000000003db000000")) -print(decoded_type) -``` -If you don't wish to use the ABI, there is another way to do it. First, let's encode a struct. +## Single ESDT payment with references -```py -from multiversx_sdk.abi import Serializer, U64Value, StructValue, Field, StringValue, BigUIntValue +Sometimes we don't have ownership of the token identifier object, or amount, and we would like to avoid unnecessary clones. For this reason, we have created the `EsdtTokenPaymentRefs`, which contains references and can be used as the payment object. -struct = StructValue([ - Field(name="token_identifier", value=StringValue("TEST-8b028f")), - Field(name="token_nonce", value=U64Value()), - Field(name="amount", value=BigUIntValue(10000)), -]) +For brevity, instead of `payment(EsdtTokenPaymentRefs::new(&token_identifier, token_nonce, &amount))`, we can use `.single_esdt(&token_identifier, token_nonce, &amount)`. -serializer = Serializer() -serialized_struct = serializer.serialize([struct]) -print(serialized_struct) +```rust title=contract.rs + #[payable] + #[endpoint] + fn send_esdt(&self, to: ManagedAddress) { + let payment = self.call_value().single(); + let half_payment = &payment.amount / 2u32; + + self.tx() + .to(&to) + .payment(PaymentRefs::new( + &payment.token_identifier, + 0, + &half_payment, + )) + .transfer(); + + self.tx() + .to(&self.blockchain().get_caller()) + .payment(PaymentRefs::new( + &payment.token_identifier, + 0, + &half_payment, + )) + .transfer(); + } ``` -Now, let's decode a struct without using the ABI. +In this case, adding the ESDT token as payment through `.single_esdt(...)` gives the developer the possibility to keep using the referenced values afterwards without cloning. -```py -from multiversx_sdk.abi import Serializer, U64Value, OptionValue, BytesValue, ListValue, StructValue, Field -tx_nonce = U64Value() -function = OptionValue(BytesValue()) -arguments = OptionValue(ListValue([BytesValue()])) -gas_limit = OptionValue(U64Value()) -attributes = StructValue([ - Field("tx_nonce", tx_nonce), - Field("opt_function", function), - Field("opt_arguments", arguments), - Field("opt_gas_limit", gas_limit) -]) +## Multi ESDT payment -serializer = Serializer() -serializer.deserialize("00000000000003db000000", [attributes]) +The framework defines the alias `type MultiEsdtPayment = ManagedVec>;`, which is how multi-esdt payments are held in memory. If we have an object of this type, we can pass it directly as payment. -print(tx_nonce.get_payload()) -print(function.get_payload()) -print(arguments.get_payload()) -print(gas_limit.get_payload()) +```rust +let tokens_to_claim = MultiEsdtPayment::::new(); // multiple tokens +self.tx().to(&caller).payment(tokens_to_claim).transfer(); // multi token payment ``` -### Smart Contract queries +It is also possible to pass a reference or `ManagedRef` as payment, e.g.: -When querying a smart contract, a **view function** is called. That function does not modify the state of the contract, thus we don't need to send a transaction. +```rust +// type annotation added for clarity, normally inferred +let payments: ManagedRef<'static, MultiEsdtPayment> = self.call_value().all_esdt_transfers(); +self.tx().to(&caller).payments(payments).transfer(); +``` -To query a smart contract, we need to use the `SmartContractController`. Of course, we can use the contract's abi file to encode the arguments of the query, but also parse the result. In this example, we are going to use the [adder](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/adder) smart contract and we'll call the `getSum` endpoint. +The input type is enough for the `payment` method. We also have `.multi_esdt(...)`, which does the same as `payment`, but will additionally convert the argument to `MultiEsdtPayment`. For instance, sending an `EsdtTokenPayment` to `multi_esdt` will set the payment to `MultiEsdtPayment` instead of `EsdtTokenPayment`. -```py -from pathlib import Path +It is also possible to construct a `MultiEsdtPayment` by calling `.esdt(...)` at least twice, as seen [earlier](#general-esdt-payment). -from multiversx_sdk import Address, DevnetEntrypoint -from multiversx_sdk.abi import Abi -# load the abi file -abi = Abi.load(Path("contracts/adder.abi.json")) -# the contract address we'll query -contract_address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug") +## Mixed transfers -# create the controller -sc_controller = DevnetEntrypoint().create_smart_contract_controller(abi=abi) +Sometimes we don't know at compile time what kind of transfers we are going to perform. For this reason, we also provide contract call types that work with both EGLD and ESDT tokens. -# creates the query, runs the query, parses the result -response = sc_controller.query( - contract=contract_address, - function="getSum", - arguments=[] # our function expects no arguments, so we provide an empty list -) -``` -If we need more granular control, we can split the process in three steps: create the query, run the query and parse the query response. This does the exact same as the example above. +### EGLD or single ESDT -```py -from pathlib import Path +`EgldOrEsdtTokenPayment` allows us to decide at runtime for either EGLD or single ESDT payments. The object can be used as payment. -from multiversx_sdk import Address, DevnetEntrypoint -from multiversx_sdk.abi import Abi +```rust +// type annotation added for clarity, normally inferred +let payment: EgldOrEsdtTokenPayment = self.call_value().egld_or_single_esdt(); +self.tx().to(to).payment(payment).transfer(); +``` -# load the abi file -abi = Abi.load(Path("contracts/adder.abi.json")) -# the contract address we'll query -contract_address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug") +### EGLD or single ESDT references -# create the controller -sc_controller = DevnetEntrypoint().create_smart_contract_controller(abi=abi) +We also have the type `EgldOrEsdtTokenPaymentRefs`, which contains references to the token identifier and amount. -# creates the query -query = sc_controller.create_query( - contract=contract_address, - function="getSum", - arguments=[] # our function expects no arguments, so we provide an empty list -) +For brevity, instead of `payment(EgldOrEsdtTokenPaymentRefs::new(&egld_or_esdt_token_identifier, token_nonce, &amount))`, we can use `.single_esdt(&egld_or_esdt_token_identifier, token_nonce, &amount)`. -# run the query -result = sc_controller.run_query(query) -# parse the result -parsed_result = sc_controller.parse_query_response(result) + +### EGLD or multi-ESDT + +`EgldOrMultiEsdtPayment` allows us to decide at runtime for either EGLD or multiple ESDT payments. The object can be used as payment. + +```rust +// type annotation added for clarity, normally inferred +let payments: EgldOrMultiEsdtPayment = self.call_value().any_payment(); +self.tx().to(to).payment(payment).transfer(); ``` -### Upgrading a smart contract -Contract upgrade transactions are similar to deployment transactions (see above), in the sense that they also require a contract bytecode. In this context though, the contract address is already known. Similar to deploying a smart contract, we can upgrade a smart contract using either the `controller` or the `factory`. +## Normalization -#### Uprgrading a smart contract using the controller +We call normalization the logic of converting transactions with an ESDT payments fields into ESDT built-in function calls. Depending on the type of payment, the generated call be to either one of: +- ESDTTransfer, +- ESDTNFTTransfer, +- MultiESDTNFTTransfer. -```py -from pathlib import Path +For ESDTNFTTransfer and MultiESDTNFTTransfer, the recipient field also needs to be changed into the sender, and the real recipient added to the arguments. -from multiversx_sdk import Account, Address, DevnetEntrypoint -from multiversx_sdk.abi import Abi, BigUIntValue +This operation is done automatically by the framework before sending transactions, so the developer should normally not worry about it. -# prepare the account -account = Account.new_from_keystore( - file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), - password="password", - address_index=0 -) +--- -entrypoint = DevnetEntrypoint() +### Preparing SCs for Supernova -# the developer is responsible for managing the nonce -account.nonce = entrypoint.recall_account_nonce(account.address) +The MultiversX Supernova upgrade reduces block time from **6 seconds to 0.6 seconds**, enabling sub-second blocks. While this is a major improvement, it can impact existing smart contracts, especially those relying on assumptions about timestamp behavior. -# load the abi file -abi = Abi.load(Path("contracts/adder.abi.json")) +This guide explains how to prepare your contracts for Supernova safely. -# get the smart contracts controller -controller = entrypoint.create_smart_contract_controller(abi=abi) -# load the contract bytecode; this is the new contract code, the one we want to upgrade to -bytecode = Path("contracts/adder.wasm").read_bytes() -# For deploy arguments, use typed value objects if you haven't provided an ABI -args = [BigUIntValue(42)] -# Or use simple, plain Python values and objects if you have provided an ABI -args = [42] -contract_address = Address.new_from_bech32( - "erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug") +## Understand What Changes — and What Doesn’t -deploy_transaction = controller.create_transaction_for_upgrade( - sender=account, - nonce=account.get_nonce_then_increment(), - contract=contract_address, - bytecode=bytecode, - gas_limit=5000000, - arguments=args, - is_upgradeable=True, - is_readable=True, - is_payable=True, - is_payable_by_sc=True -) +All existing timestamp APIs continue returning **seconds** unless you explicitly call the millisecond versions. Nothing changes silently in the VM, the framework, or deployed contracts. -# broadcasting the transaction -tx_hash = entrypoint.send_transaction(deploy_transaction) -print(tx_hash.hex()) -``` +Obviously, contracts that have the 6 seconds between blocks hardcoded will have to change. But more importantly, block times expressed in seconds no longer uniquely identify a block, which leads to potential problems. -#### Upgrading a smart contract using the factory +We are going to go through the most important patterns to look out for. -Let's create the same upgrade transaction using the `factory`. -```py -from pathlib import Path -from multiversx_sdk import Account, Address, DevnetEntrypoint -from multiversx_sdk.abi import Abi, BigUIntValue +## Potential problems to look out for -# load the abi file -abi = Abi.load(Path("contracts/adder.abi.json")) -# get the smart contracts factory -entrypoint = DevnetEntrypoint() -factory = entrypoint.create_smart_contract_transactions_factory(abi=abi) +### Replace Hardcoded Block Timing -# load the contract bytecode; this is the new contract code, the one we want to upgrade to -bytecode = Path("contracts/adder.wasm").read_bytes() +If your contract uses hard-coded constants (like `6 seconds` or `6000 milliseconds`) to estimate the time between blocks, this logic will need to be changed. -# For deploy arguments, use typed value objects if you haven't provided an ABI to the factory: -args = [BigUIntValue(42)] -# Or use simple, plain Python values and objects if you have provided an ABI to the factory: -args = [42] +It might estimate durations from nonce deltas, or vice-versa, it might estimate number of blocks by timestamps. -alice_address = Address.new_from_bech32( - "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +**Fix:** Use the API instead: `self.blockchain().get_block_round_time_millis()`. This returns `6000` (as `DurationMillis`) today and `600` after Supernova. -contract_address = Address.new_from_bech32( - "erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug") -deploy_transaction = factory.create_transaction_for_upgrade( - sender=alice_address, - contract=contract_address, - bytecode=bytecode, - gas_limit=5000000, - arguments=args, - is_upgradeable=True, - is_readable=True, - is_payable=True, - is_payable_by_sc=True -) -# load the account -alice = Account.new_from_keystore( - file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), - password="password", - address_index=0 -) -# the developer is responsible for managing the nonce -alice.nonce = entrypoint.recall_account_nonce(alice.address) -# set the nonce -deploy_transaction.nonce = alice.nonce +### Avoid Timestamp-Based Monotonicity -# sign transaction -deploy_transaction.signature = alice.sign_transaction(deploy_transaction) +Logic such as: -# broadcasting the transaction -tx_hash = entrypoint.send_transaction(deploy_transaction) -print(tx_hash.hex()) +``` +require!(ts_now > last_ts) ``` -### Token management +may break because multiple blocks can share the same timestamp. -In this section, we're going to create transactions to issue fungible tokens, issue semi-fungible tokens, create NFTs, set token roles, but also parse these transactions to extract their outcome (e.g. get the token identifier of the newly issued token). +**Fix:** Use **block nonces** for guaranteed monotonicity. -Of course, the methods used here are available through the `TokenManagementController` or through the `TokenManagementTransactionsFactory`. The controller also contains methods for awaiting transaction completion and for parsing the transaction outcome. The same can be achieved for the transactions factory by using the `TokenManagementTransactionsOutcomeParser`. For scripts or quick network interactions we advise you use the controller, but for a more granular approach (e.g. DApps) we suggest using the factory. -#### Issuing fungible tokens using the controller -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint -# create the entrypoint and the token management controller -entrypoint = DevnetEntrypoint() -controller = entrypoint.create_token_management_controller() +### Prevent Rate-Limit Bypasses -# create the issuer of the token -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +If your contract allows one action “per block” but checks the difference in **seconds**, multiple blocks in the same second can bypass restrictions. -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +**Fix:** Use block nonces or switch to millisecond timestamps. -transaction = controller.create_transaction_for_issuing_fungible( - sender=alice, - nonce=alice.get_nonce_then_increment(), - token_name="NEWFNG", - token_ticker="FNG", - initial_supply=1_000_000_000000, # 1 million tokens, with 6 decimals - num_decimals=6, - can_freeze=False, - can_wipe=True, - can_pause=False, - can_change_owner=True, - can_upgrade=True, - can_add_special_roles=True -) -# sending the transaction -tx_hash = entrypoint.send_transaction(transaction) -# wait for transaction to execute, extract the token identifier -outcome = controller.await_completed_issue_fungible(tx_hash) -token_identifier = outcome[0].token_identifier -print(token_identifier) -``` +### Revisit Expiration Logic -#### Issuing fungible tokens using the factory +Expiration logic written assuming a fixed block interval may accidentally allow: -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint, TokenManagementTransactionsOutcomeParser +* extra blocks before expiration +* longer-than-intended windows for execution -# create the entrypoint and the token management transactions factory -entrypoint = DevnetEntrypoint() -factory = entrypoint.create_token_management_transactions_factory() +If your expiration logic uses seconds, double-check assumptions. -# create the issuer of the token -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -transaction = factory.create_transaction_for_issuing_fungible( - sender=alice.address, - token_name="NEWFNG", - token_ticker="FNG", - initial_supply=1_000_000_000000, # 1 million tokens, with 6 decimals - num_decimals=6, - can_freeze=False, - can_wipe=True, - can_pause=False, - can_change_owner=True, - can_upgrade=True, - can_add_special_roles=True -) -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) -transaction.nonce = alice.get_nonce_then_increment() -# sign the transaction -transaction.signature = alice.sign_transaction(transaction) +### Stop Using Timestamps as Block Identifiers -# sending the transaction -tx_hash = entrypoint.send_transaction(transaction) +Before Supernova, a timestamp could uniquely identify a block. +After Supernova, multiple blocks may share the same timestamp. -# if we know that the transaction is completed, we can simply call `entrypoint.get_transaction(tx_hash)` -transaction_on_network = entrypoint.await_transaction_completed(tx_hash) +If you use timestamps as map keys or identifiers: -# extract the token identifier -parser = TokenManagementTransactionsOutcomeParser() -outcome = parser.parse_issue_fungible(transaction_on_network) +* collisions will occur +* data may be overwritten or skipped -token_identifier = outcome[0].token_identifier -print(token_identifier) -``` +**Fix:** Use block **nonces**. -#### Setting special roles for fungible tokens using the controller -```py -from pathlib import Path -from multiversx_sdk import Account, Address, DevnetEntrypoint -# create the entrypoint and the token management controller -entrypoint = DevnetEntrypoint() -controller = entrypoint.create_token_management_controller() -# create the issuer of the token -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +### Review Reward and Accumulation Calculations -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +Reward logic that uses: -bob = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") +``` +delta = ts_now - last_ts +``` -transaction = controller.create_transaction_for_setting_special_role_on_fungible_token( - sender=alice, - nonce=alice.get_nonce_then_increment(), - user=bob, - token_identifier="TEST-123456", - add_role_local_mint=True, - add_role_local_burn=True, - add_role_esdt_transfer_role=True -) +may behave unexpectedly: -# sending the transaction -tx_hash = entrypoint.send_transaction(transaction) +* delta may be zero over multiple blocks +* rewards might accumulate slower or unevenly +* divisions by delta may cause division-by-zero errors -# wait for transaction to execute, extract the roles -outcome = controller.await_completed_set_special_role_on_fungible_token(tx_hash) +Consider switching to milliseconds for finer granularity. -roles = outcome[0].roles -uaser = outcome[0].user_address -``` -#### Setting special roles for fungible tokens using the factory -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint, TokenManagementTransactionsOutcomeParser -# create the entrypoint and the token management transactions factory -entrypoint = DevnetEntrypoint() -factory = entrypoint.create_token_management_transactions_factory() +## Migration Guidance -# create the issuer of the token -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +Just as important as fixing potential issues with Supernova, it is essential not to introduce new bugs in the process. -transaction = factory.create_transaction_for_setting_special_role_on_fungible_token( - sender=alice.address, - user=bob, - token_identifier="TEST-123456", - add_role_local_mint=True, - add_role_local_burn=True, - add_role_esdt_transfer_role=True -) +Make sure to take the following into consideration when migrating: -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) -transaction.nonce = alice.get_nonce_then_increment() -# sign the transaction -transaction.signature = alice.sign_transaction(transaction) -# sending the transaction -tx_hash = entrypoint.send_transaction(transaction) -# waits until the transaction is processed and fetches it from the network -transaction_on_network = entrypoint.await_transaction_completed(tx_hash) +### Seconds vs. Milliseconds -# extract the roles -parser = TokenManagementTransactionsOutcomeParser() -outcome = parser.parse_set_special_role(transaction_on_network) +Switching from second to millisecond timestamp can be error-prone. -roles = outcome[0].roles -uaser = outcome[0].user_address -``` +The most dangerous bug is accidentally mixing second and millisecond values, causing storage and logic corruption. -#### Issuing semi-fungible tokens using the controller +To prevent this issue, use the [strongly typed timestamp and duration objects](time-types). -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint +Starting in **multiversx-sc v0.63.0**, all timestamp APIs have typed replacements: -# create the entrypoint and the token management controller -entrypoint = DevnetEntrypoint() -controller = entrypoint.create_token_management_controller() +* `get_block_timestamp_seconds()` +* `get_block_timestamp_millis()` +* `get_prev_block_timestamp_seconds()` +* `get_prev_block_timestamp_millis()` +* `get_block_round_time_millis()` +* `epoch_start_block_timestamp_millis()` -# create the issuer of the token -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +And avoid using raw `u64` for time values. -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +Make sure to convert form `u64` to type timestamps **before** doing any other refactoring. It is much safer this way. -transaction = controller.create_transaction_for_issuing_semi_fungible( - sender=alice, - nonce=alice.get_nonce_then_increment(), - token_name="NEWSEMI", - token_ticker="SEMI", - can_freeze=False, - can_wipe=True, - can_pause=False, - can_transfer_nft_create_role=True, - can_change_owner=True, - can_upgrade=True, - can_add_special_roles=True -) -# sending the transaction -tx_hash = entrypoint.send_transaction(transaction) -# wait for transaction to execute, extract the token identifier -outcome = controller.await_completed_issue_semi_fungible(tx_hash) -token_identifier = outcome[0].token_identifier -print(token_identifier) -``` +### Backwards compatibility -#### Issuing semi-fungible tokens using the factory +When upgrading an existing contract from second to millisecond timestamps, it is essential to: -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint, TokenManagementTransactionsOutcomeParser +* Ensure storage remains consistent +* Update ESDT metadata carefully +* Add compatibility logic if needed -# create the entrypoint and the token management transactions factory -entrypoint = DevnetEntrypoint() -factory = entrypoint.create_token_management_transactions_factory() +If you are unsure that this can be done safely, it might be safer to keep the contract running on second timestamps. -# create the issuer of the token -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -transaction = factory.create_transaction_for_issuing_semi_fungible( - sender=alice.address, - token_name="NEWSEMI", - token_ticker="SEMI", - can_freeze=False, - can_wipe=True, - can_pause=False, - can_transfer_nft_create_role=True, - can_change_owner=True, - can_upgrade=True, - can_add_special_roles=True -) -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) -transaction.nonce = alice.get_nonce_then_increment() -# sign the transaction -transaction.signature = alice.sign_transaction(transaction) +## Summary Checklist -# sending the transaction -tx_hash = entrypoint.send_transaction(transaction) +To prepare for Supernova: -# waits until the transaction is processed and fetches it from the network -transaction_on_network = entrypoint.await_transaction_completed(tx_hash) +* [ ] Upgrade to `multiversx-sc v0.63.0` +* [ ] Use typed timestamp/duration APIs +* [ ] Remove all hardcoded 6-second or 6000-millisecond assumptions +* [ ] Avoid using timestamps as block identifiers +* [ ] Review expiration, reward, and accumulation logic +* [ ] Switch to millisecond timestamps where appropriate +* [ ] Ensure storage/metadata compatibility +* [ ] Update tests to use for millisecond block timestamps, where needed -# extract the token identifier -parser = TokenManagementTransactionsOutcomeParser() -outcome = parser.parse_issue_semi_fungible(transaction_on_network) +--- -token_identifier = outcome[0].token_identifier -print(token_identifier) -``` +### Propose a multisig action -#### Issuing NFT collection & creating NFTs using the controller +A multisig contract does nothing until someone proposes an action. A proposal is +just a transaction to the multisig naming what it should eventually do; it does +not execute yet. It becomes a pending action with an id, waiting for board members +to sign it to quorum and for someone to perform it (see +[Sign and perform](/sdk-and-tools/sdk-js/cookbook/multisig/sign-perform-action)). +This recipe proposes two representative actions, `proposeTransferExecute` (move +EGLD out, optionally calling a function) and `proposeAddBoardMember` (change the +board), each through the real `MultisigController` and +`MultisigTransactionsFactory`, then parses the new action id. -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint +The default `npm start` parses a real, already-completed devnet propose for its +action id, so you see parsing work without a funded wallet. -# create the entrypoint and the token management controller -entrypoint = DevnetEntrypoint() -controller = entrypoint.create_token_management_controller() +## Prerequisites -# create the issuer of the token -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +- Node.js >= 20.13.1. +- For the default `parse` and `payload` demos: devnet network access only. +- For an actual propose: a devnet PEM whose address is a proposer or board member + on the target multisig, with a little EGLD for gas. -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +## Install -# issue NFT collection -transaction = controller.create_transaction_for_issuing_non_fungible( - sender=alice, - nonce=alice.get_nonce_then_increment(), - token_name="NEWNFT", - token_ticker="NFT", - can_freeze=False, - can_wipe=True, - can_pause=False, - can_transfer_nft_create_role=True, - can_change_owner=True, - can_upgrade=True, - can_add_special_roles=True -) +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/multisig-propose-action +npm install +npm run build +``` -# sending the transaction -tx_hash = entrypoint.send_transaction(transaction) +## Proposing + +```ts title="src/propose.ts" +// src/propose.ts - the subject of this recipe: proposing an action on an +// existing multisig contract, two ways (controller and factory), then parsing +// the resulting action id. +// +// A multisig contract does nothing until a *proposer* or *board member* +// proposes an action. The proposal is just a transaction to the multisig +// contract naming what it should eventually do; it does not execute yet. It +// sits as a pending action (with an id) until board members sign it to quorum +// and someone performs it (see the sign-and-perform recipe). +// +// This recipe shows two representative proposals: +// - proposeTransferExecute: move EGLD out of the multisig (optionally calling +// a function on the receiver). The everyday "spend" proposal. +// - proposeAddBoardMember: change the board itself. The everyday "governance +// of the multisig" proposal. +// Both go through the real MultisigController / MultisigTransactionsFactory. + +import { Account, Address } from '@multiversx/sdk-core'; +import type { Abi, DevnetEntrypoint, Transaction, TransactionOnNetwork } from '@multiversx/sdk-core'; + +// Gas is REQUIRED for every multisig write: the factory throws without it and +// the controller silently builds a gasLimit:0 transaction (see Pitfalls). A +// simple propose comfortably fits in 10M gas. +const PROPOSE_GAS_LIMIT = 10_000_000n; + +/** + * Propose a transfer-execute, path 1 - the controller. + * `createTransactionForProposeTransferExecute` builds, sets the nonce, and + * signs in one call. Passing only `to` + `nativeTokenAmount` (no `functionName`) + * proposes a plain EGLD transfer out of the multisig. + */ +export async function proposeTransferViaController( + entrypoint: DevnetEntrypoint, + abi: Abi, + proposer: Account, + multisigContract: string, + to: string, + amount: bigint, +): Promise { + const controller = entrypoint.createMultisigController(abi); + return controller.createTransactionForProposeTransferExecute(proposer, proposer.getNonceThenIncrement(), { + multisigContract: Address.newFromBech32(multisigContract), + to: Address.newFromBech32(to), + nativeTokenAmount: amount, + gasLimit: PROPOSE_GAS_LIMIT, + }); +} -# wait for transaction to execute, extract the collection identifier -outcome = controller.await_completed_issue_non_fungible(tx_hash) +/** + * Propose a transfer-execute, path 2 - the factory. Builds the unsigned + * transaction only; the caller sets the nonce and signs. Use this when a + * wallet or hardware device signs. + */ +export async function proposeTransferViaFactory( + entrypoint: DevnetEntrypoint, + abi: Abi, + proposer: Account, + multisigContract: string, + to: string, + amount: bigint, +): Promise { + const factory = entrypoint.createMultisigTransactionsFactory(abi); + const transaction = await factory.createTransactionForProposeTransferExecute(proposer.address, { + multisigContract: Address.newFromBech32(multisigContract), + to: Address.newFromBech32(to), + nativeTokenAmount: amount, + gasLimit: PROPOSE_GAS_LIMIT, + }); + transaction.nonce = proposer.getNonceThenIncrement(); + transaction.signature = await proposer.signTransaction(transaction); + return transaction; +} + +/** + * Propose adding a board member (controller). A different action type that + * changes the board rather than spending funds - but the propose/sign/perform + * lifecycle is identical. + */ +export async function proposeAddBoardMemberViaController( + entrypoint: DevnetEntrypoint, + abi: Abi, + proposer: Account, + multisigContract: string, + newBoardMember: string, +): Promise { + const controller = entrypoint.createMultisigController(abi); + return controller.createTransactionForProposeAddBoardMember(proposer, proposer.getNonceThenIncrement(), { + multisigContract: Address.newFromBech32(multisigContract), + boardMember: Address.newFromBech32(newBoardMember), + gasLimit: PROPOSE_GAS_LIMIT, + }); +} -collection_identifier = outcome[0].token_identifier +export interface ProposePayload { + function: string; + args: string[]; + receiver: string; + valueWei: bigint; + gasLimit: bigint; +} -# set roles -transaction = controller.create_transaction_for_setting_special_role_on_non_fungible_token( - sender=alice, - nonce=alice.get_nonce_then_increment(), - user=alice.address, - token_identifier=collection_identifier, - add_role_nft_create=True, - add_role_nft_burn=True, - add_role_nft_update_attributes=True, - add_role_nft_add_uri=True, - add_role_esdt_transfer_role=True, -) +/** Decode a propose transaction's wire fields. */ +export function describeProposePayload(transaction: Transaction): ProposePayload { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + function: parts[0] ?? '', + args: parts.slice(1), + receiver: transaction.receiver.toBech32(), + valueWei: transaction.value, + gasLimit: transaction.gasLimit, + }; +} -# sending the transaction and waiting for completion -tx_hash = entrypoint.send_transaction(transaction) -entrypoint.await_transaction_completed(tx_hash) +/** + * Parse a completed propose transaction for the new action id. Every propose + * endpoint returns the freshly-assigned action id; the board will use it to + * sign and perform the action. `parseProposeAction` reads it back. + */ +export function parseProposedActionId( + entrypoint: DevnetEntrypoint, + abi: Abi, + transactionOnNetwork: TransactionOnNetwork, +): number { + const controller = entrypoint.createMultisigController(abi); + return controller.parseProposeAction(transactionOnNetwork); +} +``` -# create a NFT -transaction = controller.create_transaction_for_creating_nft( - sender=alice, - nonce=alice.get_nonce_then_increment(), - token_identifier=collection_identifier, - initial_quantity=1, - name="TEST", - royalties=2500, # 25% - hash="", - attributes=b"", - uris=["emptyUri"] -) +## Run it -# sending the transaction -tx_hash = entrypoint.send_transaction(transaction) +```bash +# Parse a real completed devnet propose for its action id - no wallet, no funds: +npm start -# wait for transaction to execute, extract the nft identifier -outcome = controller.await_completed_create_nft(tx_hash) +# Inspect the two propose wire payloads offline: +npm start -- payload -identifier = outcome[0].token_identifier -nonce = outcome[0].nonce -initial_quantity = outcome[0].initial_quantity -print(identifier) -print(nonce) -print(initial_quantity) +# Actually propose (needs a proposer/board-member devnet PEM); add --factory or +# --add-board-member: +npm start -- propose ./wallet.pem ``` -#### Issuing NFT collection & creating NFTs using the factory +Output of the default `parse` mode, and of `payload`: -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint, TokenManagementTransactionsOutcomeParser +```text +Parsing completed propose 6f4992b6...05a83cfc437 ... + proposed action id: 4 + (board members now sign this id; then someone performs it) + +proposeTransferExecute: + function: proposeTransferExecute + args: 6d40...e45a | 0de0b6b3a7640000 | (to hex | amount hex | empty gas option) + receiver: erd1qqqqqqqqqqqqqpgqvjcswsav2rw4ccvguyuk0f0u6qycpsuvu3dq9rse9w (the multisig contract) + value: 0 wei (0 - the moved EGLD is an argument, paid later on perform) + gasLimit: 10000000 +proposeAddBoardMember: + function: proposeAddBoardMember + args: 6d40...e45a (the new board member's public key) +``` + +## How it works + +**Controller vs factory.** +`controller.createTransactionForProposeTransferExecute(account, nonce, options)` +builds, sets the nonce, and signs in one call. +`factory.createTransactionForProposeTransferExecute(address, options)` only builds +the unsigned transaction; you set `nonce` and `signature`. Both take +`{ multisigContract, to, nativeTokenAmount, gasLimit }` and both need the multisig +ABI at construction (`entrypoint.createMultisigController(abi)`). + +**The moved amount is an argument, not the value.** `proposeTransferExecute` puts +the receiver and the EGLD amount in the data; the proposing transaction's own +`value` is `0`. The EGLD moves out of the multisig's balance later, when the action +is performed, not out of the proposer's wallet now. + +**Parsing the outcome.** Every propose endpoint returns the freshly assigned action +id. `controller.parseProposeAction(transactionOnNetwork)` reads it back; the default +`npm start` runs it against a historical propose, which is why it needs no funds. +Keep this id, it is what you sign and perform. + +## Pitfalls + +:::warning[Pitfall 1: gas is required; the controller does NOT protect you] +Every multisig write needs an explicit `gasLimit`. The factory throws `Either +provide a gasLimit parameter or initialize the factory with a gasLimitEstimator` +when it is missing, but the **controller silently coerces a missing gasLimit to +`0n`** and builds a broadcastable `gasLimit: 0` transaction that the network then +rejects for too-low gas. Confirmed against sdk-core v15.4.1. Always pass `gasLimit`. +::: -# create the entrypoint and the token management transactions factory -entrypoint = DevnetEntrypoint() -factory = entrypoint.create_token_management_transactions_factory() +:::note[Pitfall 2: only proposers and board members may propose] +The multisig contract accepts a proposal only from an address with the proposer or +board-member role. An unauthorized proposer is rejected by the contract at +execution, not by the SDK when building. Check roles first with +[Read a multisig's state](/sdk-and-tools/sdk-js/cookbook/multisig/read-multisig-state). +::: -# create the issuer of the token -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +:::note[Pitfall 3: proposeTransferExecute's value is always 0] +The EGLD that a transfer-execute will move is encoded as the `nativeTokenAmount` +argument and paid from the multisig's own balance on perform. The proposing +transaction carries `value: 0`. Do not attach EGLD to the proposal expecting it to +be the transferred amount. +::: -# issue NFT collection -transaction = factory.create_transaction_for_issuing_non_fungible( - sender=alice.address, - token_name="NEWTOKEN", - token_ticker="TKN", - can_freeze=False, - can_wipe=True, - can_pause=False, - can_transfer_nft_create_role=True, - can_change_owner=True, - can_upgrade=True, - can_add_special_roles=True -) +:::note[Pitfall 4: the propose payload's trailing empty part is the gas option] +`proposeTransferExecute@to@amount@` ends with an empty part: it is the +`Option` gas argument set to `None` (an empty top-level arg). A plain EGLD +transfer with no function call adds nothing after it. Pass `optGasLimit` / +`functionName` to fill it. +::: -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) -transaction.nonce = alice.get_nonce_then_increment() +## See also -# sign the transaction -transaction.signature = alice.sign_transaction(transaction) +- [Sign and perform a multisig action](/sdk-and-tools/sdk-js/cookbook/multisig/sign-perform-action) + is what happens to the action id this recipe creates. +- [Read a multisig's state](/sdk-and-tools/sdk-js/cookbook/multisig/read-multisig-state) + checks roles, quorum, and pending actions before proposing. +- The multisig itself is a contract; deploying one follows the smart-contracts + recipes. -# sending the transaction -tx_hash = entrypoint.send_transaction(transaction) +--- -# if we know that the transaction is completed, we can simply call `get_transaction(tx_hash)` -transaction_on_network = entrypoint.await_transaction_completed(tx_hash) - -# extract the collection identifier -parser = TokenManagementTransactionsOutcomeParser() -outcome = parser.parse_issue_non_fungible(transaction_on_network) - -collection_identifier = outcome[0].token_identifier +### Proxies -# set roles -transaction = factory.create_transaction_for_setting_special_role_on_non_fungible_token( - sender=alice.address, - user=alice.address, - token_identifier=collection_identifier, - add_role_nft_create=True, - add_role_nft_burn=True, - add_role_nft_update_attributes=True, - add_role_nft_add_uri=True, - add_role_esdt_transfer_role=True, -) -transaction.nonce = alice.get_nonce_then_increment() +## Overview -# sign the transaction -transaction.signature = alice.sign_transaction(transaction) +Proxies are objects that mimic the contract, they provide methods with the same names and the same argument types. When called, they will format a transaction for the contract. So they act as translators: you can call them just like regular functions, and they will translate it for the blockchain and pass on the call. They also tell you what type the function is expected to return. -# sending the transaction and waiting for completion -tx_hash = entrypoint.send_transaction(transaction) -entrypoint.await_transaction_completed(tx_hash) +New proxies are generated as structures. If you have the proxy, you have all its methods. The generated code is in plain sight and readable, designed to add no overhead to the contract binaries once compiled. -# create a NFT -transaction = factory.create_transaction_for_creating_nft( - sender=alice.address, - token_identifier=collection_identifier, - initial_quantity=1, - name="TEST", - royalties=2500, # 25% - hash="", - attributes=b"", - uris=["emptyUri"] -) -# set the nonce -transaction.nonce = alice.get_nonce_then_increment() +Proxies can be simply copied between crates, so there is no more need for dependencies between contract crates. Proxies do not mind what their source contract code looks like or what framework version it uses. -# sign the transaction -transaction.signature = alice.sign_transaction(transaction) -# sending the transaction -tx_hash = entrypoint.send_transaction(transaction) +## How to generate -# waits until the transaction is processed and fetches it from the network -transaction_on_network = entrypoint.await_transaction_completed(tx_hash) +Proxy generation can be triggered by calling `sc-meta all proxy` in the contract root folder. This command generates the proxy of the main contract in `/output` folder with name `proxy.rs` if it has no configuration. To configure it, you need to modify sc-config.toml. -# extract the nft identifier -parser = TokenManagementTransactionsOutcomeParser() -outcome = parser.parse_nft_create(transaction_on_network) +This first image represents the structure of the project without calling a proxy generator, while the second one shows how it is called and what files it generates. -identifier = outcome[0].token_identifier -nonce = outcome[0].nonce -initial_quantity = outcome[0].initial_quantity -print(identifier) -print(nonce) -print(initial_quantity) -``` +![img](/img/tree_before_proxy.png) -These are just a few examples of what we can do using the token management controller or factory. For a full list of what methods are supported for both, check out the autogenerated documentation: -- [TokenManagementController](https://multiversx.github.io/mx-sdk-py/multiversx_sdk.token_management.html#module-multiversx_sdk.token_management.token_management_controller) -- [TokenManagementTransactionsFactory](https://multiversx.github.io/mx-sdk-py/multiversx_sdk.token_management.html#module-multiversx_sdk.token_management.token_management_transactions_factory) +![img](/img/tree_after_proxy.png) -### Account management -The account management controller and factory allow us to create transactions for managing accounts, like guarding and unguarding accounts and saving key-value pairs. +## How to set up project to re-generate easily -To read more about Guardians, check out the [documentation](/developers/built-in-functions/#setguardian). +In order to configure the proxy, you need to change configuration file. If this file is absent from the project directory, you have to create it. More details about how to set up a configuration file are available [here](../meta/sc-config/). -A guardian can also be set using the WebWallet. The wallet uses our hosted `Trusted Co-Signer Service`. Check out the steps to guard an account using the wallet [here](/wallet/web-wallet/#guardian). +### Path -#### Guarding an account using the controller +First, you need to specify the output path where the generated proxy file will be saved. This ensures that whenever you regenerate the proxy, the configured path will be automatically updated with the latest version. -```py -from pathlib import Path -from multiversx_sdk import Account, Address, DevnetEntrypoint +```toml title=sc-config.toml +[settings] -# create the entrypoint and the account controller -entrypoint = DevnetEntrypoint() -controller = entrypoint.create_account_controller() +[[proxy]] +path = "src/adder_proxy.rs" +``` -# create the account to guard -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +After the proxy is generated via "sc-meta all proxy" command, you have to import the module in the contract. -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +```rust title=adder.rs +#![no_std] -# we can use a trusted service that provides a guardian, or simply set another address we own or trust -guardian = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") +use multiversx_sc::imports::*; -transaction = controller.create_transaction_for_setting_guardian( - sender=alice, - nonce=alice.get_nonce_then_increment(), - guardian_address=guardian, - service_id="SelfOwnedAddress" # this is just an example -) +pub mod adder_proxy; -tx_hash = entrypoint.send_transaction(transaction) +#[multiversx_sc::contract] +pub trait Adder { + ... +} ``` -#### Guarding an account using the factory - -```py -from pathlib import Path -from multiversx_sdk import Account, Address, DevnetEntrypoint +:::note +Changing the settings requires configuring the output path. +::: +### Override imports +If you need to override the proxy imports that are encapsulated in the line `use multiversx_sc::proxy_imports::*;` from proxy, you can achieve this by adding the `override_import`. -# create the entrypoint and the account transactions factory -entrypoint = DevnetEntrypoint() -factory = entrypoint.create_account_transactions_factory() -# create the account to guard -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +```toml title=sc-config.toml +[settings] -# we can use a trusted service that provides a guardian, or simply set another address we own or trust -guardian = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") +[[proxy]] +path = "src/adder_proxy.rs" +override-import = "use multiversx_sc::abi::{ContractAbi, EndpointAbi};" +``` -transaction = factory.create_transaction_for_setting_guardian( - sender=alice.address, - guardian_address=guardian, - service_id="SelfOwnedAddress" # this is just an example -) +### Rename paths +If you want to rename paths from structures and enumerations in a generated proxy, use the `path-rename` setting. Specify both the original path and the new name you want. -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +```toml title=sc-config.toml +[[proxy]] +path = "src/adder_proxy.rs" +[[proxy.path-rename]] +from = "adder" +to = "new_path::adder" +``` -# set the nonce -transaction.nonce = alice.get_nonce_then_increment() +### Generate variant from multi-contract +To generate a proxy for a specific variant within a multi-contract project, use the `variant` setting and specify the desired variant. -# sign the transaction -transaction.signature = alice.sign_transaction(transaction) +```toml title=multicontract.toml +[settings] +main = "multi-contract-main" -tx_hash = entrypoint.send_transaction(transaction) +[[proxy]] +variant = "multi_contract_example_feature" +path = "src/multi_contract_example_feature_proxy.rs" ``` -After we've set a guardian, we have to wait 20 epochs until we can activate the guardian. After the guardian is set, all the transactions we send should be signed by the guardian, as well. +### Generate custom proxy -#### Activating the guardian using the controller +First, it needs to be specified that all unlabelled endpoints should **not** be added to this contract. This can be configured using: +- `add-unlabelled` + - values: `true` | `false` + - default: `true` -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint +```toml title=multicontract.toml +[[proxy]] +path = "src/multisig_view_proxy.rs" +add-unlabelled = false # unlabelled endpoints are not included +``` +If you want to generate a proxy for all endpoints labelled with a certain tag, you can use: +- `add-labels` + - values: `list of strings`, e.g. add-labels = ["label1", "label2"] + - default: `[]` -# create the entrypoint and the account controller -entrypoint = DevnetEntrypoint() -controller = entrypoint.create_account_controller() +The example below will create a proxy for all endpoints that are tagged with *"label(proxy_one)"* and *"label(proxy_two)"*. -# create the account to guard -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +```toml title=multicontract.toml +[[proxy]] +path = "src/multisig_view_proxy.rs" +add-unlabelled = false +add-labels = ["proxy_one", "proxy_two"] +``` -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +If you want to generate a proxy based on a specific list of endpoints, there is: +- `add-endpoints` + - values: `list of strings`, e.g. add-endpoints = ["endpoint_one", "endpoint_two"] + - default: `[]` -transaction = controller.create_transaction_for_guarding_account( - sender=alice, - nonce=alice.get_nonce_then_increment() -) +The following example will create a proxy only with the functions specified in the list of `add-endpoints`. -tx_hash = entrypoint.send_transaction(transaction) +```toml title=multicontract.toml +[[proxy]] +path = "src/multisig_view_proxy.rs" +add-unlabelled = false +add-endpoints = ["init", "user_role"] +``` +The custom proxy can be generated using simultaneously both configurations: specified endpoints and endpoints labelled with a particular tag. +```toml title=multicontract.toml +[[proxy]] +path = "src/multisig_view_proxy.rs" +add-unlabelled = false +add-labels = ["proxy_one"] +add-endpoints = ["user_role"] ``` -#### Activating the guardian using the factory -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint +## Adjustments in contracts -# create the entrypoint and the account transactions factory -entrypoint = DevnetEntrypoint() -factory = entrypoint.create_account_transactions_factory() +Before generating a proxy, you have to change every structure and enumeration that contains `#[derive(TypeAbi)]` with `#[type_abi]`. If you do not update structures and enumerations, they will not be correctly generated in the proxy. -# create the account to guard -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +Before version 0.49.0: +```rust title=lib.rs +#[derive(TopEncode, TopDecode, PartialEq, Eq, Clone, Copy, Debug, TypeAbi)] +pub enum Status { + FundingPeriod, + Successful, + Failed, +} +``` +After version 0.49.0: +```rust title=lib.rs +#[type_abi] +#[derive(TopEncode, TopDecode, PartialEq, Eq, Clone, Copy, Debug)] +pub enum Status { + FundingPeriod, + Successful, + Failed, +} +``` +If the custom type is in a private module or another crate is better to replace it because the proxy will be useless. -transaction = factory.create_transaction_for_guarding_account( - sender=alice.address, -) -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) -# set the nonce -transaction.nonce = alice.get_nonce_then_increment() +## Diagram -# sign the transaction -transaction.signature = alice.sign_transaction(transaction) +This is the diagram for how to populate the data field using proxies. For the raw setup, see [here](tx-data#diagram) -tx_hash = entrypoint.send_transaction(transaction) +```mermaid +graph LR + subgraph "Data (via proxies)" + data-unit["()"] + data-unit -->|typed| Proxy + Proxy -->|"init(args, ...)"| deploy + Proxy -->|"upgrade(args, ...)"| upgrade + Proxy -->|"«endpoint»(args, ...)"| fc[Function Call] + deploy -->|from_source| deploy-from-source["DeployCall<FromSource<ManagedAddress>>"] + deploy -->|code| deploy-code["DeployCall<Code<ManagedBuffer>>"] + deploy -->|code_metadata| deploy + upgrade -->|from_source| upgrade-from-source["UpgradeCall<CodeSource<ManagedAddress>>"] + upgrade -->|code| upgrade-code["UpgradeCall<Code<ManagedBuffer>>"] + upgrade -->|code_metadata| upgrade + end ``` -#### Unguarding the account using the controller - -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint -# create the entrypoint and the account controller -entrypoint = DevnetEntrypoint() -controller = entrypoint.create_account_controller() -# the account to unguard -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +## Original type -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +Proxies have the power to define the **original type** by themselves because, when they are generated, they have access to the ABI. -# the guardian account -guardian = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/bob.pem")) +The proxy will add the type definition by calling `.original_type()`, which places an [`OriginalResultMarker`](tx-result-handlers#original-result-marker) object as the transaction result handler. This is a zero-sized type, it is only there for type inference and safety in result handlers. -transaction = controller.create_transaction_for_unguarding_account( - sender=alice, - nonce=alice.get_nonce_then_increment(), - guardian=guardian.address -) -# the transaction should also be signed by the guardian before being sent, otherwise it won't be executed -transaction.guardian_signature = guardian.sign_transaction(transaction) -# broadcast the transaction -tx_hash = entrypoint.send_transaction(transaction) -``` +## NotPayable protection -#### Unguarding the account using the factory +All non-payment endpoints in the generated proxy have a safeguard to prevent accidental payments. This involves automatically setting the payment to [**NotPayable**](tx-payment#notpayable). -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint +--- -# create the entrypoint and the account transactions factory -entrypoint = DevnetEntrypoint() -factory = entrypoint.create_account_transactions_factory() +### Proxy architecture -# the account to unguard -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +Overview of the MultiversX Proxy -# the guardian account -guardian = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/bob.pem")) -transaction = factory.create_transaction_for_unguarding_account( - sender=alice.address, - guardian=guardian.address -) +## **Introduction** -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +The MultiversX Proxy acts as an entry point into the MultiversX Network, through a set of Observer Nodes, and (partly) abstracts away the particularities and complexity of sharding. -# set the nonce -transaction.nonce = alice.get_nonce_then_increment() +The Proxy is a project written in **go**, and it serves as foundation for *gateway.multiversx.com*. -# sign the transaction -transaction.signature = alice.sign_transaction(transaction) +The source code of the Proxy can be found here: [mx-chain-proxy-go](https://github.com/multiversx/mx-chain-proxy-go). -# the transaction should also be signed by the guardian before being sent otherwise it won't be executed -transaction.guardian_signature = guardian.sign_transaction(transaction) -# broadcast the transaction -tx_hash = entrypoint.send_transaction(transaction) -``` +## **Architectural Overview** -#### Saving a key-value pair to an account using the controller +While any Node in the Network can accept Transaction requests, the Transactions are usually submitted to the **Proxy** application, which maintains a list of Nodes - **Observers** - to forward Transaction requests to - these Observers are selected in such manner that any Transaction submitted to them will be processed by the Network **as soon and as efficiently as possible**. -We can store key-value pairs for an account on the network. To do so, we create the following transaction: +The Proxy will submit a Transaction on behalf of the user to the REST API of one of its listed Observers, selected for **(a)** being _online_ at the moment and **(b)** being located **within the Shard to which the Sender's Account belongs**. After receiving the Transaction on its REST API, that specific Observer will propagate the Transaction throughout the Network, which will lead to its execution. -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint +The Observer Nodes of the Proxy thus act as a **default dedicated entry point into the Network**. -# create the entrypoint and the account controller -entrypoint = DevnetEntrypoint() -controller = entrypoint.create_account_controller() +It is worth repeating here, though, that submitting a Transaction through the Proxy is completely optional - any Node of the Network will accept Transactions to propagate, given it has not disabled its REST API. -# create the account to guard -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +![img](/technology/proxy-overview.png) -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +Overview of the MultiversX Proxy -# creating the key-value pairs we want to save -values = { - "testKey".encode(): "testValue".encode(), - b"anotherKey": b"anotherValue" -} +In the figure above: -transaction = controller.create_transaction_for_saving_key_value( - sender=alice, - nonce=alice.get_nonce_then_increment(), - key_value_pairs=values -) +1. The **MultiversX Network** - consisting of Nodes grouped within Shards. Some of these Nodes are **Observers**. +2. One or more instances of the **MultiversX Proxy** - including the official one - connect to Observer Nodes in order to forward incoming user Transactions to the Network and to query state within the Blockchain. +3. The **client applications** connect to the Network through the MultiversX Proxy. It is also possible for a blockchain-powered application to talk directly to an Observer or even to a Validator. -# broadcast the transaction -tx_hash = entrypoint.send_transaction(transaction) -``` -#### Saving a key-value pair to an account using the factory +## **Official MultiversX Proxy** -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint +The official instance of the MultiversX Proxy is located at [https://gateway.multiversx.com](https://gateway.multiversx.com/). -# create the entrypoint and the account transactions factory -entrypoint = DevnetEntrypoint() -factory = entrypoint.create_account_transactions_factory() -# create the account to guard -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +## **Swagger docs** -# creating the key-value pairs we want to save -values = { - "testKey".encode(): "testValue".encode(), - b"anotherKey": b"anotherValue" -} +The Swagger docs of the proxy can be found at the root of the gateway. For example: [https://gateway.multiversx.com](https://gateway.multiversx.com/). -transaction = factory.create_transaction_for_saving_key_value( - sender=alice.address, - key_value_pairs=values -) -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +## **Set up a Proxy Instance** -# set the nonce -transaction.nonce = alice.get_nonce_then_increment() +:::caution +Documentation for setting up a Proxy is preliminary and subject to change +::: -# sign the transaction -transaction.signature = alice.sign_transaction(transaction) +In order to host a Proxy instance on a web server, one has to first clone and build the repository: -# broadcast the transaction -tx_hash = entrypoint.send_transaction(transaction) +```bash +git clone https://github.com/multiversx/mx-chain-proxy-go.git +cd elrond-proxy-go/cmd/proxy +go build . ``` -### Delegation management - -To read more about staking providers and delegation, please check out the [docs](/validators/delegation-manager/#introducing-staking-providers). -In this section, we are going to create a new delegation contract, get the address of the contract, delegate funds to the contract, redelegate rewards, claim rewards, undelegate and withdraw funds from the contract. The operations can be performed using both the `controller` and the `factory`. For a full list of all the methods supported check out the auto-generated documentation: -- [DelegationController](https://multiversx.github.io/mx-sdk-py/multiversx_sdk.delegation.html#module-multiversx_sdk.delegation.delegation_controller) -- [DelegationTransactionsFactory](https://multiversx.github.io/mx-sdk-py/multiversx_sdk.delegation.html#module-multiversx_sdk.delegation.delegation_transactions_factory) +### **Configuration** -#### Creating a new delegation contract using the controller +The Proxy holds its configuration within the `config` folder: -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint +- `config.toml` - this is the main configuration file. It has to be adjusted so that the Proxy points to a list of chosen Observer Nodes. +- `external.toml` - this file holds configuration necessary to Proxy components that interact with external systems. An example of such an external system is **Elasticsearch** - currently, MultiversX Proxy requires an Elasticsearch instance to implement some of its functionality. +- `apiConfig/credentials.toml` - this file holds the configuration needed for enabling secured endpoints - only accessible by using BasicAuth. +- `apiConfig/v1_0.toml` - this file contains all the endpoints with their settings (open, secured and rate limit). -# create the entrypoint and the delegation controller -entrypoint = DevnetEntrypoint() -controller = entrypoint.create_delegation_controller() +:::note +If the provided observers' addresses from `config.toml` are not physical addresses of each observer, but rather addresses of providers that manage multiple observers for each shard, with self handling of health checks, the Proxy must be started with `--no-status-check` flag. +::: -# the owner of the contract -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +## **Snapshotless observers support** -transaction = controller.create_transaction_for_new_delegation_contract( - sender=alice, - nonce=alice.get_nonce_then_increment(), - total_delegation_cap=0, # uncapped, - service_fee=0, - amount=1250000000000000000000 # 1250 EGLD -) +Instead of nodes that perform regular trie operations, such as snapshots and so on, one could use snapshotless nodes, which are, as the name suggests, nodes that have a different configuration which allows them to "bypass" certain costly trie operations, with the downside of losing access to anything but real-time. -tx_hash = entrypoint.send_transaction(transaction) +A proxy that only needs real-time data (that is, they are not interested in historical data such as "give me block with nonce X from last month") are a very good use-case for snapshotless observers -# wait for transaction completion, extract delegation contract's address -outcome = controller.await_completed_create_new_delegation_contract(tx_hash) -contract_address = outcome[0].contract_address -``` +### **Proxy snapshotless endpoints** -#### Creating a new delegation contract using the factory +Although there are more endpoints that can be used exclusively with snapshotless observers, here's a list with the most common ones: +- `/address/{address}` : returns data about an address (balance, nonce, username, root hash and so on) +- `/address/{address}/balance` : returns the balance of an address +- `/address/{address}/nonce` : returns the nonce of an address +- `/address/{address}/username` : returns the username of an address +- `/address/{address}/esdt` : returns all the ESDT tokens managed by an address +- `/address/{address}/esdt/{tokenIdentifier}` : returns the token data of an address with a specific token identifier +- `/address/{address}/nft/{tokenIdentifier}/nonce/{nonce}` : returns the NFT/SFT/MetaESDT data of an address for a specific token identifier and nonce +- `/address/{address}/guardian-data` : returns guardian data of an address +- `/address/{address}/keys` : returns the storage key-value pairs of an address +- `/address/{address}/key/{key}` : returns the value of a specific key under an address +- `/network/config` : returns the network configuration +- `/network/status/{shard}` : returns the status of the specific shard +- `/node/heartbeatstatus` : return the heartbeat status of the entire network's nodes +- `/transaction/send` : broadcasts a transaction to the network +- `/vm-values/int` : performing SC Queries (gas-free queries) expecting the result as int +- `/vm-values/hex` : performing SC Queries (gas-free queries) expecting the result as hex +- `/vm-values/string` : performing SC Queries (gas-free queries) expecting the result as string +- `/vm-values/query` : performing SC Queries (gas-free queries) expecting the result as raw +- and so on -```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint, DelegationTransactionsOutcomeParser +Basically, every endpoint that doesn't require historical data access can be used with snapshotless observers -# create the entrypoint and the delegation transactions factory -entrypoint = DevnetEntrypoint() -factory = entrypoint.create_delegation_transactions_factory() -# the owner of the contract -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +## **Dependency on Elasticsearch** -transaction = factory.create_transaction_for_new_delegation_contract( - sender=alice.address, - total_delegation_cap=0, # uncapped, - service_fee=0, - amount=1250000000000000000000 # 1250 EGLD -) +Currently, Proxy uses the dependency to Elasticsearch in order to satisfy the [Get Address Transactions](/sdk-and-tools/rest-api/addresses/#get-address-transactions) endpoint. -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +In order to connect a Proxy instance to an Elasticsearch cluster, one must update the `external.toml` file. -# set the nonce -transaction.nonce = alice.get_nonce_then_increment() +--- -# sign the transaction -transaction.signature = alice.sign_transaction(transaction) +### Python SDK -# send the transaction -tx_hash = entrypoint.send_transaction(transaction) +## Overview -# waits until the transaction is processed and fetches it from the network -transaction_on_network = entrypoint.await_transaction_completed(tx_hash) +This page will guide you through the process of handling common tasks using the MultiversX Python SDK (libraries) **v2 (latest, stable version)**. -# extract the contract's address -parser = DelegationTransactionsOutcomeParser() -outcome = parser.parse_create_new_delegation_contract(transaction_on_network) +:::important +This cookbook makes use of `sdk-py v2`. In order to migrate from `sdk-py v1` to `sdk-py v2`, please also follow [the migration guide](https://github.com/multiversx/mx-sdk-py/issues?q=label:migration). +::: -contract_address = outcome[0].contract_address -``` +:::note +All examples depicted here are captured in **(interactive) [Jupyter notebooks](https://github.com/multiversx/mx-sdk-py/blob/main/examples/Cookbook.ipynb)**. +::: -#### Delegating funds to the contract using the controller +We are going to use the [multiversx-sdk-py](https://github.com/multiversx/mx-sdk-py) package. This package can be installed directly from GitHub or from [**PyPI**](https://pypi.org/project/multiversx-sdk/). -We can send funds to a delegation contract to earn rewards. + + +## Creating an Entrypoint + +The Entrypoint represents a network client that makes the most common operations easily accessible. We have an entrypoint for each network: `MainnetEntrypoint`, `DevnetEntrypoint`, `TestnetEntrypoint` and `LocalnetEntrypoint`. For example, this is how to create a Devnet entrypoint: ```py -from pathlib import Path -from multiversx_sdk import Account, Address, DevnetEntrypoint +from multiversx_sdk import DevnetEntrypoint -# create the entrypoint and the delegation controller entrypoint = DevnetEntrypoint() -controller = entrypoint.create_delegation_controller() +``` -# create the account delegating funds -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +If we want to create an entrypoint that uses a third party API, we can do so as follows: -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +```py +from multiversx_sdk import DevnetEntrypoint -# delegation contract -contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva") +entrypoint = DevnetEntrypoint(url="https://custom-multiversx-devnet-api.com") +``` -transaction = controller.create_transaction_for_delegating( - sender=alice, - nonce=alice.get_nonce_then_increment(), - delegation_contract=contract, - amount=5000000000000000000000 # 5000 EGLD -) +By default, an Entrypoint, in our case the `DevnetEntrypoint`, uses the API, but we can also create a custom one that interacts with the proxy. -tx_hash = entrypoint.send_transaction(transaction) +```py +from multiversx_sdk import DevnetEntrypoint + +custom_entrypoint = DevnetEntrypoint(url="https://devnet-gateway.multiversx.com", kind="proxy") ``` -#### Delegating funds to the contract using the factory +We can create an entrypoint from a network provider. ```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint - -# create the entrypoint and the delegation transactions factory -entrypoint = DevnetEntrypoint() -factory = entrypoint.create_delegation_transactions_factory() +from multiversx_sdk import NetworkEntrypoint, ApiNetworkProvider -# create the account delegating funds -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +api = ApiNetworkProvider("https://devnet-api.multiversx.com") +entrypoint = NetworkEntrypoint.new_from_network_provider(network_provider=api, chain_id="D") +``` -transaction = factory.create_transaction_for_delegating( - sender=alice.address, - delegation_contract=contract, - amount=5000000000000000000000 # 5000 EGLD -) +## Creating Accounts -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +We can create an account directly from the entrypoint. Keep in mind that the account you create is network agnostic, it does not matter which entrypoint is used. -# set the nonce -transaction.nonce = alice.get_nonce_then_increment() +The account can be used for signing and for storing the nonce of the account. It can also be saved to a `pem` or `keystore` file. -# sign the transaction -transaction.signature = alice.sign_transaction(transaction) +```py +from multiversx_sdk import DevnetEntrypoint -# send the transaction -tx_hash = entrypoint.send_transaction(transaction) +entrypoint = DevnetEntrypoint() +account = entrypoint.create_account() ``` -#### Redelegating rewards using the controller +There are also other ways to instantiate an `Account`. -After a period of time, we might have enough rewards that we want to redelegate to the contract to earn even more rewards. +#### Instantiating an Account using a secret key ```py -from pathlib import Path -from multiversx_sdk import Account, Address, DevnetEntrypoint - -# create the entrypoint and the delegation controller -entrypoint = DevnetEntrypoint() -controller = entrypoint.create_delegation_controller() +from multiversx_sdk import Account, UserSecretKey -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +secret_key_hex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9" +secret_key = UserSecretKey(bytes.fromhex(secret_key_hex)) -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +account = Account(secret_key) +``` -# delegation contract -contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva") +#### Instantiating an Account from a PEM file -transaction = controller.create_transaction_for_redelegating_rewards( - sender=alice, - nonce=alice.get_nonce_then_increment(), - delegation_contract=contract -) +```py +from pathlib import Path +from multiversx_sdk import Account -tx_hash = entrypoint.send_transaction(transaction) +account = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) ``` -#### Redelegating rewards using the factory +#### Instantiating an Account from a Keystore file ```py from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint +from multiversx_sdk import Account -# create the entrypoint and the delegation transactions factory -entrypoint = DevnetEntrypoint() -factory = entrypoint.create_delegation_transactions_factory() +account = Account.new_from_keystore( + file_path=Path("../multiversx_sdk/testutils/testwallets/alice.json"), + password="password" +) +``` -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +#### Instantiating an Account from a mnemonic -transaction = factory.create_transaction_for_redelegating_rewards( - sender=alice.address, - delegation_contract=contract -) +```py +from multiversx_sdk import Account, Mnemonic -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +mnemonic = Mnemonic.generate() +account = Account.new_from_mnemonic(mnemonic.get_text()) +``` -# set the nonce -transaction.nonce = alice.get_nonce_then_increment() +#### Instantiating an Account from a KeyPair -# sign the transaction -transaction.signature = alice.sign_transaction(transaction) +```py +from multiversx_sdk import Account, KeyPair -# send the transaction -tx_hash = entrypoint.send_transaction(transaction) +keypair = KeyPair.generate() +account = Account.new_from_keypair(keypair) ``` -#### Claiming rewards using the controller +### Managing the Account nonce -We can also claim our rewards. +The account has a `nonce` property that the user is responsible for keeping up to date. We can fetch the nonce of the account from the network once and then we can increment it with each transaction we create. Each transaction sent **must** have the correct nonce set, otherwise it will not be executed. For more details check out the [Creating Transactions](#creating-transactions) section below. ```py -from pathlib import Path -from multiversx_sdk import Account, Address, DevnetEntrypoint +from multiversx_sdk import Account, DevnetEntrypoint, UserSecretKey + +secret_key_hex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9" +secret_key = UserSecretKey(bytes.fromhex(secret_key_hex)) + +account = Account(secret_key) -# create the entrypoint and the delegation controller entrypoint = DevnetEntrypoint() -controller = entrypoint.create_delegation_controller() +account.nonce = entrypoint.recall_account_nonce(account.address) -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +# create any sort of transaction +... -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +# When needed, we can get the nonce and increment it +nonce = account.get_nonce_then_increment() +``` -# delegation contract -contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva") +### Saving the Account to a file -transaction = controller.create_transaction_for_claiming_rewards( - sender=alice, - nonce=alice.get_nonce_then_increment(), - delegation_contract=contract -) +We can save the account to either a `pem` file or a `keystore` file. **We discourage the use of PEM wallets for storing cryptocurrencies due to their lower security level.** However, they prove to be highly convenient and user-friendly for application testing purposes. -tx_hash = entrypoint.send_transaction(transaction) +#### Saving the Account for a PEM file + +```py +from pathlib import Path +from multiversx_sdk import Account, UserSecretKey + +secret_key_hex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9" +secret_key = UserSecretKey(bytes.fromhex(secret_key_hex)) + +account = Account(secret_key) +account.save_to_pem(path=Path("wallet.pem")) ``` -#### Claiming rewards using the factory +#### Saving the Account to a Keystore file ```py from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint - -# create the entrypoint and the delegation transactions factory -entrypoint = DevnetEntrypoint() -factory = entrypoint.create_delegation_transactions_factory() +from multiversx_sdk import Account, UserSecretKey -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +secret_key_hex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9" +secret_key = UserSecretKey(bytes.fromhex(secret_key_hex)) -transaction = factory.create_transaction_for_claiming_rewards( - sender=alice.address, - delegation_contract=contract -) +account = Account(secret_key) +account.save_to_keystore(path=Path("keystoreWallet.json"), password="password") +``` -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +### Ledger Account -# set the nonce -transaction.nonce = alice.get_nonce_then_increment() +It is possible to use a Ledger Device to manage your account. The Ledger account allows you to sign both transactions and messages, but it can also store the nonce of the account. -# sign the transaction -transaction.signature = alice.sign_transaction(transaction) +By default, the package does not include all the dependencies required to communicate with a Ledger device. To enable Ledger support, install the package with the following command: -# send the transaction -tx_hash = entrypoint.send_transaction(transaction) +```sh +pip install multiversx-sdk[ledger] ``` -#### Undelegating funds using the controller +This will install the necessary dependencies for interacting with a Ledger device. -By undelegating we let the contract know we want to get back our staked funds. This operation has a 10 epochs unbonding period. +When instantiating a `LedgerAccount`, the index of the address that will be used should be provided. By default, the index `0` is used. ```py -from pathlib import Path -from multiversx_sdk import Account, Address, DevnetEntrypoint +from multiversx_sdk import LedgerAccount -# create the entrypoint and the delegation controller -entrypoint = DevnetEntrypoint() -controller = entrypoint.create_delegation_controller() +account = LedgerAccount() +``` -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +When signing transactions with a Ledger device, the transaction details will appear on the device, awaiting your confirmation. The same process applies when signing messages. -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +Both **Account** and **LedgerAccount** are compatible with the **IAccount** interface and can be used wherever the interface is expected (e.g. in transaction controllers). -# delegation contract -contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva") +## Calling the Faucet -transaction = controller.create_transaction_for_undelegating( - sender=alice, - nonce=alice.get_nonce_then_increment(), - delegation_contract=contract, - amount=1000000000000000000000 # 1000 EGLD -) +This functionality is not yet available through the entrypoint, but we recommend using the faucet available within the Web Wallet. -tx_hash = entrypoint.send_transaction(transaction) -``` +- [Testnet Wallet](https://testnet-wallet.multiversx.com/) +- [Devnet Wallet](https://devnet-wallet.multiversx.com/) -#### Undelegating funds using the factory +## Interacting with the network + +The entrypoint exposes a few methods to directly interact with the network, such as: + +- `recall_account_nonce(address: Address) -> int;` +- `send_transaction(transaction: Transaction) -> bytes;` +- `send_transactions(transactions: list[Transaction]) -> tuple[int, list[bytes]];` +- `get_transaction(tx_hash: str | bytes) -> TransactionOnNetwork;` +- `await_transaction_completed(tx_hash: str | bytes) -> TransactionOnNetwork;` + +Some other methods are exposed through a so called network provider. There are two types of network providers: ApiNetworkProvider and ProxyNetworkProvider. The ProxyNetworkProvider interacts directly with the proxy of an observing squad. The ApiNetworkProvider, as the name suggests, interacts with the API, that is a layer over the proxy. It fetches data from the network but also from Elastic Search. + +To get the underlying network provider from our entrypoint, we can do as follows: ```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint +from multiversx_sdk import DevnetEntrypoint -# create the entrypoint and the delegation transactions factory entrypoint = DevnetEntrypoint() -factory = entrypoint.create_delegation_transactions_factory() +api = entrypoint.create_network_provider() +``` -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +## Creating a network provider -transaction = factory.create_transaction_for_undelegating( - sender=alice.address, - delegation_contract=contract, - amount=1000000000000000000000 # 1000 EGLD +Additionally, when manually instantiating a network provider, a config can be provided to specify the client name and set custom request options. + +```py +from multiversx_sdk import NetworkProviderConfig, ApiNetworkProvider + +config = NetworkProviderConfig( + client_name="hello-multiversx", + requests_options={ + "timeout": 1, + "auth": ("user", "password") + } ) -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +api = ApiNetworkProvider(url="https://devnet-api.multiversx.com", config=config) +``` -# set the nonce -transaction.nonce = alice.get_nonce_then_increment() +The network providers support a retry mechanism for failing requests. If you'd like to change the default values you can do so as follows: -# sign the transaction -transaction.signature = alice.sign_transaction(transaction) +```py +from multiversx_sdk import NetworkProviderConfig, ApiNetworkProvider, RequestsRetryOptions -# send the transaction -tx_hash = entrypoint.send_transaction(transaction) -``` +retry_options = RequestsRetryOptions( + retries=5, + backoff_factor=0.1, + status_forcelist=[500, 502, 503] +) -#### Withdrawing funds using the controller +config = NetworkProviderConfig( + client_name="hello-multiversx", + requests_options={ + "timeout": 1, + "auth": ("user", "password") + }, + requests_retry_options=retry_options, +) -After the unbonding period has passed, we can withdraw our funds from the contract. +api = ApiNetworkProvider(url="https://devnet-api.multiversx.com", config=config) +``` -```py -from pathlib import Path -from multiversx_sdk import Account, Address, DevnetEntrypoint +A list of all the available methods from the `ApiNetworkProviders` can be found [here](https://multiversx.github.io/mx-sdk-py/multiversx_sdk.network_providers.html#module-multiversx_sdk.network_providers.api_network_provider). -# create the entrypoint and the delegation controller -entrypoint = DevnetEntrypoint() -controller = entrypoint.create_delegation_controller() +Both the `ApiNetworkProvider` and the `ProxyNetworkProvider` implement a common interface, that can be seen [here](https://multiversx.github.io/mx-sdk-py/multiversx_sdk.network_providers.html#multiversx_sdk.network_providers.interface.INetworkProvider). Therefore, the two network providers can be used interchangeably. -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +The classes returned by the API have the most used fields easily accessible, but each object has a `raw` field where the raw API response is stored in case some other fields are needed. -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +## Fetching data from the network -# delegation contract -contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva") +### Fetching the network config -transaction = controller.create_transaction_for_withdrawing( - sender=alice, - nonce=alice.get_nonce_then_increment(), - delegation_contract=contract -) +```py +from multiversx_sdk import DevnetEntrypoint -tx_hash = entrypoint.send_transaction(transaction) +entrypoint = DevnetEntrypoint() +api = entrypoint.create_network_provider() + +network_config = api.get_network_config() ``` -#### Withdrawing funds using the factory +### Fetching the network status + +The status is fetched by default from the metachain, but a specific shard number can be provided. ```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint +from multiversx_sdk import DevnetEntrypoint -# create the entrypoint and the delegation transactions factory entrypoint = DevnetEntrypoint() -factory = entrypoint.create_delegation_transactions_factory() +api = entrypoint.create_network_provider() -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +network_status = api.get_network_status() # fetches status from metachain +network_status = api.get_network_status(shard=1) # fetches status from shard 1 +``` -transaction = factory.create_transaction_for_withdrawing( - sender=alice.address, - delegation_contract=contract -) +### Fetching a block from the network -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +We instantiate the args and we are going to fetch the block using it's hash. The `API` only supports fetching blocks by hash, while the `PROXY` can fetch blocks by hash or by nonce. Keep in mind, that for the `PROXY` the shard should also be specified in the arguments. -# set the nonce -transaction.nonce = alice.get_nonce_then_increment() +#### Fetching a block using the API -# sign the transaction -transaction.signature = alice.sign_transaction(transaction) +```py +from multiversx_sdk import ApiNetworkProvider -# send the transaction -tx_hash = entrypoint.send_transaction(transaction) +api = ApiNetworkProvider("https://devnet-api.multiversx.com") + +block_hash="1147e111ce8dd860ae43a0f0d403da193a940bfd30b7d7f600701dd5e02f347a" +block = api.get_block(block_hash=block_hash) ``` -### Relayed transactions +Additionally, we can fetch the latest block from the network: -We are currently on the third iteration (V3) of relayed transactions. V1 and V2 will be deactivated soon, so we'll focus on V3. +```py +from multiversx_sdk import ApiNetworkProvider -For V3, two new fields have been added on transactions: `relayer` and `relayerSignature`. +api = ApiNetworkProvider("https://devnet-api.multiversx.com") +latest_block = api.get_latest_block() +``` -Note that: -1. the sender and the relayer can sign the transaction in any order. -2. before any of the sender or relayer can sign the transaction, the `relayer` field must be set. -3. relayed transactions require an additional `50,000` of gas. -4. the sender and the relayer must be in the same network shard. +#### Fetching a block using the PROXY -Let’s see how to create a relayed transaction: +When using the proxy, we have to provide the shard, as well. ```py -from pathlib import Path -from multiversx_sdk import Account, Address, DevnetEntrypoint, Transaction +from multiversx_sdk import ProxyNetworkProvider -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -bob = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") +proxy = ProxyNetworkProvider("https://devnet-gateway.multiversx.com") -# carol will be our relayer, that means she is paying the gas for the transaction -carol = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/carol.pem")) +block_hash="1147e111ce8dd860ae43a0f0d403da193a940bfd30b7d7f600701dd5e02f347a" +block = proxy.get_block(shard=1, block_hash=block_hash) +``` -# fetch the sender's nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +We can also fetch the latest block from the network. The default shard will be the metachain, but we can specify a shard to fetch the latest block from. -# create the transaction -transaction = Transaction( - sender=alice.address, - receiver=bob, - gas_limit=110_000, - chain_id="D", - nonce=alice.get_nonce_then_increment(), - relayer=carol.address, - data="hello".encode() -) +```py +from multiversx_sdk import ProxyNetworkProvider -# sender signs the transaction -transaction.signature = alice.sign_transaction(transaction) +proxy = ProxyNetworkProvider("https://devnet-gateway.multiversx.com") +block = proxy.get_latest_block() +``` -# relayer signs the transaction -transaction.relayer_signature = carol.sign_transaction(transaction) +### Fetching an account + +To fetch an account we'll need its address. Once we have the address, we simply create an `Address` object and pass it as an argument to the method. + +```py +from multiversx_sdk import Address, DevnetEntrypoint -# broadcast the transaction entrypoint = DevnetEntrypoint() -tx_hash = entrypoint.send_transaction(transaction) +api = entrypoint.create_network_provider() + +alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +account = api.get_account(address=alice) ``` -#### Creating relayed transactions using controllers +### Fetching an account's storage -We can create relayed transactions using any of the controllers. Each controller has a `relayer` argument, that can be set if we want to create a relayed transaction. Let's issue a fungible token creating a relayed transaction: +We can also fetch an account's storage, which means we can get all the key-value pairs saved for an account. ```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint +from multiversx_sdk import Address, DevnetEntrypoint -# create the entrypoint and the token management controller entrypoint = DevnetEntrypoint() -controller = entrypoint.create_token_management_controller() - -# create the issuer of the token -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +api = entrypoint.create_network_provider() -# carol will be our relayer, that means she is paying the gas for the transaction -carol = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/carol.pem")) +address = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +account = api.get_account_storage(address=address) +``` -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +If we only want a specific key, we can fetch it as follows: -transaction = controller.create_transaction_for_issuing_fungible( - sender=alice, - nonce=alice.get_nonce_then_increment(), - token_name="NEWTOKEN", - token_ticker="TKN", - initial_supply=1_000_000_000000, # 1 million tokens, with 6 decimals - num_decimals=6, - can_freeze=False, - can_wipe=True, - can_pause=False, - can_change_owner=True, - can_upgrade=True, - can_add_special_roles=True, - relayer=carol.address -) +```py +from multiversx_sdk import Address, DevnetEntrypoint -# relayer also signs the transaction -transaction.relayer_signature = carol.sign_transaction(transaction) +entrypoint = DevnetEntrypoint() +api = entrypoint.create_network_provider() -# sending the transaction -tx_hash = entrypoint.send_transaction(transaction) +address = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +account = api.get_account_storage_entry(address=address, entry_key="testKey") ``` -#### Create relayed transactions using factories +### Waiting for an account to meet a condition -The transactions factories do not have a `relayer` argument, the relayer needs to be set after creating the transaction. This is good because the transaction is not signed by the sender when created. Let's issue a fungible token using the `TokenManagementTransactionsFactory`: +There are times when we need to wait for a specific condition to be met before proceeding with an action. For example, let's say we want to send 7 EGLD from Alice to Bob, but this can only happen once Alice's balance reaches at least 7 EGLD. This approach is useful in scenarios where you are waiting for external funds to be sent to Alice, allowing her to then transfer the required amount to another recipient. + +We need to define our condition that will be checked each time the account is fetched from the network. For this, we create a function that takes as an argument an `AccountOnNetwork` object and returns a `bool`. + +Keep in mind that, this method has a default timeout that can be adjusted using the `AwaitingOptions` class. ```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint +from multiversx_sdk import Address, AccountOnNetwork, DevnetEntrypoint -# create the entrypoint and the token management transactions factory entrypoint = DevnetEntrypoint() -factory = entrypoint.create_token_management_transactions_factory() +api = entrypoint.create_network_provider() -# create the issuer of the token -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -# carol will be our relayer, that means she is paying the gas for the transaction -carol = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/carol.pem")) +def condition_to_be_satisfied(account: AccountOnNetwork) -> bool: + return account.balance >= 7000000000000000000 # 7 EGLD -transaction = factory.create_transaction_for_issuing_fungible( - sender=alice.address, - token_name="NEWTOKEN", - token_ticker="TKN", - initial_supply=1_000_000_000000, # 1 million tokens, with 6 decimals - num_decimals=6, - can_freeze=False, - can_wipe=True, - can_pause=False, - can_change_owner=True, - can_upgrade=True, - can_add_special_roles=True -) - -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) - -# set the nonce of the sender -transaction.nonce = alice.get_nonce_then_increment() -# set the relayer -transaction.relayer = carol.address - -# sender signs the transaction -transaction.signature = alice.sign_transaction(transaction) - -# relayer signs the transaction -transaction.relayer_signature = carol.sign_transaction(transaction) - -# broadcast the transaction -tx_hash = entrypoint.send_transaction(transaction) +alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +account = api.await_account_on_condition(address=alice, condition=condition_to_be_satisfied) ``` -### Guarded Transactions +### Sending and simulating transactions -#### Creating guarded transactions using controllers +In order for our transactions to be executed, we use the network providers to broadcast them to the network. Keep in mind that, in order for transactions to be processed they need to be signed. -Very similar to relayers, we have a field `guardian` and a field `guardianSignature`. Each controller has an argument for the guardian. The transaction can be sent to a service that signs it using the guardian's account or we can use another account as a guardian. Let's issue a token using a guarded account. +#### Sending a transaction ```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint +from multiversx_sdk import Address, DevnetEntrypoint, Transaction -# create the entrypoint and the token management controller entrypoint = DevnetEntrypoint() -controller = entrypoint.create_token_management_controller() - -# create the issuer of the token -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) - -# carol is the guardian -carol = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/carol.pem")) +api = entrypoint.create_network_provider() -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +bob = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") -transaction = controller.create_transaction_for_issuing_fungible( +transaction = Transaction( sender=alice, - nonce=alice.get_nonce_then_increment(), - token_name="NEWTOKEN", - token_ticker="TKN", - initial_supply=1_000_000_000000, # 1 million tokens, with 6 decimals - num_decimals=6, - can_freeze=False, - can_wipe=True, - can_pause=False, - can_change_owner=True, - can_upgrade=True, - can_add_special_roles=True, - guardian=carol.address + receiver=bob, + gas_limit=50000, + chain_id="D" ) -# guardian also signs the transaction -transaction.guardian_signature = carol.sign_transaction(transaction) +# set correct nonce and sign the transaction +... -# sending the transaction -tx_hash = entrypoint.send_transaction(transaction) +# broadcast the transaction to the network +transaction_hash = api.send_transaction(transaction) ``` -#### Creating guarded transactions using factories - -The transactions factories do not have a `guardian` argument, the guardian needs to be set after creating the transaction. This is good because the transaction is not signed by the sender when created. Let's issue a fungible token using the `TokenManagementTransactionsFactory`: +#### Sending multiple transactions ```py -from pathlib import Path -from multiversx_sdk import Account, DevnetEntrypoint +from multiversx_sdk import Address, DevnetEntrypoint, Transaction -# create the entrypoint and the token management transactions factory entrypoint = DevnetEntrypoint() -factory = entrypoint.create_token_management_transactions_factory() +api = entrypoint.create_network_provider() -# create the issuer of the token -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +bob = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") -# carol is the guardian -carol = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/carol.pem")) +first_transaction = Transaction( + sender=alice, + receiver=bob, + gas_limit=50000, + chain_id="D", + nonce=2 +) +# set correct nonce and sign the transaction +... -transaction = factory.create_transaction_for_issuing_fungible( - sender=alice.address, - token_name="NEWTOKEN", - token_ticker="TKN", - initial_supply=1_000_000_000000, # 1 million tokens, with 6 decimals - num_decimals=6, - can_freeze=False, - can_wipe=True, - can_pause=False, - can_change_owner=True, - can_upgrade=True, - can_add_special_roles=True +second_transaction = Transaction( + sender=bob, + receiver=alice, + gas_limit=50000, + chain_id="D", + nonce=1 ) +# set correct nonce and sign the transaction +... -# fetch the nonce of the network -alice.nonce = entrypoint.recall_account_nonce(alice.address) +third_transaction = Transaction( + sender=alice, + receiver=alice, + gas_limit=60000, + chain_id="D", + nonce=3, + data=b"hello" +) +# set correct nonce and sign the transaction +... -# set the nonce of the sender -transaction.nonce = alice.get_nonce_then_increment() +# broadcast the transactions to the network +num_of_txs, hashes = api.send_transactions([first_transaction, second_transaction, third_transaction]) +``` -# set the guardian -transaction.guardian = carol.address +#### Simulating transactions -# sender signs the transaction -transaction.signature = alice.sign_transaction(transaction) +A transaction can be simulated before being sent to be processed by the network. It is mostly used for smart contract calls to see what smart contract results are produced. -# guardian signs the transaction -transaction.guardian_signature = carol.sign_transaction(transaction) +```py +from multiversx_sdk import Address, DevnetEntrypoint, Transaction -# broadcast the transaction -tx_hash = entrypoint.send_transaction(transaction) -``` +entrypoint = DevnetEntrypoint() +api = entrypoint.create_network_provider() -We can also create guarded relayed transactions the same way we did before. Keep in mind that, only the sender can be guarded, the relayer cannot. The same flow can be used. Using controllers, we set both `guardian` and `relayer` fields and then the transaction should be signed by both. Using a factory, we create the transaction, set both both fields and then sign the transaction using the sender's account, then the the guardian and the relayer sign the transaction. +alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgqccmyzj9sade2495w78h42erfrw7qmqxpd8sss6gmgn") -### Multisig +transaction = Transaction( + sender=alice, + receiver=contract, + gas_limit=5000000, + chain_id="D", + nonce=entrypoint.recall_account_nonce(alice), # nonce needs to be properly set + data=b"add@07", + signature=b'0' * 64, # signature is not checked by default, but a dummy value must be provided +) +transaction_on_network = api.simulate_transaction(transaction) +``` -The sdk contains components to interact with the [Multisig Contract](https://github.com/multiversx/mx-contracts-rs/releases/tag/v0.45.5). We can deploy a multisig smart contract, add members, propose and execute actions and query the contract. The same as the other components, to interact with a multisig smart contract we can use either the `MultisigController` or the `MultisigTransactionsFactory`. +#### Estimating the gas cost of a transaction -#### Deploying a Multisig Smart Contract using the controller +Before sending a transaction to the network to be processed, one can get the estimated gas limit that is required for the transaction to be executed. ```py -from pathlib import Path -from multiversx_sdk import Account, Address, ApiNetworkProvider, MultisigController -from multiversx_sdk.abi import Abi +from multiversx_sdk import Address, DevnetEntrypoint, Transaction +entrypoint = DevnetEntrypoint() +api = entrypoint.create_network_provider() -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -api = ApiNetworkProvider(url="https://devnet-api.multiversx.com") -alice.nonce = api.get_account(alice.address).nonce +alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgqccmyzj9sade2495w78h42erfrw7qmqxpd8sss6gmgn") -abi = Abi.load(Path("../multiversx_sdk/testutils/testdata/multisig-full.abi.json")) -controller = MultisigController(chain_id="D", network_provider=api, abi=abi) +nonce = entrypoint.recall_account_nonce(alice) -transaction = controller.create_transaction_for_deploy( +transaction = Transaction( sender=alice, - nonce=alice.get_nonce_then_increment(), - bytecode=Path("../multiversx_sdk/testutils/testdata/multisig-full.wasm"), - quorum=2, - board=[alice.address, Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx")], - gas_limit=100_000_000, + receiver=contract, + gas_limit=5000000, + chain_id="D", + data=b"add@07", + nonce=nonce ) -# send the transaction -tx_hash = api.send_transaction(transaction) +transaction_cost_response = api.estimate_transaction_cost(transaction) ``` -#### Deploying a Multisig Smart Contract using the factory - -```py -from pathlib import Path -from multiversx_sdk import Account, Address, ApiNetworkProvider, MultisigTransactionsFactory, TransactionsFactoryConfig -from multiversx_sdk.abi import Abi - +### Waiting for transaction completion -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -api = ApiNetworkProvider(url="https://devnet-api.multiversx.com") -alice.nonce = api.get_account(alice.address).nonce +After sending a transaction, we may want to wait until the transaction is processed in order to proceed with another action. Keep in mind that, this method has a default timeout that can be adjusted using the `AwaitingOptions` class. -abi = Abi.load(Path("../multiversx_sdk/testutils/testdata/multisig-full.abi.json")) -config = TransactionsFactoryConfig(chain_id="D") -factory = MultisigTransactionsFactory(config=config, abi=abi) +```py +from multiversx_sdk import DevnetEntrypoint -transaction = factory.create_transaction_for_deploy( - sender=alice.address, - bytecode=Path("../multiversx_sdk/testutils/testdata/multisig-full.wasm"), - quorum=2, - board=[alice.address, Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx")], - gas_limit=100_000_000, -) -transaction.nonce = alice.get_nonce_then_increment() -transaction.signature = alice.sign_transaction(transaction) +entrypoint = DevnetEntrypoint() +api = entrypoint.create_network_provider() -tx_hash = api.send_transaction(transaction) +tx_hash = "exampletransactionhash" +transaction_on_network = api.await_transaction_completed(transaction_hash=tx_hash) ``` -#### Propose an action using the controller +### Waiting for a transaction to specify a condition -We'll propose an action to send some EGLD to Carol. After we sent the proposal, we'll also parse the outcome of the transaction to get the `proposal id`. The id can be later for signing and performing the proposal. +Similar to accounts, we can wait until a transaction satisfies a specific condition. ```py -from pathlib import Path -from multiversx_sdk import Account, Address, ApiNetworkProvider, MultisigController -from multiversx_sdk.abi import Abi - +from multiversx_sdk import DevnetEntrypoint, TransactionOnNetwork -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -api = ApiNetworkProvider(url="https://devnet-api.multiversx.com") -alice.nonce = api.get_account(alice.address).nonce +entrypoint = DevnetEntrypoint() +api = entrypoint.create_network_provider() -abi = Abi.load(Path("../multiversx_sdk/testutils/testdata/multisig-full.abi.json")) -controller = MultisigController(chain_id="D", network_provider=api, abi=abi) -transaction = controller.create_transaction_for_propose_transfer_execute( - sender=alice, - nonce=alice.get_nonce_then_increment(), - contract=Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq2ukrsg73nwgu3uz6sp8vequuyrhtv2akd8ssyrg7wj"), - receiver=Address.new_from_bech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"), - gas_limit=10_000_000, - native_token_amount=1_000000000000000000, # 1 EGLD -) +def condition_to_be_satisfied(transaction_on_network: TransactionOnNetwork) -> bool: + # can be the creation of an event or something else + ... -# send the transaction -tx_hash = api.send_transaction(transaction) -# parse the outcome and get the proposal id -action_id = controller.await_completed_execute_propose_any(tx_hash) +tx_hash = "exampletransactionhash" +transaction_on_network = api.await_transaction_on_condition(transaction_hash=tx_hash, condition=condition_to_be_satisfied) ``` -#### Propose an action using the factory +### Fetching transactions from the network -Proposing an action for a multisig contract using the `MultisigFactory` is very similar to using the controller, but in order to get the proposal id we need to use `MultisigTransactionsOutcomeParser`. +After sending transactions, we can fetch the transactions from the network. To do so, we need the transaction hash that we got after broadcasting the transaction. ```py -from pathlib import Path -from multiversx_sdk import Account, Address, ApiNetworkProvider, MultisigTransactionsFactory, TransactionsFactoryConfig, MultisigTransactionsOutcomeParser, TransactionAwaiter -from multiversx_sdk.abi import Abi +from multiversx_sdk import DevnetEntrypoint +entrypoint = DevnetEntrypoint() +api = entrypoint.create_network_provider() -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -api = ApiNetworkProvider(url="https://devnet-api.multiversx.com") -alice.nonce = api.get_account(alice.address).nonce +tx_hash = "exampletransactionhash" +transaction_on_network = api.get_transaction(tx_hash) +``` -abi = Abi.load(Path("../multiversx_sdk/testutils/testdata/multisig-full.abi.json")) -config = TransactionsFactoryConfig(chain_id="D") -factory = MultisigTransactionsFactory(config=config, abi=abi) +### Fetching a token from an account -transaction = factory.create_transaction_for_propose_transfer_execute( - sender=alice.address, - contract=Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq2ukrsg73nwgu3uz6sp8vequuyrhtv2akd8ssyrg7wj"), - receiver=Address.new_from_bech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"), - gas_limit=10_000_000, - native_token_amount=1_000000000000000000, # 1 EGLD -) -transaction.nonce = alice.get_nonce_then_increment() -transaction.signature = alice.sign_transaction(transaction) +We can fetch a specific token (ESDT, MetaESDT, SFT, NFT) of an account by providing the address and the token. -tx_hash = api.send_transaction(transaction) +```py +from multiversx_sdk import Address, DevnetEntrypoint, Token -# wait for the transaction to execute -transaction_awaiter = TransactionAwaiter(fetcher=api) -transaction_awaiter.await_completed(tx_hash) +entrypoint = DevnetEntrypoint() +api = entrypoint.create_network_provider() -# parse the outcome of the transaction -parser = MultisigTransactionsOutcomeParser(abi=abi) -transaction_on_network = api.get_transaction(tx_hash) -action_id = parser.parse_propose_action(transaction_on_network) +alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") + +# these tokens are just for the example, they do not belong to Alice +token = Token(identifier="TEST-123456") # ESDT +token_on_network = api.get_token_of_account(address=alice, token=token) + +token = Token(identifier="NFT-987654", nonce=11) # NFT +token_on_network = api.get_token_of_account(address=alice, token=token) ``` -#### Querying the Multisig Smart Contract +### Fetching all fungible tokens of an account -Unlike creating transactions, querying the multisig can be performed only using the controller. Let's query the contract to get all board members. +Fetches all fungible tokens held by an account. This method does not handle pagination, that can be achieved by using `do_get_generic`. ```py -from pathlib import Path -from multiversx_sdk import Address, ApiNetworkProvider, MultisigController -from multiversx_sdk.abi import Abi +from multiversx_sdk import Address, DevnetEntrypoint -abi = Abi.load(Path("../multiversx_sdk/testutils/testdata/multisig-full.abi.json")) -api = ApiNetworkProvider(url="https://devnet-api.multiversx.com") -controller = MultisigController(chain_id="D", network_provider=api, abi=abi) +entrypoint = DevnetEntrypoint() +api = entrypoint.create_network_provider() -contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq2ukrsg73nwgu3uz6sp8vequuyrhtv2akd8ssyrg7wj") -board_members = controller.get_all_board_members(contract) +alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +fungible_tokens = api.get_fungible_tokens_of_account(address=alice) ``` -### Governance - -We can create transactions for creating a new governance proposal, vote for a proposal or query the governance contract. +### Fetching all non-fungible tokens of an account -#### Creating a new proposal using the controller +Fetches all non-fungible tokens held by an account. This method does not handle pagination, but can be achieved by using `do_get_generic`. ```py -from multiversx_sdk import Account, ProxyNetworkProvider, GovernanceController - -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -proxy = ProxyNetworkProvider("https://devnet-gateway.multiversx.com") -alice.nonce = proxy.get_account(alice.address).nonce - -controller = GovernanceController(chain_id="D", network_provider=proxy) -commit_hash = "1db734c0315f9ec422b88f679ccfe3e0197b9d67" - -transaction = controller.create_transaction_for_new_proposal( - sender=alice, - nonce=alice.get_nonce_then_increment(), - commit_hash=commit_hash, - start_vote_epoch=10, - end_vote_epoch=15, - native_token_amount=500_000000000000000000, -) +from multiversx_sdk import Address, DevnetEntrypoint -# send the transaction -tx_hash = proxy.send_transaction(transaction) +entrypoint = DevnetEntrypoint() +api = entrypoint.create_network_provider() -# get proposal outcome -[proposal] = controller.await_completed_new_proposal(tx_hash) -print(proposal.proposal_nonce) -print(proposal.commit_hash) -print(proposal.start_vote_epoch) -print(proposal.end_vote_epoch) +alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +nfts = api.get_non_fungible_tokens_of_account(address=alice) ``` -#### Creating a new proposal using the factory +### Fetching token metadata + +If we want to fetch the metadata of a token, like `owner`, `decimals` and so on, we can use the following methods: ```py -from multiversx_sdk import (Account, ProxyNetworkProvider, GovernanceTransactionsFactory, - GovernanceTransactionsOutcomeParser, TransactionsFactoryConfig, TransactionAwaiter) +from multiversx_sdk import DevnetEntrypoint -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -proxy = ProxyNetworkProvider("https://devnet-gateway.multiversx.com") -alice.nonce = proxy.get_account(alice.address).nonce +entrypoint = DevnetEntrypoint() +api = entrypoint.create_network_provider() -config = TransactionsFactoryConfig(chain_id="D") -factory = GovernanceTransactionsFactory(config) -commit_hash = "1db734c0315f9ec422b88f679ccfe3e0197b9d67" +# used for ESDT +fungible_token_definition = api.get_definition_of_fungible_token(token_identifier="TEST-123456") -transaction = factory.create_transaction_for_new_proposal( - sender=alice.address, - commit_hash=commit_hash, - start_vote_epoch=10, - end_vote_epoch=15, - native_token_amount=500_000000000000000000, -) -transaction.nonce = alice.get_nonce_then_increment() -transaction.signature = alice.sign_transaction(transaction) +# used for MetaESDT, SFT, NFT +non_fungible_token_definition = api.get_definition_of_tokens_collection( + collection_name="NFT-987654") +``` -# send the transaction -tx_hash = proxy.send_transaction(transaction) +### Querying Smart Contracts -# make sure the transaction is complete -awaiter = TransactionAwaiter(fetcher=proxy) -transaction_on_network = awaiter.await_completed(tx_hash) +Smart contract queries or view functions, are endpoints of a contract that only read data from the contract. To send a query to the observer nodes, we can proceed as follows: -# get proposal outcome -parser = GovernanceTransactionsOutcomeParser() -[proposal] = parser.parse_new_proposal(transaction_on_network=transaction_on_network) +```py +from multiversx_sdk import Address, DevnetEntrypoint, SmartContractQuery -print(proposal.proposal_nonce) -print(proposal.commit_hash) -print(proposal.start_vote_epoch) -print(proposal.end_vote_epoch) -``` +entrypoint = DevnetEntrypoint() +api = entrypoint.create_network_provider() -#### Vote for a proposal using the controller +query = SmartContractQuery( + contract=Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen"), + function="getSum", + arguments=[] +) +response = api.query_contract(query=query) +``` -```py -from multiversx_sdk import Account, ProxyNetworkProvider, GovernanceController, VoteType +### Custom Api/Proxy calls +The methods exposed by the `ApiNetworkProvider` or `ProxyNetworkProvider` are the most common and used ones. There might be times when custom API calls are needed. For that we have createad generic methods for both `GET` and `POST` requests. -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -proxy = ProxyNetworkProvider("https://devnet-gateway.multiversx.com") -alice.nonce = proxy.get_account(alice.address).nonce +Let's assume we want to get all the transactions that are sent by Alice where the `delegate` function was called. -controller = GovernanceController(chain_id="D", network_provider=proxy) +```py +from multiversx_sdk import Address, DevnetEntrypoint -transaction = controller.create_transaction_for_voting( - sender=alice, - nonce=alice.get_nonce_then_increment(), - proposal_nonce=1, - vote=VoteType.YES -) +entrypoint = DevnetEntrypoint() +api = entrypoint.create_network_provider() -# send the transaction -tx_hash = proxy.send_transaction(transaction) +alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +url_params = { + "sender": alice.to_bech32(), + "function": "delegate" +} -# get vote outcome -[vote] = controller.await_completed_vote(tx_hash) -print(vote.proposal_nonce) -print(vote.vote) -print(vote.total_stake) -print(vote.total_voting_power) +transactions = api.do_get_generic(url="transactions", url_parameters=url_params) ``` -#### Vote for a proposal using the factory +## Creating transactions -```py -from multiversx_sdk import (Account, ProxyNetworkProvider, GovernanceTransactionsFactory, - GovernanceTransactionsOutcomeParser, TransactionsFactoryConfig, - TransactionAwaiter, VoteType) +In this section, we'll learn how to create different types of transactions. For creating transactions, we can use `controllers` or `factories`. The `controllers` can be used for scripts or quick network interactions, while the `factories` provide a more granular and lower-level approach, usually needed for DApps. Usually, the `controllers` use the same parameters as the `factories` but also take an `Account` and the `nonce` of the sender as arguments. The `controllers` also hold some extra functionality, like waiting for transaction completion and parsing transactions. The same functionality can be obtained for transactions built using the `factories` as well, we'll see how in the sections below. In the following section we'll learn how to create transactions using both. -alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -proxy = ProxyNetworkProvider("https://devnet-gateway.multiversx.com") -alice.nonce = proxy.get_account(alice.address).nonce +### Instantiating controllers and factories -config = TransactionsFactoryConfig(chain_id="D") -factory = GovernanceTransactionsFactory(config) +There are two ways to create controllers and factories: the first one is to get them from the entrypoint and the second one is to manually create them. -transaction = factory.create_transaction_for_voting( - sender=alice.address, - proposal_nonce=1, - vote=VoteType.YES, -) -transaction.nonce = alice.get_nonce_then_increment() -transaction.signature = alice.sign_transaction(transaction) +```py +from multiversx_sdk import DevnetEntrypoint, TransfersController, TransferTransactionsFactory, TransactionsFactoryConfig -# send the transaction -tx_hash = proxy.send_transaction(transaction) +entrypoint = DevnetEntrypoint() -# make sure the transaction is complete -awaiter = TransactionAwaiter(fetcher=proxy) -transaction_on_network = awaiter.await_completed(tx_hash) +# getting the controller and the factory from the entrypoint +transfers_controller = entrypoint.create_transfers_controller() +transfers_factory = entrypoint.create_transfers_transactions_factory() -# get vote outcome -parser = GovernanceTransactionsOutcomeParser() -[vote] = parser.parse_vote(transaction_on_network=transaction_on_network) +# manually instantiating the controller and the factory +controller = TransfersController(chain_id="D") -print(vote.proposal_nonce) -print(vote.vote) -print(vote.total_stake) -print(vote.total_voting_power) +config = TransactionsFactoryConfig(chain_id="D") +factory = TransferTransactionsFactory(config=config) ``` -#### Querying the governance contract +### Estimating the Gas Limit for a Transaction -Unlike creating transactions, querying the contract is only possible using the controller. Let's query the contract to get more details about a proposal. +When creating transaction factories or controllers, we can pass an additional argument, a **gas limit estimator**. This gas estimator simulates the transaction before being sent and computes the `gasLimit` that it will require. The `GasLimitEstimator` can be initialized with a multiplier, so that the estimated value will be multiplied by the specified value. It is recommended to use a small multiplier (e.g. 1.1) to cover any possible changes that may occur from the time the transaction is simulated to the time it is actually sent and processed on-chain. The gas limit estimator can be provided to any factory or controller available. Let's see how we can create a `GasLimitEstimator` and use it. ```py -from multiversx_sdk import ProxyNetworkProvider, GovernanceController +from multiversx_sdk import ApiNetworkProvider, GasLimitEstimator, TransferTransactionsFactory, TransactionsFactoryConfig +api = ApiNetworkProvider("https://devnet-api.multiversx.com") +gas_estimator = GasLimitEstimator(network_provider=api) # create a gas limit estimator with default multiplier of 1.0 +gas_estimator = GasLimitEstimator(network_provider=api, gas_multiplier=1.1) # create a gas limit estimator with a multiplier of 1.1 -proxy = ProxyNetworkProvider("https://devnet-gateway.multiversx.com") -controller = GovernanceController(chain_id="D", network_provider=proxy) - -proposal_info = controller.get_proposal(proposal_nonce=1) +config = TransactionsFactoryConfig(chain_id="D") +transfers_factory = TransferTransactionsFactory(config=config, gas_limit_estimator=gas_estimator) ``` -## Addresses - -Create an `Address` object from a _bech32-encoded_ string: +Also, factories or controllers created through the entrypoints can use the `GasLimitEstimator` as well: ```py -from multiversx_sdk import Address - -address = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") - -print("Address (bech32-encoded)", address.to_bech32()) -print("Public key (hex-encoded):", address.to_hex()) -print("Public key (hex-encoded):", address.get_public_key().hex()) -``` - -Create an address from a _hex-encoded_ string - note that you have to provide the address prefix, also known as the **HRP** (_human-readable part_ of the address). If not provided, the default one will be used. +from multiversx_sdk import DevnetEntrypoint -```py -from multiversx_sdk import Address +entrypoint = DevnetEntrypoint(with_gas_limit_estimator=True, gas_limit_multiplier=1.1) -address = Address.new_from_hex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1", "erd") +transfers_controller = entrypoint.create_transfers_controller() # will create the controller using the GasLimitEstimator with a 1.1 multiplier +transfers_factory = entrypoint.create_transfers_transactions_factory() # will create the factory using the GasLimitEstimator with a 1.1 multiplier ``` -Create an address from a raw public key: +### Token transfers -```py -from multiversx_sdk import Address +We can send native tokens (EGLD) and ESDT tokens using both the `controller` and the `factory`. -pubkey = bytes.fromhex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") -address = Address(pubkey, "erd") -``` +#### Native token transfers using the controller -Alternatively, you can use an `AddressFactory` (initialized with a specific **HRP**) to create addresses. If the hrp is not provided, the default one will be used. +Because we'll use an `Account`, the transaction will be signed. ```py -from multiversx_sdk import AddressFactory - -factory = AddressFactory("erd") - -address = factory.create_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -address = factory.create_from_hex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") -address = factory.create_from_public_key(bytes.fromhex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1")) -``` +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint -### Getting the shard of an address +entrypoint = DevnetEntrypoint() -```py -from multiversx_sdk import Address, AddressComputer +account = Account.new_from_keystore( + file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), + password="password", + address_index=0 +) +# the developer is responsible for managing the nonce +account.nonce = entrypoint.recall_account_nonce(account.address) -address_computer = AddressComputer() -address = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +transfers_controller = entrypoint.create_transfers_controller() +transaction = transfers_controller.create_transaction_for_transfer( + sender=account, + nonce=account.get_nonce_then_increment(), + receiver=Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), + native_transfer_amount=1000000000000000000, # 1 EGLD +) -print("Shard:", address_computer.get_shard_of_address(address)) +tx_hash = entrypoint.send_transaction(transaction) ``` -### Checking if the address is a smart contract address - -```py -from multiversx_sdk import Address - -address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgquzmh78klkqwt0p4rjys0qtp3la07gz4d396qn50nnm") -print("Is contract address:", address.is_smart_contract()) -``` +If you know you'll only send native tokens, the same transaction can be created using the `create_transaction_for_native_token_transfer` method. -### Changing the default hrp +#### Native token transfers using the factory -We have a configuration class, called `LibraryConfig`, that only stores (for the moment) the **default hrp** of the addresses. The default value is `erd`. The hrp can be changed when instantiating an address, or it can be changed in the `LibraryConfig` class, and all the addresses created will have the newly set hrp. +Because we only use the address of the sender, the transactions are not going to be signed or have the nonce field set properly. This should be taken care after the transaction is created. ```py -from multiversx_sdk import Address -from multiversx_sdk import LibraryConfig - - -print(LibraryConfig.default_address_hrp) -address = Address.new_from_hex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") -print(address.to_bech32()) - -LibraryConfig.default_address_hrp = "test" -address = Address.new_from_hex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") -print(address.to_bech32()) - -# setting back the default value -LibraryConfig.default_address_hrp = "erd" -``` +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint -## Wallets +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_transfers_transactions_factory() -### Generating a mnemonic +alice = Account.new_from_keystore( + file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), + password="password", + address_index=0 +) +# the developer is responsible for managing the nonce +alice.nonce = entrypoint.recall_account_nonce(alice.address) -Mnemonic generation is based on [`trezor/python-mnemonic`](https://github.com/trezor/python-mnemonic) and can be achieved as follows: +bob = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") -```py -from multiversx_sdk import Mnemonic +transaction = factory.create_transaction_for_transfer( + sender=alice.address, + receiver=bob, + native_amount=1000000000000000000 # 1 EGLD +) +# set the sender's nonce +transaction.nonce = alice.get_nonce_then_increment() -mnemonic = Mnemonic.generate() -words = mnemonic.get_words() +# sign the transaction using the sender's account +transaction.signature = alice.sign_transaction(transaction) -print(words) +tx_hash = entrypoint.send_transaction(transaction) ``` -### Saving the mnemonic to a keystore file +If you know you'll only send native tokens, the same transaction can be created using the `create_transaction_for_native_token_transfer` method. -The mnemonic can be saved to a keystore file: +#### Custom token transfers using the controller ```py from pathlib import Path -from multiversx_sdk import Mnemonic, UserWallet - -mnemonic = Mnemonic.generate() +from multiversx_sdk import Account, DevnetEntrypoint, Token, TokenTransfer -# saves the mnemonic to a keystore file with kind=mnemonic -wallet = UserWallet.from_mnemonic(mnemonic.get_text(), "password") -wallet.save(Path("walletWithMnemonic.json")) -``` +entrypoint = DevnetEntrypoint() -#### Deriving secret keys from a mnemonic +alice = Account.new_from_keystore( + file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), + password="password", + address_index=0 +) +# the developer is responsible for managing the nonce +alice.nonce = entrypoint.recall_account_nonce(alice.address) -Given a mnemonic, we can derive keypairs: +esdt = Token(identifier="TEST-123456") +first_transfer = TokenTransfer(token=esdt, amount=1000000000) -```py -from multiversx_sdk import Mnemonic +nft = Token(identifier="NFT-987654", nonce=10) +second_transfer = TokenTransfer(token=nft, amount=1) # when sending NFTs we set the amount to `1` -mnemonic = Mnemonic.generate() +sft = Token(identifier="SFT-123987", nonce=10) +third_transfer = TokenTransfer(token=nft, amount=7) -secret_key = mnemonic.derive_key(0) -public_key = secret_key.generate_public_key() +transfers_controller = entrypoint.create_transfers_controller() +transaction = transfers_controller.create_transaction_for_transfer( + sender=alice, + nonce=alice.get_nonce_then_increment(), + receiver=Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), + token_transfers=[first_transfer, second_transfer, third_transfer] +) -print("Secret key:", secret_key.hex()) -print("Public key:", public_key.hex()) +tx_hash = entrypoint.send_transaction(transaction) ``` -#### Saving a secret key to a keystore file +If you know you'll only send ESDT tokens, the same transaction can be created using `create_transaction_for_esdt_token_transfer`. -The secret key can also be saved to a keystore file: +#### Custom token transafers using the factory + +Because we only use the address of the sender, the transactions are not going to be signed or have the nonce field set properly. This should be taken care after the transaction is created. ```py from pathlib import Path -from multiversx_sdk import Mnemonic, UserWallet -mnemonic = Mnemonic.generate() - -# by default, derives using the index = 0 -secret_key = mnemonic.derive_key() - -# saves the mnemonic to a keystore file with kind=secretKey -wallet = UserWallet.from_secret_key(secret_key, "password") -wallet.save(Path("walletWithSecretKey.json")) -``` +from multiversx_sdk import Account, DevnetEntrypoint, Token, TokenTransfer -#### Saving a secrey key to a PEM file +entrypoint = DevnetEntrypoint() -We can save a secret key to a pem file. **This is not recommended as it is not secure, but it's very convenient for testing purposes.** +alice = Account.new_from_keystore( + file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), + password="password", + address_index=0 +) +# the developer is responsible for managing the nonce +alice.nonce = entrypoint.recall_account_nonce(alice.address) -```py -from pathlib import Path -from multiversx_sdk import Address, UserPEM +bob = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") -mnemonic = Mnemonic.generate() +esdt = Token(identifier="TEST-123456") # fungible tokens don't have nonce +first_transfer = TokenTransfer(token=esdt, amount=1000000000) # we set the desired amount we want to send -# by default, derives using the index = 0 -secret_key = mnemonic.derive_key() +nft = Token(identifier="NFT-987654", nonce=10) +second_transfer = TokenTransfer(token=nft, amount=1) # when sending NFTs we set the amount to `1` -label = Address(public_key.buffer, "erd").to_bech32() -pem = UserPEM(label=label, secret_key=secret_key) -pem.save(Path("wallet.pem")) -``` +sft = Token(identifier="SFT-123987", nonce=10) +third_transfer = TokenTransfer(token=nft, amount=7) # for SFTs we set the desired amount we want to send -### Generating a KeyPair +factory = entrypoint.create_transfers_transactions_factory() +transaction = factory.create_transaction_for_transfer( + sender=alice.address, + receiver=bob, + token_transfers=[first_transfer, second_transfer, third_transfer] +) -A `KeyPair` is a wrapper over a secret key and a public key. We can create a keypair and use it for signing or verifying. +# set the sender's nonce +transaction.nonce = alice.get_nonce_then_increment() -```py -from multiversx_sdk import KeyPair +# sign the transaction using the sender's account +transaction.signature = alice.sign_transaction(transaction) -keypair = KeyPair.generate() +tx_hash = entrypoint.send_transaction(transaction) +``` -# get secret key -secret_key = keypair.get_secret_key() +If you know you'll only send ESDT tokens, the same transaction can be created using `create_transaction_for_esdt_token_transfer`. -# get public key -public_key = keypair.get_public_key() -``` +#### Sending native and custom tokens -### Loading a wallets from keystore mnemonic file +Also, sending both native and custom tokens is now supported. If a `native_amount` is provided together with `token_transfers`, the native token will also be included in the `MultiESDTNFTTrasfer` built-in function call. -Load a keystore that holds an _encrypted mnemonic_ (and perform wallet derivation at the same time): +We can send both types of tokens using either the `controller` or the `factory`, but we'll use the controller for the sake of simplicity. ```py from pathlib import Path -from multiversx_sdk import UserWallet - -# loads the mnemonic and derives the a secret key; default index = 0 -secret_key = UserWallet.load_secret_key(Path("walletWithMnemonic.json"), "password") -address = secret_key.generate_public_key().to_address("erd") +from multiversx_sdk import Account, DevnetEntrypoint, Token, TokenTransfer -print("Secret key:", secret_key.hex()) -print("Address:", address.to_bech32()) +entrypoint = DevnetEntrypoint() -# derive secret key with index = 7 -secret_key = UserWallet.load_secret_key( - path=Path("walletWithMnemonic.json"), +account = Account.new_from_keystore( + file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), password="password", - address_index=7 + address_index=0 ) -address = secret_key.generate_public_key().to_address() - -print("Secret key:", secret_key.hex()) -print("Address:", address.to_bech32()) -``` +# the developer is responsible for managing the nonce +account.nonce = entrypoint.recall_account_nonce(account.address) -#### Loading a wallet from a keystore secret key file +esdt = Token(identifier="TEST-123456") +first_transfer = TokenTransfer(token=esdt, amount=1000000000) -```py -from pathlib import Path -from multiversx_sdk import UserWallet +nft = Token(identifier="NFT-987654", nonce=10) +second_transfer = TokenTransfer(token=nft, amount=1) -secret_key = UserWallet.load_secret_key(Path("walletWithSecretKey.json"), "password") -address = secret_key.generate_public_key().to_address("erd") +transfers_controller = entrypoint.create_transfers_controller() +transaction = transfers_controller.create_transaction_for_transfer( + sender=account, + nonce=account.get_nonce_then_increment(), + receiver=Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), + native_transfer_amount=1000000000000000000, # 1 EGLD + token_transfers=[first_transfer, second_transfer] +) -print("Secret key:", secret_key.hex()) -print("Address:", address.to_bech32()) +tx_hash = entrypoint.send_transaction(transaction) ``` -#### Loading a wallet from a PEM file +### Decoding transaction data + +For example, when sending multiple ESDT and NFT tokens, the receiver field of the transaction is the same as the sender field and also the value is set to `0` because all the information is encoded in the `data` field of the transaction. + +For decoding the data field we have a so called `TransactionDecoder`. We fetch the transaction from the network and then use the decoder. ```py -from pathlib import Path -from multiversx_sdk import UserPEM +from multiversx_sdk import DevnetEntrypoint, TransactionDecoder -pem = UserPEM.from_file(Path("wallet.pem")) +entrypoint = DevnetEntrypoint() +transaction = entrypoint.get_transaction("3e7b39f33f37716186b6ffa8761d066f2139bff65a1075864f612ca05c05c05d") -print("Secret key:", pem.secret_key.hex()) -print("Public key:", pem.public_key.hex()) +decoder = TransactionDecoder() +decoded_transaction = decoder.get_transaction_metadata(transaction) + +print(decoded_transaction.to_dict()) ``` -## Signing objects +### Smart Contracts -The signing is performed using the **secret key** of an account. We have a few wrappers over the secret key, like [Account](#creating-accounts) that make siging easier. We'll first learn how we can sign using an `Account` and then we'll see how we can sign using the secret key. +#### Contract ABIs -#### Signing a Transaction using an Account +A contract's ABI describes the endpoints, data structure and events that a contract exposes. While contract interactions are possible without the ABI, they are easier to implement when the definitions are available. -We are going to assume we have an account at this point. If you don't fell free to check out the [creating an account section](#creating-accounts). +##### Loading the ABI from a file ```py from pathlib import Path -from multiversx_sdk import Account, Address, Transaction - -account = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) - -transaction = Transaction( - nonce=90, - sender=account.address, - receiver=Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), - value=1000000000000000000, - gas_limit=50000, - chain_id="D" -) - -# apply the signature on the transaction -transaction.signature = account.sign_transaction(transaction) +from multiversx_sdk.abi import Abi -print(transaction.to_dictionary()) +abi = Abi.load(Path("./contracts/adder.abi.json")) ``` -#### Signing a Transaction using a SecretKey +#### Manually construct the ABI + +If an ABI file isn't directly available, but you do have knowledge of the contract's endpoints and types, you can manually construct the ABI. ```py -from multiversx_sdk import Transaction, TransactionComputer, UserSecretKey +from multiversx_sdk.abi import Abi, AbiDefinition -secret_key_hex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9" -secret_key = UserSecretKey(bytes.fromhex(secret_key_hex)) -public_key = secret_key.generate_public_key() +abi_definition = AbiDefinition.from_dict({ + "endpoints": [{ + "name": "add", + "inputs": [ + { + "name": "value", + "type": "BigUint" + } + ], + "outputs": [] + }] +}) -transaction = Transaction( - nonce=90, - sender=public_key.to_address(), - receiver=Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), - value=1000000000000000000, - gas_limit=50000, - chain_id="D" -) +abi = Abi(definition=abi_definition) +``` -# serialize the transaction -transaction_computer = TransactionComputer() -serialized_transaction = transaction_computer.compute_bytes_for_signing(transaction) +### Smart Contract deployments -# apply the signature on the transaction -transaction.signature = secret_key.sign(serialized_transaction) +For creating smart contract deploy transactions, we have two options, as well: a `controller` and a `factory`. Both of these are similar to the ones presented above for transferring tokens. -print(transaction.to_dictionary()) -``` +When creating transactions that interact with smart contracts, we should provide the ABI file to the `controller` or `factory` if possible, so we can pass the arguments as native values. If the abi is not provided and we know what types the contract expects, we can pass the arguments as `typed values` (ex: BigUIntValue, ListValue, StructValue, etc.) or `bytes`. -#### Signing a Transaction by hash +#### Deploying a smart contract using the controller ```py from pathlib import Path -from multiversx_sdk import Account, Address, Transaction, TransactionComputer -account = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +from multiversx_sdk import Account, DevnetEntrypoint +from multiversx_sdk.abi import Abi, BigUIntValue -transaction = Transaction( - nonce=90, - sender=account.address, - receiver=Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), - value=1000000000000000000, - gas_limit=50000, - chain_id="D" +# prepare the account +account = Account.new_from_keystore( + file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), + password="password", + address_index=0 ) +# the developer is responsible for managing the nonce +account.nonce = entrypoint.recall_account_nonce(account.address) -transaction_computer = TransactionComputer() +# load the abi file +abi = Abi.load(Path("contracts/adder.abi.json")) -# sets the least significant bit of the options field to `1` -transaction_computer.apply_options_for_hash_signing(transaction) +# get the smart contracts controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_smart_contract_controller(abi=abi) -# compute a keccak256 hash for signing -hash = transaction_computer.compute_hash_for_signing(transaction) +# load the contract bytecode +bytecode = Path("contracts/adder.wasm").read_bytes() -# sign and apply the signature on the transaction -transaction.signature = account.sign(hash) +# For deploy arguments, use typed value objects if you haven't provided an ABI +args = [BigUIntValue(42)] +# Or use simple, plain Python values and objects if you have provided an ABI +args = [42] -print(transaction.to_dictionary()) +deploy_transaction = controller.create_transaction_for_deploy( + sender=account, + nonce=account.get_nonce_then_increment(), + bytecode=bytecode, + gas_limit=5000000, + arguments=args, + is_upgradeable=True, + is_readable=True, + is_payable=True, + is_payable_by_sc=True +) + +# broadcasting the transaction +tx_hash = entrypoint.send_transaction(deploy_transaction) ``` -#### Signing a Message using an Account +When creating transactions using `SmartContractController` or `SmartContractTransactionsFactory`, even if the ABI is available and provided, you can still use _typed value_ objects as arguments for deployments and interactions. +Even further, you can use a mix of typed value objects and plain Python values and objects. For example: ```py -from pathlib import Path -from multiversx_sdk import Account, Message +args = [U32Value(42), "hello", { "foo": "bar" }, TokenIdentifierValue("TEST-123456")] +``` -account = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +#### Parsing contract deployment transactions -# creating a message -message = Message(data="this is a test message".encode(), address=account.address) +After broadcasting the transaction, we can wait for it's execution to be completed and parse the processed transaction to extract the address of newly deployed smart contract. -# signing the message -message.signature = account.sign_message(message) +```py +# we use the transaction hash we got when broadcasting the transaction +contract_deploy_outcome = controller.await_completed_deploy(tx_hash) # waits for transaction completion and parses the result +contract_address = contract_deploy_outcome.contracts[0].address +print(contract_address.to_bech32()) ``` -#### Signing a message using a SecretKey +If we want to wait for transaction completion and parse the result in two different steps, we can do as follows: ```py -from multiversx_sdk import UserSecretKey, Message, MessageComputer - -secret_key_hex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9" -secret_key = UserSecretKey(bytes.fromhex(secret_key_hex)) -public_key = secret_key.generate_public_key() - -message_computer = MessageComputer() - -# creating a message -message = Message(data="this is a test message".encode(), address=public_key.to_address()) - -# serialize the message -serialized_message = message_computer.compute_bytes_for_signing(message) +# we use the transaction hash we got when broadcasting the transaction +# waiting for transaction completion +transaction_on_network = entrypoint.await_transaction_completed(tx_hash) -# signing the message -message.signature = secret_key.sign(serialized_message) +# parsing the transaction +contract_deploy_outcome = controller.parse_deploy(transaction_on_network) ``` -## Verifying signatures - -The verification of a signature is done using the **public key** of an account. We have a few wrappers over public keys that make the verification of signatures a little bit easier. +#### Computing the smart contract address -#### Verifying Transaction signature using a UserVerifier +Even before broadcasting, at the moment you know the sender's address and the nonce for your deployment transaction, you can (deterministically) compute the (upcoming) address of the smart contract: ```py -from pathlib import Path -from multiversx_sdk import Account, Address, Transaction, TransactionComputer, UserVerifier - -account = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) - -transaction = Transaction( - nonce=90, - sender=account.address, - receiver=Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), - value=1000000000000000000, - gas_limit=50000, - chain_id="D" -) - -# apply the signature on the transaction -transaction.signature = account.sign_transaction(transaction) +from multiversx_sdk import Address, AddressComputer -# instantiating a user verifier; basically gets the public key +# we used Alice for deploying the contract, so we are using her address alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -alice_verifier = UserVerifier.from_address(alice) - -# serialize the transaction for verification -transaction_computer = TransactionComputer() -serialized_transaction = transaction_computer.compute_bytes_for_verifying(transaction) -# verify the signature -is_signed_by_alice = alice_verifier.verify( - data=serialized_transaction, - signature=transaction.signature +address_computer = AddressComputer() +contract_address = address_computer.compute_contract_address( + deployer=alice, + deployment_nonce=deploy_transaction.nonce # the same nonce we set on the deploy transaction ) -print("Transaction is signed by Alice:", is_signed_by_alice) +print("Contract address:", contract_address.to_bech32()) ``` -#### Verifying Message signature using a UserVerifier +#### Deploying a smart contract using the factory + +After the transaction is created the `nonce` needs to be properly set and the transaction should be signed before broadcasting it. ```py from pathlib import Path -from multiversx_sdk import Account, Address, Message, MessageComputer, UserVerifier -account = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +from multiversx_sdk import Address, DevnetEntrypoint, SmartContractTransactionsOutcomeParser +from multiversx_sdk.abi import Abi, BigUIntValue -message = Message( - data="this is a test message".encode(), - address=account.address -) -# apply the signature on the message -message.signature = account.sign_message(message) +# load the abi file +abi = Abi.load(Path("contracts/adder.abi.json")) -# instantiating a user verifier; basically gets the public key -alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -alice_verifier = UserVerifier.from_address(alice) +# get the smart contracts transaction factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_smart_contract_transactions_factory(abi=abi) -# serialize the message for verification -message_computer = MessageComputer() -serialized_message = message_computer.compute_bytes_for_verifying(message) +# load the contract bytecode +bytecode = Path("contracts/adder.wasm").read_bytes() -# verify the signature -is_signed_by_alice = alice_verifier.verify( - data=serialized_message, - signature=message.signature +# For deploy arguments, use typed value objects if you haven't provided an ABI to the factory: +args = [BigUIntValue(42)] +# Or use simple, plain Python values and objects if you have provided an ABI to the factory: +args = [42] + +alice_address = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") + +deploy_transaction = factory.create_transaction_for_deploy( + sender=alice_address, + bytecode=bytecode, + gas_limit=5000000, + arguments=args, + is_upgradeable=True, + is_readable=True, + is_payable=True, + is_payable_by_sc=True ) -print("Message is signed by Alice:", is_signed_by_alice) +# load the account +alice = Account.new_from_keystore( + file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), + password="password", + address_index=0 +) +# the developer is responsible for managing the nonce +alice.nonce = entrypoint.recall_account_nonce(alice.address) + +# set the nonce +deploy_transaction.nonce = alice.nonce + +# sign transaction +deploy_transaction.signature = alice.sign_transaction(deploy_transaction) + +# broadcasting the transaction +tx_hash = entrypoint.send_transaction(deploy_transaction) +print(tx_hash.hex()) + +# waiting for transaction to complete +transaction_on_network = entrypoint.await_transaction_completed(tx_hash) + +# parsing transaction +parser = SmartContractTransactionsOutcomeParser(abi) +contract_deploy_outcome = parser.parse_deploy(transaction_on_network) + +contract_address = contract_deploy_outcome.contracts[0].address +print(contract_address.to_bech32()) ``` -#### Verifying a signature using the public key +### Smart Contract calls + +In this section we'll see how we can call an endpoint of our previously deployed smart contract using both approaches with the `controller` and the `factory`. + +#### Calling a smart contract using the controller ```py from pathlib import Path -from multiversx_sdk import Account, Address, Transaction, TransactionComputer, UserPublicKey -account = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +from multiversx_sdk import Account, DevnetEntrypoint +from multiversx_sdk.abi import Abi, BigUIntValue -transaction = Transaction( - nonce=90, - sender=account.address, - receiver=Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), - value=1000000000000000000, - gas_limit=50000, - chain_id="D" +# prepare the account +account = Account.new_from_keystore( + file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), + password="password", + address_index=0 ) +# the developer is responsible for managing the nonce +account.nonce = entrypoint.recall_account_nonce(account.address) -# apply the signature on the transaction -transaction.signature = account.sign_transaction(transaction) +# load the abi file +abi = Abi.load(Path("contracts/adder.abi.json")) -# instantiating a public key -alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -public_key = UserPublicKey(alice.get_public_key()) +# get the smart contracts controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_smart_contract_controller(abi=abi) -# serialize the transaction for verification -transaction_computer = TransactionComputer() -serialized_transaction = transaction_computer.compute_bytes_for_verifying(transaction) +contract_address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug") -# verify the signature -is_signed_by_alice = public_key.verify( - data=serialized_transaction, - signature=transaction.signature +# For deploy arguments, use typed value objects if you haven't provided an ABI +args = [BigUIntValue(42)] +# Or use simple, plain Python values and objects if you have provided an ABI +args = [42] + +deploy_transaction = controller.create_transaction_for_execute( + sender=account, + nonce=account.get_nonce_then_increment(), + contract=contract_address, + gas_limit=5000000, + function="add", + arguments=args ) -print("Transaction is signed by Alice:", is_signed_by_alice) +# broadcasting the transaction +tx_hash = entrypoint.send_transaction(deploy_transaction) +print(tx_hash.hex()) ``` -#### Sending messages over boundaries +#### Parsing smart contract call transactions -Generally speaking, signed `Message` objects are meant to be sent to a remote party (e.g. a service), which can then verify the signature. +In our case, calling the `add` endpoint does not return anything, but similar to the example above, we could parse this transaction to get the output values of a smart contract call. -In order to prepare a message for transmission, you can use the `MessageComputer.packMessage()` utility method: +```py +# waits for transaction completion and parses the result +# we use the transaction hash we got when broadcasting the transaction +contract_call_outcome = controller.await_completed_execute(tx_hash) +values = contract_call_outcome.values +``` + +#### Calling a smart contract and sending tokens (transfer & execute) + +Additionally, if our endpoint requires a payment when called, we can also send tokens to the contract when creating a smart contract call transaction. We can send EGLD, ESDT tokens or both. This is supported both on the `controller` and the `factory`. ```py from pathlib import Path -from multiversx_sdk import Account, Message, MessageComputer - -account = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -# creating a message -message = Message(data="this is a test message".encode(), address=account.address) +from multiversx_sdk import Account, DevnetEntrypoint, Token, TokenTransfer +from multiversx_sdk.abi import Abi, BigUIntValue -# signing the message -message.signature = account.sign_message(message) +# prepare the account +account = Account.new_from_keystore( + file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), + password="password", + address_index=0 +) +# the developer is responsible for managing the nonce +account.nonce = entrypoint.recall_account_nonce(account.address) -message_computer = MessageComputer() -packed_message = message_computer.pack_message(message) -print(packed_message) -``` +# load the abi file +abi = Abi.load(Path("contracts/adder.abi.json")) -Then, on the receiving side, you can use `MessageComputer.unpackMessage()` to reconstruct the message, prior verification: +# get the smart contracts controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_smart_contract_controller(abi=abi) -```py -from multiversx_sdk import Address, MessageComputer, UserPublicKey +contract_address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug") -alice = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +# For deploy arguments, use typed value objects if you haven't provided an ABI +args = [BigUIntValue(42)] +# Or use simple, plain Python values and objects if you have provided an ABI +args = [42] -message_computer = MessageComputer() +# creating the transfer +first_token = Token("TEST-38f249", 10) +first_transfer = TokenTransfer(first_token, 1) -# restore message -message = message_computer.unpack_message(packed_message) +second_token = Token("BAR-c80d29") +second_transfer = TokenTransfer(second_token, 10000000000000000000) -# verify signature -public_key = UserPublicKey(alice.get_public_key()) +execute_transaction = controller.create_transaction_for_execute( + sender=account, + nonce=account.get_nonce_then_increment(), + contract=contract_address, + gas_limit=5000000, + function="add", + arguments=args, + native_transfer_amount=1000000000000000000, # 1 EGLD, + token_transfers=[first_transfer, second_transfer] +) -is_signed_by_alice = public_key.verify(message_computer.compute_bytes_for_verifying(message), message.signature) -print("Is signed by Alice:", is_signed_by_alice) +# broadcasting the transaction +tx_hash = entrypoint.send_transaction(execute_transaction) +print(tx_hash.hex()) ``` -## Generating a Native Auth Token +#### Calling a smart contract using the factory -The sdk implements a native auth client that can be used to generate a native auth token. +Let's create the same smart contract call transaction, but using the `factory`. ```py from pathlib import Path -from multiversx_sdk import Account, Message, NativeAuthClient, NativeAuthClientConfig - -config = NativeAuthClientConfig(origin="https://devnet-api.multiversx.com", api_url="https://devnet-api.multiversx.com") -client = NativeAuthClient(config) +from multiversx_sdk import Account, DevnetEntrypoint, Token, TokenTransfer +from multiversx_sdk.abi import Abi, BigUIntValue +# prepare the account account = Account.new_from_keystore( file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), password="password", address_index=0 ) +# the developer is responsible for managing the nonce +account.nonce = entrypoint.recall_account_nonce(account.address) -init_token = client.initialize() -token_for_signing = client.get_token_for_signing(account.address, init_token) -signature = account.sign_message(Message(token_for_signing)) -access_token = client.get_token(address=account.address, token=init_token, signature=signature.hex()) +# load the abi file +abi = Abi.load(Path("contracts/adder.abi.json")) -print(access_token) -``` +# get the smart contracts factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_smart_contract_transactions_factory(abi=abi) -## Validating a Native Auth Token +contract_address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug") -The sdk implements native auth server-side components that can be used to validate a native auth token. If you want to see the validated token, you can simply do the following: +# For deploy arguments, use typed value objects if you haven't provided an ABI to the factory: +args = [BigUIntValue(42)] +# Or use simple, plain Python values and objects if you have provided an ABI to the factory: +args = [42] -```py -from multiversx_sdk import NativeAuthServerConfig, NativeAuthServer +# creating the transfer +first_token = Token("TEST-38f249", 10) +first_transfer = TokenTransfer(first_token, 1) -config = config = NativeAuthServerConfig( - api_url="https://devnet-api.multiversx.com", - accepted_origins=["https://devnet-api.multiversx.com"], - max_expiry_seconds=86400, -) +second_token = Token("BAR-c80d29") +second_transfer = TokenTransfer(second_token, 10000000000000000000) -server = NativeAuthServer(config) +execute_transaction = factory.create_transaction_for_execute( + sender=account.address, + contract=contract_address, + gas_limit=5000000, + function="add", + arguments=args, + native_transfer_amount=1000000000000000000, # 1 EGLD, + token_transfers=[first_transfer, second_transfer] +) -# we are using the token generated above -validated_token = server.validate(access_token) +execute_transaction.nonce = account.get_nonce_then_increment() +execute_transaction.signature = account.sign_transaction(execute_transaction) -print(validated_token.address.to_bech32()) -print(validated_token.signer_address.to_bech32()) -print(validated_token.issued) -print(validated_token.expires) -print(validated_token.origin) -print(validated_token.extra_info) +# broadcasting the transaction +tx_hash = entrypoint.send_transaction(execute_transaction) +print(tx_hash.hex()) ``` -Or, alternatively, if you just want to check if the token is valid, you can do the following: +### Parsing transaction outcome + +As said before, the `add` endpoint we called does not return anything, but we could parse the outcome of smart contract call transactions, as follows: ```py -from multiversx_sdk import NativeAuthServerConfig, NativeAuthServer +from pathlib import Path -config = config = NativeAuthServerConfig( - api_url="https://devnet-api.multiversx.com", - accepted_origins=["https://devnet-api.multiversx.com"], - max_expiry_seconds=86400, -) +from multiversx_sdk import SmartContractTransactionsOutcomeParser +from multiversx_sdk.abi import Abi -server = NativeAuthServer(config) +# load the abi file +abi = Abi.load(Path("contracts/adder.abi.json")) -# we are using the token generated above -is_valid = server.is_valid(access_token) -print(is_valid) -``` +# create the parser +parser = SmartContractTransactionsOutcomeParser(abi=abi) -## Start your first project +# fetch the transaction of the network +transaction_on_network = entrypoint.get_transaction(tx_hash) # the tx_hash from the transaction sent above -We recommend using a Python version equal to `3.11` or higher, but the sdk should work with any version higher than `3.8`. We also recommend using a virtual environment for managing dependencies. Make sure you also have `pip` installed on your machine. +outcome = parser.parse_execute(transaction=transaction_on_network, function="add") +``` -Using a Terminal or Console, create a directory on your system (hello-multiversx in this example) and make it the working directory. +### Decoding transaction events -```sh -mkdir hello-multiversx -cd hello-multiversx -``` +You might be interested into decoding events emitted by a contract. You can do so by using the `TransactionEventsParser`. -### Create a virtual environment +Suppose we'd like to decode a `startPerformAction` event emitted by the [multisig](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/multisig) contract. -Run the following command to create and activate your virtual environment: +First, we load the abi file, then we fetch the transaction, we extract the event from the transaction and then we parse it. -```sh -python3 -m venv ./venv -source ./venv/bin/activate -``` +```py +from pathlib import Path -After the virtual environment is created, we can install the sdk running the following command: +from multiversx_sdk import DevnetEntrypoint, TransactionEventsParser, find_events_by_first_topic +from multiversx_sdk.abi import Abi -```sh -pip install multiversx-sdk -``` +# load the abi file +abi = Abi.load(Path("contracts/multisig-full.abi.json")) -If you wish to interact with Ledger devices through the sdk, install the sdk as follows: +# fetch the transaction of the network +network_provider = DevnetEntrypoint().create_network_provider() +transaction_on_network = network_provider.get_transaction("exampleTransactionHash") -```sh -pip install multiversx-sdk[ledger] -``` +# extract the event from the transaction +[event] = find_events_by_first_topic(transaction_on_network, "startPerformAction") -If your project has multiple dependencies, we recommend using a `requirements.txt` file for having all dependencies in one place. Inside the file we are going to place each dependency on a new line: +# create the parser +events_parser = TransactionEventsParser(abi=abi) -```sh -multiversx-sdk +# parse the event +parsed_event = events_parser.parse_event(event) ``` -Additionally, we can also install it directly from GitHub. Place this line on a new line of your `requirements.txt` file. In this example, we are going to install the version `2.0.0`: +### Encoding/Decoding custom types -```sh -git+https://git@github.com/multiversx/mx-sdk-py.git@v2.0.0#egg=multiversx_sdk -``` +Whenever needed, the contract ABI can be used for manually encoding or decoding custom types. -If you've places all dependencies in a `requirements.txt` file, make sure you also install them by running: +Let's encode a struct called `EsdtTokenPayment` (of [multisig](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/multisig) contract) into binary data. -```sh -pip install -r requirements.txt +```py +from pathlib import Path +from multiversx_sdk.abi import Abi + +abi = Abi.load(Path("contracts/multisig-full.abi.json")) +encoded = abi.encode_custom_type("EsdtTokenPayment", ["TEST-8b028f", 0, 10000]) +print(encoded) ``` -We can then create a `main.py` file where we can write our code. +Now, let's decode a struct using the ABI. -### Importing objects from the sdk +```py +from multiversx_sdk.abi import Abi, AbiDefinition -The most common classes can be imported from package level: +abi_definition = AbiDefinition.from_dict( + { + "endpoints": [], + "events": [], + "types": { + "DepositEvent": { + "type": "struct", + "fields": [ + {"name": "tx_nonce", "type": "u64"}, + {"name": "opt_function", "type": "Option"}, + {"name": "opt_arguments", "type": "Option>"}, + {"name": "opt_gas_limit", "type": "Option"}, + ], + } + }, + } +) +abi = Abi(abi_definition) -```py -from multiversx_sdk import Address, Transaction +decoded_type = abi.decode_custom_type(name="DepositEvent", data=bytes.fromhex("00000000000003db000000")) +print(decoded_type) ``` -When interacting with smart contracts, we might want to make use of the abi file or other contract types. We should import those from the abi subpackage. +If you don't wish to use the ABI, there is another way to do it. First, let's encode a struct. ```py -from multiversx_sdk.abi import Abi, BigUIntValue, StringValue +from multiversx_sdk.abi import Serializer, U64Value, StructValue, Field, StringValue, BigUIntValue + +struct = StructValue([ + Field(name="token_identifier", value=StringValue("TEST-8b028f")), + Field(name="token_nonce", value=U64Value()), + Field(name="amount", value=BigUIntValue(10000)), +]) + +serializer = Serializer() +serialized_struct = serializer.serialize([struct]) +print(serialized_struct) ``` - ---- +Now, let's decode a struct without using the ABI. -### Random Numbers in Smart Contracts +```py +from multiversx_sdk.abi import Serializer, U64Value, OptionValue, BytesValue, ListValue, StructValue, Field -## Introduction +tx_nonce = U64Value() +function = OptionValue(BytesValue()) +arguments = OptionValue(ListValue([BytesValue()])) +gas_limit = OptionValue(U64Value()) -Randomness in the blockchain environment is a challenging task to accomplish. Due to the nature of the environment, nodes must all have the same "random" generator to be able to reach consensus. This is solved by using Golang's standard seeded random number generator, directly in the VM: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/math/rand/ +attributes = StructValue([ + Field("tx_nonce", tx_nonce), + Field("opt_function", function), + Field("opt_arguments", arguments), + Field("opt_gas_limit", gas_limit) +]) -The VM function `mBufferSetRandom` uses this library, seeded with the concatenation of: +serializer = Serializer() +serializer.deserialize("00000000000003db000000", [attributes]) -- previous block random seed -- current block random seed -- tx hash +print(tx_nonce.get_payload()) +print(function.get_payload()) +print(arguments.get_payload()) +print(gas_limit.get_payload()) +``` -We're not going to go into details about how exactly the Golang library uses the seed or how it generates said random numbers, as that's not the purpose of this tutorial. +### Smart Contract queries +When querying a smart contract, a **view function** is called. That function does not modify the state of the contract, thus we don't need to send a transaction. -## Random numbers in smart contracts +To query a smart contract, we need to use the `SmartContractController`. Of course, we can use the contract's abi file to encode the arguments of the query, but also parse the result. In this example, we are going to use the [adder](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/adder) smart contract and we'll call the `getSum` endpoint. -The `ManagedBuffer` type has two methods you can use for this: +```py +from pathlib import Path -- `fn new_random(nr_bytes: usize) -> Self`, which creates a new `ManagedBuffer` of `nr_bytes` random bytes -- `fn set_random(&mut self, nr_bytes: usize)`, which sets an already existing buffer to random bytes +from multiversx_sdk import Address, DevnetEntrypoint +from multiversx_sdk.abi import Abi -For convenience, a wrapper over these methods was created, namely the `RandomnessSource` struct, which contains methods for generating a random number for all base rust unsigned numerical types, and a method for generating random bytes. +# load the abi file +abi = Abi.load(Path("contracts/adder.abi.json")) -For example, let's say you wanted to generate `n` random `u16`: +# the contract address we'll query +contract_address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug") -```rust -let mut rand_source = RandomnessSource::new(); -for _ in 0..n { - let my_rand_nr = rand_source.next_u16(); - // do something with the number -} +# create the controller +sc_controller = DevnetEntrypoint().create_smart_contract_controller(abi=abi) + +# creates the query, runs the query, parses the result +response = sc_controller.query( + contract=contract_address, + function="getSum", + arguments=[] # our function expects no arguments, so we provide an empty list +) ``` -Similar methods exist for all Rust unsigned numerical types. +If we need more granular control, we can split the process in three steps: create the query, run the query and parse the query response. This does the exact same as the example above. +```py +from pathlib import Path -## Random numbers in a specific range +from multiversx_sdk import Address, DevnetEntrypoint +from multiversx_sdk.abi import Abi -Let's say you wanted to implement a Fisher-Yates shuffling algorithm inside your smart contract (https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle). +# load the abi file +abi = Abi.load(Path("contracts/adder.abi.json")) -The `RandomnessSource` struct provides methods for generating numbers within a range, namely `fn next_usize_in_range(min: usize, max: usize) -> usize`, which generates a random `usize` in the `[min, max)` range. These methods are available for the rest of the numerical types as well, but for this example, we need `usize` (in Rust, indexes are `usize`). +# the contract address we'll query +contract_address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug") -For that, you would probably have a vector of some kind. For example, let's assume you wanted to shuffle a vector of `ManagedBuffer`s. +# create the controller +sc_controller = DevnetEntrypoint().create_smart_contract_controller(abi=abi) -```rust -let mut my_vec = ManagedVec::new(); -// ... -// fill my_vec with elements -// ... +# creates the query +query = sc_controller.create_query( + contract=contract_address, + function="getSum", + arguments=[] # our function expects no arguments, so we provide an empty list +) -let vec_len = my_vec.len(); -let mut rand_source = RandomnessSource::new(); -for i in 0..vec_len { - let rand_index = rand_source.next_usize_in_range(i, vec_len); - let first_item = my_vec.get(i).unwrap(); - let second_item = my_vec.get(rand_index).unwrap(); +# run the query +result = sc_controller.run_query(query) - my_vec.set(i, &second_item); - my_vec.set(rand_index, &first_item); -} +# parse the result +parsed_result = sc_controller.parse_query_response(result) ``` -This algorithm will shuffle each element at position `i`, with an element from position `[i, vec_len)`. +### Upgrading a smart contract +Contract upgrade transactions are similar to deployment transactions (see above), in the sense that they also require a contract bytecode. In this context though, the contract address is already known. Similar to deploying a smart contract, we can upgrade a smart contract using either the `controller` or the `factory`. -## Random bytes +#### Uprgrading a smart contract using the controller -Let's say you want to create some NFTs in your contract, and want to give each of them a random hash of 32 bytes. To do that, you would use the `next_bytes(len: usize)` method of the `RandomnessSource` struct: +```py +from pathlib import Path -```rust -let mut rand_source = RandomnessSource::new(); -let rand_hash = rand_source.next_bytes(32); -// NFT create logic here -``` +from multiversx_sdk import Account, Address, DevnetEntrypoint +from multiversx_sdk.abi import Abi, BigUIntValue +# prepare the account +account = Account.new_from_keystore( + file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), + password="password", + address_index=0 +) -## Considerations +entrypoint = DevnetEntrypoint() -:::caution -NEVER have logic in your smart contract that only depends on the current state. -::: +# the developer is responsible for managing the nonce +account.nonce = entrypoint.recall_account_nonce(account.address) -Example of BAD implementation: +# load the abi file +abi = Abi.load(Path("contracts/adder.abi.json")) -```rust -#[payable("EGLD")] -#[endpoint(rollDie)] -fn roll_die(&self) { - // ... - let payment = self.call_value().egld(); - let rand_nr = rand_source.next_u8(); - if rand_nr % 6 == 0 { - let prize = payment * 2u32; - self.send().direct(&caller, &prize); - } - // ... -} +# get the smart contracts controller +controller = entrypoint.create_smart_contract_controller(abi=abi) + +# load the contract bytecode; this is the new contract code, the one we want to upgrade to +bytecode = Path("contracts/adder.wasm").read_bytes() + +# For deploy arguments, use typed value objects if you haven't provided an ABI +args = [BigUIntValue(42)] +# Or use simple, plain Python values and objects if you have provided an ABI +args = [42] + +contract_address = Address.new_from_bech32( + "erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug") + +deploy_transaction = controller.create_transaction_for_upgrade( + sender=account, + nonce=account.get_nonce_then_increment(), + contract=contract_address, + bytecode=bytecode, + gas_limit=5000000, + arguments=args, + is_upgradeable=True, + is_readable=True, + is_payable=True, + is_payable_by_sc=True +) + +# broadcasting the transaction +tx_hash = entrypoint.send_transaction(deploy_transaction) +print(tx_hash.hex()) ``` -This is very easy to abuse, as you can simply simulate your transactions, and only send them when you see you've won. Therefore, guaranteeing a 100% win chance! +#### Upgrading a smart contract using the factory -Keep in mind you are not running this on your own private server, you are running it on a public blockchain, so you need a complete shift in design. +Let's create the same upgrade transaction using the `factory`. -Example of GOOD implementation: +```py +from pathlib import Path -```rust -#[payable("EGLD")] -#[endpoint(signUp)] -fn sign_up(&self) { - let already_signed_up = self.user_list().insert(caller.clone()); - if already_signed_up { - sc_panic!("Already signed up"); - } -} +from multiversx_sdk import Account, Address, DevnetEntrypoint +from multiversx_sdk.abi import Abi, BigUIntValue -#[only_owner] -#[endpoint(selectWinners)] -fn select_winners(&self) { - for user in self.user_list().iter() { - let rand_nr = rand_source.next_u8(); - if rand_nr % 6 == 0 { - self.winners_list().insert(user.clone()); - } - } -} -#[endpoint] -fn claim(&self) { - let was_winner = self.winners_list().swap_remove(&caller); - if was_winner { - self.send().direct_egld(&caller, &prize); - } -} +# load the abi file +abi = Abi.load(Path("contracts/adder.abi.json")) -#[storage_mapper("userList")] -fn user_list(&self) -> UnorderedSetMapper; +# get the smart contracts factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_smart_contract_transactions_factory(abi=abi) -#[storage_mapper("winnersList")] -fn winners_list(&self) -> UnorderedSetMapper; -``` +# load the contract bytecode; this is the new contract code, the one we want to upgrade to +bytecode = Path("contracts/adder.wasm").read_bytes() +# For deploy arguments, use typed value objects if you haven't provided an ABI to the factory: +args = [BigUIntValue(42)] +# Or use simple, plain Python values and objects if you have provided an ABI to the factory: +args = [42] -## Conclusion +alice_address = Address.new_from_bech32( + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -This random number generator should be enough for most purposes. Enjoy using it for your lotteries and such! +contract_address = Address.new_from_bech32( + "erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug") ---- +deploy_transaction = factory.create_transaction_for_upgrade( + sender=alice_address, + contract=contract_address, + bytecode=bytecode, + gas_limit=5000000, + arguments=args, + is_upgradeable=True, + is_readable=True, + is_payable=True, + is_payable_by_sc=True +) -### rating +# load the account +alice = Account.new_from_keystore( + file_path=Path("../multiversx_sdk/testutils/testwallets/withDummyMnemonic.json"), + password="password", + address_index=0 +) +# the developer is responsible for managing the nonce +alice.nonce = entrypoint.recall_account_nonce(alice.address) -This page describes the structure of the `rating` index (Elasticsearch), and also depicts a few examples of how to query it. +# set the nonce +deploy_transaction.nonce = alice.nonce +# sign transaction +deploy_transaction.signature = alice.sign_transaction(deploy_transaction) -## _id +# broadcasting the transaction +tx_hash = entrypoint.send_transaction(deploy_transaction) +print(tx_hash.hex()) +``` -The `_id` field of this index is composed in this way: `{validator_bls_key}_{epoch}` (example: `blskey_37`). +### Token management +In this section, we're going to create transactions to issue fungible tokens, issue semi-fungible tokens, create NFTs, set token roles, but also parse these transactions to extract their outcome (e.g. get the token identifier of the newly issued token). -## Fields +Of course, the methods used here are available through the `TokenManagementController` or through the `TokenManagementTransactionsFactory`. The controller also contains methods for awaiting transaction completion and for parsing the transaction outcome. The same can be achieved for the transactions factory by using the `TokenManagementTransactionsOutcomeParser`. For scripts or quick network interactions we advise you use the controller, but for a more granular approach (e.g. DApps) we suggest using the factory. +#### Issuing fungible tokens using the controller -| Field | Description | -|-----------|------------------------------------------------------------------| -| rating | The rating of the validator, which can be in the [0, 100] range. | +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint +# create the entrypoint and the token management controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_token_management_controller() -## Query examples +# create the issuer of the token +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -### Fetch rating of a validator for a specific epoch +transaction = controller.create_transaction_for_issuing_fungible( + sender=alice, + nonce=alice.get_nonce_then_increment(), + token_name="NEWFNG", + token_ticker="FNG", + initial_supply=1_000_000_000000, # 1 million tokens, with 6 decimals + num_decimals=6, + can_freeze=False, + can_wipe=True, + can_pause=False, + can_change_owner=True, + can_upgrade=True, + can_add_special_roles=True +) -``` -curl --request GET \ - --url ${ES_URL}/rating/_search \ - --header 'Content-Type: application/json' \ - --data '{ - "query": { - "match": { - "_id":"${BLS_KEY}_600" - } - } -}' +# sending the transaction +tx_hash = entrypoint.send_transaction(transaction) + +# wait for transaction to execute, extract the token identifier +outcome = controller.await_completed_issue_fungible(tx_hash) + +token_identifier = outcome[0].token_identifier +print(token_identifier) ``` ---- +#### Issuing fungible tokens using the factory -### React Development +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint, TokenManagementTransactionsOutcomeParser -## Introduction +# create the entrypoint and the token management transactions factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_token_management_transactions_factory() -Every developer has his/her own code style that has been developed along the way, with bits and quirks that, at some point, become a part of oneself. +# create the issuer of the token +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -However, in a big team and in a big project, small quirks and personal preferences can add up and turn the codebase into a big lasagna. +transaction = factory.create_transaction_for_issuing_fungible( + sender=alice.address, + token_name="NEWFNG", + token_ticker="FNG", + initial_supply=1_000_000_000000, # 1 million tokens, with 6 decimals + num_decimals=6, + can_freeze=False, + can_wipe=True, + can_pause=False, + can_change_owner=True, + can_upgrade=True, + can_add_special_roles=True +) -Given this, we have established some basic principles and a code style we would like to follow. These are, of course, not set in stone, and can be changed, given a valid reason. +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) +transaction.nonce = alice.get_nonce_then_increment() +# sign the transaction +transaction.signature = alice.sign_transaction(transaction) -## Using Git +# sending the transaction +tx_hash = entrypoint.send_transaction(transaction) -:::important -* We use **yarn** as a package manager. -::: +# if we know that the transaction is completed, we can simply call `entrypoint.get_transaction(tx_hash)` +transaction_on_network = entrypoint.await_transaction_completed(tx_hash) +# extract the token identifier +parser = TokenManagementTransactionsOutcomeParser() +outcome = parser.parse_issue_fungible(transaction_on_network) -### Branch naming -We use a system for **branch naming**: \[your initials\]/-[feature || fix || redesign\]/-\[2-3 words describing the branch\] -> e.g. John Doe creates `jd/feature/fix-thing-called-twice` +token_identifier = outcome[0].token_identifier +print(token_identifier) +``` -:::note -All branch names are lowercase -::: +#### Setting special roles for fungible tokens using the controller +```py +from pathlib import Path +from multiversx_sdk import Account, Address, DevnetEntrypoint -## Basic principles +# create the entrypoint and the token management controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_token_management_controller() +# create the issuer of the token +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -### Imports and exports -We import **lodash-specific functions** instead of the whole library for the tree shaking to take effect. +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -```jsx -// DON'T import _ from 'lodash'; -// this also doesn't shake that tree, unfortunately. -// You can find more info on webpack website about tree shaking. -import {cloneDeep, isEmpty} from 'lodash'; // DO import cloneDeep from 'lodash/cloneDeep'; -import isEmpty from 'lodash/isEmpty'; -import last from 'lodash/last'; -import uniqBy from 'lodash/uniqBy'; -import get from 'lodash/get';` -``` +bob = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") -Do not use `default` exports. **Use named exports** instead. +transaction = controller.create_transaction_for_setting_special_role_on_fungible_token( + sender=alice, + nonce=alice.get_nonce_then_increment(), + user=bob, + token_identifier="TEST-123456", + add_role_local_mint=True, + add_role_local_burn=True, + add_role_esdt_transfer_role=True +) +# sending the transaction +tx_hash = entrypoint.send_transaction(transaction) -### Using conditionals -Avoid using nested conditionals. **Use early returns** instead. +# wait for transaction to execute, extract the roles +outcome = controller.await_completed_set_special_role_on_fungible_token(tx_hash) -```jsx -// 🚫 DON'T -if (condition) { - if (anotherCondition) { - // do stuff - } -} -// ✅ DO -if (!condition) { - return; -} -if (!anotherCondition) { - return; -} -// do stuff +roles = outcome[0].roles +uaser = outcome[0].user_address ``` +#### Setting special roles for fungible tokens using the factory -### Defining function arguments -If a function has more than 2 arguments and the second argument is not optional, **use an object** instead. +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint, TokenManagementTransactionsOutcomeParser -```jsx -// 🚫 DON'T -const myFunction = (arg1, arg2, arg3) => { - // do stuff -} +# create the entrypoint and the token management transactions factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_token_management_transactions_factory() -// ⚠️ AVOID -const myFunction = (arg1: string, arg2?: boolean) => { // not recommended but acceptable - // do stuff -} +# create the issuer of the token +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -// ✅ DO -const myFunction = ({arg1, arg2, arg3}) => { - // do stuff -} -``` +transaction = factory.create_transaction_for_setting_special_role_on_fungible_token( + sender=alice.address, + user=bob, + token_identifier="TEST-123456", + add_role_local_mint=True, + add_role_local_burn=True, + add_role_esdt_transfer_role=True +) +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) +transaction.nonce = alice.get_nonce_then_increment() -### Validity checks -We use **`!=` or `== null` verifications** for all variables, and `!myBool` for booleans only. +# sign the transaction +transaction.signature = alice.sign_transaction(transaction) -```jsx -// 🚫 DON'T -const user = userSelector(state); -if (!user) { - //do something - if (!refetchAttempted){ - refetch(); - } -} +# sending the transaction +tx_hash = entrypoint.send_transaction(transaction) -// ✅ DO -const user = userSelector(state); -if (user == null) { - //do something - if (!refetchAttempted) { - refetch(); - } -} -``` +# waits until the transaction is processed and fetches it from the network +transaction_on_network = entrypoint.await_transaction_completed(tx_hash) -When using a property from an object inside a condition, check for null with **optional chaining operator**; +# extract the roles +parser = TokenManagementTransactionsOutcomeParser() +outcome = parser.parse_set_special_role(transaction_on_network) -```jsx -// 🚫 DON'T -if (array != null && array.length){ - // do stuff -} -// ✅ DO -if (array?.length > 0){ - //do stuff -} +roles = outcome[0].roles +uaser = outcome[0].user_address ``` +#### Issuing semi-fungible tokens using the controller -### Folder structure -For folder and file naming we're using the following convention: -**camelCase for all folders and files, except when it's a React Component or a Module Root Folder**, in which case we're using PascalCase. -Also, for components' and containers' subcomponents, we create separate folders, even if there is no style file present. +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint -Each folder that has an exportable component will have an **`index.tsx`** file for ease of import.
-Each folder that has an exportable file will have an **`index.ts`** file for ease of import. +# create the entrypoint and the token management controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_token_management_controller() +# create the issuer of the token +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -### File length conventions -- < 100 lines of code - ✅ OK -- 100 - 200 lines of code - try to split the file into smaller files -- 200 - 300 lines of code - should be split the file into smaller files -- \> 300 lines of code 🚫 DON'T +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) +transaction = controller.create_transaction_for_issuing_semi_fungible( + sender=alice, + nonce=alice.get_nonce_then_increment(), + token_name="NEWSEMI", + token_ticker="SEMI", + can_freeze=False, + can_wipe=True, + can_pause=False, + can_transfer_nft_create_role=True, + can_change_owner=True, + can_upgrade=True, + can_add_special_roles=True +) -### Naming conventions -* When naming types, use the suffix **`Type`**. This helps us differentiate between types and components. When naming component props types, use MyComponentPropsType. When naming a type that is not a component, use MyFunctionType. When naming return values, use MyFunctionReturnType. +# sending the transaction +tx_hash = entrypoint.send_transaction(transaction) -**Try to extract at the top of the function all constants** such as strings, numbers, objects, instead of declaring this ad hoc inside the code. +# wait for transaction to execute, extract the token identifier +outcome = controller.await_completed_issue_semi_fungible(tx_hash) -```jsx -// 🚫 DON'T -if (x === 'rejected' && y === 4) { - // do stuff -} -// ✅ DO -enum PermissionEnum { // all enums should be in PascalCase and suffixed with "Enum" - rejected = "rejected" -} -const ACCESS_LEVEL = 4; // all constants declared on top of functions should be in UPPER_CASE -if (x === PermissionsEnum.rejected && y === ACCESS_LEVEL) -{ - //do stuff -} +token_identifier = outcome[0].token_identifier +print(token_identifier) ``` +#### Issuing semi-fungible tokens using the factory -## React guidelines - - -### Using functional components -We're using **functional components** for almost all new components, no classes, except when strictly necessary (e.g. error boundaries); +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint, TokenManagementTransactionsOutcomeParser +# create the entrypoint and the token management transactions factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_token_management_transactions_factory() -### Using selectors -We use `useSelector` and `useDispatch` hooks to connect to redux store via react-redux. 🚫 **No** `mapStateToProps` in functional components. +# create the issuer of the token +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -We use **reselect** for memoizing complex state variables and composing those into optimized selectors that don't rerender the whole tree when the values don't change. This package needs to be added only when there is a performance bottleneck, either existing or expected. +transaction = factory.create_transaction_for_issuing_semi_fungible( + sender=alice.address, + token_name="NEWSEMI", + token_ticker="SEMI", + can_freeze=False, + can_wipe=True, + can_pause=False, + can_transfer_nft_create_role=True, + can_change_owner=True, + can_upgrade=True, + can_add_special_roles=True +) +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) +transaction.nonce = alice.get_nonce_then_increment() -### Defining handlers +# sign the transaction +transaction.signature = alice.sign_transaction(transaction) -We're using **"handle" prefix** for handlers defined in the function and **"on"** prefix for handlers passed via props. `handleTouchStart` vs `props.onTouchStart`, to distinguish between own handlers and parent handlers. +# sending the transaction +tx_hash = entrypoint.send_transaction(transaction) -```jsx -function handleClick(e) { - props.onClickClick(); -} +# waits until the transaction is processed and fetches it from the network +transaction_on_network = entrypoint.await_transaction_completed(tx_hash) -
+# extract the token identifier +parser = TokenManagementTransactionsOutcomeParser() +outcome = parser.parse_issue_semi_fungible(transaction_on_network) -//destructured before, instantly known to be from parent -
` +token_identifier = outcome[0].token_identifier +print(token_identifier) ``` +#### Issuing NFT collection & creating NFTs using the controller -### Number of props per component +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint -If a component has more than 7 props, it should draw a red flag and be refactored. If it has >= 10 props, it should be refactored immediately. Strategies for refactoring: -- split into smaller components and pass them as props -- use a local context provider - -```jsx -// ⚠️ AVOID - - -// ✅ DO group props into logical components - - } - prop={ - - } -> - - -``` +# create the entrypoint and the token management controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_token_management_controller() +# create the issuer of the token +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -### Inline functions -No **inline functions** in TSX. +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -```jsx -// 🚫 DON'T - setPressed(true)}/> -// ✅ DO -const handlePress = () => { - setPressed(true) -} - -``` +# issue NFT collection +transaction = controller.create_transaction_for_issuing_non_fungible( + sender=alice, + nonce=alice.get_nonce_then_increment(), + token_name="NEWNFT", + token_ticker="NFT", + can_freeze=False, + can_wipe=True, + can_pause=False, + can_transfer_nft_create_role=True, + can_change_owner=True, + can_upgrade=True, + can_add_special_roles=True +) +# sending the transaction +tx_hash = entrypoint.send_transaction(transaction) -### Implicit values -Use implicit `true` for **boolean** props +# wait for transaction to execute, extract the collection identifier +outcome = controller.await_completed_issue_non_fungible(tx_hash) -```jsx -// 🚫 DON'T - -// ✅ DO - -``` +collection_identifier = outcome[0].token_identifier +# set roles +transaction = controller.create_transaction_for_setting_special_role_on_non_fungible_token( + sender=alice, + nonce=alice.get_nonce_then_increment(), + user=alice.address, + token_identifier=collection_identifier, + add_role_nft_create=True, + add_role_nft_burn=True, + add_role_nft_update_attributes=True, + add_role_nft_add_uri=True, + add_role_esdt_transfer_role=True, +) -### Destructuring arguments -Always destructure arguments, with minor exceptions. +# sending the transaction and waiting for completion +tx_hash = entrypoint.send_transaction(transaction) +entrypoint.await_transaction_completed(tx_hash) -```jsx -// 🚫 DON'T -function printUser(user) { - console.log(user.name, user.name); -} -// ✅ DO -function printUser({ name, age }) { - console.log(name, age); -} -``` +# create a NFT +transaction = controller.create_transaction_for_creating_nft( + sender=alice, + nonce=alice.get_nonce_then_increment(), + token_identifier=collection_identifier, + initial_quantity=1, + name="TEST", + royalties=2500, # 25% + hash="", + attributes=b"", + uris=["emptyUri"] +) -There are exceptions to this rule like: -1. The arguments are optional +# sending the transaction +tx_hash = entrypoint.send_transaction(transaction) -```jsx -function logWithOptions(options?: {green?: boolean}) { - if (options?.green) { - return console.log('\x1b[42m%s\x1b[0m', 'Some green text'); - } - console.log('Some normal text'); -} +# wait for transaction to execute, extract the nft identifier +outcome = controller.await_completed_create_nft(tx_hash) +identifier = outcome[0].token_identifier +nonce = outcome[0].nonce +initial_quantity = outcome[0].initial_quantity +print(identifier) +print(nonce) +print(initial_quantity) ``` -2. There is a name clash with variables defined above +#### Issuing NFT collection & creating NFTs using the factory -```jsx -const type = 'admin'; -function verifyUser(user) { - console.log(user.type === type); -} +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint, TokenManagementTransactionsOutcomeParser -``` -3. Same props are passed below to a component, or are used for further processing +# create the entrypoint and the token management transactions factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_token_management_transactions_factory() -```jsx -// 🚫 DON'T -const DisplayUser = ({name, age}: UserType) { - return ; -} -// ✅ DO -const DisplayUser = (user: UserType) { - return ; -} -const UserList = (users: UserType[]) { - return users.map((user, index) => { - // destructuring avoids typechecking so always specify the type - // before passing destructured props to a component - const userProps: UserType = processUser(user); - return ; - }) - -} -``` +# create the issuer of the token +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +# issue NFT collection +transaction = factory.create_transaction_for_issuing_non_fungible( + sender=alice.address, + token_name="NEWTOKEN", + token_ticker="TKN", + can_freeze=False, + can_wipe=True, + can_pause=False, + can_transfer_nft_create_role=True, + can_change_owner=True, + can_upgrade=True, + can_add_special_roles=True +) -### Over-optimization -No **`useCallback` or `useMemo` or `React.memo` unless really necessary**. Since the release of hooks, over-optimization has become a big problem. +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) +transaction.nonce = alice.get_nonce_then_increment() -```jsx -// 🚫 DON'T -const handlePress = useCallback(() => setPressed(true), []); - -// 🚫 DON'T -const value = useMemo(() => user.level * multiplicator); +# sign the transaction +transaction.signature = alice.sign_transaction(transaction) -// ✅ DO -const handlePress = useCallback(() => setPressed(true), []); - - {children} - -``` +# sending the transaction +tx_hash = entrypoint.send_transaction(transaction) +# if we know that the transaction is completed, we can simply call `get_transaction(tx_hash)` +transaction_on_network = entrypoint.await_transaction_completed(tx_hash) -### Conditionally rendered TSX +# extract the collection identifier +parser = TokenManagementTransactionsOutcomeParser() +outcome = parser.parse_issue_non_fungible(transaction_on_network) -In React, conditionally rendered TSX is very common. Given the ability to render it inline, it's very easy to include it inside normal TSX: +collection_identifier = outcome[0].token_identifier -```jsx - - {hasAchievements ? : } - - {title} - {mysteryBoxEnabled && } - - -``` +# set roles +transaction = factory.create_transaction_for_setting_special_role_on_non_fungible_token( + sender=alice.address, + user=alice.address, + token_identifier=collection_identifier, + add_role_nft_create=True, + add_role_nft_burn=True, + add_role_nft_update_attributes=True, + add_role_nft_add_uri=True, + add_role_esdt_transfer_role=True, +) +transaction.nonce = alice.get_nonce_then_increment() -However, TSX sometimes tends to grow very big and it requires a certain amount of mental load to stop at these conditionals and understand what's rendered inside. +# sign the transaction +transaction.signature = alice.sign_transaction(transaction) -One could argue that there's an "organism" inside, a certain piece of logic that results in a component being rendered after some calculations and state changes. We try to give names to these operations that result in a TSX, so the developer knows what's in that TSX. +# sending the transaction and waiting for completion +tx_hash = entrypoint.send_transaction(transaction) +entrypoint.await_transaction_completed(tx_hash) -**Thus, all conditionally rendered TSX** **goes into a constant**. We don't render conditional TSX inline +# create a NFT +transaction = factory.create_transaction_for_creating_nft( + sender=alice.address, + token_identifier=collection_identifier, + initial_quantity=1, + name="TEST", + royalties=2500, # 25% + hash="", + attributes=b"", + uris=["emptyUri"] +) +# set the nonce +transaction.nonce = alice.get_nonce_then_increment() -```jsx -const achievementsContainer = hasAchievements? : ; -const mysteryBoxesContainer = mysteryBoxEnabled && ; - - - {achievementsContainer} - - {title} - {mysteryBoxesContainer} - - -``` +# sign the transaction +transaction.signature = alice.sign_transaction(transaction) +# sending the transaction +tx_hash = entrypoint.send_transaction(transaction) -## Rules for hooks +# waits until the transaction is processed and fetches it from the network +transaction_on_network = entrypoint.await_transaction_completed(tx_hash) -1. **Fake modularization**: - * Custom hooks may give the impression of modularization, but their logic runs inline in the parent component. - * State changes and reactive behavior in the hook will cause a rerender of the host component and any other hooks that depend on the updated hook. -2. **Hook return values**: - * Custom hooks should return either a single function (for a lazy hook) or an object containing a function and some properties; - * Avoid returning objects with multiple functions to ensure consistency and maintainability; - * Use interfaces for hook return type: - ```jsx - useMyHook({firstParam, secondParam}: UseMyHookParamsType): UseMyHookReturnType => {} - ``` -3. **State management and data fetching**: - * Lifting state up should be used sparingly; hooks should primarily be used to gather state in one container and distribute it to child components. - * Strive to couple data fetching with UI rendering as isolating as possible to prevent unnecessary rerenders. - * Ensure one hook does not trigger the rerender of another by carefully managing dependencies and side effects. - * If only 10% of the lines of code in a hook do specific React logic like state management, or calling a selector from Redux, consider splitting the hook into: - - a smaller hook that does the React logic and returns the state, and - - a statless function that will be called inside the hook and will do the rest of the logic. This stateless function should be tested separately. -4. **Hook interfaces**: - * Use interfaces for hook params if you're passing more than one argument to the hook invocation: - ```jsx - useMyHook({firstParam, secondParam}: UseMyHookParamsType) => {} - ``` -5. If a hook exports > 4 values, it should draw a red flag and be refactored. If it exports >= 7 values, it should be refactored immediately. +# extract the nft identifier +parser = TokenManagementTransactionsOutcomeParser() +outcome = parser.parse_nft_create(transaction_on_network) + +identifier = outcome[0].token_identifier +nonce = outcome[0].nonce +initial_quantity = outcome[0].initial_quantity +print(identifier) +print(nonce) +print(initial_quantity) +``` +These are just a few examples of what we can do using the token management controller or factory. For a full list of what methods are supported for both, check out the autogenerated documentation: +- [TokenManagementController](https://multiversx.github.io/mx-sdk-py/multiversx_sdk.token_management.html#module-multiversx_sdk.token_management.token_management_controller) +- [TokenManagementTransactionsFactory](https://multiversx.github.io/mx-sdk-py/multiversx_sdk.token_management.html#module-multiversx_sdk.token_management.token_management_transactions_factory) -## Modularisation +### Account management -Given the size of the project, we have agreed on a couple of modularisation techniques that will help us to: +The account management controller and factory allow us to create transactions for managing accounts, like guarding and unguarding accounts and saving key-value pairs. -* Split the logic into more readable chunks of logic; -* Test all bits of logic with unit tests; -* Reuse components/utils/hooks as needed; +To read more about Guardians, check out the [documentation](/developers/built-in-functions/#setguardian). -There are a couple of rules that we agreed upon and will be enforced in all PRs, to try and maintain the code in a state that is easy to navigate, read, debug and change. We try to move as much mental load as we can to the develop who is writing the code, instead of the developer who is reading the code. +A guardian can also be set using the WebWallet. The wallet uses our hosted `Trusted Co-Signer Service`. Check out the steps to guard an account using the wallet [here](/wallet/web-wallet/#guardian). -Therefore, we agreed on the certain principles: +#### Guarding an account using the controller +```py +from pathlib import Path +from multiversx_sdk import Account, Address, DevnetEntrypoint -### Abstracting the logic away into hooks and functions +# create the entrypoint and the account controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_account_controller() -If there is a piece of code in a component or container that holds a certain amount of logic and can be converted to a testable hook or utils function, we should move it to a separate function/hook and add props interface, return type interface and a test file. +# create the account to guard +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -For example: +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -```jsx -const lastExpiringNotificationMissionId = useSelector( expiringNotificationMissionIdSelector ); -useEffect(() => { - if ( missionEndDate && missionId && missionId !== lastExpiringNotificationMissionId ) { - const earlierWith = ONE_HOUR_MS * 2; - const calculatedDate = missionEndDate - earlierWith; - const instantiateLocalNotification = async () => { - NotificationManager.startLocalNotification( - t('modules.mysteryBox.notification.title'), - t('modules.mysteryBox.notification.description'), - calculatedDate?.toString(), - NotificationPayloadTypesEnum.MYSTERY_BOX_EXPIRING - ); - }; - dispatch(setExpiringNotificationMissionId(missionId)); - instantiateLocalNotification(); - } -}, [lastExpiringNotificationMissionId]); -``` +# we can use a trusted service that provides a guardian, or simply set another address we own or trust +guardian = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") -👆 This piece of logic is a perfect candidate to be moved to a separate hook file, because it contains -a very specific piece of logic, can be tested and the behavior is easier to predict and debug. +transaction = controller.create_transaction_for_setting_guardian( + sender=alice, + nonce=alice.get_nonce_then_increment(), + guardian_address=guardian, + service_id="SelfOwnedAddress" # this is just an example +) -```jsx -const sanitizedHerotag = name ? sanitizeHerotag(name) : undefined; -const herotagName = sanitizedHerotag != null && sanitizedHerotag !== name ? sanitizedHerotag : undefined; -const nameWithInitials = name || savedAddress; -const initials = getInitials(nameWithInitials); -return herotagName ? getHerotagPrefix(avatarIconTextMaxLength, herotagName) : initials; +tx_hash = entrypoint.send_transaction(transaction) ``` -👆 This is another piece of logic that is a single "organism", meaning it can be moved to a function that takes in a certain set of arguments and returns a specific value. +#### Guarding an account using the factory -We can abstract these kind of calculations into separate functions, where the logic doesn't pollute the container's file, is easily testable and can be debugged and changed more easily. +```py +from pathlib import Path +from multiversx_sdk import Account, Address, DevnetEntrypoint -Try to identify this kind of "organisms" in your code and move them to a separate file only if the logic is worth it. **Don't overdo it for simple pieces of logic, **unless they are either taking a lot of space or mental load to read through. +# create the entrypoint and the account transactions factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_account_transactions_factory() -```jsx -const sanitizedHerotag = name ? sanitizeHerotag(name) : undefined; -const herotagName = sanitizedHerotag != null && sanitizedHerotag !== name ? sanitizedHerotag : undefined; -``` +# create the account to guard +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -👆 This piece of code would not be worth it, since the logic is very simple, straightforward and there is not much to test. - -However, +# we can use a trusted service that provides a guardian, or simply set another address we own or trust +guardian = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") -```jsx -const avatar = useSelector(avatarSelector); -const sanitizedHerotag = name ? sanitizeHerotag(name) : undefined; -const herotagName = sanitizedHerotag != null && sanitizedHerotag !== name ? sanitizedHerotag : undefined; -const isHerotagValid = herotagName == null; -const shouldAllowHerotagCreation = !isHerotagValid; -const canUserCreateAvatar = !shouldAllowHerotagCreation && avatar == null; -``` +transaction = factory.create_transaction_for_setting_guardian( + sender=alice.address, + guardian_address=guardian, + service_id="SelfOwnedAddress" # this is just an example +) -👆 This logic, even though might seem simple, has a lot of steps that need to be take to reach the final solution, `canUserCreateAvatar`, and would be a good candidate for a separate function. +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -In this case, it would be a good idea to abstract away the mental load needed to read through all this just to understand the container's code. The logic is abstracted away and tested, and if the `canUserCreateAvatar` result is buggy, there is a start and an end for debugging. - -If a piece of logic is a bit complex, works like an entity that could have an input and an output and has more than 7-10 lines of code, consider moving it to a hook/function. +# set the nonce +transaction.nonce = alice.get_nonce_then_increment() -> input → **function** → output +# sign the transaction +transaction.signature = alice.sign_transaction(transaction) -###Abstracting complex calculations into constants +tx_hash = entrypoint.send_transaction(transaction) +``` -Certain inline calculations are not worth moving into a hook/function, but a constant will help remove some complexity and will attach a "name" to the calculation, making it easier to understand what's inside: +After we've set a guardian, we have to wait 20 epochs until we can activate the guardian. After the guardian is set, all the transactions we send should be signed by the guardian, as well. -```jsx -if (!isMissionCountdownLoading && missionCountdownData && isMIssionCountdownDataReady && currentMysteryBox && missionCountdownData?.status === MysteryBoxStatus.FINISHED ) -{ - //... -} -``` +#### Activating the guardian using the controller -👆 This would be a very good example of an inline if that we try to avoid. It does have a lot of simple conditions that are being tested, but there is a certain mental load needed to parse every single && and the comparison seems to ask for a name. +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint -We could rewrite it to something like this: +# create the entrypoint and the account controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_account_controller() -```jsx -const isMissionCountdownDataReady = !isMissionCountdownLoading && missionCountdownData; -const isMissionAlreadyFinished = !currentMysteryBox && missionCountdownData?.status === MysteryBoxStatus.FINISHED; -if (isMysteryBoxMissionStautsChanged && isMIssionCountdownDataReady ) -{ - //... -} -``` +# create the account to guard +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -If we assign complex or even simple but long operations to local variables, we give them a name that can be used to infer what's inside, instead of calculating it ourselves. Sort of like a memoization. By naming a piece of logic, we memoize it and avoid recomputing it inside our heads, unless necessary. +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -Again, as with hooks and functions, **don't overdo it. **There are certain calculations that, like in JavaScript, are easy for the brain to parse and understand, so it's not worth moving them to a local variable: +transaction = controller.create_transaction_for_guarding_account( + sender=alice, + nonce=alice.get_nonce_then_increment() +) -```jsx -if (myClaimableAuctions != null && myClaimableAuctions.length > 0) { - //... -} +tx_hash = entrypoint.send_transaction(transaction) ``` -👆 Here, it's not worth moving the if logic inside a local variable, it would be redundant, as it's very easy to read through it. +#### Activating the guardian using the factory +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint -### New functions/hooks +# create the entrypoint and the account transactions factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_account_transactions_factory() -When creating new functions and hooks, the new entity must have: +# create the account to guard +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -* A props interface, if it accepts any arguments, declared in the function's file; -* A return interface, it the function or hook returns more than a simple primitive, declared in the function's file; -* A test function that tests the function and covers all test cases; As far as possible try to adhere to the ZOMBIES testing technique. The test should be created in the \_\_tests\_ folder; -* At most 50 lines of code, ideally 20 lines. +transaction = factory.create_transaction_for_guarding_account( + sender=alice.address, +) -```jsx -interface UseKYCModalStatePropsType { - isStatusFailed: boolean; - handleOpenInitiateKYCModal: () => void; -} +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -interface UseKYCModalStateReturnType { - KYCInitialModalState: KYCInitialModalStateEnum; - setKYCInitialModalState: (newState: KYCInitialModalStateEnum) => void; -} +# set the nonce +transaction.nonce = alice.get_nonce_then_increment() -export const useKYCModalState = ({ isStatusFailed, handleOpenInitiateKYCModal }: UseKYCModalStatePropsType): UseKYCModalStateReturnType => {} -``` +# sign the transaction +transaction.signature = alice.sign_transaction(transaction) -We believe that adhering to these concepts will help us maintain the codebase at a sane level and will allow us a lot of manoeuvrability in the long run, both in building new features and in solving bugs quickly and reliably in times of crisis. +tx_hash = entrypoint.send_transaction(transaction) +``` ---- +#### Unguarding the account using the controller -### receipts +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint -This page describes the structure of the `receipts` index (Elasticsearch), and also depicts a few examples of how to query it. +# create the entrypoint and the account controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_account_controller() +# the account to unguard +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -## _id - -The `_id` field of this index is composed of hex encoded receipt hash. -(example: `ced4692a092226d68fde24840586bdf36b30e02dc4bf2a73516730867545d53c`) +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) +# the guardian account +guardian = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/bob.pem")) -## Fields +transaction = controller.create_transaction_for_unguarding_account( + sender=alice, + nonce=alice.get_nonce_then_increment(), + guardian=guardian.address +) +# the transaction should also be signed by the guardian before being sent, otherwise it won't be executed +transaction.guardian_signature = guardian.sign_transaction(transaction) -| Field | Description | -|-----------|-----------------------------------------------------------------------------------------------------| -| value | The value field represents the amount of EGLD that was refunded/penalized from the transaction fee. | -| sender | The sender field represents the sender of the transaction that generated the receipt. | -| data | The data field holds a message with the reason why the receipt was generated. | -| txHash | The txHash field represents the hash of the transaction that generated the receipt. | -| timestamp | The timestamp field represents the timestamp of the block in which the receipt was generated. | +# broadcast the transaction +tx_hash = entrypoint.send_transaction(transaction) +``` +#### Unguarding the account using the factory -## Query examples +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint +# create the entrypoint and the account transactions factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_account_transactions_factory() -### Fetch the receipt generated by a transaction +# the account to unguard +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -``` -curl --request GET \ - --url ${ES_URL}/receipts/_search \ - --header 'Content-Type: application/json' \ - --data '{ - "query": { - "match": { - "txHash":"d6.." - } - } -}' -``` +# the guardian account +guardian = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/bob.pem")) ---- +transaction = factory.create_transaction_for_unguarding_account( + sender=alice.address, + guardian=guardian.address +) -### Receiver +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -[comment]: # "mx-abstract" +# set the nonce +transaction.nonce = alice.get_nonce_then_increment() -## Overview +# sign the transaction +transaction.signature = alice.sign_transaction(transaction) -Among the seven distinct generics defining a transaction, `To` signifies the **third** generic field - **the entity that receives the transaction**. With the exception of deployments, it is required to be specified for every transaction in any environment. +# the transaction should also be signed by the guardian before being sent otherwise it won't be executed +transaction.guardian_signature = guardian.sign_transaction(transaction) +# broadcast the transaction +tx_hash = entrypoint.send_transaction(transaction) +``` -## Diagram +#### Saving a key-value pair to an account using the controller -The sender is being set using the `.to(...)` method. Several types can be specified: +We can store key-value pairs for an account on the network. To do so, we create the following transaction: -```mermaid -graph LR - subgraph To - to-unit["()"] - to-unit -->|to| to-man-address[ManagedAddress] - to-unit -->|to| to-address[Address] - to-unit -->|to| to-bech32[Bech32Address] - to-unit -->|to| to-esdt-system-sc[ESDTSystemSCAddress] - to-unit -->|to| to-caller[ToCaller] - to-unit -->|to| to-self[ToSelf] - end -``` +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint +# create the entrypoint and the account controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_account_controller() -## No recipient +# create the account to guard +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -Across the three distinct environments in which a transaction can be initialised, `deploy`, also known as the `init` function, stands alone as the only invocation that cannot designate the recipient. +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -```rust title=blackbox.rs -fn deploy() { - self.world - .tx() - .from(OWNER_ADDRESS) - .typed(proxy::Proxy) - .init(init_value) - .code(CODE_PATH) - .new_address(DEPLOY_ADDRESS) // Sets the new mock address to be used for the newly deployed contract. - .run(); +# creating the key-value pairs we want to save +values = { + "testKey".encode(): "testValue".encode(), + b"anotherKey": b"anotherValue" } -``` - +transaction = controller.create_transaction_for_saving_key_value( + sender=alice, + nonce=alice.get_nonce_then_increment(), + key_value_pairs=values +) -## Explicit recipient - -Transactions, excluding deployments, require the designation of a recipient. This means that most transaction calls must utilise the `.to` method, explicitly specifying the receiving entity. +# broadcast the transaction +tx_hash = entrypoint.send_transaction(transaction) +``` -In the subsequent section, we will go into the various data types that are permissible for recipient nomination. +#### Saving a key-value pair to an account using the factory +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint -### Address +# create the entrypoint and the account transactions factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_account_transactions_factory() -Below there is an interactor that funds a specific contract with an amount of EGLD. The recipient contract is instantiated as an address object within the interactor's context. +# create the account to guard +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -```rust title=interactor.rs -async fn feed_contract_egld(&mut self) { - self.interactor - .tx() - .from(&self.wallet_address) - .to(self.state.current_adder_address()) - .egld(NumExpr("0,050000000000000000")) - .prepare_async() - .run() - .await; +# creating the key-value pairs we want to save +values = { + "testKey".encode(): "testValue".encode(), + b"anotherKey": b"anotherValue" } -``` -This function, which is a sample from a **blackbox test**, increases a value and sends it to a particular wallet. This example specifically focuses on the `.to` call, which establishes the **receiver**. In this case, it is a hardcoded **ManagedAddress** instance. +transaction = factory.create_transaction_for_saving_key_value( + sender=alice.address, + key_value_pairs=values +) -```rust title=blackbox_test.rs -fn add_one(&mut self, from: &AddressValue) { - let to_wallet: ManagedAddress = ManagedAddress::new_from_bytes(&[7u8; 32]); - self.world - .tx() - .from(OWNER_ADDRESS) - .to(to_wallet) - .typed(proxy::Proxy) - .add(1u32) - .run(); -} -``` +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -## **TestSCAddress** +# set the nonce +transaction.nonce = alice.get_nonce_then_increment() -For parametric testing, there are particular address types. `TestSCAddress` encodes a dummy smart contract address, equivalent to `"sc:{}"`; For the example below it is equivalent to `"sc:example_contract"`; - - contains two functions: - - **`.eval_to_array()`** parses the address into an array of u8. - - **`.eval_to_expr()`** returns the address as a string object. - - **`.to_address()`** returns the actual Address object. - -The following example is a fragment from a blackbox test designed for *Price Aggregator Smart Contract* (code available [here](https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/core/price-aggregator)). This function simulates the staking of an amount of EGLD within the *Price Aggregator Smart Contract*. -```rust title=price_aggregator_blackbox_test.rs -const STAKE_AMOUNT: u64 = 20; -const PRICE_AGGREGATOR_ADDRESS: TestSCAddress = TestSCAddress::new("price-aggregator"); +# sign the transaction +transaction.signature = alice.sign_transaction(transaction) -fn stake(&mut self) { - self.world - .tx() - .from(self.address) - .to(PRICE_AGGREGATOR_ADDRESS) - .typed(price_aggregator_proxy::PriceAggregatorProxy) - .stake() - .egld(STAKE_AMOUNT) - .run(); -} +# broadcast the transaction +tx_hash = entrypoint.send_transaction(transaction) ``` -### Bech32Address -In order to avoid repeated conversions, it keeps the **Bech32** representation **inside**. It wraps the address and presents it as a Bech32 expression. - -The example below is a piece of an interactor for the *Adder Smart Contract* (code available [here](https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples/adder)). The aim of the function is to print the total sum of the contract. The receiver is set via the Bech32Address variable. +### Delegation management -```rust title=interact.rs -async fn print_sum(&mut self, adder_address: &Bech32Address) { - let sum = self - .interactor - .query() - .to(adder_address) - .typed(adder_proxy::AdderProxy) - .sum() - .returns(ReturnsResultUnmanaged) - .prepare_async() - .run() - .await; +To read more about staking providers and delegation, please check out the [docs](/validators/delegation-manager/#introducing-staking-providers). - println!("sum: {sum}"); -} -``` +In this section, we are going to create a new delegation contract, get the address of the contract, delegate funds to the contract, redelegate rewards, claim rewards, undelegate and withdraw funds from the contract. The operations can be performed using both the `controller` and the `factory`. For a full list of all the methods supported check out the auto-generated documentation: +- [DelegationController](https://multiversx.github.io/mx-sdk-py/multiversx_sdk.delegation.html#module-multiversx_sdk.delegation.delegation_controller) +- [DelegationTransactionsFactory](https://multiversx.github.io/mx-sdk-py/multiversx_sdk.delegation.html#module-multiversx_sdk.delegation.delegation_transactions_factory) +#### Creating a new delegation contract using the controller -## Special recipient +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint +# create the entrypoint and the delegation controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_delegation_controller() -### ESDTSystemSCAddress -This type indicates the system smart contract address, which is the same on any MultiversX blockchain. - - **`.to_managed_address()`**: converts the address to **ManagedAddress**. - - **`.to_bech32_str()`**: returns the **str** value of the address. - - **`.to_bech32_string()`**: returns the **String** value of the address. +# the owner of the contract +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -The next example represents a part of a **smart contract** whose aim is to issue semi-fungible tokens. The call is made via a system proxy for the ESDT system smart contract. More details regarding the system proxy can be found [here TBD](https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples/adder). -```rust title=lib.rs -fn sft_issue( - issue_cost: BigUint, - token_display_name: ManagedBuffer, - token_ticker: ManagedBuffer, -) -> IssueCallTo { - Tx::new_tx_from_sc() - .to(ESDTSystemSCAddress) - .typed(ESDTSystemSCProxy) - .issue_semi_fungible( - issue_cost, - &token_display_name, - &token_ticker, - SemiFungibleTokenProperties::default(), - ) -} -``` +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -### ToSelf -It indicates that the transaction should be sent to itself. +transaction = controller.create_transaction_for_new_delegation_contract( + sender=alice, + nonce=alice.get_nonce_then_increment(), + total_delegation_cap=0, # uncapped, + service_fee=0, + amount=1250000000000000000000 # 1250 EGLD +) -The following lines illustrate an example of changing attributes of an NFT via a system proxy function. -```rust title=lib.rs -pub fn nft_update_attributes( - &self, - token_id: &TokenIdentifier, - nft_nonce: u64, - new_attributes: &T, -) { - Tx::new_tx_from_sc() - .to(ToSelf) - .gas(GasLeft) - .typed(system_proxy::UserBuiltinProxy) - .nft_update_attributes(token_id, nft_nonce, new_attributes) - .sync_call() -} -``` +tx_hash = entrypoint.send_transaction(transaction) -### ToCaller -It indicates that the transaction should be sent to the caller, which is the sender of the current transaction. +# wait for transaction completion, extract delegation contract's address +outcome = controller.await_completed_create_new_delegation_contract(tx_hash) -The next example is a snippet from an endpoint that transfers ESDT to the sender of the transaction. -```rust -self.tx().to(ToCaller).single_esdt(&id, nonce, &BigUint::from(1u8)).transfer(); +contract_address = outcome[0].contract_address ``` ---- - -### Relayed Transactions +#### Creating a new delegation contract using the factory -On this page, you will find comprehensive information on all aspects of relayed transactions. +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint, DelegationTransactionsOutcomeParser +# create the entrypoint and the delegation transactions factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_delegation_transactions_factory() -## Introduction +# the owner of the contract +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -Relayed transactions (or meta-transactions) are transactions with the fee paid by a so-called relayer. -In other words, if a relayer is willing to pay for an interaction, it is not mandatory that the address -interacting with a Smart Contract has any EGLD for fees. +transaction = factory.create_transaction_for_new_delegation_contract( + sender=alice.address, + total_delegation_cap=0, # uncapped, + service_fee=0, + amount=1250000000000000000000 # 1250 EGLD +) -More details and specifications can be found on [MultiversX Specs](https://github.com/multiversx/mx-specs/blob/main/sc-meta-transactions.md). +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) +# set the nonce +transaction.nonce = alice.get_nonce_then_increment() -## Types of relayed transactions +# sign the transaction +transaction.signature = alice.sign_transaction(transaction) -Currently, there are 3 versions of relayed transactions: v1, v2 and v3. In the end, they all have the same effect. +# send the transaction +tx_hash = entrypoint.send_transaction(transaction) -Relayed v2 was meant to bring optimisations in terms of gas usage. But v3 reduces the costs even further, **making it our recommendation**. +# waits until the transaction is processed and fetches it from the network +transaction_on_network = entrypoint.await_transaction_completed(tx_hash) -Once all applications will completely transition to relayed v3 model, v1 and v2 will be removed. +# extract the contract's address +parser = DelegationTransactionsOutcomeParser() +outcome = parser.parse_create_new_delegation_contract(transaction_on_network) +contract_address = outcome[0].contract_address +``` -## Relayed transactions version 1 +#### Delegating funds to the contract using the controller -:::note -Legacy version. Please use [version 3](#relayed-transactions-version-3), instead. -::: +We can send funds to a delegation contract to earn rewards. +```py +from pathlib import Path +from multiversx_sdk import Account, Address, DevnetEntrypoint +# create the entrypoint and the delegation controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_delegation_controller() -## Relayed transactions version 2 +# create the account delegating funds +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -:::note -Legacy version. Please use [version 3](#relayed-transactions-version-3), instead. -::: +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) +# delegation contract +contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva") -## Relayed transactions version 3 +transaction = controller.create_transaction_for_delegating( + sender=alice, + nonce=alice.get_nonce_then_increment(), + delegation_contract=contract, + amount=5000000000000000000000 # 5000 EGLD +) -Relayed transactions v3 feature comes with a change on the entire transaction structure, adding two new optional fields: -- `relayer`, which is the relayer address that will pay the fees. -- `relayerSignature`, the signature of the relayer that proves the agreement of the relayer. +tx_hash = entrypoint.send_transaction(transaction) +``` -That being said, relayed transactions v3 will look and behave very similar to a regular transaction, the only difference being the gas consumption from the relayer. It is no longer needed to specify the user transaction in the data field. +#### Delegating funds to the contract using the factory -In terms of gas limit computation, an extra base cost will be consumed. Let's consider the following example: relayed transaction with inner transaction of type move balance, that also has a data field `test` of length 4. -```js - gasLimitInnerTx = + * length(txData) - gasLimitInnerTx = 50_000 + 4 * 1_500 - gasLimitInnerTx = 56_000 - - gasLimitRelayedTx = + - gasLimitRelayedTx = 50_000 + 56_000 - gasLimitRelayedTx = 106_000 -``` +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint -It would look like: +# create the entrypoint and the delegation transactions factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_delegation_transactions_factory() -```rust -RelayedV3Transaction { - Sender: - Receiver: - Value: - GasLimit: + + * length(txData) - Relayer: - RelayerSignature: - Signature: -} -``` +# create the account delegating funds +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -Therefore, in order to build such a transaction, one has to follow the next steps: - - set the `relayer` field to the address that would pay the gas - - add the extra base cost for the relayed operation - - add sender's signature - - add relayer's signature +transaction = factory.create_transaction_for_delegating( + sender=alice.address, + delegation_contract=contract, + amount=5000000000000000000000 # 5000 EGLD +) -:::note -1. For a guarded relayed transaction, the guarded operation fee will also be consumed from the relayer. -2. Relayer must be different from guardian, in case of guarded sender. -3. Guarded relayers are not allowed. -4. Relayer address must be in the same shard as the transaction sender. -::: +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -### Example +# set the nonce +transaction.nonce = alice.get_nonce_then_increment() -Here's an example of a relayed v3 transaction. Its intent is to call the `add` method of a previously deployed adder contract, with parameter `01` +# sign the transaction +transaction.signature = alice.sign_transaction(transaction) -```json -{ - "nonce": 0, - "value": "0", - "receiver": "erd1qqqqqqqqqqqqqpgqeunf87ar9nqeey5ssvpqwe74ehmztx74qtxqs63nmx", - "sender": "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", - "data": "YWRkQDAx", - "gasPrice": 1000000000, - "gasLimit": 5000000, - "signature": "...", - "chainID": "T", - "version": 2, - "relayer": "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - "relayerSignature": "..." -} +# send the transaction +tx_hash = entrypoint.send_transaction(transaction) ``` -### Preparing relayed transactions using the SDKs - -The SDKs have built-in support for relayed transactions. Please follow: - - [mxpy support](/sdk-and-tools/mxpy/mxpy-cli/#relayed-transactions-v3) - - [sdk-py support](/sdk-and-tools/sdk-py/#relayed-transactions) - - [sdk-js v15 support](/sdk-and-tools/sdk-js/sdk-js-cookbook#relayed-transactions) +#### Redelegating rewards using the controller ---- +After a period of time, we might have enough rewards that we want to redelegate to the contract to earn even more rewards. -### Reproducible Builds +```py +from pathlib import Path +from multiversx_sdk import Account, Address, DevnetEntrypoint -This page will guide you through the process of supporting [reproducible contract builds](https://en.wikipedia.org/wiki/Reproducible_builds), by leveraging Docker and a set of [_frozen_ Docker images available on DockerHub](https://hub.docker.com/r/multiversx/sdk-rust-contract-builder/tags). +# create the entrypoint and the delegation controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_delegation_controller() -You will also learn how to reproduce a contract build, given its source code and the name (tag) of a _frozen_ Docker image that was used for its previous build (that we want to reproduce). +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -> **Reproducible builds**, also known as **deterministic compilation**, is a process of compiling software which ensures the resulting binary code can be reproduced. Source code compiled using deterministic compilation will always output the same binary [[Wikipedia]](https://en.wikipedia.org/wiki/Reproducible_builds). +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -:::important -As of May 2024, the Rust toolchain does not support reproducible builds out-of-the-box, thus we recommend smart contract developers to follow this tutorial in order to achieve deterministic compilation. -::: +# delegation contract +contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva") +transaction = controller.create_transaction_for_redelegating_rewards( + sender=alice, + nonce=alice.get_nonce_then_increment(), + delegation_contract=contract +) -## Smart Contract "codehash" +tx_hash = entrypoint.send_transaction(transaction) +``` -Before diving into contract build reproducibility, let's grasp the concept of `codehash`. +#### Redelegating rewards using the factory -When a smart contract is deployed, the network stores the bytecode, and also computes its `blake2b` checksum (using a digest length of 256 bits). This is called the `codehash`. +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint -Assume that we are interested into the following contract (a simple on-chain **adder**), deployed on _devnet_: [erd1qqqqqqqqqqqqqpgqws44xjx2t056nn79fn29q0rjwfrd3m43396ql35kxy](https://devnet-explorer.multiversx.com/accounts/erd1qqqqqqqqqqqqqpgqws44xjx2t056nn79fn29q0rjwfrd3m43396ql35kxy). It's source code is published on [GitHub](https://github.com/multiversx/mx-contracts-rs). +# create the entrypoint and the delegation transactions factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_delegation_transactions_factory() -We can fetch the _codehash_ of the contract from the API: +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -```bash -curl -s https://devnet-api.multiversx.com/accounts/erd1qqqqqqqqqqqqqpgqws44xjx2t056nn79fn29q0rjwfrd3m43396ql35kxy \ -| jq -r -j .codeHash \ -| base64 -d \ -| xxd -p \ -| tr -d '\n' -``` +transaction = factory.create_transaction_for_redelegating_rewards( + sender=alice.address, + delegation_contract=contract +) -The output is: +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -``` -384b680df7a95ebceca02ffb3e760a2fc288dea1b802685ef15df22ae88ba15b -``` +# set the nonce +transaction.nonce = alice.get_nonce_then_increment() -If the `WASM` file is directly available, we can also use the utility `b2sum` to locally compute the _codehash_: +# sign the transaction +transaction.signature = alice.sign_transaction(transaction) -```bash -b2sum -l 256 adder.wasm +# send the transaction +tx_hash = entrypoint.send_transaction(transaction) ``` -The output would be the same: +#### Claiming rewards using the controller -``` -384b680df7a95ebceca02ffb3e760a2fc288dea1b802685ef15df22ae88ba15b -``` +We can also claim our rewards. -All in all, in order to verify the bytecode equality of two given builds of a contract we can simply compare the _codehash_ property. +```py +from pathlib import Path +from multiversx_sdk import Account, Address, DevnetEntrypoint +# create the entrypoint and the delegation controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_delegation_controller() -## Supporting reproducible builds +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -As of May 2024, the recommended approach to support reproducible builds for your smart contract is to use a build script relying on a specially-designed, [publicly-available, tagged Docker image](https://hub.docker.com/r/multiversx/sdk-rust-contract-builder/tags), that includes tagged, explicit versions of the build tools (_Rust_, _wasm-opt_ etc.). +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -This approach is recommended in order to counteract eventual pieces of non-determinism related to `cargo`'s (essential component of the Rust toolchain) sensibility on the environment. +# delegation contract +contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva") -:::important -If the code source of your smart contract is hosted on GitHub, then it's a good practice to define a GitHub Workflow similar to [release.yml](https://github.com/multiversx/mx-contracts-rs/blob/main/.github/workflows/release.yml), which performs the deployment (production-ready) build within the _release_ procedure. Additionally, define a dry-run reproducible build on all your branches. See this workflow as an example: [on_pull_request_build_contracts.yml](https://github.com/multiversx/mx-contracts-rs/blob/main/.github/workflows/on_pull_request_build_contracts.yml). -::: +transaction = controller.create_transaction_for_claiming_rewards( + sender=alice, + nonce=alice.get_nonce_then_increment(), + delegation_contract=contract +) +tx_hash = entrypoint.send_transaction(transaction) +``` -### Choose an image tag +#### Claiming rewards using the factory -For a new smart contract that isn't released yet (deployed on the network), it's recommended to pick the tag with the **largest index number**, which typically includes recent versions of `rust` and other necessary dependencies. +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint -However, for minor releases or patches, it's wise to stick to the previously chosen image tag, for the same (nuanced) reasons you would not embrace an update of your development tools in the middle of fixing a critical bug (in any development context). +# create the entrypoint and the delegation transactions factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_delegation_transactions_factory() -The chosen, _frozen_ image tag **should accompany the versioned source code (e.g. via _release notes_), in order to inform others on how to reproduce a specific build** (of a specific source code version). In this context, a _frozen_ image tag refers to a Docker image tag that will never get any updates after its initial publishing. +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -:::tip -It's perfectly normal to switch to a newer image tag on each (major) release of your contract. Just make sure you spread this information - i.e. using _release notes_. -::: +transaction = factory.create_transaction_for_claiming_rewards( + sender=alice.address, + delegation_contract=contract +) -:::caution -Never pick the tag called `latest` or `next` for production-ready builds. -::: +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) +# set the nonce +transaction.nonce = alice.get_nonce_then_increment() -## Building via Docker (reproducible build) +# sign the transaction +transaction.signature = alice.sign_transaction(transaction) -In this section, you'll learn how to run a reproducible build, or, to put it differently, how to reproduce a previous build (made by you or by someone else in the past), on the local machine, using Docker - without the need to install other tools such as _mxpy_ (nor its dependencies). +# send the transaction +tx_hash = entrypoint.send_transaction(transaction) +``` +#### Undelegating funds using the controller -### Fetch the source code +By undelegating we let the contract know we want to get back our staked funds. This operation has a 10 epochs unbonding period. -Let's clone [mx-contracts-rs](https://github.com/multiversx/mx-contracts-rs) locally, and switch to [a certain version](https://github.com/multiversx/mx-contracts-rs/releases/tag/v0.45.4) that we'd like to build: +```py +from pathlib import Path +from multiversx_sdk import Account, Address, DevnetEntrypoint -```bash -mkdir -p ~/contracts && cd ~/contracts -git clone https://github.com/multiversx/mx-contracts-rs.git --branch=v0.45.4 --depth=1 -``` +# create the entrypoint and the delegation controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_delegation_controller() -By inspecting the release notes, we see that [`v0.45.4`](https://github.com/multiversx/mx-contracts-rs/releases/tag/v0.45.4) was built using the `image:tag = multiversx/sdk-rust-contract-builder:v5.4.1`. +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -### Download the build wrapper +# delegation contract +contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva") -The build process (via Docker) is wrapped in a easy-to-use, friendly Python script. Let's download it: +transaction = controller.create_transaction_for_undelegating( + sender=alice, + nonce=alice.get_nonce_then_increment(), + delegation_contract=contract, + amount=1000000000000000000000 # 1000 EGLD +) -```bash -wget https://raw.githubusercontent.com/multiversx/mx-sdk-build-contract/main/build_with_docker.py +tx_hash = entrypoint.send_transaction(transaction) ``` +#### Undelegating funds using the factory -### Prepare environment variables +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint -Export the following variables: +# create the entrypoint and the delegation transactions factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_delegation_transactions_factory() -```bash -export PROJECT=~/contracts/mx-contracts-rs -export BUILD_OUTPUT=~/contracts/output-from-docker -# Below, the image tag is just an example: -export IMAGE=multiversx/sdk-rust-contract-builder:v1.2.3 -``` +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -The latter export statement explicitly selects the **chosen, _frozen_ Docker image tag** to be used. +transaction = factory.create_transaction_for_undelegating( + sender=alice.address, + delegation_contract=contract, + amount=1000000000000000000000 # 1000 EGLD +) +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -### Perform the build +# set the nonce +transaction.nonce = alice.get_nonce_then_increment() -Now let's build the contract by invoking the previously-downloaded build wrapper: +# sign the transaction +transaction.signature = alice.sign_transaction(transaction) -```bash -python3 ./build_with_docker.py --image=${IMAGE} \ - --project=${PROJECT} \ - --output=${BUILD_OUTPUT} +# send the transaction +tx_hash = entrypoint.send_transaction(transaction) ``` -In the `output` folder(s), you should see the following files (example): +#### Withdrawing funds using the controller -- `adder.wasm`: the actual bytecode of the smart contract, to be deployed on the network; -- `adder.abi.json`: the ABI of the smart contract (a listing of endpoints and types definitions), to be used when developing dApps or simply interacting with the contract (e.g. using _erdjs_); -- `adder.codehash.txt`: a file containing the computed `codehash` of the contract. -- **`adder.source.json`** : packaged (bundled) source code. +After the unbonding period has passed, we can withdraw our funds from the contract. +```py +from pathlib import Path +from multiversx_sdk import Account, Address, DevnetEntrypoint -### TL;DR build snippet +# create the entrypoint and the delegation controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_delegation_controller() -These being said, let's summarize the steps above into a single bash snippet: +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -```bash -wget https://raw.githubusercontent.com/multiversx/mx-sdk-build-contract/main/build_with_docker.py +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -export PROJECT=~/contracts/mx-contracts-rs -export BUILD_OUTPUT=~/contracts/output-from-docker -# Below, the image tag is just an example: -export IMAGE=multiversx/sdk-rust-contract-builder:v1.2.3 +# delegation contract +contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva") -python3 ./build_with_docker.py --image=${IMAGE} \ - --project=${PROJECT} \ - --output=${BUILD_OUTPUT} +transaction = controller.create_transaction_for_withdrawing( + sender=alice, + nonce=alice.get_nonce_then_increment(), + delegation_contract=contract +) + +tx_hash = entrypoint.send_transaction(transaction) ``` +#### Withdrawing funds using the factory -### Reproducible build using mxpy +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint -A more straightforward alternative to the previous bash script is to use **mxpy** to build a contract in a reproducible manner. +# create the entrypoint and the delegation transactions factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_delegation_transactions_factory() -First, make sure you have the: +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -- latest [mxpy](/sdk-and-tools/mxpy/installing-mxpy) installed, -- latest [docker engine](https://docs.docker.com/engine/install/) installed. +transaction = factory.create_transaction_for_withdrawing( + sender=alice.address, + delegation_contract=contract +) -Then, use the `reproducible-build` command (below, the image tag is just an example): +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -``` -mxpy contract reproducible-build --docker-image="multiversx/sdk-rust-contract-builder:v1.2.3" -``` +# set the nonce +transaction.nonce = alice.get_nonce_then_increment() -This will build all the smart contracts inside the current working directory. If you want to build the smart contracts inside another directory, you can specify an input directory: +# sign the transaction +transaction.signature = alice.sign_transaction(transaction) -``` -mxpy contract reproducible-build ~/contracts/mx-contracts-rs --docker-image="multiversx/sdk-rust-contract-builder:v1.2.3" +# send the transaction +tx_hash = entrypoint.send_transaction(transaction) ``` -Upon a successful build, an output folder named `output-docker` will be generated. It contains one subfolder for each contract, each holding the following files: +### Relayed transactions -- `contract.wasm`: the actual bytecode of the smart contract, to be deployed on the network; -- `contract.abi.json`: the ABI of the smart contract (a listing of endpoints and types definitions), to be used when developing dApps or simply interacting with the contract (e.g. using _sdk-js_); -- `contract.codehash.txt`: the computed `codehash` of the contract. -- **`contract-1.2.3.source.json`** : packaged (bundled) source code. +We are currently on the third iteration (V3) of relayed transactions. V1 and V2 will be deactivated soon, so we'll focus on V3. -:::tip -You can run a local test using [these example contracts](https://github.com/multiversx/mx-contracts-rs). -::: +For V3, two new fields have been added on transactions: `relayer` and `relayerSignature`. +Note that: +1. the sender and the relayer can sign the transaction in any order. +2. before any of the sender or relayer can sign the transaction, the `relayer` field must be set. +3. relayed transactions require an additional `50,000` of gas. +4. the sender and the relayer must be in the same network shard. -### Comparing the codehashes +Let’s see how to create a relayed transaction: -Once the build is ready, you can check the codehash of the generated `*.wasm`, by inspecting the file `*.codehash.txt` +```py +from pathlib import Path +from multiversx_sdk import Account, Address, DevnetEntrypoint, Transaction -For our example, that should be: +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +bob = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") -``` -adder.codehash.txt: 384b680df7a95ebceca02ffb3e760a2fc288dea1b802685ef15df22ae88ba15b -``` +# carol will be our relayer, that means she is paying the gas for the transaction +carol = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/carol.pem")) -We can see that it matches the previously fetched (or computed) codehash. That is, the contract deployed at [erd1qqqqqqqqqqqqqpgqws44xjx2t056nn79fn29q0rjwfrd3m43396ql35kxy](https://devnet-explorer.multiversx.com/accounts/erd1qqqqqqqqqqqqqpgqws44xjx2t056nn79fn29q0rjwfrd3m43396ql35kxy) is guaranteed to have been built from the same source code version as the one that we've checked out. +# fetch the sender's nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -**Congratulations!** You've achieved a reproducible contract build 🎉 +# create the transaction +transaction = Transaction( + sender=alice.address, + receiver=bob, + gas_limit=110_000, + chain_id="D", + nonce=alice.get_nonce_then_increment(), + relayer=carol.address, + data="hello".encode() +) +# sender signs the transaction +transaction.signature = alice.sign_transaction(transaction) -## How to verify a smart contract on Explorer? +# relayer signs the transaction +transaction.relayer_signature = carol.sign_transaction(transaction) -The new MultiversX Explorer provides a convenient way to visualise the source code of deployed smart contracts on blockchain. This is the beauty of the Web3 vision for a decentralized internet, where anyone can deploy contracts that everyone can interact with. +# broadcast the transaction +entrypoint = DevnetEntrypoint() +tx_hash = entrypoint.send_transaction(transaction) +``` -:::caution -Please note that as a **Beta** feature still in development, certain steps described may undergo changes. -::: +#### Creating relayed transactions using controllers -:::tip -Make sure that you have the latest `mxpy` installed. In order to install mxpy, follow the instructions at [install mxpy](/sdk-and-tools/mxpy/installing-mxpy). -::: +We can create relayed transactions using any of the controllers. Each controller has a `relayer` argument, that can be set if we want to create a relayed transaction. Let's issue a fungible token creating a relayed transaction: -1. The contract must be deterministically built as described [above](/developers/reproducible-contract-builds#building-via-docker-reproducible-build). -2. To start with the verification process, we need to first deploy the smart contract. For deploying contracts have a look [here](/sdk-and-tools/mxpy/mxpy-cli#deploying-a-smart-contract). -3. Upon deploying, the output will not only provide information such as the transaction hash and data, but also the address of the newly deployed contract. -4. In order to verify your contract the command you have to use is (below, the image tag is just an example): +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint -``` -mxpy --verbose contract verify "erd1qqqqqqqqqqqqqpgq6u07hhkfsvuk5aae92g549s6pc2s9ycq0dps368jr5" --packaged-src=./output-docker/contract/contract-0.0.0.source.json --verifier-url="https://play-api.multiversx.com" --docker-image="multiversx/sdk-rust-contract-builder:v1.2.3" --pem=contract-owner.pem -``` +# create the entrypoint and the token management controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_token_management_controller() -:::tip -For the above code snippet: -- `erd1qqqqqqqqqqqqqpgq6u07hhkfsvuk5aae92g549s6pc2s9ycq0dps368jr5` - should be your contract address (in this case is a dummy address); -- `--packaged-src=./output-docker/contract/contract-0.0.0.source.json` - should be found in the output folder after deterministically building your contract; -- `--verifier-url="https://play-api.multiversx.com"` - this is the verifier api address. Be advised that it may be subject to change; -- `--docker-image="multiversx/sdk-rust-contract-builder:v1.2.3"` - the same version utilized in constructing the contract must be utilized here too; -- `--pem=contract-owner.pem` - represents the owner of the contract. -::: +# create the issuer of the token +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -5. Given the current limited bandwidth, it might take some time to be processed. +# carol will be our relayer, that means she is paying the gas for the transaction +carol = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/carol.pem")) ---- +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -### Rest API Addresses +transaction = controller.create_transaction_for_issuing_fungible( + sender=alice, + nonce=alice.get_nonce_then_increment(), + token_name="NEWTOKEN", + token_ticker="TKN", + initial_supply=1_000_000_000000, # 1 million tokens, with 6 decimals + num_decimals=6, + can_freeze=False, + can_wipe=True, + can_pause=False, + can_change_owner=True, + can_upgrade=True, + can_add_special_roles=True, + relayer=carol.address +) -```mdx-code-block -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; +# relayer also signs the transaction +transaction.relayer_signature = carol.sign_transaction(transaction) + +# sending the transaction +tx_hash = entrypoint.send_transaction(transaction) ``` +#### Create relayed transactions using factories -This component of the REST API allows one to query information about Addresses (Accounts). +The transactions factories do not have a `relayer` argument, the relayer needs to be set after creating the transaction. This is good because the transaction is not signed by the sender when created. Let's issue a fungible token using the `TokenManagementTransactionsFactory`: +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint -## GET **Get Address** {#get-address} +# create the entrypoint and the token management transactions factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_token_management_transactions_factory() -`https://gateway.multiversx.com/address/:bech32Address` +# create the issuer of the token +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -This endpoint allows one to retrieve basic information about an Address (Account). +# carol will be our relayer, that means she is paying the gas for the transaction +carol = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/carol.pem")) - - +transaction = factory.create_transaction_for_issuing_fungible( + sender=alice.address, + token_name="NEWTOKEN", + token_ticker="TKN", + initial_supply=1_000_000_000000, # 1 million tokens, with 6 decimals + num_decimals=6, + can_freeze=False, + can_wipe=True, + can_pause=False, + can_change_owner=True, + can_upgrade=True, + can_add_special_roles=True +) -Path Parameters +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -| Param | Required | Type | Description | -| ----- | ----------------------------------------- | -------- | ------------------------- | -| bech32Address | REQUIRED | `string` | The Address to query. | +# set the nonce of the sender +transaction.nonce = alice.get_nonce_then_increment() - - +# set the relayer +transaction.relayer = carol.address -🟢 200: OK +# sender signs the transaction +transaction.signature = alice.sign_transaction(transaction) -Address details retrieved successfully. +# relayer signs the transaction +transaction.relayer_signature = carol.sign_transaction(transaction) -```json -{ - "data": { - "account": { - "address": "erd1...", - "nonce": 11, - "balance": "100000000000000000000", - "username": "", - "code": "", - "codeHash": null, - "rootHash": "qBFvpFeF6...", - "codeMetadata": null, - "developerReward": "0", - "ownerAddress": "", - } - }, - "blockInfo":{ - "nonce": 555, - "hash": "f55fe00...", - "rootHash": "294360..." - } - "error": "", - "code": "successful" -} +# broadcast the transaction +tx_hash = entrypoint.send_transaction(transaction) ``` - - - -:::important -If an account (that is not a smart contract, smart contracts cannot be guarded) has an ```activeGuardian``` and is ```guarded```, the ```codeMetadata``` of the account should be [Guarded](https://github.com/multiversx/mx-chain-vm-common-go/blob/master/codeMetadata.go). -::: - +### Guarded Transactions -## GET **Get Address Guardian Data** {#get-address-guardian-data} +#### Creating guarded transactions using controllers -`https://gateway.multiversx.com/address/:bech32Address/guardian-data` +Very similar to relayers, we have a field `guardian` and a field `guardianSignature`. Each controller has an argument for the guardian. The transaction can be sent to a service that signs it using the guardian's account or we can use another account as a guardian. Let's issue a token using a guarded account. -This endpoint allows one to retrieve the guardian data of an Address. +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint - - +# create the entrypoint and the token management controller +entrypoint = DevnetEntrypoint() +controller = entrypoint.create_token_management_controller() -Path Parameters +# create the issuer of the token +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -| Param | Required | Type | Description | -| ------------- | ----------------------------------------- | -------- | --------------------- | -| bech32Address | REQUIRED | `string` | The Address to query. | +# carol is the guardian +carol = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/carol.pem")) - - +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -🟢 200: OK +transaction = controller.create_transaction_for_issuing_fungible( + sender=alice, + nonce=alice.get_nonce_then_increment(), + token_name="NEWTOKEN", + token_ticker="TKN", + initial_supply=1_000_000_000000, # 1 million tokens, with 6 decimals + num_decimals=6, + can_freeze=False, + can_wipe=True, + can_pause=False, + can_change_owner=True, + can_upgrade=True, + can_add_special_roles=True, + guardian=carol.address +) -Guardian Data successfully retrieved. +# guardian also signs the transaction +transaction.guardian_signature = carol.sign_transaction(transaction) -```json -{ - "data": { - "blockInfo": { - "hash":"a11aa...", - "nonce":197, - "rootHash":"a6d70..." - }, - "guardianData": { - "activeGuardian": { - "activationEpoch": 15, - "address": "erd1...", - "serviceUID": "uuid" - }, - "guarded": true, - "pendingGuardian": { - "activationEpoch": 35, - "address": "erd1...", - "serviceUID": "uuid" - }, - }, - }, - "error": "", - "code": "successful" -} +# sending the transaction +tx_hash = entrypoint.send_transaction(transaction) ``` - - - -:::caution -In the response example mentioned above, the account has already set the ```activeGuardian``` (using a ```SetGuardian``` transaction), guarded the account (with a ```GuardAccount``` transaction), and also set the ```pendingGuardian``` (using an unguarded ```SetGuardian``` transaction). We intentionally chose this scenario to display all the fields for ```blockInfo``` and ```guardianData```. -::: - - -## GET **Get Address Nonce** {#get-address-nonce} +#### Creating guarded transactions using factories -`https://gateway.multiversx.com/address/:bech32Address/nonce` +The transactions factories do not have a `guardian` argument, the guardian needs to be set after creating the transaction. This is good because the transaction is not signed by the sender when created. Let's issue a fungible token using the `TokenManagementTransactionsFactory`: -This endpoint allows one to retrieve the nonce of an Address. +```py +from pathlib import Path +from multiversx_sdk import Account, DevnetEntrypoint - - +# create the entrypoint and the token management transactions factory +entrypoint = DevnetEntrypoint() +factory = entrypoint.create_token_management_transactions_factory() -Path Parameters +# create the issuer of the token +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -| Param | Required | Type | Description | -| ------------- | ----------------------------------------- | -------- | --------------------- | -| bech32Address | REQUIRED | `string` | The Address to query. | +# carol is the guardian +carol = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/carol.pem")) - - +transaction = factory.create_transaction_for_issuing_fungible( + sender=alice.address, + token_name="NEWTOKEN", + token_ticker="TKN", + initial_supply=1_000_000_000000, # 1 million tokens, with 6 decimals + num_decimals=6, + can_freeze=False, + can_wipe=True, + can_pause=False, + can_change_owner=True, + can_upgrade=True, + can_add_special_roles=True +) -🟢 200: OK +# fetch the nonce of the network +alice.nonce = entrypoint.recall_account_nonce(alice.address) -Nonce successfully retrieved. +# set the nonce of the sender +transaction.nonce = alice.get_nonce_then_increment() -```json -{ - "data": { - "nonce": 5 - }, - "error": "", - "code": "successful" -} -``` +# set the guardian +transaction.guardian = carol.address - - +# sender signs the transaction +transaction.signature = alice.sign_transaction(transaction) +# guardian signs the transaction +transaction.guardian_signature = carol.sign_transaction(transaction) -## GET **Get Address Balance** {#get-address-balance} +# broadcast the transaction +tx_hash = entrypoint.send_transaction(transaction) +``` -`https://gateway.multiversx.com/address/:bech32Address/balance` +We can also create guarded relayed transactions the same way we did before. Keep in mind that, only the sender can be guarded, the relayer cannot. The same flow can be used. Using controllers, we set both `guardian` and `relayer` fields and then the transaction should be signed by both. Using a factory, we create the transaction, set both both fields and then sign the transaction using the sender's account, then the the guardian and the relayer sign the transaction. -This endpoint allows one to retrieve the balance of an Address. +### Multisig - - +The sdk contains components to interact with the [Multisig Contract](https://github.com/multiversx/mx-contracts-rs/releases/tag/v0.45.5). We can deploy a multisig smart contract, add members, propose and execute actions and query the contract. The same as the other components, to interact with a multisig smart contract we can use either the `MultisigController` or the `MultisigTransactionsFactory`. -Path Parameters +#### Deploying a Multisig Smart Contract using the controller -| Param | Required | Type | Description | -| ------------- | ----------------------------------------- | -------- | --------------------- | -| bech32Address | REQUIRED | `string` | The Address to query. | +```py +from pathlib import Path +from multiversx_sdk import Account, Address, ApiNetworkProvider, MultisigController +from multiversx_sdk.abi import Abi - - -🟢 200: OK +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +api = ApiNetworkProvider(url="https://devnet-api.multiversx.com") +alice.nonce = api.get_account(alice.address).nonce -Balance successfully retrieved. +abi = Abi.load(Path("../multiversx_sdk/testutils/testdata/multisig-full.abi.json")) +controller = MultisigController(chain_id="D", network_provider=api, abi=abi) -```json -{ - "data": { - "balance": "100000000000000000000" - }, - "error": "", - "code": "successful" -} +transaction = controller.create_transaction_for_deploy( + sender=alice, + nonce=alice.get_nonce_then_increment(), + bytecode=Path("../multiversx_sdk/testutils/testdata/multisig-full.wasm"), + quorum=2, + board=[alice.address, Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx")], + gas_limit=100_000_000, +) + +# send the transaction +tx_hash = api.send_transaction(transaction) ``` - - +#### Deploying a Multisig Smart Contract using the factory +```py +from pathlib import Path +from multiversx_sdk import Account, Address, ApiNetworkProvider, MultisigTransactionsFactory, TransactionsFactoryConfig +from multiversx_sdk.abi import Abi -## GET **Get Address Username (herotag)** {#get-address-username-herotag} -`https://gateway.multiversx.com/address/:bech32Address/username` +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +api = ApiNetworkProvider(url="https://devnet-api.multiversx.com") +alice.nonce = api.get_account(alice.address).nonce -This endpoint allows one to retrieve the username / herotag of an Address (if any). +abi = Abi.load(Path("../multiversx_sdk/testutils/testdata/multisig-full.abi.json")) +config = TransactionsFactoryConfig(chain_id="D") +factory = MultisigTransactionsFactory(config=config, abi=abi) - - +transaction = factory.create_transaction_for_deploy( + sender=alice.address, + bytecode=Path("../multiversx_sdk/testutils/testdata/multisig-full.wasm"), + quorum=2, + board=[alice.address, Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx")], + gas_limit=100_000_000, +) +transaction.nonce = alice.get_nonce_then_increment() +transaction.signature = alice.sign_transaction(transaction) -Path Parameters +tx_hash = api.send_transaction(transaction) +``` -| Param | Required | Type | Description | -| ------------- | ----------------------------------------- | -------- | --------------------- | -| bech32Address | REQUIRED | `string` | The Address to query. | +#### Propose an action using the controller - - +We'll propose an action to send some EGLD to Carol. After we sent the proposal, we'll also parse the outcome of the transaction to get the `proposal id`. The id can be later for signing and performing the proposal. -🟢 200: OK +```py +from pathlib import Path +from multiversx_sdk import Account, Address, ApiNetworkProvider, MultisigController +from multiversx_sdk.abi import Abi -Balance successfully retrieved. -```json -{ - "data": { - "username": "docs.elrond" - }, - "error": "", - "code": "successful" -} -``` +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +api = ApiNetworkProvider(url="https://devnet-api.multiversx.com") +alice.nonce = api.get_account(alice.address).nonce - - +abi = Abi.load(Path("../multiversx_sdk/testutils/testdata/multisig-full.abi.json")) +controller = MultisigController(chain_id="D", network_provider=api, abi=abi) +transaction = controller.create_transaction_for_propose_transfer_execute( + sender=alice, + nonce=alice.get_nonce_then_increment(), + contract=Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq2ukrsg73nwgu3uz6sp8vequuyrhtv2akd8ssyrg7wj"), + receiver=Address.new_from_bech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"), + gas_limit=10_000_000, + native_token_amount=1_000000000000000000, # 1 EGLD +) -## GET **Get Address Transactions (deprecated)** {#get-address-transactions} +# send the transaction +tx_hash = api.send_transaction(transaction) -`https://gateway.multiversx.com/address/:bech32Address/transactions` +# parse the outcome and get the proposal id +action_id = controller.await_completed_execute_propose_any(tx_hash) +``` -:::caution -This endpoint is deprecated. In order to fetch the Transactions involving an Address, use the [transactions](https://api.multiversx.com/#/accounts/AccountController_getAccountTransactions) endpoint of MultiversX API, instead. -::: +#### Propose an action using the factory -This endpoint allows one to retrieve the latest 20 Transactions sent from an Address. +Proposing an action for a multisig contract using the `MultisigFactory` is very similar to using the controller, but in order to get the proposal id we need to use `MultisigTransactionsOutcomeParser`. - - +```py +from pathlib import Path +from multiversx_sdk import Account, Address, ApiNetworkProvider, MultisigTransactionsFactory, TransactionsFactoryConfig, MultisigTransactionsOutcomeParser, TransactionAwaiter +from multiversx_sdk.abi import Abi -Path Parameters -| Param | Required | Type | Description | -| ------------- | ----------------------------------------- | -------- | --------------------- | -| bech32Address | REQUIRED | `string` | The Address to query. | +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +api = ApiNetworkProvider(url="https://devnet-api.multiversx.com") +alice.nonce = api.get_account(alice.address).nonce - - +abi = Abi.load(Path("../multiversx_sdk/testutils/testdata/multisig-full.abi.json")) +config = TransactionsFactoryConfig(chain_id="D") +factory = MultisigTransactionsFactory(config=config, abi=abi) -🟢 200: OK +transaction = factory.create_transaction_for_propose_transfer_execute( + sender=alice.address, + contract=Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq2ukrsg73nwgu3uz6sp8vequuyrhtv2akd8ssyrg7wj"), + receiver=Address.new_from_bech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"), + gas_limit=10_000_000, + native_token_amount=1_000000000000000000, # 1 EGLD +) +transaction.nonce = alice.get_nonce_then_increment() +transaction.signature = alice.sign_transaction(transaction) -Transactions successfully retrieved. +tx_hash = api.send_transaction(transaction) -```json -{ - "data": { - "transactions": [ - { - "hash": "1a3e...", - "fee": "10000000000000000", - "miniBlockHash": "9673...", - "nonce": 68, - "round": 33688, - "value": "1000000000000000000", - "receiver": "erd1...", - "sender": "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz", - "receiverShard": 0, - "senderShard": 0, - "gasPrice": 200000000000, - "gasLimit": 50000, - "gasUsed": 50000, - "data": "", - "signature": "ed75...", - "timestamp": 1591258128, - "status": "Success", - "scResults": null - }, - { - "hash": "d72d...", - "fee": "10000000000000000", - "miniBlockHash": "fd45...", - "nonce": 67, - "round": 27353, - "value": "100000000000000000000000000", - "receiver": "erd1...", - "sender": "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz", - "receiverShard": 1, - "senderShard": 0, - "gasPrice": 200000000000, - "gasLimit": 50000, - "gasUsed": 50000, - "data": "", - "signature": "bb98...", - "timestamp": 1591220142, - "status": "Success", - "scResults": null - }, - ... - ] - }, - "error": "", - "code": "successful" -} +# wait for the transaction to execute +transaction_awaiter = TransactionAwaiter(fetcher=api) +transaction_awaiter.await_completed(tx_hash) + +# parse the outcome of the transaction +parser = MultisigTransactionsOutcomeParser(abi=abi) +transaction_on_network = api.get_transaction(tx_hash) +action_id = parser.parse_propose_action(transaction_on_network) ``` - - +#### Querying the Multisig Smart Contract -:::caution -This endpoint is not available on Observer Nodes. It is only available on MultiversX Proxy. +Unlike creating transactions, querying the multisig can be performed only using the controller. Let's query the contract to get all board members. -**Currently, this endpoint is only available on the Official MultiversX Proxy instance.** +```py +from pathlib import Path +from multiversx_sdk import Address, ApiNetworkProvider, MultisigController +from multiversx_sdk.abi import Abi -This endpoint requires the presence of an Elasticsearch instance (populated through Observers) as well. -::: +abi = Abi.load(Path("../multiversx_sdk/testutils/testdata/multisig-full.abi.json")) +api = ApiNetworkProvider(url="https://devnet-api.multiversx.com") +controller = MultisigController(chain_id="D", network_provider=api, abi=abi) +contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq2ukrsg73nwgu3uz6sp8vequuyrhtv2akd8ssyrg7wj") +board_members = controller.get_all_board_members(contract) +``` -## GET **Get Storage Value for Address** {#get-storage-value-for-address} +### Governance -`https://gateway.multiversx.com/address/:bech32Address/key/:key` +We can create transactions for creating a new governance proposal, vote for a proposal or query the governance contract. -This endpoint allows one to retrieve a value stored within the Blockchain for a given Address. +#### Creating a new proposal using the controller - - +```py +from multiversx_sdk import Account, ProxyNetworkProvider, GovernanceController -Path Parameters +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +proxy = ProxyNetworkProvider("https://devnet-gateway.multiversx.com") +alice.nonce = proxy.get_account(alice.address).nonce -| Param | Required | Type | Description | -| ------------- | ----------------------------------------- | -------- | ----------------------- | -| bech32Address | REQUIRED | `string` | The Address to query. | -| key | REQUIRED | `string` | The key entry to fetch. | +controller = GovernanceController(chain_id="D", network_provider=proxy) +commit_hash = "1db734c0315f9ec422b88f679ccfe3e0197b9d67" -The key must be hex-encoded. +transaction = controller.create_transaction_for_new_proposal( + sender=alice, + nonce=alice.get_nonce_then_increment(), + commit_hash=commit_hash, + start_vote_epoch=10, + end_vote_epoch=15, + native_token_amount=500_000000000000000000, +) - - +# send the transaction +tx_hash = proxy.send_transaction(transaction) -🟢 200: OK +# get proposal outcome +[proposal] = controller.await_completed_new_proposal(tx_hash) +print(proposal.proposal_nonce) +print(proposal.commit_hash) +print(proposal.start_vote_epoch) +print(proposal.end_vote_epoch) +``` -Value (hex-encoded) successfully retrieved. +#### Creating a new proposal using the factory -```json -{ - "data": { - "value": "abba" - }, - "error": "", - "code": "successful" -} -``` +```py +from multiversx_sdk import (Account, ProxyNetworkProvider, GovernanceTransactionsFactory, + GovernanceTransactionsOutcomeParser, TransactionsFactoryConfig, TransactionAwaiter) - - +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +proxy = ProxyNetworkProvider("https://devnet-gateway.multiversx.com") +alice.nonce = proxy.get_account(alice.address).nonce +config = TransactionsFactoryConfig(chain_id="D") +factory = GovernanceTransactionsFactory(config) +commit_hash = "1db734c0315f9ec422b88f679ccfe3e0197b9d67" -## GET **Get all storage for Address** {#get-all-storage-for-address} +transaction = factory.create_transaction_for_new_proposal( + sender=alice.address, + commit_hash=commit_hash, + start_vote_epoch=10, + end_vote_epoch=15, + native_token_amount=500_000000000000000000, +) +transaction.nonce = alice.get_nonce_then_increment() +transaction.signature = alice.sign_transaction(transaction) -`https://gateway.multiversx.com/address/:bech32Address/keys` +# send the transaction +tx_hash = proxy.send_transaction(transaction) -This endpoint allows one to retrieve all the key-value pairs stored under a given account. +# make sure the transaction is complete +awaiter = TransactionAwaiter(fetcher=proxy) +transaction_on_network = awaiter.await_completed(tx_hash) - - +# get proposal outcome +parser = GovernanceTransactionsOutcomeParser() +[proposal] = parser.parse_new_proposal(transaction_on_network=transaction_on_network) -Path Parameters +print(proposal.proposal_nonce) +print(proposal.commit_hash) +print(proposal.start_vote_epoch) +print(proposal.end_vote_epoch) +``` -| Param | Required | Type | Description | -| ------------- | ----------------------------------------- | -------- | --------------------- | -| bech32Address | REQUIRED | `string` | The Address to query. | +#### Vote for a proposal using the controller - - +```py +from multiversx_sdk import Account, ProxyNetworkProvider, GovernanceController, VoteType -🟢 200: OK -Key-value pairs (both hex-encoded) successfully retrieved. +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +proxy = ProxyNetworkProvider("https://devnet-gateway.multiversx.com") +alice.nonce = proxy.get_account(alice.address).nonce -```json -{ - "data": { - "pairs": { - "abba": "6f6b" - ... - } - }, - "error": "", - "code": "successful" -} +controller = GovernanceController(chain_id="D", network_provider=proxy) + +transaction = controller.create_transaction_for_voting( + sender=alice, + nonce=alice.get_nonce_then_increment(), + proposal_nonce=1, + vote=VoteType.YES +) + +# send the transaction +tx_hash = proxy.send_transaction(transaction) + +# get vote outcome +[vote] = controller.await_completed_vote(tx_hash) +print(vote.proposal_nonce) +print(vote.vote) +print(vote.total_stake) +print(vote.total_voting_power) ``` - - +#### Vote for a proposal using the factory +```py +from multiversx_sdk import (Account, ProxyNetworkProvider, GovernanceTransactionsFactory, + GovernanceTransactionsOutcomeParser, TransactionsFactoryConfig, + TransactionAwaiter, VoteType) -## **ESDT tokens endpoints** +alice = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) +proxy = ProxyNetworkProvider("https://devnet-gateway.multiversx.com") +alice.nonce = proxy.get_account(alice.address).nonce -There are a number of ESDT tokens endpoints that one can use to check all tokens of an address, balance for -specific fungible or non-fungible tokens or so on. +config = TransactionsFactoryConfig(chain_id="D") +factory = GovernanceTransactionsFactory(config) -Fungible tokens endpoints can be found [here](/tokens/fungible-tokens/#rest-api) and non-fungible tokens -endpoints can be found [here](/tokens/nft-tokens/#rest-api). +transaction = factory.create_transaction_for_voting( + sender=alice.address, + proposal_nonce=1, + vote=VoteType.YES, +) +transaction.nonce = alice.get_nonce_then_increment() +transaction.signature = alice.sign_transaction(transaction) ---- +# send the transaction +tx_hash = proxy.send_transaction(transaction) -### Rest API Blocks +# make sure the transaction is complete +awaiter = TransactionAwaiter(fetcher=proxy) +transaction_on_network = awaiter.await_completed(tx_hash) -```mdx-code-block -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; +# get vote outcome +parser = GovernanceTransactionsOutcomeParser() +[vote] = parser.parse_vote(transaction_on_network=transaction_on_network) + +print(vote.proposal_nonce) +print(vote.vote) +print(vote.total_stake) +print(vote.total_voting_power) ``` +#### Querying the governance contract -This component of the REST API allows one to query information about Blocks and Hyperblocks. +Unlike creating transactions, querying the contract is only possible using the controller. Let's query the contract to get more details about a proposal. +```py +from multiversx_sdk import ProxyNetworkProvider, GovernanceController -## GET **Get Hyperblock by Nonce** {#get-hyperblock-by-nonce} -`https://gateway.multiversx.com/hyperblock/by-nonce/:nonce` +proxy = ProxyNetworkProvider("https://devnet-gateway.multiversx.com") +controller = GovernanceController(chain_id="D", network_provider=proxy) -This endpoint allows one to query a Hyperblock by its nonce. +proposal_info = controller.get_proposal(proposal_nonce=1) +``` - - +## Addresses -Path Parameters +Create an `Address` object from a _bech32-encoded_ string: -| Param | Required | Type | Description | -| ----- | ----------------------------------------- | -------- | ------------------------- | -| nonce | REQUIRED | `number` | The Block nonce (height). | +```py +from multiversx_sdk import Address - - +address = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -🟢 200: OK +print("Address (bech32-encoded)", address.to_bech32()) +print("Public key (hex-encoded):", address.to_hex()) +print("Public key (hex-encoded):", address.get_public_key().hex()) +``` -Block details retrieved successfully. +Create an address from a _hex-encoded_ string - note that you have to provide the address prefix, also known as the **HRP** (_human-readable part_ of the address). If not provided, the default one will be used. -```json -{ - "hyperblock": { - "nonce": 185833, - "round": 186582, - "hash": "6a33...", - "prevBlockHash": "aa7e...", - "epoch": 12, - "numTxs": 1, - "shardBlocks": [ - { - "hash": "cba4...", - "nonce": 186556, - "shard": 0 - }, - { - "hash": "50a16...", - "nonce": 186535, - "shard": 1 - }, - { - "hash": "7981...", - "nonce": 186536, - "shard": 2 - } - ], - "transactions": [ - { - "type": "normal", - "hash": "b035...", - "nonce": 3, - "value": "1000000000000000000", - "receiver": "erd1...", - "sender": "erd1...", - "gasPrice": 1000000000, - "gasLimit": 70000, - "data": "Zm9yIHRlc3Rz", - "signature": "1047...", - "status": "executed" - } - ] - } -} +```py +from multiversx_sdk import Address + +address = Address.new_from_hex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1", "erd") ``` - - +Create an address from a raw public key: -:::important -This endpoint is only defined by the Proxy. The Observer does not expose this endpoint. -::: +```py +from multiversx_sdk import Address -:::tip -A **Hyperblock** is a block-like abstraction that reunites the data from all shards, and contains only **fully-executed transactions** (that is, transactions executed both in _source_ and in _destination_ shard). +pubkey = bytes.fromhex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") +address = Address(pubkey, "erd") +``` -A **hyperblock** is composed using a **metablock** as a starting point - therefore, the `nonce` or `hash` of a hyperblock is the same as the `nonce` or `hash` of the base metablock. -::: +Alternatively, you can use an `AddressFactory` (initialized with a specific **HRP**) to create addresses. If the hrp is not provided, the default one will be used. +```py +from multiversx_sdk import AddressFactory -## GET **Get Hyperblock by Hash** {#get-hyperblock-by-hash} +factory = AddressFactory("erd") -`https://gateway.multiversx.com/hyperblock/by-hash/:hash` +address = factory.create_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +address = factory.create_from_hex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") +address = factory.create_from_public_key(bytes.fromhex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1")) +``` -This endpoint allows one to query a Hyperblock by its hash. +### Getting the shard of an address - - +```py +from multiversx_sdk import Address, AddressComputer -Path Parameters +address_computer = AddressComputer() +address = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -| Param | Required | Type | Description | -| ----- | ----------------------------------------- | -------- | --------------- | -| hash | OPTIONAL | `string` | The Block hash. | +print("Shard:", address_computer.get_shard_of_address(address)) +``` - - +### Checking if the address is a smart contract address -🟢 200: OK +```py +from multiversx_sdk import Address -```json -{ - "hyperblock": { - "nonce": 185833, - "round": 186582, - "hash": "6a33...", - "prevBlockHash": "aa7e...", - "epoch": 12, - "numTxs": 1, - "shardBlocks": [ - { - "hash": "cba4...", - "nonce": 186556, - "shard": 0 - }, - { - "hash": "50a16...", - "nonce": 186535, - "shard": 1 - }, - { - "hash": "7981...", - "nonce": 186536, - "shard": 2 - } - ], - "transactions": [ - { - "type": "normal", - "hash": "b035...", - "nonce": 3, - "value": "1000000000000000000", - "receiver": "erd1...", - "sender": "erd1...", - "gasPrice": 1000000000, - "gasLimit": 70000, - "data": "Zm9yIHRlc3Rz", - "signature": "1047...", - "status": "executed" - } - ] - } -} +address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgquzmh78klkqwt0p4rjys0qtp3la07gz4d396qn50nnm") +print("Is contract address:", address.is_smart_contract()) ``` - - +### Changing the default hrp -:::important -This endpoint is only is only defined by the Proxy. The Observer does not expose this endpoint. -::: +We have a configuration class, called `LibraryConfig`, that only stores (for the moment) the **default hrp** of the addresses. The default value is `erd`. The hrp can be changed when instantiating an address, or it can be changed in the `LibraryConfig` class, and all the addresses created will have the newly set hrp. +```py +from multiversx_sdk import Address +from multiversx_sdk import LibraryConfig -## GET **Get Block by Nonce** {#get-block-by-nonce} -`https://gateway.multiversx.com/block/:shard/by-nonce/:nonce` +print(LibraryConfig.default_address_hrp) +address = Address.new_from_hex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") +print(address.to_bech32()) -This endpoint allows one to query a Shard Block by its nonce (or height). +LibraryConfig.default_address_hrp = "test" +address = Address.new_from_hex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") +print(address.to_bech32()) - - +# setting back the default value +LibraryConfig.default_address_hrp = "erd" +``` -Path Parameters +## Wallets -| Param | Required | Type | Description | -| ----- | ----------------------------------------- | -------- | ------------------------- | -| shard | OPTIONAL | `number` | The Shard. | -| nonce | REQUIRED | `number` | The Block nonce (height). | +### Generating a mnemonic -Query Parameters +Mnemonic generation is based on [`trezor/python-mnemonic`](https://github.com/trezor/python-mnemonic) and can be achieved as follows: -| Param | Required | Type | Description | -| ------- | ----------------------------------------- | --------- | ---------------------------------------------------- | -| withTxs | OPTIONAL | `boolean` | Whether to include the transactions in the response. | +```py +from multiversx_sdk import Mnemonic - - +mnemonic = Mnemonic.generate() +words = mnemonic.get_words() -🟢 200: OK +print(words) +``` -Block retrieved successfully, with transactions included. +### Saving the mnemonic to a keystore file -```json -{ - "data": { - "block": { - "nonce": 186532, - "round": 186576, - "hash": "7aa3...", - "prevBlockHash": "2580...", - "epoch": 12, - "shard": 2, - "numTxs": 1, - "miniBlocks": [ - { - "hash": "e927...", - "type": "TxBlock", - "sourceShard": 2, - "destinationShard": 1, - "transactions": [ - { - "type": "normal", - "hash": "b035...", - "nonce": 3, - "value": "1000000000000000000", - "receiver": "erd1...", - "sender": "erd1...", - "gasPrice": 1000000000, - "gasLimit": 70000, - "data": "Zm9yIHRlc3Rz", - "signature": "1047...", - "status": "partially-executed" - } - ] - } - ] - } - }, - "error": "", - "code": "successful" -} -``` +The mnemonic can be saved to a keystore file: - - +```py +from pathlib import Path +from multiversx_sdk import Mnemonic, UserWallet -:::important -For Observers, the `shard` parameter should not be set. -::: +mnemonic = Mnemonic.generate() +# saves the mnemonic to a keystore file with kind=mnemonic +wallet = UserWallet.from_mnemonic(mnemonic.get_text(), "password") +wallet.save(Path("walletWithMnemonic.json")) +``` -## GET **Get Block by Hash** {#get-hyperblock-by-hash} +#### Deriving secret keys from a mnemonic -`https://gateway.multiversx.com/block/:shard/by-hash/:hash` +Given a mnemonic, we can derive keypairs: -This endpoint allows one to query a Shard Block by its hash. +```py +from multiversx_sdk import Mnemonic - - +mnemonic = Mnemonic.generate() -Path Parameters +secret_key = mnemonic.derive_key(0) +public_key = secret_key.generate_public_key() -| Param | Required | Type | Description | -| ----- | ----------------------------------------- | -------- | --------------- | -| shard | OPTIONAL | `number` | The Shard. | -| hash | REQUIRED | `string` | The Block hash. | +print("Secret key:", secret_key.hex()) +print("Public key:", public_key.hex()) +``` -Query Parameters +#### Saving a secret key to a keystore file -| Param | Required | Type | Description | -| ------- | ----------------------------------------- | --------- | ---------------------------------------------------- | -| withTxs | OPTIONAL | `boolean` | Whether to include the transactions in the response. | +The secret key can also be saved to a keystore file: - - +```py +from pathlib import Path +from multiversx_sdk import Mnemonic, UserWallet -🟢 200: OK +mnemonic = Mnemonic.generate() -Block retrieved successfully, with transactions included. +# by default, derives using the index = 0 +secret_key = mnemonic.derive_key() -```json -{ - "data": { - "block": { - "nonce": 186532, - "round": 186576, - "hash": "7aa3...", - "prevBlockHash": "2580...", - "epoch": 12, - "shard": 2, - "numTxs": 1, - "miniBlocks": [ - { - "hash": "e927...", - "type": "TxBlock", - "sourceShard": 2, - "destinationShard": 1, - "transactions": [ - { - "type": "normal", - "hash": "b035...", - "nonce": 3, - "value": "1000000000000000000", - "receiver": "erd1...", - "sender": "erd1...", - "gasPrice": 1000000000, - "gasLimit": 70000, - "data": "Zm9yIHRlc3Rz", - "signature": "1047...", - "status": "partially-executed" - } - ] - } - ] - } - }, - "error": "", - "code": "successful" -} +# saves the mnemonic to a keystore file with kind=secretKey +wallet = UserWallet.from_secret_key(secret_key, "password") +wallet.save(Path("walletWithSecretKey.json")) ``` - - +#### Saving a secrey key to a PEM file -:::important -For Observers, the `shard` parameter should not be set. -::: +We can save a secret key to a pem file. **This is not recommended as it is not secure, but it's very convenient for testing purposes.** ---- +```py +from pathlib import Path +from multiversx_sdk import Address, UserPEM -### Rest API Network +mnemonic = Mnemonic.generate() -```mdx-code-block -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -``` +# by default, derives using the index = 0 +secret_key = mnemonic.derive_key() +label = Address(public_key.buffer, "erd").to_bech32() +pem = UserPEM(label=label, secret_key=secret_key) +pem.save(Path("wallet.pem")) +``` -This component of the REST API allows one to query information about the Network, such as network configuration and parameters. +### Generating a KeyPair +A `KeyPair` is a wrapper over a secret key and a public key. We can create a keypair and use it for signing or verifying. -## GET **Get Network Configuration** {#get-network-configuration} +```py +from multiversx_sdk import KeyPair -`https://gateway.multiversx.com/network/config` +keypair = KeyPair.generate() -This endpoint allows one to query basic details about the configuration of the Network. +# get secret key +secret_key = keypair.get_secret_key() - - +# get public key +public_key = keypair.get_public_key() +``` -🟢 200: OK +### Loading a wallets from keystore mnemonic file -Configuration details retrieved successfully. +Load a keystore that holds an _encrypted mnemonic_ (and perform wallet derivation at the same time): -```json -{ - "data": { - "config": { - "erd_chain_id": "1", - "erd_denomination": 18, - "erd_gas_per_data_byte": 1500, - "erd_latest_tag_software_version": "v1.1.0.0", - "erd_meta_consensus_group_size": 400, - "erd_min_gas_limit": 50000, - "erd_min_gas_price": 1000000000, - "erd_min_transaction_version": 1, - "erd_num_metachain_nodes": 400, - "erd_num_nodes_in_shard": 400, - "erd_num_shards_without_meta": 3, - "erd_round_duration": 6000, - "erd_shard_consensus_group_size": 63, - "erd_start_time": 1596117600 - } - }, - "error": "", - "code": "successful" -} -``` +```py +from pathlib import Path +from multiversx_sdk import UserWallet - - +# loads the mnemonic and derives the a secret key; default index = 0 +secret_key = UserWallet.load_secret_key(Path("walletWithMnemonic.json"), "password") +address = secret_key.generate_public_key().to_address("erd") +print("Secret key:", secret_key.hex()) +print("Address:", address.to_bech32()) -## GET **Get Shard Status** {#get-shard-status} +# derive secret key with index = 7 +secret_key = UserWallet.load_secret_key( + path=Path("walletWithMnemonic.json"), + password="password", + address_index=7 +) +address = secret_key.generate_public_key().to_address() -`https://gateway.multiversx.com/network/status/:shardId` +print("Secret key:", secret_key.hex()) +print("Address:", address.to_bech32()) +``` -This endpoint allows one to query the status of a given Shard. +#### Loading a wallet from a keystore secret key file - - +```py +from pathlib import Path +from multiversx_sdk import UserWallet -Path Parameters +secret_key = UserWallet.load_secret_key(Path("walletWithSecretKey.json"), "password") +address = secret_key.generate_public_key().to_address("erd") -| Param | Required | Type | Description | -| ------- | ----------------------------------------- | -------- | -------------------------------------------------------------------------- | -| shardID | REQUIRED | `number` | The Shard ID. 0, 1, 2 etc. Use 4294967295 in order to query the Metachain. | +print("Secret key:", secret_key.hex()) +print("Address:", address.to_bech32()) +``` - - +#### Loading a wallet from a PEM file -🟢 200: OK +```py +from pathlib import Path +from multiversx_sdk import UserPEM -Shard Status retrieved successfully. +pem = UserPEM.from_file(Path("wallet.pem")) -```json -{ - "data": { - "status": { - "erd_current_round": 187068, - "erd_epoch_number": 12, - "erd_highest_final_nonce": 187019, - "erd_nonce": 187023, - "erd_nonce_at_epoch_start": 172770, - "erd_nonces_passed_in_current_epoch": 14253, - "erd_round_at_epoch_start": 172814, - "erd_rounds_passed_in_current_epoch": 14254, - "erd_rounds_per_epoch": 14400 - } - }, - "error": "", - "code": "successful" -} +print("Secret key:", pem.secret_key.hex()) +print("Public key:", pem.public_key.hex()) ``` - - +## Signing objects -:::important -The path parameter `**shardId**` is only applicable on the Proxy endpoint. The Observer endpoint does not define this parameter. -::: +The signing is performed using the **secret key** of an account. We have a few wrappers over the secret key, like [Account](#creating-accounts) that make siging easier. We'll first learn how we can sign using an `Account` and then we'll see how we can sign using the secret key. ---- +#### Signing a Transaction using an Account -### Rest API Nodes +We are going to assume we have an account at this point. If you don't fell free to check out the [creating an account section](#creating-accounts). -```mdx-code-block -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -``` +```py +from pathlib import Path +from multiversx_sdk import Account, Address, Transaction +account = Account.new_from_pem(Path("../multiversx_sdk/testutils/testwallets/alice.pem")) -This component of the REST API allows one to query information about Nodes within the Network (peers). +transaction = Transaction( + nonce=90, + sender=account.address, + receiver=Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), + value=1000000000000000000, + gas_limit=50000, + chain_id="D" +) +# apply the signature on the transaction +transaction.signature = account.sign_transaction(transaction) -## GET **Get Heartbeat Status** {#get-heartbeat-status} +print(transaction.to_dictionary()) +``` -`https://gateway.multiversx.com/node/heartbeatstatus` +#### Signing a Transaction using a SecretKey -This endpoint allows one to query the status of the Nodes. +```py +from multiversx_sdk import Transaction, TransactionComputer, UserSecretKey - + +--- + +### Query a read-only view + +Query a smart contract's read-only view function. A view function does not +modify the state of the contract, so there is no transaction to send: no wallet, +no nonce, no gas, no signing, no devnet EGLD. This is the cheapest possible way +to read on-chain state. + +This recipe queries two real, currently-deployed devnet contracts: **adder** +(`getSum()`, zero arguments, the same contract as +[Load an ABI](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi) and +[Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint)) +and **ping-pong** (`didUserPing(address)`, which itself takes a native JS +argument, the same contract as +[Call a payable endpoint with EGLD](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-payable-endpoint)). + +## Prerequisites + +- Node.js >= 20.13.1. +- Network access to devnet. No wallet, no PEM, no devnet EGLD. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/query-contract-view +npm install +npm run build +npm start +``` + +## Querying + +```ts title="src/queryView.ts" +// src/queryView.ts — querying a read-only view function. A view function does +// not modify the state of the contract, so there is no transaction to send: +// no wallet, no nonce, no gas, no signing. +// +// Targets two real, currently-deployed devnet contracts: +// - adder (erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug) +// `getSum()` — zero-argument view, the mx-sdk-js-core cookbook's own example. +// - ping-pong (erd1qqqqqqqqqqqqqpgqm6ad6xrsjvxlcdcffqe8w58trpec09ug9l5qde96pq) +// `didUserPing(address)` — a view that itself takes a native JS argument +// (an `Address`), to show query arguments go through the same +// NativeSerializer conversion as mutating-endpoint arguments do. + +import { Address } from '@multiversx/sdk-core'; +import type { Abi, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export const ADDER_CONTRACT_ADDRESS = + 'erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug'; +export const PING_PONG_CONTRACT_ADDRESS = + 'erd1qqqqqqqqqqqqqpgqm6ad6xrsjvxlcdcffqe8w58trpec09ug9l5qde96pq'; + +/** + * Queries adder's `getSum()` — the one-step way, via + * `SmartContractController.query()`. Returns already-decoded native values + * (each with a `.valueOf()`/`.toString()` you can read directly) because the + * controller was constructed with the ABI. + */ +export async function queryAdderSum(entrypoint: DevnetEntrypoint, abi: Abi): Promise { + const controller = entrypoint.createSmartContractController(abi); + + const [sum] = await controller.query({ + contract: Address.newFromBech32(ADDER_CONTRACT_ADDRESS), + function: 'getSum', + arguments: [], + }); + + return BigInt((sum as { toString(base: number): string }).toString(10)); +} + +/** + * The same query, split into its three granular steps — create, run, parse. + * Produces the same result; useful when you want to inspect or cache the raw + * `SmartContractQuery` / `SmartContractQueryResponse` in between. + */ +export async function queryAdderSumGranular(entrypoint: DevnetEntrypoint, abi: Abi): Promise { + const controller = entrypoint.createSmartContractController(abi); + + const query = controller.createQuery({ + contract: Address.newFromBech32(ADDER_CONTRACT_ADDRESS), + function: 'getSum', + arguments: [], + }); + const response = await controller.runQuery(query); + const [sum] = controller.parseQueryResponse(response); + + return BigInt((sum as { toString(base: number): string }).toString(10)); +} + +/** + * Queries ping-pong's `didUserPing(address)` — a view that takes a native JS + * argument (an `Address` instance, or equally a bech32 string, per + * NativeSerializer's `Address` conversion rule) instead of none. + */ +export async function queryDidUserPing( + entrypoint: DevnetEntrypoint, + abi: Abi, + userAddress: string, +): Promise { + const controller = entrypoint.createSmartContractController(abi); + + const [didPing] = await controller.query({ + contract: Address.newFromBech32(PING_PONG_CONTRACT_ADDRESS), + function: 'didUserPing', + arguments: [Address.newFromBech32(userAddress)], + }); + + return Boolean((didPing as { valueOf(): boolean }).valueOf()); +} +``` + +## Run it + +```bash +npm start [addressToCheck] +``` + +Expected output: + +```text +Querying getSum() on adder (erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug)... + one-step controller.query(): 84 + three-step create/run/parseQueryResponse(): 84 + (both queries hit the same live contract; equal: true) + +Querying didUserPing(erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th) on ping-pong (erd1qqqqqqqqqqqqqpgqm6ad6xrsjvxlcdcffqe8w58trpec09ug9l5qde96pq)... + didUserPing: false +``` + +(The adder sum grows over time as other cookbook readers call `add()`; expect a +different number, not necessarily `84`.) + +## How it works + +**Two ways to run the same query.** +`controller.query({ contract, function, arguments })` creates the query, runs +it, and parses the result in one call. The three-step form (`createQuery()`, +`runQuery()`, `parseQueryResponse()`) does the same work, split apart for when +you want to inspect or cache the raw `SmartContractQuery` / +`SmartContractQueryResponse` in between. Both are verified here to return +identical results against the real, live adder contract. + +**Query arguments use the exact same `NativeSerializer` conversion as +mutating-endpoint arguments.** `didUserPing(address)` takes an `Address`; this +recipe passes `Address.newFromBech32(userAddress)` directly in the `arguments` +array. + +**No wallet is constructed anywhere in this recipe.** `SmartContractController` +only needs a `chainID` and a network provider (both supplied internally by +`DevnetEntrypoint`) plus the ABI: no `IAccount`, no nonce, no signing. Compare +with [Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint), +whose `createTransactionForExecute` takes a `sender: IAccount` because it builds +a transaction that must be signed; `query()` never does. + +## Pitfalls + +:::note[Pitfall 1: a query result's static type is any] +The SDK decodes the result using the ABI at runtime, but TypeScript's static +type is not narrowed to `bigint` / `boolean` / etc. This recipe casts explicitly +rather than trusting an implicit `any`, worth doing in your own code too, since +`strict` mode will not catch a wrong assumption about a query result's shape for +you. +::: + +:::warning[Pitfall 2: a query reflects state at the moment it runs, not a fixed snapshot] +Two calls to the same query moments apart can return different values if someone +else's transaction lands in between. Nothing wrong with the code, that is what +"live" state means. Neither query here waits for or depends on a particular +block. +::: + +:::note[Pitfall 3: this recipe reads from contracts it does not own] +Both adder and ping-pong are used here only because they are real, stable, +publicly queryable devnet fixtures, the same ones `mx-sdk-js-core`'s cookbook and +`mx-template-dapp`'s default config already point at. This recipe never deploys +or controls either one. +::: + +## See also + +- [Load an ABI](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi) + is the ABI-loading step this recipe builds on. +- [Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint) + is the mutating-endpoint counterpart to this read-only recipe. +- [Call a payable endpoint with EGLD](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-payable-endpoint) + uses this recipe's query pattern to discover how much EGLD to attach. + +--- + +### Random Numbers in Smart Contracts + +## Introduction + +Randomness in the blockchain environment is a challenging task to accomplish. Due to the nature of the environment, nodes must all have the same "random" generator to be able to reach consensus. This is solved by using Golang's standard seeded random number generator, directly in the VM: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/math/rand/ + +The VM function `mBufferSetRandom` uses this library, seeded with the concatenation of: + +- previous block random seed +- current block random seed +- tx hash + +We're not going to go into details about how exactly the Golang library uses the seed or how it generates said random numbers, as that's not the purpose of this tutorial. + + +## Random numbers in smart contracts + +The `ManagedBuffer` type has two methods you can use for this: + +- `fn new_random(nr_bytes: usize) -> Self`, which creates a new `ManagedBuffer` of `nr_bytes` random bytes +- `fn set_random(&mut self, nr_bytes: usize)`, which sets an already existing buffer to random bytes + +For convenience, a wrapper over these methods was created, namely the `RandomnessSource` struct, which contains methods for generating a random number for all base rust unsigned numerical types, and a method for generating random bytes. + +For example, let's say you wanted to generate `n` random `u16`: + +```rust +let mut rand_source = RandomnessSource::new(); +for _ in 0..n { + let my_rand_nr = rand_source.next_u16(); + // do something with the number +} +``` + +Similar methods exist for all Rust unsigned numerical types. + + +## Random numbers in a specific range + +Let's say you wanted to implement a Fisher-Yates shuffling algorithm inside your smart contract (https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle). + +The `RandomnessSource` struct provides methods for generating numbers within a range, namely `fn next_usize_in_range(min: usize, max: usize) -> usize`, which generates a random `usize` in the `[min, max)` range. These methods are available for the rest of the numerical types as well, but for this example, we need `usize` (in Rust, indexes are `usize`). + +For that, you would probably have a vector of some kind. For example, let's assume you wanted to shuffle a vector of `ManagedBuffer`s. + +```rust +let mut my_vec = ManagedVec::new(); +// ... +// fill my_vec with elements +// ... + +let vec_len = my_vec.len(); +let mut rand_source = RandomnessSource::new(); +for i in 0..vec_len { + let rand_index = rand_source.next_usize_in_range(i, vec_len); + let first_item = my_vec.get(i).unwrap(); + let second_item = my_vec.get(rand_index).unwrap(); + + my_vec.set(i, &second_item); + my_vec.set(rand_index, &first_item); +} +``` + +This algorithm will shuffle each element at position `i`, with an element from position `[i, vec_len)`. + + +## Random bytes + +Let's say you want to create some NFTs in your contract, and want to give each of them a random hash of 32 bytes. To do that, you would use the `next_bytes(len: usize)` method of the `RandomnessSource` struct: + +```rust +let mut rand_source = RandomnessSource::new(); +let rand_hash = rand_source.next_bytes(32); +// NFT create logic here +``` + + +## Considerations + +:::caution +NEVER have logic in your smart contract that only depends on the current state. +::: + +Example of BAD implementation: + +```rust +#[payable("EGLD")] +#[endpoint(rollDie)] +fn roll_die(&self) { + // ... + let payment = self.call_value().egld(); + let rand_nr = rand_source.next_u8(); + if rand_nr % 6 == 0 { + let prize = payment * 2u32; + self.send().direct(&caller, &prize); + } + // ... +} +``` + +This is very easy to abuse, as you can simply simulate your transactions, and only send them when you see you've won. Therefore, guaranteeing a 100% win chance! + +Keep in mind you are not running this on your own private server, you are running it on a public blockchain, so you need a complete shift in design. + +Example of GOOD implementation: + +```rust +#[payable("EGLD")] +#[endpoint(signUp)] +fn sign_up(&self) { + let already_signed_up = self.user_list().insert(caller.clone()); + if already_signed_up { + sc_panic!("Already signed up"); + } +} + +#[only_owner] +#[endpoint(selectWinners)] +fn select_winners(&self) { + for user in self.user_list().iter() { + let rand_nr = rand_source.next_u8(); + if rand_nr % 6 == 0 { + self.winners_list().insert(user.clone()); + } + } +} + +#[endpoint] +fn claim(&self) { + let was_winner = self.winners_list().swap_remove(&caller); + if was_winner { + self.send().direct_egld(&caller, &prize); + } +} + +#[storage_mapper("userList")] +fn user_list(&self) -> UnorderedSetMapper; + +#[storage_mapper("winnersList")] +fn winners_list(&self) -> UnorderedSetMapper; +``` + + +## Conclusion + +This random number generator should be enough for most purposes. Enjoy using it for your lotteries and such! + +--- + +### rating + +This page describes the structure of the `rating` index (Elasticsearch), and also depicts a few examples of how to query it. + + +## _id + +The `_id` field of this index is composed in this way: `{validator_bls_key}_{epoch}` (example: `blskey_37`). + + +## Fields + + +| Field | Description | +|-----------|------------------------------------------------------------------| +| rating | The rating of the validator, which can be in the [0, 100] range. | + + +## Query examples + + +### Fetch rating of a validator for a specific epoch + +``` +curl --request GET \ + --url ${ES_URL}/rating/_search \ + --header 'Content-Type: application/json' \ + --data '{ + "query": { + "match": { + "_id":"${BLS_KEY}_600" + } + } +}' +``` + +--- + +### React Development + +## Introduction + +Every developer has his/her own code style that has been developed along the way, with bits and quirks that, at some point, become a part of oneself. + +However, in a big team and in a big project, small quirks and personal preferences can add up and turn the codebase into a big lasagna. + +Given this, we have established some basic principles and a code style we would like to follow. These are, of course, not set in stone, and can be changed, given a valid reason. + + +## Using Git + +:::important +* We use **yarn** as a package manager. +::: + + +### Branch naming +We use a system for **branch naming**: \[your initials\]/-[feature || fix || redesign\]/-\[2-3 words describing the branch\] +> e.g. John Doe creates `jd/feature/fix-thing-called-twice` + +:::note +All branch names are lowercase +::: + + +## Basic principles + + +### Imports and exports +We import **lodash-specific functions** instead of the whole library for the tree shaking to take effect. + +```jsx +// DON'T import _ from 'lodash'; +// this also doesn't shake that tree, unfortunately. +// You can find more info on webpack website about tree shaking. +import {cloneDeep, isEmpty} from 'lodash'; // DO import cloneDeep from 'lodash/cloneDeep'; +import isEmpty from 'lodash/isEmpty'; +import last from 'lodash/last'; +import uniqBy from 'lodash/uniqBy'; +import get from 'lodash/get';` +``` + +Do not use `default` exports. **Use named exports** instead. + + +### Using conditionals +Avoid using nested conditionals. **Use early returns** instead. + +```jsx +// 🚫 DON'T +if (condition) { + if (anotherCondition) { + // do stuff + } +} +// ✅ DO +if (!condition) { + return; +} +if (!anotherCondition) { + return; +} +// do stuff +``` + + +### Defining function arguments +If a function has more than 2 arguments and the second argument is not optional, **use an object** instead. + +```jsx +// 🚫 DON'T +const myFunction = (arg1, arg2, arg3) => { + // do stuff +} + +// ⚠️ AVOID +const myFunction = (arg1: string, arg2?: boolean) => { // not recommended but acceptable + // do stuff +} + +// ✅ DO +const myFunction = ({arg1, arg2, arg3}) => { + // do stuff +} +``` + + +### Validity checks +We use **`!=` or `== null` verifications** for all variables, and `!myBool` for booleans only. + +```jsx +// 🚫 DON'T +const user = userSelector(state); +if (!user) { + //do something + if (!refetchAttempted){ + refetch(); + } +} + +// ✅ DO +const user = userSelector(state); +if (user == null) { + //do something + if (!refetchAttempted) { + refetch(); + } +} +``` + +When using a property from an object inside a condition, check for null with **optional chaining operator**; + +```jsx +// 🚫 DON'T +if (array != null && array.length){ + // do stuff +} +// ✅ DO +if (array?.length > 0){ + //do stuff +} +``` + + +### Folder structure +For folder and file naming we're using the following convention: +**camelCase for all folders and files, except when it's a React Component or a Module Root Folder**, in which case we're using PascalCase. +Also, for components' and containers' subcomponents, we create separate folders, even if there is no style file present. + +Each folder that has an exportable component will have an **`index.tsx`** file for ease of import.
+Each folder that has an exportable file will have an **`index.ts`** file for ease of import. + + +### File length conventions +- < 100 lines of code - ✅ OK +- 100 - 200 lines of code - try to split the file into smaller files +- 200 - 300 lines of code - should be split the file into smaller files +- \> 300 lines of code 🚫 DON'T + + +### Naming conventions +* When naming types, use the suffix **`Type`**. This helps us differentiate between types and components. When naming component props types, use MyComponentPropsType. When naming a type that is not a component, use MyFunctionType. When naming return values, use MyFunctionReturnType. + +**Try to extract at the top of the function all constants** such as strings, numbers, objects, instead of declaring this ad hoc inside the code. + +```jsx +// 🚫 DON'T +if (x === 'rejected' && y === 4) { + // do stuff +} +// ✅ DO +enum PermissionEnum { // all enums should be in PascalCase and suffixed with "Enum" + rejected = "rejected" +} +const ACCESS_LEVEL = 4; // all constants declared on top of functions should be in UPPER_CASE +if (x === PermissionsEnum.rejected && y === ACCESS_LEVEL) +{ + //do stuff +} +``` + + +## React guidelines + + +### Using functional components +We're using **functional components** for almost all new components, no classes, except when strictly necessary (e.g. error boundaries); + + +### Using selectors +We use `useSelector` and `useDispatch` hooks to connect to redux store via react-redux. 🚫 **No** `mapStateToProps` in functional components. + +We use **reselect** for memoizing complex state variables and composing those into optimized selectors that don't rerender the whole tree when the values don't change. This package needs to be added only when there is a performance bottleneck, either existing or expected. + + +### Defining handlers + +We're using **"handle" prefix** for handlers defined in the function and **"on"** prefix for handlers passed via props. `handleTouchStart` vs `props.onTouchStart`, to distinguish between own handlers and parent handlers. + +```jsx +function handleClick(e) { + props.onClickClick(); +} + +
+ +//destructured before, instantly known to be from parent +
` +``` + + +### Number of props per component + +If a component has more than 7 props, it should draw a red flag and be refactored. If it has >= 10 props, it should be refactored immediately. Strategies for refactoring: +- split into smaller components and pass them as props +- use a local context provider + +```jsx +// ⚠️ AVOID + + +// ✅ DO group props into logical components + + } + prop={ + + } +> + + +``` + + +### Inline functions +No **inline functions** in TSX. + +```jsx +// 🚫 DON'T + setPressed(true)}/> +// ✅ DO +const handlePress = () => { + setPressed(true) +} + +``` + + +### Implicit values +Use implicit `true` for **boolean** props + +```jsx +// 🚫 DON'T + +// ✅ DO + +``` + + +### Destructuring arguments +Always destructure arguments, with minor exceptions. + +```jsx +// 🚫 DON'T +function printUser(user) { + console.log(user.name, user.name); +} +// ✅ DO +function printUser({ name, age }) { + console.log(name, age); +} +``` + +There are exceptions to this rule like: +1. The arguments are optional + +```jsx +function logWithOptions(options?: {green?: boolean}) { + if (options?.green) { + return console.log('\x1b[42m%s\x1b[0m', 'Some green text'); + } + console.log('Some normal text'); +} + +``` + +2. There is a name clash with variables defined above + +```jsx +const type = 'admin'; +function verifyUser(user) { + console.log(user.type === type); +} + +``` +3. Same props are passed below to a component, or are used for further processing + +```jsx +// 🚫 DON'T +const DisplayUser = ({name, age}: UserType) { + return ; +} +// ✅ DO +const DisplayUser = (user: UserType) { + return ; +} +const UserList = (users: UserType[]) { + return users.map((user, index) => { + // destructuring avoids typechecking so always specify the type + // before passing destructured props to a component + const userProps: UserType = processUser(user); + return ; + }) + +} +``` + + +### Over-optimization +No **`useCallback` or `useMemo` or `React.memo` unless really necessary**. Since the release of hooks, over-optimization has become a big problem. + +```jsx +// 🚫 DON'T +const handlePress = useCallback(() => setPressed(true), []); + +// 🚫 DON'T +const value = useMemo(() => user.level * multiplicator); + +// ✅ DO +const handlePress = useCallback(() => setPressed(true), []); + + {children} + +``` + + +### Conditionally rendered TSX + +In React, conditionally rendered TSX is very common. Given the ability to render it inline, it's very easy to include it inside normal TSX: + +```jsx + + {hasAchievements ? : } + + {title} + {mysteryBoxEnabled && } + + +``` + +However, TSX sometimes tends to grow very big and it requires a certain amount of mental load to stop at these conditionals and understand what's rendered inside. + +One could argue that there's an "organism" inside, a certain piece of logic that results in a component being rendered after some calculations and state changes. We try to give names to these operations that result in a TSX, so the developer knows what's in that TSX. + +**Thus, all conditionally rendered TSX** **goes into a constant**. We don't render conditional TSX inline + +```jsx +const achievementsContainer = hasAchievements? : ; +const mysteryBoxesContainer = mysteryBoxEnabled && ; + + + {achievementsContainer} + + {title} + {mysteryBoxesContainer} + + +``` + + +## Rules for hooks + +1. **Fake modularization**: + * Custom hooks may give the impression of modularization, but their logic runs inline in the parent component. + * State changes and reactive behavior in the hook will cause a rerender of the host component and any other hooks that depend on the updated hook. +2. **Hook return values**: + * Custom hooks should return either a single function (for a lazy hook) or an object containing a function and some properties; + * Avoid returning objects with multiple functions to ensure consistency and maintainability; + * Use interfaces for hook return type: + ```jsx + useMyHook({firstParam, secondParam}: UseMyHookParamsType): UseMyHookReturnType => {} + ``` +3. **State management and data fetching**: + * Lifting state up should be used sparingly; hooks should primarily be used to gather state in one container and distribute it to child components. + * Strive to couple data fetching with UI rendering as isolating as possible to prevent unnecessary rerenders. + * Ensure one hook does not trigger the rerender of another by carefully managing dependencies and side effects. + * If only 10% of the lines of code in a hook do specific React logic like state management, or calling a selector from Redux, consider splitting the hook into: + - a smaller hook that does the React logic and returns the state, and + - a statless function that will be called inside the hook and will do the rest of the logic. This stateless function should be tested separately. +4. **Hook interfaces**: + * Use interfaces for hook params if you're passing more than one argument to the hook invocation: + ```jsx + useMyHook({firstParam, secondParam}: UseMyHookParamsType) => {} + ``` +5. If a hook exports > 4 values, it should draw a red flag and be refactored. If it exports >= 7 values, it should be refactored immediately. + + +## Modularisation + +Given the size of the project, we have agreed on a couple of modularisation techniques that will help us to: + +* Split the logic into more readable chunks of logic; +* Test all bits of logic with unit tests; +* Reuse components/utils/hooks as needed; + +There are a couple of rules that we agreed upon and will be enforced in all PRs, to try and maintain the code in a state that is easy to navigate, read, debug and change. We try to move as much mental load as we can to the develop who is writing the code, instead of the developer who is reading the code. + +Therefore, we agreed on the certain principles: + + +### Abstracting the logic away into hooks and functions + +If there is a piece of code in a component or container that holds a certain amount of logic and can be converted to a testable hook or utils function, we should move it to a separate function/hook and add props interface, return type interface and a test file. + +For example: + +```jsx +const lastExpiringNotificationMissionId = useSelector( expiringNotificationMissionIdSelector ); +useEffect(() => { + if ( missionEndDate && missionId && missionId !== lastExpiringNotificationMissionId ) { + const earlierWith = ONE_HOUR_MS * 2; + const calculatedDate = missionEndDate - earlierWith; + const instantiateLocalNotification = async () => { + NotificationManager.startLocalNotification( + t('modules.mysteryBox.notification.title'), + t('modules.mysteryBox.notification.description'), + calculatedDate?.toString(), + NotificationPayloadTypesEnum.MYSTERY_BOX_EXPIRING + ); + }; + dispatch(setExpiringNotificationMissionId(missionId)); + instantiateLocalNotification(); + } +}, [lastExpiringNotificationMissionId]); +``` + +👆 This piece of logic is a perfect candidate to be moved to a separate hook file, because it contains +a very specific piece of logic, can be tested and the behavior is easier to predict and debug. + +```jsx +const sanitizedHerotag = name ? sanitizeHerotag(name) : undefined; +const herotagName = sanitizedHerotag != null && sanitizedHerotag !== name ? sanitizedHerotag : undefined; +const nameWithInitials = name || savedAddress; +const initials = getInitials(nameWithInitials); +return herotagName ? getHerotagPrefix(avatarIconTextMaxLength, herotagName) : initials; +``` + +👆 This is another piece of logic that is a single "organism", meaning it can be moved to a function that takes in a certain set of arguments and returns a specific value. + +We can abstract these kind of calculations into separate functions, where the logic doesn't pollute the container's file, is easily testable and can be debugged and changed more easily. + +Try to identify this kind of "organisms" in your code and move them to a separate file only if the logic is worth it. **Don't overdo it for simple pieces of logic, **unless they are either taking a lot of space or mental load to read through. + +```jsx +const sanitizedHerotag = name ? sanitizeHerotag(name) : undefined; +const herotagName = sanitizedHerotag != null && sanitizedHerotag !== name ? sanitizedHerotag : undefined; +``` + +👆 This piece of code would not be worth it, since the logic is very simple, straightforward and there is not much to test. + +However, + +```jsx +const avatar = useSelector(avatarSelector); +const sanitizedHerotag = name ? sanitizeHerotag(name) : undefined; +const herotagName = sanitizedHerotag != null && sanitizedHerotag !== name ? sanitizedHerotag : undefined; +const isHerotagValid = herotagName == null; +const shouldAllowHerotagCreation = !isHerotagValid; +const canUserCreateAvatar = !shouldAllowHerotagCreation && avatar == null; +``` + +👆 This logic, even though might seem simple, has a lot of steps that need to be take to reach the final solution, `canUserCreateAvatar`, and would be a good candidate for a separate function. + +In this case, it would be a good idea to abstract away the mental load needed to read through all this just to understand the container's code. The logic is abstracted away and tested, and if the `canUserCreateAvatar` result is buggy, there is a start and an end for debugging. + +If a piece of logic is a bit complex, works like an entity that could have an input and an output and has more than 7-10 lines of code, consider moving it to a hook/function. + +> input → **function** → output + +###Abstracting complex calculations into constants + +Certain inline calculations are not worth moving into a hook/function, but a constant will help remove some complexity and will attach a "name" to the calculation, making it easier to understand what's inside: + +```jsx +if (!isMissionCountdownLoading && missionCountdownData && isMIssionCountdownDataReady && currentMysteryBox && missionCountdownData?.status === MysteryBoxStatus.FINISHED ) +{ + //... +} +``` + +👆 This would be a very good example of an inline if that we try to avoid. It does have a lot of simple conditions that are being tested, but there is a certain mental load needed to parse every single && and the comparison seems to ask for a name. + +We could rewrite it to something like this: + +```jsx +const isMissionCountdownDataReady = !isMissionCountdownLoading && missionCountdownData; +const isMissionAlreadyFinished = !currentMysteryBox && missionCountdownData?.status === MysteryBoxStatus.FINISHED; +if (isMysteryBoxMissionStautsChanged && isMIssionCountdownDataReady ) +{ + //... +} +``` + +If we assign complex or even simple but long operations to local variables, we give them a name that can be used to infer what's inside, instead of calculating it ourselves. Sort of like a memoization. By naming a piece of logic, we memoize it and avoid recomputing it inside our heads, unless necessary. + +Again, as with hooks and functions, **don't overdo it. **There are certain calculations that, like in JavaScript, are easy for the brain to parse and understand, so it's not worth moving them to a local variable: + +```jsx +if (myClaimableAuctions != null && myClaimableAuctions.length > 0) { + //... +} +``` + +👆 Here, it's not worth moving the if logic inside a local variable, it would be redundant, as it's very easy to read through it. + + +### New functions/hooks + +When creating new functions and hooks, the new entity must have: + +* A props interface, if it accepts any arguments, declared in the function's file; +* A return interface, it the function or hook returns more than a simple primitive, declared in the function's file; +* A test function that tests the function and covers all test cases; As far as possible try to adhere to the ZOMBIES testing technique. The test should be created in the \_\_tests\_ folder; +* At most 50 lines of code, ideally 20 lines. + +```jsx +interface UseKYCModalStatePropsType { + isStatusFailed: boolean; + handleOpenInitiateKYCModal: () => void; +} + +interface UseKYCModalStateReturnType { + KYCInitialModalState: KYCInitialModalStateEnum; + setKYCInitialModalState: (newState: KYCInitialModalStateEnum) => void; +} + +export const useKYCModalState = ({ isStatusFailed, handleOpenInitiateKYCModal }: UseKYCModalStatePropsType): UseKYCModalStateReturnType => {} +``` + +We believe that adhering to these concepts will help us maintain the codebase at a sane level and will allow us a lot of manoeuvrability in the long run, both in building new features and in solving bugs quickly and reliably in times of crisis. + +--- + +### Read a delegation contract's state + +A delegation contract is a normal smart contract, so you read its state with the +same read-only VM queries as any other, no wallet, no nonce, no gas. This recipe +queries a live devnet staking provider for its config (owner, service fee), its +total active stake and delegator count, and one delegator's active stake and +claimable rewards, decoding the raw return bytes by hand because the delegation +system contract does not ship an ABI with sdk-core. + +The whole recipe runs against real devnet data out of the box, `npm start` needs +nothing but network access. + +## Prerequisites + +- Node.js >= 20.13.1. +- Devnet network access. No wallet, no funds. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/query-delegation-contract +npm install +npm run build +``` + +## Querying + +```ts title="src/queryDelegation.ts" +// src/queryDelegation.ts - the actual subject of this recipe: reading a +// staking-provider (delegation) contract's state with read-only VM queries. +// No wallet, no nonce, no gas, no signing - every function here is a plain +// query against a live devnet delegation contract. A view function does not +// modify contract state, so there is no transaction to send. +// +// A delegation contract is a normal smart contract that happens to be +// created by the delegation manager. Its views are queried exactly like any +// other contract's, through `SmartContractController.query()`. We construct +// the controller WITHOUT an ABI (the delegation system contract does not +// ship one with sdk-core), so: +// - query results come back as RAW `Uint8Array[]` return-data parts, which +// we decode by hand (a top-level BigUint is just big-endian bytes; +// an empty part is zero); +// - address arguments must be passed as `TypedValue`s (an `AddressValue`), +// because without an ABI there is no NativeSerializer to convert a plain +// bech32 string. See the Pitfalls in the recipe page. +// +// The example contract and delegator below are real and live on devnet; the +// recipe page shows the actual values they returned. + +import { Address, AddressValue } from '@multiversx/sdk-core'; +import type { DevnetEntrypoint } from '@multiversx/sdk-core'; + +/** A real, live devnet staking-provider contract (666 delegators at the time of writing). */ +export const EXAMPLE_DELEGATION_CONTRACT = + 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllllscktaww'; + +/** A real delegator with active stake in the contract above (its owner). */ +export const EXAMPLE_DELEGATOR = + 'erd1kv5mkar6fvt6vhqj7evfqr9jnmmlqps3q9dp0t0gr9tcpqupyrsshlnvd0'; + +/** + * Decode a top-level `BigUint` returned by a view. On MultiversX a top-level + * (top-encoded) BigUint is its minimal big-endian byte string, and an empty + * return part means zero - so `[]` decodes to `0n`, not an error. + */ +export function decodeBigUint(part: Uint8Array): bigint { + if (part.length === 0) { + return 0n; + } + return BigInt('0x' + Buffer.from(part).toString('hex')); +} + +/** Query a zero-argument view that returns a single BigUint. */ +async function queryBigUintView( + entrypoint: DevnetEntrypoint, + contract: string, + functionName: string, +): Promise { + const controller = entrypoint.createSmartContractController(); // no ABI + const parts = (await controller.query({ + contract: Address.newFromBech32(contract), + function: functionName, + arguments: [], + })) as Uint8Array[]; + return decodeBigUint(parts[0] ?? new Uint8Array()); +} + +/** Query a view that takes one delegator address and returns a single BigUint. */ +async function queryDelegatorView( + entrypoint: DevnetEntrypoint, + contract: string, + functionName: string, + delegator: string, +): Promise { + const controller = entrypoint.createSmartContractController(); // no ABI + const parts = (await controller.query({ + contract: Address.newFromBech32(contract), + function: functionName, + // Without an ABI, arguments must be TypedValues (or raw buffers), never a + // plain bech32 string. An AddressValue encodes to the 32-byte public key. + arguments: [new AddressValue(Address.newFromBech32(delegator))], + })) as Uint8Array[]; + return decodeBigUint(parts[0] ?? new Uint8Array()); +} + +/** Total EGLD actively staked into this contract (all delegators). */ +export async function queryTotalActiveStake( + entrypoint: DevnetEntrypoint, + contract: string, +): Promise { + return queryBigUintView(entrypoint, contract, 'getTotalActiveStake'); +} + +/** Number of distinct delegators in this contract. */ +export async function queryNumUsers(entrypoint: DevnetEntrypoint, contract: string): Promise { + return queryBigUintView(entrypoint, contract, 'getNumUsers'); +} + +/** One delegator's currently-active (staked) amount, in wei. */ +export async function queryUserActiveStake( + entrypoint: DevnetEntrypoint, + contract: string, + delegator: string, +): Promise { + return queryDelegatorView(entrypoint, contract, 'getUserActiveStake', delegator); +} + +/** One delegator's unclaimed rewards, in wei. */ +export async function queryClaimableRewards( + entrypoint: DevnetEntrypoint, + contract: string, + delegator: string, +): Promise { + return queryDelegatorView(entrypoint, contract, 'getClaimableRewards', delegator); +} + +export interface DelegationConfig { + /** The contract owner (the staking provider operator). */ + owner: string; + /** Service fee in basis points of 10,000 (e.g. 1000 = 10%). */ + serviceFeePerTenThousand: number; +} + +/** + * Read the contract's configuration. `getContractConfig` returns many parts; + * the first is the owner's 32-byte public key and the second is the service + * fee as a BigUint over 10,000. We decode just those two here; the remaining + * parts (delegation cap, activation flags, etc.) decode the same way. + */ +export async function queryContractConfig( + entrypoint: DevnetEntrypoint, + contract: string, +): Promise { + const controller = entrypoint.createSmartContractController(); // no ABI + const parts = (await controller.query({ + contract: Address.newFromBech32(contract), + function: 'getContractConfig', + arguments: [], + })) as Uint8Array[]; + + const ownerBytes = parts[0] ?? new Uint8Array(); + const serviceFeeBytes = parts[1] ?? new Uint8Array(); + return { + owner: new Address(ownerBytes).toBech32(), + serviceFeePerTenThousand: Number(decodeBigUint(serviceFeeBytes)), + }; +} +``` + +## Run it + +```bash +# Query the built-in example contract + delegator: +npm start + +# Or point it at any delegation contract and delegator: +npm start -- erd1qqq...delegationContract erd1...delegator +``` + +Real output against the example contract (values move as the chain lives): + +```text +Delegation contract: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllllscktaww + owner: erd1kv5mkar6fvt6vhqj7evfqr9jnmmlqps3q9dp0t0gr9tcpqupyrsshlnvd0 + service fee: 10% + total active stake: 162444.3154 EGLD (162444315455774609992988 wei) + delegators: 666 + +Delegator: erd1kv5mkar6fvt6vhqj7evfqr9jnmmlqps3q9dp0t0gr9tcpqupyrsshlnvd0 + active stake: 10000.0000 EGLD (10000000000000000000000 wei) + claimable rewards: 19559.2005 EGLD (19559200521796778772671 wei) +``` + +## How it works + +**No ABI, so decode by hand.** `entrypoint.createSmartContractController()` with +no ABI makes `query()` return the raw `Uint8Array[]` return-data parts. A top-level +`BigUint` is just its big-endian bytes, and an empty part is zero, so +`getUserActiveStake` and friends decode with a one-line `BigInt('0x' + hex)` (or +`0n` when empty). + +**Address arguments must be TypedValues.** Without an ABI there is no +NativeSerializer, so a delegator argument cannot be a plain bech32 string. Wrap it +as `new AddressValue(Address.newFromBech32(...))`, which encodes to the 32-byte +public key the view expects. + +**The views used.** `getContractConfig` returns many parts (part 0 is the owner's +public key, part 1 is the service fee over 10,000); `getTotalActiveStake` and +`getNumUsers` take no arguments; `getUserActiveStake` and `getClaimableRewards` +take one delegator address. + +For the same pattern with an ABI that auto-decodes results, see the +query-contract-view recipe. + +## Pitfalls + +:::warning[Pitfall 1: without an ABI you get bytes, and address args must be typed] +`query()` with no ABI returns `Uint8Array[]`, not decoded values, and rejects +plain-string arguments with "cannot encode arguments: when ABI is not available, +they must be either typed values or buffers." Pass an `AddressValue` (or a raw +buffer) and decode the results yourself. +::: + +:::note[Pitfall 2: an empty return part means zero, not an error] +A delegator with no stake or no rewards makes the contract return an empty part for +that view. Decode it as `0n`; do not treat the empty `Uint8Array` as a failure. +::: + +:::note[Pitfall 3: service fee is over 10,000] +`getContractConfig` returns the service fee as an integer over 10,000 (e.g. `1000` += 10%), matching the value you pass when creating a contract. Divide by 100 for a +percentage. +::: + +## See also + +- [Delegate (stake) EGLD](/sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake) + acts on the state you just read. +- [Claim and re-delegate rewards](/sdk-and-tools/sdk-js/cookbook/delegation/claim-and-redelegate-rewards) + uses the claimable rewards this recipe reads. +- Querying a contract view with an ABI decodes results automatically, covered in + the smart-contracts recipes. + +--- + +### Read a multisig's state + +Before you sign or perform anything on a multisig, you read it: who is on the +board, how many signatures the quorum needs, and what is currently pending. This +recipe reads all of that with the `MultisigController`'s view helpers. None of it +needs a wallet, it is read-only `queryContract` under the hood, decoded through the +multisig ABI. + +The reads split in two: the **board configuration** (quorum, board members, +proposers) and the **pending actions** (what has been proposed but not performed, +and for each, its signers and whether it has reached quorum). The default +`npm start` reads a real devnet multisig, so it shows live output out of the box. + +## Prerequisites + +- Node.js >= 20.13.1 and devnet network access. No wallet, ever. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/multisig-read-state +npm install +npm run build +``` + +## Reading + +```ts title="src/read.ts" +// src/read.ts - the subject of this recipe: reading a multisig contract's +// state with the MultisigController's view helpers. None of this needs a +// wallet - it is all read-only `queryContract` under the hood, decoded through +// the multisig ABI. +// +// What you can read splits into two groups: +// - the board configuration: quorum, board members, proposers; +// - the pending actions: what has been proposed but not yet performed, and +// for each, who has signed and whether it has reached quorum. +// +// Together these answer "who controls this multisig, and what is it about to +// do?" - the questions you ask before you sign or perform anything. + +import { Address } from '@multiversx/sdk-core'; +import type { Abi, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export interface MultisigBoard { + quorum: number; + numBoardMembers: number; + numProposers: number; + actionLastIndex: number; + boardMembers: string[]; + proposers: string[]; +} + +/** Read the board configuration: quorum and membership. */ +export async function readBoard( + entrypoint: DevnetEntrypoint, + abi: Abi, + multisigAddress: string, +): Promise { + const controller = entrypoint.createMultisigController(abi); + const [quorum, numBoardMembers, numProposers, actionLastIndex, boardMembers, proposers] = await Promise.all([ + controller.getQuorum({ multisigAddress }), + controller.getNumBoardMembers({ multisigAddress }), + controller.getNumProposers({ multisigAddress }), + controller.getActionLastIndex({ multisigAddress }), + controller.getAllBoardMembers({ multisigAddress }), + controller.getAllProposers({ multisigAddress }), + ]); + return { quorum, numBoardMembers, numProposers, actionLastIndex, boardMembers, proposers }; +} + +export interface PendingAction { + actionId: number; + groupId: number; + type: string; + signers: string[]; + quorumReached: boolean; +} + +/** + * Read every pending action, and for each, its signers and whether it has + * reached quorum. `getPendingActionFullInfo` returns the action id, group id, + * decoded action data (its `type` names what it will do), and the raw signer + * list; `quorumReached` tells you if it can be performed now. + */ +export async function readPendingActions( + entrypoint: DevnetEntrypoint, + abi: Abi, + multisigAddress: string, +): Promise { + const controller = entrypoint.createMultisigController(abi); + const pending = await controller.getPendingActionFullInfo({ multisigAddress }); + + const result: PendingAction[] = []; + for (const action of pending) { + const quorumReached = await controller.quorumReached({ multisigAddress, actionId: action.actionId }); + result.push({ + actionId: action.actionId, + groupId: action.groupId, + type: action.actionData.type, + // `signers` here is `Address[]` (from FullMultisigAction), unlike the + // `getActionSigners` helper whose declared type is wrong - see Pitfalls. + signers: action.signers.map((address: Address) => address.toBech32()), + quorumReached, + }); + } + return result; +} + +/** Look up one user's role on the multisig: None, Proposer, or BoardMember. */ +export async function readUserRole( + entrypoint: DevnetEntrypoint, + abi: Abi, + multisigAddress: string, + userAddress: string, +): Promise { + const controller = entrypoint.createMultisigController(abi); + const role = await controller.getUserRole({ multisigAddress, userAddress }); + return role.valueOf(); +} +``` + +## Run it + +```bash +# Read the bundled example multisig - live, no wallet: +npm start + +# Read any multisig you pass: +npm start -- erd1qqqqqqqqqqqqqpgq... +``` + +Expected output (a live snapshot, the pending action changes as the board acts): + +```text +Multisig: erd1qqqqqqqqqqqqqpgqvjcswsav2rw4ccvguyuk0f0u6qycpsuvu3dq9rse9w + +Board configuration: + quorum: 2 of 2 board members + proposers (non-board): 0 + actions ever created: 5 + board[0]: erd1d4q9a3qx5695gwd02rap2qqad3hv7lga7s06y5e6m3shuy6pu3dqagj7qe + board[1]: erd1c8fjemhv646hwlladfna6ce4m85y5hr4esqlysceelqgl987y92sp248zr + role of board[0]: BoardMember + +Pending actions: 1 + action 4 (group 0): SendTransferExecuteEgld + signers: erd1d4q9a3qx5695gwd02rap2qqad3hv7lga7s06y5e6m3shuy6pu3dqagj7qe + quorum reached: false +``` + +## How it works + +**One controller, many views.** `entrypoint.createMultisigController(abi)` gives +you `getQuorum`, `getNumBoardMembers`, `getAllBoardMembers`, `getAllProposers`, +`getActionLastIndex`, `getUserRole`, `getPendingActionFullInfo`, `quorumReached`, +and more, all read-only queries. The ABI is required so the controller can decode +the raw query responses into numbers, addresses, and typed action data. + +**Pending actions carry decoded action data.** `getPendingActionFullInfo` returns +each pending action's id, group id, signer list (`Address[]`), and an `actionData` +whose `type` names what it will do (`SendTransferExecuteEgld`, `AddBoardMember`, +`ChangeQuorum`, ...). That `type` is how you tell a spend proposal from a board +change before you sign. + +**Roles gate everything.** `getUserRole` returns `None`, `Proposer`, or +`BoardMember`. Only proposers and board members may propose; only board members' +signatures count toward quorum. + +## Pitfalls + +:::note[Pitfall 1: the ABI is required even for reads] +`createMultisigController(abi)` needs the multisig ABI to decode query responses. +Without it, the controller cannot turn raw returned bytes into the board list or the +pending `actionData`. Each recipe bundles `multisig-full.abi.json`; ship the ABI +with your app too. +::: + +:::warning[Pitfall 2: getActionSigners returns Address objects, not strings] +If you reach for `getActionSigners(...)` (used in the sign-and-perform recipe) +rather than `getPendingActionFullInfo(...).signers`, note it is declared +`Promise` but actually returns `Address[]` at runtime (sdk-core v15.4.1). +The `signers` on a `FullMultisigAction` here is correctly typed `Address[]`. +::: + +:::note[Pitfall 3: the output is a live snapshot] +Quorum and board membership are stable, but pending actions come and go as the board +signs, performs, and discards. The example's `action 4` may be gone by the time you +run this; pass your own multisig address as an argument to read a contract you +control. +::: + +## See also + +- [Propose a multisig action](/sdk-and-tools/sdk-js/cookbook/multisig/propose-action) + creates a pending action to read here. +- [Sign and perform a multisig action](/sdk-and-tools/sdk-js/cookbook/multisig/sign-perform-action) + acts on what you read. +- [Read a delegation contract's state](/sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract) + is the same read-only, no-wallet pattern for delegation. + +--- + +### Read the connected account with useGetAccount + +Once a user is logged in, `useGetAccount()` is how you read their address, +balance, and nonce back out of the sdk-dapp store. This recipe goes field by +field through `AccountType`, shows the two sibling hooks (`useGetAccountInfo()`, +`useGetLatestNonce()`) you will reach for less often, and gets the +balance-formatting pitfall out of the way early. + +## Prerequisites + +- A working sdk-dapp v5 setup (Next.js or Vite). +- A logged-in account on devnet. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/read-connected-account +npm install +npm run dev +# open https://localhost:5173 +``` + +## The account card + +```tsx title="src/AccountCard.tsx" +// src/AccountCard.tsx — the useGetAccount() field-by-field breakdown. +// +// This is the piece this recipe is actually about. useGetAccount() returns +// AccountType (node_modules/@multiversx/sdk-dapp/out/types/account.types.d.ts) +// — a plain object read straight from the Zustand store sdk-dapp populates +// on login. No network call happens here; the values are whatever the store +// currently holds (refreshed automatically after transactions settle). + +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetAccountInfo } from '@multiversx/sdk-dapp/out/react/account/useGetAccountInfo'; +import { useGetLatestNonce } from '@multiversx/sdk-dapp/out/react/account/useGetLatestNonce'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { formatAmount } from '@multiversx/sdk-dapp/out/lib/sdkDappUtils'; + +export function AccountCard(): JSX.Element { + // The hook you want almost all of the time. Returns AccountType directly — + // no destructuring through a bigger "everything" object. + const account = useGetAccount(); + + // The latest nonce, alone — a thin convenience wrapper around the same + // store value as account.nonce. Handy when a component only cares about + // the nonce and you don't want to pull in the whole AccountType. + const latestNonce = useGetLatestNonce(); + + // The network config, so we can label the balance with the right symbol + // (EGLD on mainnet, xEGLD on some custom networks) instead of hard-coding it. + const { network } = useGetNetworkConfig(); + + // The lower-level, back-compat-shaped hook. Useful when you also need + // provider-adjacent fields (publicKey, ledgerAccount, walletConnectAccount, + // websocket event snapshots) that AREN'T part of AccountType. Note this is + // NOT the same shape as sdk-dapp v4's useGetAccountInfo() — v5 dropped + // isLoggedIn and tokenLogin from it (those moved to useGetLoginInfo() / + // useGetIsLoggedIn()). + const accountInfo = useGetAccountInfo(); + + return ( +
+

useGetAccount()

+
+
address
+
+ {account.address} +
+ +
balance
+
+ + {formatAmount({ + input: account.balance, + decimals: 18, + digits: 4, + showLastNonZeroDecimal: true, + })}{' '} + {network.egldLabel} + {' '} + (raw: {account.balance}) +
+ +
nonce
+
+ {account.nonce}{' '} + + (useGetLatestNonce() agrees: {latestNonce}) + +
+ +
shard
+
+ {account.shard ?? 'unknown — optional field'} +
+ +
username
+
+ {account.username || '(none set — optional field)'} +
+ +
txCount / scrCount
+
+ + {account.txCount} / {account.scrCount} + +
+ +
isGuarded
+
+ {String(account.isGuarded)} +
+
+ +

useGetAccountInfo() — the fields AccountType doesn't have

+
+
publicKey
+
+ {accountInfo.publicKey} +
+
ledgerAccount
+
+ {accountInfo.ledgerAccount ? 'connected via Ledger' : 'null'} +
+
walletConnectAccount
+
+ {accountInfo.walletConnectAccount ?? 'null'} +
+
+
+ ); +} + +const cardStyle: React.CSSProperties = { + border: '1px solid #444', + borderRadius: '8px', + padding: '1.25rem', + marginTop: '1rem', +}; + +const dlStyle: React.CSSProperties = { + display: 'grid', + gridTemplateColumns: 'max-content 1fr', + columnGap: '1rem', + rowGap: '0.4rem', + margin: 0, +}; + +const rawStyle: React.CSSProperties = { + color: '#888', + fontSize: '0.85em', +}; +``` + +## The demo page + +```tsx title="src/App.tsx" +// src/App.tsx — demo page: connect, then show every account field. + +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import { AccountCard } from './AccountCard'; + +export function App(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + + const handleConnect = (): void => { + void UnlockPanelManager.getInstance().openUnlockPanel(); + }; + + const handleDisconnect = async (): Promise => { + const provider = getAccountProvider(); + await provider.logout(); + }; + + return ( +
+

Reading the connected account

+ + {!isLoggedIn ? ( + + ) : ( + <> + + + + )} +
+ ); +} + +const buttonStyle: React.CSSProperties = { + padding: '0.75rem 1.25rem', + fontSize: '1rem', + cursor: 'pointer', +}; +``` + +## Provider bootstrap + +Every sdk-dapp recipe shares the same init wrapper. `providers.tsx` calls +`initApp()` once and gates rendering until the store is ready; `lib/multiversx.ts` +holds the environment config it passes in. They are shown here so the recipe +compiles as a complete unit, but the subject of this recipe is `AccountCard.tsx` +above. + +```tsx title="src/providers.tsx" +// src/providers.tsx — sdk-dapp v5 init wrapper. +// +// This recipe's subject is AccountCard.tsx, not this file — this is the +// same login bootstrap every recipe in this Cookbook uses. + +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { dappConfig } from './lib/multiversx'; + +let initStarted = false; + +export function Providers({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [ready, setReady] = useState(false); + + useEffect(() => { + if (initStarted) { + setReady(true); + return; + } + initStarted = true; + + let cancelled = false; + void initApp(dappConfig).then(() => { + if (cancelled) return; + + UnlockPanelManager.init({ + loginHandler: async (): Promise => { + // eslint-disable-next-line no-console + console.log('Logged in.'); + }, + onClose: async (): Promise => { + // eslint-disable-next-line no-console + console.log('Unlock panel closed.'); + }, + }); + + setReady(true); + }); + + return () => { + cancelled = true; + }; + }, []); + + if (!ready) { + return ( +
+ Initializing wallet SDK… +
+ ); + } + + return <>{children}; +} +``` + +```ts title="src/lib/multiversx.ts" +// src/lib/multiversx.ts — environment configuration for sdk-dapp's initApp. +// Same shape as every other Vite recipe in this Cookbook. + +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +const WALLET_CONNECT_PROJECT_ID = + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; + +export const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + nativeAuth: true, + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + }, +}; + +export { EnvironmentsEnum }; +``` + +## How it works + +**`useGetAccount()` is a synchronous store read, not a network call.** It returns +`AccountType`, verified field by field against +`node_modules/@multiversx/sdk-dapp/out/types/account.types.d.ts` on the +actually-installed `@multiversx/sdk-dapp@5.6.23`: + +```ts +interface AccountType { + address: string; + balance: string; // smallest denomination, decimal string + nonce: number; + txCount: number; + scrCount: number; + claimableRewards: string; + isGuarded: boolean; + // optional: username, shard, code, ownerAddress, developerReward, + // deployedAt, scamInfo, isUpgradeable, isReadable, isPayable, + // isPayableBySmartContract, assets, and the active/pending guardian fields +} +``` + +The store refreshes this automatically, on initial login and after each tracked +transaction settles. Calling the hook again on every render is fine and expected; +it does not refetch anything by itself. + +**Format the balance, always.** `account.balance` is the raw smallest-unit +string. `formatAmount({ input: account.balance, decimals: 18, digits: 4, showLastNonZeroDecimal: true })` +gives you the human-readable decimal EGLD amount with the precision behavior you +want. + +**`useGetAccountInfo()` still exists in v5, with a narrower shape than v4.** It +dropped `isLoggedIn` and `tokenLogin` (now `useGetIsLoggedIn()` / +`useGetLoginInfo()`), but adds fields `AccountType` does not have: `publicKey`, +`ledgerAccount`, `walletConnectAccount`, `websocketEvent`, `websocketBatchEvent`. +Reach for it only when you need one of those; `useGetAccount()` is the right +default for everything else. + +## Pitfalls + +:::danger[Pitfall 1: never render account.balance directly] +It is the raw smallest-unit string (`"1500000000000000000"`, not `"1.5"`). Always +pass it through `formatAmount()` first. Rendering it raw is the single most common +"why does my balance say a huge number" support question. +::: + +:::warning[Pitfall 2: shard and username are optional] +A freshly created account, or one the API has not fully indexed, may have +`shard: undefined`. Guard with `??` or a conditional. Do not assume every optional +`AccountType` field is always present, as `AccountCard.tsx` does for both. +::: + +:::warning[Pitfall 3: the hook doesn't refetch, refreshAccount() does] +`useGetAccount()` returns whatever the store currently holds. It updates +automatically after login and after tracked transactions settle. If you need to +force a fresh read outside those triggers, call `refreshAccount()` +(`@multiversx/sdk-dapp/out/utils/account/refreshAccount`) instead of expecting the +hook itself to hit the network. +::: + +:::warning[Pitfall 4: getAccount vs getAccountFromApi] +`useGetAccount()` (and its non-React twin `getAccount()`) are free, instant store +reads. `getAccountFromApi` is a *different* function that makes a real network +round-trip on every call. Do not reach for it inside a render loop thinking it is +just a renamed getter. +::: + +## See also + +- [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld) + is the sdk-core, script-side counterpart, for when your own code holds the key. +- [Configure a network provider](/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider) + shows the read-side provider the sdk-dapp store wraps for you. +- [docs.multiversx.com, sdk-dapp](https://docs.multiversx.com/sdk-and-tools/sdk-dapp) + is the conceptual reference. + +--- + +### receipts + +This page describes the structure of the `receipts` index (Elasticsearch), and also depicts a few examples of how to query it. + + +## _id + +The `_id` field of this index is composed of hex encoded receipt hash. +(example: `ced4692a092226d68fde24840586bdf36b30e02dc4bf2a73516730867545d53c`) + + +## Fields + + +| Field | Description | +|-----------|-----------------------------------------------------------------------------------------------------| +| value | The value field represents the amount of EGLD that was refunded/penalized from the transaction fee. | +| sender | The sender field represents the sender of the transaction that generated the receipt. | +| data | The data field holds a message with the reason why the receipt was generated. | +| txHash | The txHash field represents the hash of the transaction that generated the receipt. | +| timestamp | The timestamp field represents the timestamp of the block in which the receipt was generated. | + + +## Query examples + + +### Fetch the receipt generated by a transaction + +``` +curl --request GET \ + --url ${ES_URL}/receipts/_search \ + --header 'Content-Type: application/json' \ + --data '{ + "query": { + "match": { + "txHash":"d6.." + } + } +}' +``` + +--- + +### Receiver + +[comment]: # "mx-abstract" + +## Overview + +Among the seven distinct generics defining a transaction, `To` signifies the **third** generic field - **the entity that receives the transaction**. With the exception of deployments, it is required to be specified for every transaction in any environment. + + +## Diagram + +The sender is being set using the `.to(...)` method. Several types can be specified: + +```mermaid +graph LR + subgraph To + to-unit["()"] + to-unit -->|to| to-man-address[ManagedAddress] + to-unit -->|to| to-address[Address] + to-unit -->|to| to-bech32[Bech32Address] + to-unit -->|to| to-esdt-system-sc[ESDTSystemSCAddress] + to-unit -->|to| to-caller[ToCaller] + to-unit -->|to| to-self[ToSelf] + end +``` + + +## No recipient + +Across the three distinct environments in which a transaction can be initialised, `deploy`, also known as the `init` function, stands alone as the only invocation that cannot designate the recipient. + +```rust title=blackbox.rs +fn deploy() { + self.world + .tx() + .from(OWNER_ADDRESS) + .typed(proxy::Proxy) + .init(init_value) + .code(CODE_PATH) + .new_address(DEPLOY_ADDRESS) // Sets the new mock address to be used for the newly deployed contract. + .run(); +} +``` + + + +## Explicit recipient + +Transactions, excluding deployments, require the designation of a recipient. This means that most transaction calls must utilise the `.to` method, explicitly specifying the receiving entity. + +In the subsequent section, we will go into the various data types that are permissible for recipient nomination. + + +### Address + +Below there is an interactor that funds a specific contract with an amount of EGLD. The recipient contract is instantiated as an address object within the interactor's context. + +```rust title=interactor.rs +async fn feed_contract_egld(&mut self) { + self.interactor + .tx() + .from(&self.wallet_address) + .to(self.state.current_adder_address()) + .egld(NumExpr("0,050000000000000000")) + .prepare_async() + .run() + .await; +} +``` + +This function, which is a sample from a **blackbox test**, increases a value and sends it to a particular wallet. This example specifically focuses on the `.to` call, which establishes the **receiver**. In this case, it is a hardcoded **ManagedAddress** instance. + +```rust title=blackbox_test.rs +fn add_one(&mut self, from: &AddressValue) { + let to_wallet: ManagedAddress = ManagedAddress::new_from_bytes(&[7u8; 32]); + self.world + .tx() + .from(OWNER_ADDRESS) + .to(to_wallet) + .typed(proxy::Proxy) + .add(1u32) + .run(); +} +``` + +## **TestSCAddress** + +For parametric testing, there are particular address types. `TestSCAddress` encodes a dummy smart contract address, equivalent to `"sc:{}"`; For the example below it is equivalent to `"sc:example_contract"`; + - contains two functions: + - **`.eval_to_array()`** parses the address into an array of u8. + - **`.eval_to_expr()`** returns the address as a string object. + - **`.to_address()`** returns the actual Address object. + +The following example is a fragment from a blackbox test designed for *Price Aggregator Smart Contract* (code available [here](https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/core/price-aggregator)). This function simulates the staking of an amount of EGLD within the *Price Aggregator Smart Contract*. +```rust title=price_aggregator_blackbox_test.rs +const STAKE_AMOUNT: u64 = 20; +const PRICE_AGGREGATOR_ADDRESS: TestSCAddress = TestSCAddress::new("price-aggregator"); + +fn stake(&mut self) { + self.world + .tx() + .from(self.address) + .to(PRICE_AGGREGATOR_ADDRESS) + .typed(price_aggregator_proxy::PriceAggregatorProxy) + .stake() + .egld(STAKE_AMOUNT) + .run(); +} +``` + +### Bech32Address +In order to avoid repeated conversions, it keeps the **Bech32** representation **inside**. It wraps the address and presents it as a Bech32 expression. + +The example below is a piece of an interactor for the *Adder Smart Contract* (code available [here](https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples/adder)). The aim of the function is to print the total sum of the contract. The receiver is set via the Bech32Address variable. + +```rust title=interact.rs +async fn print_sum(&mut self, adder_address: &Bech32Address) { + let sum = self + .interactor + .query() + .to(adder_address) + .typed(adder_proxy::AdderProxy) + .sum() + .returns(ReturnsResultUnmanaged) + .prepare_async() + .run() + .await; + + println!("sum: {sum}"); +} +``` + + +## Special recipient + + +### ESDTSystemSCAddress +This type indicates the system smart contract address, which is the same on any MultiversX blockchain. + - **`.to_managed_address()`**: converts the address to **ManagedAddress**. + - **`.to_bech32_str()`**: returns the **str** value of the address. + - **`.to_bech32_string()`**: returns the **String** value of the address. + +The next example represents a part of a **smart contract** whose aim is to issue semi-fungible tokens. The call is made via a system proxy for the ESDT system smart contract. More details regarding the system proxy can be found [here TBD](https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples/adder). +```rust title=lib.rs +fn sft_issue( + issue_cost: BigUint, + token_display_name: ManagedBuffer, + token_ticker: ManagedBuffer, +) -> IssueCallTo { + Tx::new_tx_from_sc() + .to(ESDTSystemSCAddress) + .typed(ESDTSystemSCProxy) + .issue_semi_fungible( + issue_cost, + &token_display_name, + &token_ticker, + SemiFungibleTokenProperties::default(), + ) +} +``` + +### ToSelf +It indicates that the transaction should be sent to itself. + +The following lines illustrate an example of changing attributes of an NFT via a system proxy function. +```rust title=lib.rs +pub fn nft_update_attributes( + &self, + token_id: &TokenIdentifier
, + nft_nonce: u64, + new_attributes: &T, +) { + Tx::new_tx_from_sc() + .to(ToSelf) + .gas(GasLeft) + .typed(system_proxy::UserBuiltinProxy) + .nft_update_attributes(token_id, nft_nonce, new_attributes) + .sync_call() +} +``` + +### ToCaller +It indicates that the transaction should be sent to the caller, which is the sender of the current transaction. + +The next example is a snippet from an endpoint that transfers ESDT to the sender of the transaction. +```rust +self.tx().to(ToCaller).single_esdt(&id, nonce, &BigUint::from(1u8)).transfer(); +``` + +--- + +### Relayed Transactions + +On this page, you will find comprehensive information on all aspects of relayed transactions. + + +## Introduction + +Relayed transactions (or meta-transactions) are transactions with the fee paid by a so-called relayer. +In other words, if a relayer is willing to pay for an interaction, it is not mandatory that the address +interacting with a Smart Contract has any EGLD for fees. + +More details and specifications can be found on [MultiversX Specs](https://github.com/multiversx/mx-specs/blob/main/sc-meta-transactions.md). + + +## Types of relayed transactions + +Currently, there are 3 versions of relayed transactions: v1, v2 and v3. In the end, they all have the same effect. + +Relayed v2 was meant to bring optimisations in terms of gas usage. But v3 reduces the costs even further, **making it our recommendation**. + +Once all applications will completely transition to relayed v3 model, v1 and v2 will be removed. + + +## Relayed transactions version 1 + +:::note +Legacy version. Please use [version 3](#relayed-transactions-version-3), instead. +::: + + + +## Relayed transactions version 2 + +:::note +Legacy version. Please use [version 3](#relayed-transactions-version-3), instead. +::: + + +## Relayed transactions version 3 + +Relayed transactions v3 feature comes with a change on the entire transaction structure, adding two new optional fields: +- `relayer`, which is the relayer address that will pay the fees. +- `relayerSignature`, the signature of the relayer that proves the agreement of the relayer. + +That being said, relayed transactions v3 will look and behave very similar to a regular transaction, the only difference being the gas consumption from the relayer. It is no longer needed to specify the user transaction in the data field. + +In terms of gas limit computation, an extra base cost will be consumed. Let's consider the following example: relayed transaction with inner transaction of type move balance, that also has a data field `test` of length 4. +```js + gasLimitInnerTx = + * length(txData) + gasLimitInnerTx = 50_000 + 4 * 1_500 + gasLimitInnerTx = 56_000 + + gasLimitRelayedTx = + + gasLimitRelayedTx = 50_000 + 56_000 + gasLimitRelayedTx = 106_000 +``` + +It would look like: + +```rust +RelayedV3Transaction { + Sender: + Receiver: + Value: + GasLimit: + + * length(txData) + Relayer: + RelayerSignature: + Signature: +} +``` + +Therefore, in order to build such a transaction, one has to follow the next steps: + - set the `relayer` field to the address that would pay the gas + - add the extra base cost for the relayed operation + - add sender's signature + - add relayer's signature + +:::note +1. For a guarded relayed transaction, the guarded operation fee will also be consumed from the relayer. +2. Relayer must be different from guardian, in case of guarded sender. +3. Guarded relayers are not allowed. +4. Relayer address must be in the same shard as the transaction sender. +::: + +### Example + +Here's an example of a relayed v3 transaction. Its intent is to call the `add` method of a previously deployed adder contract, with parameter `01` + +```json +{ + "nonce": 0, + "value": "0", + "receiver": "erd1qqqqqqqqqqqqqpgqeunf87ar9nqeey5ssvpqwe74ehmztx74qtxqs63nmx", + "sender": "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + "data": "YWRkQDAx", + "gasPrice": 1000000000, + "gasLimit": 5000000, + "signature": "...", + "chainID": "T", + "version": 2, + "relayer": "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + "relayerSignature": "..." +} +``` + +### Preparing relayed transactions using the SDKs + +The SDKs have built-in support for relayed transactions. Please follow: + - [mxpy support](/sdk-and-tools/mxpy/mxpy-cli/#relayed-transactions-v3) + - [sdk-py support](/sdk-and-tools/sdk-py/#relayed-transactions) + - [sdk-js v15 support](/sdk-and-tools/sdk-js/sdk-js-cookbook#relayed-transactions) + +--- + +### Reproducible Builds + +This page will guide you through the process of supporting [reproducible contract builds](https://en.wikipedia.org/wiki/Reproducible_builds), by leveraging Docker and a set of [_frozen_ Docker images available on DockerHub](https://hub.docker.com/r/multiversx/sdk-rust-contract-builder/tags). + +You will also learn how to reproduce a contract build, given its source code and the name (tag) of a _frozen_ Docker image that was used for its previous build (that we want to reproduce). + +> **Reproducible builds**, also known as **deterministic compilation**, is a process of compiling software which ensures the resulting binary code can be reproduced. Source code compiled using deterministic compilation will always output the same binary [[Wikipedia]](https://en.wikipedia.org/wiki/Reproducible_builds). + +:::important +As of May 2024, the Rust toolchain does not support reproducible builds out-of-the-box, thus we recommend smart contract developers to follow this tutorial in order to achieve deterministic compilation. +::: + + +## Smart Contract "codehash" + +Before diving into contract build reproducibility, let's grasp the concept of `codehash`. + +When a smart contract is deployed, the network stores the bytecode, and also computes its `blake2b` checksum (using a digest length of 256 bits). This is called the `codehash`. + +Assume that we are interested into the following contract (a simple on-chain **adder**), deployed on _devnet_: [erd1qqqqqqqqqqqqqpgqws44xjx2t056nn79fn29q0rjwfrd3m43396ql35kxy](https://devnet-explorer.multiversx.com/accounts/erd1qqqqqqqqqqqqqpgqws44xjx2t056nn79fn29q0rjwfrd3m43396ql35kxy). It's source code is published on [GitHub](https://github.com/multiversx/mx-contracts-rs). + +We can fetch the _codehash_ of the contract from the API: + +```bash +curl -s https://devnet-api.multiversx.com/accounts/erd1qqqqqqqqqqqqqpgqws44xjx2t056nn79fn29q0rjwfrd3m43396ql35kxy \ +| jq -r -j .codeHash \ +| base64 -d \ +| xxd -p \ +| tr -d '\n' +``` + +The output is: + +``` +384b680df7a95ebceca02ffb3e760a2fc288dea1b802685ef15df22ae88ba15b +``` + +If the `WASM` file is directly available, we can also use the utility `b2sum` to locally compute the _codehash_: + +```bash +b2sum -l 256 adder.wasm +``` + +The output would be the same: + +``` +384b680df7a95ebceca02ffb3e760a2fc288dea1b802685ef15df22ae88ba15b +``` + +All in all, in order to verify the bytecode equality of two given builds of a contract we can simply compare the _codehash_ property. + + +## Supporting reproducible builds + +As of May 2024, the recommended approach to support reproducible builds for your smart contract is to use a build script relying on a specially-designed, [publicly-available, tagged Docker image](https://hub.docker.com/r/multiversx/sdk-rust-contract-builder/tags), that includes tagged, explicit versions of the build tools (_Rust_, _wasm-opt_ etc.). + +This approach is recommended in order to counteract eventual pieces of non-determinism related to `cargo`'s (essential component of the Rust toolchain) sensibility on the environment. + +:::important +If the code source of your smart contract is hosted on GitHub, then it's a good practice to define a GitHub Workflow similar to [release.yml](https://github.com/multiversx/mx-contracts-rs/blob/main/.github/workflows/release.yml), which performs the deployment (production-ready) build within the _release_ procedure. Additionally, define a dry-run reproducible build on all your branches. See this workflow as an example: [on_pull_request_build_contracts.yml](https://github.com/multiversx/mx-contracts-rs/blob/main/.github/workflows/on_pull_request_build_contracts.yml). +::: + + +### Choose an image tag + +For a new smart contract that isn't released yet (deployed on the network), it's recommended to pick the tag with the **largest index number**, which typically includes recent versions of `rust` and other necessary dependencies. + +However, for minor releases or patches, it's wise to stick to the previously chosen image tag, for the same (nuanced) reasons you would not embrace an update of your development tools in the middle of fixing a critical bug (in any development context). + +The chosen, _frozen_ image tag **should accompany the versioned source code (e.g. via _release notes_), in order to inform others on how to reproduce a specific build** (of a specific source code version). In this context, a _frozen_ image tag refers to a Docker image tag that will never get any updates after its initial publishing. + +:::tip +It's perfectly normal to switch to a newer image tag on each (major) release of your contract. Just make sure you spread this information - i.e. using _release notes_. +::: + +:::caution +Never pick the tag called `latest` or `next` for production-ready builds. +::: + + +## Building via Docker (reproducible build) + +In this section, you'll learn how to run a reproducible build, or, to put it differently, how to reproduce a previous build (made by you or by someone else in the past), on the local machine, using Docker - without the need to install other tools such as _mxpy_ (nor its dependencies). + + +### Fetch the source code + +Let's clone [mx-contracts-rs](https://github.com/multiversx/mx-contracts-rs) locally, and switch to [a certain version](https://github.com/multiversx/mx-contracts-rs/releases/tag/v0.45.4) that we'd like to build: + +```bash +mkdir -p ~/contracts && cd ~/contracts +git clone https://github.com/multiversx/mx-contracts-rs.git --branch=v0.45.4 --depth=1 +``` + +By inspecting the release notes, we see that [`v0.45.4`](https://github.com/multiversx/mx-contracts-rs/releases/tag/v0.45.4) was built using the `image:tag = multiversx/sdk-rust-contract-builder:v5.4.1`. + + +### Download the build wrapper + +The build process (via Docker) is wrapped in a easy-to-use, friendly Python script. Let's download it: + +```bash +wget https://raw.githubusercontent.com/multiversx/mx-sdk-build-contract/main/build_with_docker.py +``` + + +### Prepare environment variables + +Export the following variables: + +```bash +export PROJECT=~/contracts/mx-contracts-rs +export BUILD_OUTPUT=~/contracts/output-from-docker +# Below, the image tag is just an example: +export IMAGE=multiversx/sdk-rust-contract-builder:v1.2.3 +``` + +The latter export statement explicitly selects the **chosen, _frozen_ Docker image tag** to be used. + + +### Perform the build + +Now let's build the contract by invoking the previously-downloaded build wrapper: + +```bash +python3 ./build_with_docker.py --image=${IMAGE} \ + --project=${PROJECT} \ + --output=${BUILD_OUTPUT} +``` + +In the `output` folder(s), you should see the following files (example): + +- `adder.wasm`: the actual bytecode of the smart contract, to be deployed on the network; +- `adder.abi.json`: the ABI of the smart contract (a listing of endpoints and types definitions), to be used when developing dApps or simply interacting with the contract (e.g. using _erdjs_); +- `adder.codehash.txt`: a file containing the computed `codehash` of the contract. +- **`adder.source.json`** : packaged (bundled) source code. + + +### TL;DR build snippet + +These being said, let's summarize the steps above into a single bash snippet: + +```bash +wget https://raw.githubusercontent.com/multiversx/mx-sdk-build-contract/main/build_with_docker.py + +export PROJECT=~/contracts/mx-contracts-rs +export BUILD_OUTPUT=~/contracts/output-from-docker +# Below, the image tag is just an example: +export IMAGE=multiversx/sdk-rust-contract-builder:v1.2.3 + +python3 ./build_with_docker.py --image=${IMAGE} \ + --project=${PROJECT} \ + --output=${BUILD_OUTPUT} +``` + + +### Reproducible build using mxpy + +A more straightforward alternative to the previous bash script is to use **mxpy** to build a contract in a reproducible manner. + +First, make sure you have the: + +- latest [mxpy](/sdk-and-tools/mxpy/installing-mxpy) installed, +- latest [docker engine](https://docs.docker.com/engine/install/) installed. + +Then, use the `reproducible-build` command (below, the image tag is just an example): + +``` +mxpy contract reproducible-build --docker-image="multiversx/sdk-rust-contract-builder:v1.2.3" +``` + +This will build all the smart contracts inside the current working directory. If you want to build the smart contracts inside another directory, you can specify an input directory: + +``` +mxpy contract reproducible-build ~/contracts/mx-contracts-rs --docker-image="multiversx/sdk-rust-contract-builder:v1.2.3" +``` + +Upon a successful build, an output folder named `output-docker` will be generated. It contains one subfolder for each contract, each holding the following files: + +- `contract.wasm`: the actual bytecode of the smart contract, to be deployed on the network; +- `contract.abi.json`: the ABI of the smart contract (a listing of endpoints and types definitions), to be used when developing dApps or simply interacting with the contract (e.g. using _sdk-js_); +- `contract.codehash.txt`: the computed `codehash` of the contract. +- **`contract-1.2.3.source.json`** : packaged (bundled) source code. + +:::tip +You can run a local test using [these example contracts](https://github.com/multiversx/mx-contracts-rs). +::: + + +### Comparing the codehashes + +Once the build is ready, you can check the codehash of the generated `*.wasm`, by inspecting the file `*.codehash.txt` + +For our example, that should be: + +``` +adder.codehash.txt: 384b680df7a95ebceca02ffb3e760a2fc288dea1b802685ef15df22ae88ba15b +``` + +We can see that it matches the previously fetched (or computed) codehash. That is, the contract deployed at [erd1qqqqqqqqqqqqqpgqws44xjx2t056nn79fn29q0rjwfrd3m43396ql35kxy](https://devnet-explorer.multiversx.com/accounts/erd1qqqqqqqqqqqqqpgqws44xjx2t056nn79fn29q0rjwfrd3m43396ql35kxy) is guaranteed to have been built from the same source code version as the one that we've checked out. + +**Congratulations!** You've achieved a reproducible contract build 🎉 + + +## How to verify a smart contract on Explorer? + +The new MultiversX Explorer provides a convenient way to visualise the source code of deployed smart contracts on blockchain. This is the beauty of the Web3 vision for a decentralized internet, where anyone can deploy contracts that everyone can interact with. + +:::caution +Please note that as a **Beta** feature still in development, certain steps described may undergo changes. +::: + +:::tip +Make sure that you have the latest `mxpy` installed. In order to install mxpy, follow the instructions at [install mxpy](/sdk-and-tools/mxpy/installing-mxpy). +::: + +1. The contract must be deterministically built as described [above](/developers/reproducible-contract-builds#building-via-docker-reproducible-build). +2. To start with the verification process, we need to first deploy the smart contract. For deploying contracts have a look [here](/sdk-and-tools/mxpy/mxpy-cli#deploying-a-smart-contract). +3. Upon deploying, the output will not only provide information such as the transaction hash and data, but also the address of the newly deployed contract. +4. In order to verify your contract the command you have to use is (below, the image tag is just an example): + +``` +mxpy --verbose contract verify "erd1qqqqqqqqqqqqqpgq6u07hhkfsvuk5aae92g549s6pc2s9ycq0dps368jr5" --packaged-src=./output-docker/contract/contract-0.0.0.source.json --verifier-url="https://play-api.multiversx.com" --docker-image="multiversx/sdk-rust-contract-builder:v1.2.3" --pem=contract-owner.pem +``` + +:::tip +For the above code snippet: +- `erd1qqqqqqqqqqqqqpgq6u07hhkfsvuk5aae92g549s6pc2s9ycq0dps368jr5` - should be your contract address (in this case is a dummy address); +- `--packaged-src=./output-docker/contract/contract-0.0.0.source.json` - should be found in the output folder after deterministically building your contract; +- `--verifier-url="https://play-api.multiversx.com"` - this is the verifier api address. Be advised that it may be subject to change; +- `--docker-image="multiversx/sdk-rust-contract-builder:v1.2.3"` - the same version utilized in constructing the contract must be utilized here too; +- `--pem=contract-owner.pem` - represents the owner of the contract. +::: + +5. Given the current limited bandwidth, it might take some time to be processed. + +--- + +### Rest API Addresses + +```mdx-code-block +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +``` + + +This component of the REST API allows one to query information about Addresses (Accounts). + + +## GET **Get Address** {#get-address} + +`https://gateway.multiversx.com/address/:bech32Address` + +This endpoint allows one to retrieve basic information about an Address (Account). + + + + +Path Parameters + +| Param | Required | Type | Description | +| ----- | ----------------------------------------- | -------- | ------------------------- | +| bech32Address | REQUIRED | `string` | The Address to query. | + + + + +🟢 200: OK + +Address details retrieved successfully. + +```json +{ + "data": { + "account": { + "address": "erd1...", + "nonce": 11, + "balance": "100000000000000000000", + "username": "", + "code": "", + "codeHash": null, + "rootHash": "qBFvpFeF6...", + "codeMetadata": null, + "developerReward": "0", + "ownerAddress": "", + } + }, + "blockInfo":{ + "nonce": 555, + "hash": "f55fe00...", + "rootHash": "294360..." + } + "error": "", + "code": "successful" +} +``` + + + + +:::important +If an account (that is not a smart contract, smart contracts cannot be guarded) has an ```activeGuardian``` and is ```guarded```, the ```codeMetadata``` of the account should be [Guarded](https://github.com/multiversx/mx-chain-vm-common-go/blob/master/codeMetadata.go). +::: + + +## GET **Get Address Guardian Data** {#get-address-guardian-data} + +`https://gateway.multiversx.com/address/:bech32Address/guardian-data` + +This endpoint allows one to retrieve the guardian data of an Address. + + + + +Path Parameters + +| Param | Required | Type | Description | +| ------------- | ----------------------------------------- | -------- | --------------------- | +| bech32Address | REQUIRED | `string` | The Address to query. | + + + + +🟢 200: OK + +Guardian Data successfully retrieved. + +```json +{ + "data": { + "blockInfo": { + "hash":"a11aa...", + "nonce":197, + "rootHash":"a6d70..." + }, + "guardianData": { + "activeGuardian": { + "activationEpoch": 15, + "address": "erd1...", + "serviceUID": "uuid" + }, + "guarded": true, + "pendingGuardian": { + "activationEpoch": 35, + "address": "erd1...", + "serviceUID": "uuid" + }, + }, + }, + "error": "", + "code": "successful" +} +``` + + + + +:::caution +In the response example mentioned above, the account has already set the ```activeGuardian``` (using a ```SetGuardian``` transaction), guarded the account (with a ```GuardAccount``` transaction), and also set the ```pendingGuardian``` (using an unguarded ```SetGuardian``` transaction). We intentionally chose this scenario to display all the fields for ```blockInfo``` and ```guardianData```. +::: + + +## GET **Get Address Nonce** {#get-address-nonce} + +`https://gateway.multiversx.com/address/:bech32Address/nonce` + +This endpoint allows one to retrieve the nonce of an Address. + + + + +Path Parameters + +| Param | Required | Type | Description | +| ------------- | ----------------------------------------- | -------- | --------------------- | +| bech32Address | REQUIRED | `string` | The Address to query. | + + + + +🟢 200: OK + +Nonce successfully retrieved. + +```json +{ + "data": { + "nonce": 5 + }, + "error": "", + "code": "successful" +} +``` + + + + + +## GET **Get Address Balance** {#get-address-balance} + +`https://gateway.multiversx.com/address/:bech32Address/balance` + +This endpoint allows one to retrieve the balance of an Address. + + + + +Path Parameters + +| Param | Required | Type | Description | +| ------------- | ----------------------------------------- | -------- | --------------------- | +| bech32Address | REQUIRED | `string` | The Address to query. | + + + + +🟢 200: OK + +Balance successfully retrieved. + +```json +{ + "data": { + "balance": "100000000000000000000" + }, + "error": "", + "code": "successful" +} +``` + + + + + +## GET **Get Address Username (herotag)** {#get-address-username-herotag} + +`https://gateway.multiversx.com/address/:bech32Address/username` + +This endpoint allows one to retrieve the username / herotag of an Address (if any). + + + + +Path Parameters + +| Param | Required | Type | Description | +| ------------- | ----------------------------------------- | -------- | --------------------- | +| bech32Address | REQUIRED | `string` | The Address to query. | + + + + +🟢 200: OK + +Balance successfully retrieved. + +```json +{ + "data": { + "username": "docs.elrond" + }, + "error": "", + "code": "successful" +} +``` + + + + + +## GET **Get Address Transactions (deprecated)** {#get-address-transactions} + +`https://gateway.multiversx.com/address/:bech32Address/transactions` + +:::caution +This endpoint is deprecated. In order to fetch the Transactions involving an Address, use the [transactions](https://api.multiversx.com/#/accounts/AccountController_getAccountTransactions) endpoint of MultiversX API, instead. +::: + +This endpoint allows one to retrieve the latest 20 Transactions sent from an Address. + + + + +Path Parameters + +| Param | Required | Type | Description | +| ------------- | ----------------------------------------- | -------- | --------------------- | +| bech32Address | REQUIRED | `string` | The Address to query. | + + + + +🟢 200: OK + +Transactions successfully retrieved. + +```json +{ + "data": { + "transactions": [ + { + "hash": "1a3e...", + "fee": "10000000000000000", + "miniBlockHash": "9673...", + "nonce": 68, + "round": 33688, + "value": "1000000000000000000", + "receiver": "erd1...", + "sender": "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz", + "receiverShard": 0, + "senderShard": 0, + "gasPrice": 200000000000, + "gasLimit": 50000, + "gasUsed": 50000, + "data": "", + "signature": "ed75...", + "timestamp": 1591258128, + "status": "Success", + "scResults": null + }, + { + "hash": "d72d...", + "fee": "10000000000000000", + "miniBlockHash": "fd45...", + "nonce": 67, + "round": 27353, + "value": "100000000000000000000000000", + "receiver": "erd1...", + "sender": "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz", + "receiverShard": 1, + "senderShard": 0, + "gasPrice": 200000000000, + "gasLimit": 50000, + "gasUsed": 50000, + "data": "", + "signature": "bb98...", + "timestamp": 1591220142, + "status": "Success", + "scResults": null + }, + ... + ] + }, + "error": "", + "code": "successful" +} +``` + + + + +:::caution +This endpoint is not available on Observer Nodes. It is only available on MultiversX Proxy. + +**Currently, this endpoint is only available on the Official MultiversX Proxy instance.** + +This endpoint requires the presence of an Elasticsearch instance (populated through Observers) as well. +::: + + +## GET **Get Storage Value for Address** {#get-storage-value-for-address} + +`https://gateway.multiversx.com/address/:bech32Address/key/:key` + +This endpoint allows one to retrieve a value stored within the Blockchain for a given Address. + + + + +Path Parameters + +| Param | Required | Type | Description | +| ------------- | ----------------------------------------- | -------- | ----------------------- | +| bech32Address | REQUIRED | `string` | The Address to query. | +| key | REQUIRED | `string` | The key entry to fetch. | + +The key must be hex-encoded. + + + + +🟢 200: OK + +Value (hex-encoded) successfully retrieved. + +```json +{ + "data": { + "value": "abba" + }, + "error": "", + "code": "successful" +} +``` + + + + + +## GET **Get all storage for Address** {#get-all-storage-for-address} + +`https://gateway.multiversx.com/address/:bech32Address/keys` + +This endpoint allows one to retrieve all the key-value pairs stored under a given account. + + + + +Path Parameters + +| Param | Required | Type | Description | +| ------------- | ----------------------------------------- | -------- | --------------------- | +| bech32Address | REQUIRED | `string` | The Address to query. | + + + + +🟢 200: OK + +Key-value pairs (both hex-encoded) successfully retrieved. + +```json +{ + "data": { + "pairs": { + "abba": "6f6b" + ... + } + }, + "error": "", + "code": "successful" +} +``` + + + + + +## **ESDT tokens endpoints** + +There are a number of ESDT tokens endpoints that one can use to check all tokens of an address, balance for +specific fungible or non-fungible tokens or so on. + +Fungible tokens endpoints can be found [here](/tokens/fungible-tokens/#rest-api) and non-fungible tokens +endpoints can be found [here](/tokens/nft-tokens/#rest-api). + +--- + +### Rest API Blocks + +```mdx-code-block +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +``` + + +This component of the REST API allows one to query information about Blocks and Hyperblocks. + + +## GET **Get Hyperblock by Nonce** {#get-hyperblock-by-nonce} + +`https://gateway.multiversx.com/hyperblock/by-nonce/:nonce` + +This endpoint allows one to query a Hyperblock by its nonce. + + + + +Path Parameters + +| Param | Required | Type | Description | +| ----- | ----------------------------------------- | -------- | ------------------------- | +| nonce | REQUIRED | `number` | The Block nonce (height). | + + + + +🟢 200: OK + +Block details retrieved successfully. + +```json +{ + "hyperblock": { + "nonce": 185833, + "round": 186582, + "hash": "6a33...", + "prevBlockHash": "aa7e...", + "epoch": 12, + "numTxs": 1, + "shardBlocks": [ + { + "hash": "cba4...", + "nonce": 186556, + "shard": 0 + }, + { + "hash": "50a16...", + "nonce": 186535, + "shard": 1 + }, + { + "hash": "7981...", + "nonce": 186536, + "shard": 2 + } + ], + "transactions": [ + { + "type": "normal", + "hash": "b035...", + "nonce": 3, + "value": "1000000000000000000", + "receiver": "erd1...", + "sender": "erd1...", + "gasPrice": 1000000000, + "gasLimit": 70000, + "data": "Zm9yIHRlc3Rz", + "signature": "1047...", + "status": "executed" + } + ] + } +} +``` + + + + +:::important +This endpoint is only defined by the Proxy. The Observer does not expose this endpoint. +::: + +:::tip +A **Hyperblock** is a block-like abstraction that reunites the data from all shards, and contains only **fully-executed transactions** (that is, transactions executed both in _source_ and in _destination_ shard). + +A **hyperblock** is composed using a **metablock** as a starting point - therefore, the `nonce` or `hash` of a hyperblock is the same as the `nonce` or `hash` of the base metablock. +::: + + +## GET **Get Hyperblock by Hash** {#get-hyperblock-by-hash} + +`https://gateway.multiversx.com/hyperblock/by-hash/:hash` + +This endpoint allows one to query a Hyperblock by its hash. + + + + +Path Parameters + +| Param | Required | Type | Description | +| ----- | ----------------------------------------- | -------- | --------------- | +| hash | OPTIONAL | `string` | The Block hash. | + + + + +🟢 200: OK + +```json +{ + "hyperblock": { + "nonce": 185833, + "round": 186582, + "hash": "6a33...", + "prevBlockHash": "aa7e...", + "epoch": 12, + "numTxs": 1, + "shardBlocks": [ + { + "hash": "cba4...", + "nonce": 186556, + "shard": 0 + }, + { + "hash": "50a16...", + "nonce": 186535, + "shard": 1 + }, + { + "hash": "7981...", + "nonce": 186536, + "shard": 2 + } + ], + "transactions": [ + { + "type": "normal", + "hash": "b035...", + "nonce": 3, + "value": "1000000000000000000", + "receiver": "erd1...", + "sender": "erd1...", + "gasPrice": 1000000000, + "gasLimit": 70000, + "data": "Zm9yIHRlc3Rz", + "signature": "1047...", + "status": "executed" + } + ] + } +} +``` + + + + +:::important +This endpoint is only is only defined by the Proxy. The Observer does not expose this endpoint. +::: + + +## GET **Get Block by Nonce** {#get-block-by-nonce} + +`https://gateway.multiversx.com/block/:shard/by-nonce/:nonce` + +This endpoint allows one to query a Shard Block by its nonce (or height). + + + + +Path Parameters + +| Param | Required | Type | Description | +| ----- | ----------------------------------------- | -------- | ------------------------- | +| shard | OPTIONAL | `number` | The Shard. | +| nonce | REQUIRED | `number` | The Block nonce (height). | + +Query Parameters + +| Param | Required | Type | Description | +| ------- | ----------------------------------------- | --------- | ---------------------------------------------------- | +| withTxs | OPTIONAL | `boolean` | Whether to include the transactions in the response. | + + + + +🟢 200: OK + +Block retrieved successfully, with transactions included. + +```json +{ + "data": { + "block": { + "nonce": 186532, + "round": 186576, + "hash": "7aa3...", + "prevBlockHash": "2580...", + "epoch": 12, + "shard": 2, + "numTxs": 1, + "miniBlocks": [ + { + "hash": "e927...", + "type": "TxBlock", + "sourceShard": 2, + "destinationShard": 1, + "transactions": [ + { + "type": "normal", + "hash": "b035...", + "nonce": 3, + "value": "1000000000000000000", + "receiver": "erd1...", + "sender": "erd1...", + "gasPrice": 1000000000, + "gasLimit": 70000, + "data": "Zm9yIHRlc3Rz", + "signature": "1047...", + "status": "partially-executed" + } + ] + } + ] + } + }, + "error": "", + "code": "successful" +} +``` + + + + +:::important +For Observers, the `shard` parameter should not be set. +::: + + +## GET **Get Block by Hash** {#get-hyperblock-by-hash} + +`https://gateway.multiversx.com/block/:shard/by-hash/:hash` + +This endpoint allows one to query a Shard Block by its hash. + + + + +Path Parameters + +| Param | Required | Type | Description | +| ----- | ----------------------------------------- | -------- | --------------- | +| shard | OPTIONAL | `number` | The Shard. | +| hash | REQUIRED | `string` | The Block hash. | + +Query Parameters + +| Param | Required | Type | Description | +| ------- | ----------------------------------------- | --------- | ---------------------------------------------------- | +| withTxs | OPTIONAL | `boolean` | Whether to include the transactions in the response. | + + + + +🟢 200: OK + +Block retrieved successfully, with transactions included. + +```json +{ + "data": { + "block": { + "nonce": 186532, + "round": 186576, + "hash": "7aa3...", + "prevBlockHash": "2580...", + "epoch": 12, + "shard": 2, + "numTxs": 1, + "miniBlocks": [ + { + "hash": "e927...", + "type": "TxBlock", + "sourceShard": 2, + "destinationShard": 1, + "transactions": [ + { + "type": "normal", + "hash": "b035...", + "nonce": 3, + "value": "1000000000000000000", + "receiver": "erd1...", + "sender": "erd1...", + "gasPrice": 1000000000, + "gasLimit": 70000, + "data": "Zm9yIHRlc3Rz", + "signature": "1047...", + "status": "partially-executed" + } + ] + } + ] + } + }, + "error": "", + "code": "successful" +} +``` + + + + +:::important +For Observers, the `shard` parameter should not be set. +::: + +--- + +### Rest API Network + +```mdx-code-block +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +``` + + +This component of the REST API allows one to query information about the Network, such as network configuration and parameters. + + +## GET **Get Network Configuration** {#get-network-configuration} + +`https://gateway.multiversx.com/network/config` + +This endpoint allows one to query basic details about the configuration of the Network. + + + + +🟢 200: OK + +Configuration details retrieved successfully. + +```json +{ + "data": { + "config": { + "erd_chain_id": "1", + "erd_denomination": 18, + "erd_gas_per_data_byte": 1500, + "erd_latest_tag_software_version": "v1.1.0.0", + "erd_meta_consensus_group_size": 400, + "erd_min_gas_limit": 50000, + "erd_min_gas_price": 1000000000, + "erd_min_transaction_version": 1, + "erd_num_metachain_nodes": 400, + "erd_num_nodes_in_shard": 400, + "erd_num_shards_without_meta": 3, + "erd_round_duration": 6000, + "erd_shard_consensus_group_size": 63, + "erd_start_time": 1596117600 + } + }, + "error": "", + "code": "successful" +} +``` + + + + + +## GET **Get Shard Status** {#get-shard-status} + +`https://gateway.multiversx.com/network/status/:shardId` + +This endpoint allows one to query the status of a given Shard. + + + + +Path Parameters + +| Param | Required | Type | Description | +| ------- | ----------------------------------------- | -------- | -------------------------------------------------------------------------- | +| shardID | REQUIRED | `number` | The Shard ID. 0, 1, 2 etc. Use 4294967295 in order to query the Metachain. | + + + + +🟢 200: OK + +Shard Status retrieved successfully. + +```json +{ + "data": { + "status": { + "erd_current_round": 187068, + "erd_epoch_number": 12, + "erd_highest_final_nonce": 187019, + "erd_nonce": 187023, + "erd_nonce_at_epoch_start": 172770, + "erd_nonces_passed_in_current_epoch": 14253, + "erd_round_at_epoch_start": 172814, + "erd_rounds_passed_in_current_epoch": 14254, + "erd_rounds_per_epoch": 14400 + } + }, + "error": "", + "code": "successful" +} +``` + + + + +:::important +The path parameter `**shardId**` is only applicable on the Proxy endpoint. The Observer endpoint does not define this parameter. +::: + +--- + +### Rest API Nodes + +```mdx-code-block +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +``` + + +This component of the REST API allows one to query information about Nodes within the Network (peers). + + +## GET **Get Heartbeat Status** {#get-heartbeat-status} + +`https://gateway.multiversx.com/node/heartbeatstatus` + +This endpoint allows one to query the status of the Nodes. + + + + +🟢 200: OK + +Heartbeat status is retrieved successfully. + +```json +{ + "data": { + "heartbeats": [ + ... + { + "timeStamp": "2020-06-04T16:02:41.191947208Z", + "publicKey": "006d...", + "versionNumber": "v1.0.125-0-g2164f5f04/go1.13.4/linux-amd64", + "nodeDisplayName": "DrDelphi4", + "identity": "stakingagency", + "totalUpTimeSec": 7367, + "totalDownTimeSec": 0, + "maxInactiveTime": "1m41.148001375s", + "receivedShardID": 4294967295, + "computedShardID": 4294967295, + "peerType": "eligible", + "isActive": true + }, + { + "timeStamp": "2020-06-04T16:02:29.567740999Z", + "publicKey": "667a...", + "versionNumber": "v1.0.125-0-g2164f5f04/go1.13.4/linux-amd64", + "nodeDisplayName": "DrDelphi0", + "identity": "stakingagency", + "totalUpTimeSec": 7367, + "totalDownTimeSec": 0, + "maxInactiveTime": "1m9.537847751s", + "receivedShardID": 4294967295, + "computedShardID": 4294967295, + "peerType": "eligible", + "isActive": true + }, + ... + ] + }, + "error": "", + "code": "successful" +} +``` + + + + + +## GET **Get Node Status** {#get-node-status} + +`http://localhost:8080/node/status` + +This endpoint allows one to query all the metrics of the Node. + + + + +🟢 200: OK + +Statistics retrieved successfully. + +```json +{ + "data": { + "metrics": { + "erd_shard_id": 2, + "erd_nonce": 403, + "erd_min_gas_limit": 50000, + "erd_min_gas_price": 1000000000, + "erd_denomination": 18, + ... + } + }, + "error": "", + "code": "successful" +} +``` + + + + +:::important +This endpoint is not available on the Proxy. Only Nodes (Observers) expose this endpoint. +::: + + +## GET **Get P2P Status** {#get-p2p-status} + +`http://localhost:8080/node/p2pstatus` + +This endpoint allows one to query the P2P status of the Node. + + + + +🟢 200: OK + +P2P status retrieved successfully. + +```json +{ + "data": { + "metrics": { + "erd_p2p_cross_shard_observers": "...", + "erd_p2p_cross_shard_validators": "...", + "erd_p2p_intra_shard_observers": "...", + "erd_p2p_intra_shard_validators": "...", + "erd_p2p_num_connected_peers_classification": "...", + "erd_p2p_num_receiver_peers_fast_reacting": 2, + "erd_p2p_num_receiver_peers_out_of_specs": 2, + "erd_p2p_num_receiver_peers_slow_reacting": 13, + "erd_p2p_peak_num_receiver_peers_fast_reacting": 8, + "erd_p2p_peak_num_receiver_peers_out_of_specs": 8, + "erd_p2p_peak_num_receiver_peers_slow_reacting": 13, + "erd_p2p_peak_peer_num_processed_messages_fast_reacting": 3, + "erd_p2p_peak_peer_num_processed_messages_out_of_specs": 3, + "erd_p2p_peak_peer_num_processed_messages_slow_reacting": 8, + "erd_p2p_peak_peer_num_received_messages_fast_reacting": 3, + "erd_p2p_peak_peer_num_received_messages_out_of_specs": 3, + "erd_p2p_peak_peer_num_received_messages_slow_reacting": 8, + "erd_p2p_peak_peer_size_processed_messages_fast_reacting": 1291, + "erd_p2p_peak_peer_size_processed_messages_out_of_specs": 1291, + "erd_p2p_peak_peer_size_processed_messages_slow_reacting": 4363, + "erd_p2p_peak_peer_size_received_messages_fast_reacting": 1291, + "erd_p2p_peak_peer_size_received_messages_out_of_specs": 1291, + "erd_p2p_peak_peer_size_received_messages_slow_reacting": 4363, + "erd_p2p_peer_info": "...", + "erd_p2p_peer_num_processed_messages_fast_reacting": 1, + "erd_p2p_peer_num_processed_messages_out_of_specs": 1, + "erd_p2p_peer_num_processed_messages_slow_reacting": 5, + "erd_p2p_peer_num_received_messages_fast_reacting": 1, + "erd_p2p_peer_num_received_messages_out_of_specs": 1, + "erd_p2p_peer_num_received_messages_slow_reacting": 5, + "erd_p2p_peer_size_processed_messages_fast_reacting": 289, + "erd_p2p_peer_size_processed_messages_out_of_specs": 289, + "erd_p2p_peer_size_processed_messages_slow_reacting": 2711, + "erd_p2p_peer_size_received_messages_fast_reacting": 289, + "erd_p2p_peer_size_received_messages_out_of_specs": 289, + "erd_p2p_peer_size_received_messages_slow_reacting": 2711, + "erd_p2p_unknown_shard_peers": "..." + } + }, + "error": "", + "code": "successful" +} +``` + + + + +:::important +This endpoint is not available on the Proxy. Only Nodes (Observers) expose this endpoint. +::: + + +## GET **Get Peer Information** {#get-peer-information} + +`http://localhost:8080/node/peerinfo` + +This endpoint allows one to query specific information about its peers. + + + + +🟢 200: OK + +Peer info retrieved successfully. + +```json +{ + "data": { + "info": [ + { + "isblacklisted": false, + "pid": "...", + "pk": "", + "peertype": "observer", + "addresses": [ + "/ip4/172.17.0.1/tcp/21100", + "/ip4/127.0.0.1/tcp/21100", + "/ip4/192.168.2.104/tcp/21100", + "/ip4/172.17.0.1/tcp/21100" + ] + }, + { + "isblacklisted": false, + "pid": "...", + "pk": "", + "peertype": "unknown", + "addresses": [ + "/ip4/127.0.0.1/tcp/9999", + "/ip4/127.0.0.1/tcp/9999", + "/ip4/192.168.2.104/tcp/9999", + "/ip4/172.17.0.1/tcp/9999" + ] + }, + { + "isblacklisted": false, + "pid": "...", + "pk": "...", + "peertype": "validator", + "addresses": [ + "/ip4/192.168.2.104/tcp/21504", + "/ip4/192.168.2.104/tcp/21504", + "/ip4/172.17.0.1/tcp/21504", + "/ip4/127.0.0.1/tcp/21504" + ] + }, + { + "isblacklisted": false, + "pid": "...", + "pk": "...", + "peertype": "validator", + "addresses": [ + "/ip4/172.17.0.1/tcp/21503", + "/ip4/172.17.0.1/tcp/21503", + "/ip4/127.0.0.1/tcp/21503", + "/ip4/192.168.2.104/tcp/21503" + ] + }, + { + "isblacklisted": false, + "pid": "...", + "pk": "...", + "peertype": "validator", + "addresses": [ + "/ip4/172.17.0.1/tcp/21502", + "/ip4/127.0.0.1/tcp/21502", + "/ip4/192.168.2.104/tcp/21502", + "/ip4/172.17.0.1/tcp/21502" + ] + }, + { + "isblacklisted": false, + "pid": "...", + "pk": "...", + "peertype": "validator", + "addresses": [ + "/ip4/127.0.0.1/tcp/21507", + "/ip4/127.0.0.1/tcp/21507", + "/ip4/192.168.2.104/tcp/21507", + "/ip4/172.17.0.1/tcp/21507" + ] + }, + { + "isblacklisted": false, + "pid": "...", + "pk": "...", + "peertype": "validator", + "addresses": [ + "/ip4/172.17.0.1/tcp/21501", + "/ip4/192.168.2.104/tcp/21501", + "/ip4/172.17.0.1/tcp/21501", + "/ip4/127.0.0.1/tcp/21501" + ] + }, + { + "isblacklisted": false, + "pid": "...", + "pk": "...", + "peertype": "validator", + "addresses": [ + "/ip4/172.17.0.1/tcp/21508", + "/ip4/127.0.0.1/tcp/21508", + "/ip4/192.168.2.104/tcp/21508", + "/ip4/172.17.0.1/tcp/21508" + ] + }, + { + "isblacklisted": false, + "pid": "...", + "pk": "...", + "peertype": "validator", + "addresses": [ + "/ip4/172.17.0.1/tcp/21506", + "/ip4/127.0.0.1/tcp/21506", + "/ip4/192.168.2.104/tcp/21506", + "/ip4/172.17.0.1/tcp/21506" + ] + }, + { + "isblacklisted": false, + "pid": "...", + "pk": "", + "peertype": "observer", + "addresses": [ + "/ip4/127.0.0.1/tcp/38188", + "/ip4/192.168.2.104/tcp/38188", + "/ip4/172.17.0.1/tcp/38188" + ] + }, + { + "isblacklisted": false, + "pid": "...", + "pk": "...", + "peertype": "validator", + "addresses": [ + "/ip4/172.17.0.1/tcp/21505", + "/ip4/127.0.0.1/tcp/21505", + "/ip4/192.168.2.104/tcp/21505", + "/ip4/172.17.0.1/tcp/21505" + ] + }, + { + "isblacklisted": false, + "pid": "...", + "pk": "", + "peertype": "observer", + "addresses": [ + "/ip4/172.17.0.1/tcp/21102", + "/ip4/127.0.0.1/tcp/21102", + "/ip4/192.168.2.104/tcp/21102", + "/ip4/172.17.0.1/tcp/21102" + ] + }, + { + "isblacklisted": false, + "pid": "...", + "pk": "...", + "peertype": "validator", + "addresses": [ + "/ip4/172.17.0.1/tcp/21500", + "/ip4/127.0.0.1/tcp/21500", + "/ip4/192.168.2.104/tcp/21500", + "/ip4/172.17.0.1/tcp/21500" + ] + }, + { + "isblacklisted": false, + "pid": "...", + "pk": "", + "peertype": "observer", + "addresses": [ + "/ip4/192.168.2.104/tcp/21101", + "/ip4/192.168.2.104/tcp/21101", + "/ip4/172.17.0.1/tcp/21101", + "/ip4/127.0.0.1/tcp/21101" + ] + } + ] + }, + "error": "", + "code": "successful" +} +``` + + + + +:::important +This endpoint is not available on the Proxy. Only Nodes (Observers) expose this endpoint. +::: + +--- + +### REST API overview + +## Introduction + +MultiversX has 2 layers of REST APIs that can be publicly accessed. Both of them can be recreated by anyone that +wants to have the same infrastructure, but self-hosted. + +These 2 layers of REST APIs are: + +- `https://gateway.multiversx.com`: the lower level layer (backed by `MultiversX Proxy`) that handles routing all the requests in accordance to + the sharding mechanism. More details can be found [here](/sdk-and-tools/rest-api/gateway-overview). + +- `https://api.multiversx.com`: the higher level layer (backed by `api.multiversx.com` repository) that uses the gateway level underneath, + but also integrates Elasticsearch (historical) queries, battle-tested caching mechanisms, friendly fields formatting and so on. More details + can be found [here](/sdk-and-tools/rest-api/multiversx-api). + +--- + +### Rest API Transactions + +```mdx-code-block +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +``` + + +This component of the REST API allows one to send (broadcast) Transactions to the Blockchain and query information about them. + + +## POST Send Transaction {#send-transaction} + +`https://gateway.multiversx.com/transaction/send` + +This endpoint allows one to send a signed Transaction to the Blockchain. + + + + +Body Parameters + +| Param | Required | Type | Description | +| +| ---------------- | ----------------------------------------- | -------- | -------------------------------------------------------------------------------- | +| nonce | REQUIRED | `number` | The Nonce of the Sender. | +| value | REQUIRED | `string` | The Value to transfer, as a string representation of a Big Integer (can be "0"). | +| receiver | REQUIRED | `string` | The Address (bech32) of the Receiver. | +| sender | REQUIRED | `string` | The Address (bech32) of the Sender. | +| guardian | OPTIONAL | `string` | The Address (bech32) of the Guardian. | +| senderUsername | OPTIONAL | `string` | The base64 string representation of the Sender's username. | +| receiverUsername | OPTIONAL | `string` | The base64 string representation of the Receiver's username. | +| gasPrice | REQUIRED | `number` | The desired Gas Price (per Gas Unit). | +| gasLimit | REQUIRED | `number` | The maximum amount of Gas Units to consume. | +| data | OPTIONAL | `string` | The base64 string representation of the Transaction's message (data). | +| signature | REQUIRED | `string` | The Signature (hex-encoded) of the Transaction. | +| guardianSignature| OPTIONAL | `string` | The Guardian's Signature (hex-encoded) of the Transaction. | +| chainID | REQUIRED | `string` | The Chain identifier. | +| version | REQUIRED | `number` | The Version of the Transaction (e.g. 1). | +| options | OPTIONAL | `number` | The Options of the Transaction (e.g. 1). | + + + + +🟢 200: OK + +Transaction sent with success. A Transaction Hash is returned. + +```json +{ + "data": { + "txHash": "6c41c71946b5b428c2cfb560e3ea425f8a00345de4bb2eb1b784387790914277" + }, + "error": "", + "code": "successful" +} +``` + +🔴 400: Bad request + +Invalid Transaction signature. + +```json +{ + "data": null, + "error": "transaction generation failed: ed25519: invalid signature", + "code": "bad_request" +} +``` + + + + +:::caution +For Nodes (Observers or Validators with the HTTP API enabled), this endpoint **only accepts transactions whose sender is in the Node's Shard**. +::: + +Here's an example of a request: + +```bash +POST https://gateway.multiversx.com/transaction/send HTTP/1.1 +Content-Type: application/json +``` + +```json +{ + "nonce": 42, + "value": "100000000000000000", + "receiver": "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", + "sender": "erd1njqj2zggfup4nl83x0nfgqjkjserm7mjyxdx5vzkm8k0gkh40ezqtfz9lg", + "gasPrice": 1000000000, + "gasLimit": 70000, + "data": "Zm9vZCBmb3IgY2F0cw==", #base64 representation of "food for cats" + "signature": "93207c579bf57be03add632b0e1624a73576eeda8a1687e0fa286f03eb1a17ffb125ccdb008a264c402f074a360442c7a034e237679322f62268b614e926d10f", + "chainId": "1", + "version": 1 +} +``` +:::info +More information about sending a guarded transaction can be found here: [Guarded Accounts](/developers/guard-accounts#sending-guarded-co-signed-transactions) +::: + + +## POST Send Multiple Transactions {#send-multiple-transactions} + +`https://gateway.multiversx.com/transaction/send-multiple` + +This endpoint allows one to send a bulk of Transactions to the Blockchain. + + + + +Body Parameters + +Array of: + +| Param | Required | Type | Description | +| ---------------- | ----------------------------------------- | -------- | -------------------------------------------------------------------------------- | +| nonce | REQUIRED | `number` | The Nonce of the Sender. | +| value | REQUIRED | `string` | The Value to transfer, as a string representation of a Big Integer (can be "0"). | +| receiver | REQUIRED | `string` | The Address (bech32) of the Receiver. | +| sender | REQUIRED | `string` | The Address (bech32) of the Sender. | +| senderUsername | OPTIONAL | `string` | The base64 string representation of the Sender's username. | +| receiverUsername | OPTIONAL | `string` | The base64 string representation of the Receiver's username. | +| gasPrice | REQUIRED | `number` | The desired Gas Price (per Gas Unit). | +| gasLimit | REQUIRED | `number` | The maximum amount of Gas Units to consume. | +| data | OPTIONAL | `string` | The base64 string representation of the Transaction's message (data). | +| signature | REQUIRED | `string` | The Signature (hex-encoded) of the Transaction. | +| chainID | REQUIRED | `string` | The Chain identifier. | +| version | REQUIRED | `number` | The Version of the Transaction (e.g. 1). | +| options | OPTIONAL | `number` | The Options of the Transaction (e.g. 1). | + + + + +🟢 200: OK + +A bulk of Transactions were successfully sent. + +```json +{ + "data": { + "numOfSentTxs": 2, + "txsHashes": { + "0": "6c41c71946b5b428c2cfb560e3ea425f8a00345de4bb2eb1b784387790914277", + "1": "fa8195bae93d4609a6fc5972a7a6176feece39a6c4821acae2276701aee12fb0" + } + }, + "error": "", + "code": "successful" +} +``` + + + + +:::caution +For Nodes (Observers or Validators with the HTTP API enabled), this endpoint **only accepts transactions whose sender is in the Node's Shard**. +::: + +Here's an example of a request: + +```bash +POST https://gateway.multiversx.com/transaction/send-multiple HTTP/1.1 +``` + +```json + +Content-Type: application/json + +[ + { + "nonce": 42, + "value": "100000000000000000", + "receiver": "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", + "sender": "erd1njqj2zggfup4nl83x0nfgqjkjserm7mjyxdx5vzkm8k0gkh40ezqtfz9lg", + "gasPrice": 1000000000, + "gasLimit": 70000, + "data": "Zm9vZCBmb3IgY2F0cw==", #base64 representation of "food for cats" + "signature": "93207c579bf57be03add632b0e1624a73576eeda8a1687e0fa286f03eb1a17ffb125ccdb008a264c402f074a360442c7a034e237679322f62268b614e926d10f", + "chainId": "1", + "version": 1 +} + { + "nonce": 43, + "value": "100000000000000000", + "receiver": "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", + "sender": "erd1rhp4q3qlydyrrjt7dgpfzxk8n4f7yrat4wc6hmkmcnmj0vgc543s8h7hyl", + "gasPrice": 1000000000, + "gasLimit": 70000, + "data": "YnVzIHRpY2tldHM=", #base64 representation of "bus tickets" + "signature": "01535fd1d40d98b7178ccfd1729b3f526ee4542482eb9f591d83433f9df97ce7b91db07298b1d14308e020bba80dbe4bba8617a96dd7743f91ee4b03d7f43e00", + "chainID": "1", + "version": 1 + } +] +``` + + +## POST Simulate Transaction {#simulate-transaction} + +**Nodes and observers** + +`https://gateway.multiversx.com/transaction/simulate` + +This endpoint allows one to send a signed Transaction to the Blockchain in order to simulate its execution. +This can be useful in order to check if the transaction will be successfully executed before actually sending it. +It receives the same request as the `/transaction/send` endpoint. + +Move balance successful transaction simulation + + + + +Body Parameters + +| Param | Required | Type | Description | +| ---------------- | ----------------------------------------- | -------- | -------------------------------------------------------------------------------- | +| nonce | REQUIRED | `number` | The Nonce of the Sender. | +| value | REQUIRED | `string` | The Value to transfer, as a string representation of a Big Integer (can be "0"). | +| receiver | REQUIRED | `string` | The Address (bech32) of the Receiver. | +| sender | REQUIRED | `string` | The Address (bech32) of the Sender. | +| senderUsername | OPTIONAL | `string` | The base64 string representation of the Sender's username. | +| receiverUsername | OPTIONAL | `string` | The base64 string representation of the Receiver's username. | +| gasPrice | REQUIRED | `number` | The desired Gas Price (per Gas Unit). | +| gasLimit | REQUIRED | `number` | The maximum amount of Gas Units to consume. | +| data | OPTIONAL | `string` | The base64 string representation of the Transaction's message (data). | +| signature | REQUIRED | `string` | The Signature (hex-encoded) of the Transaction. | +| chainID | REQUIRED | `string` | The Chain identifier. | +| version | REQUIRED | `number` | The Version of the Transaction (e.g. 1). | +| options | OPTIONAL | `number` | The Options of the Transaction (e.g. 1). | + + + + +A full response contains the fields above: +_SimulationResults_ +| Field | Type | Description | +|------------|---------------------------|-------------------| +| status | string | success, fail ... | +| failReason | string | the error message | +| scResults | []ApiSmartContractResult | an array of smart contract results (if any) | +| receipts | []ApiReceipt | an array of the receipts (if any) | +| hash | string | the hash of the transaction | + +❕ Note that fields that are empty won't be included in the response. This can be seen in the examples below + +--- + +🟢 200: OK + +Transaction would be successful. + +```json +{ + "data": { + "status": "success", + "hash": "bb24ccaa2da8cddd6a3a8eb162e6ff62ad4f6e1914d9aa0cacde6772246ca2dd" + }, + "error": "", + "code": "successful" +} +``` + +--- + +🟢 200: Simulation was successful, but the transaction wouldn't be executed. + +Invalid Transaction signature. + +```json +{ + "data": { + "status": "fail", + "failReason": "higher nonce in transaction", + "hash": "bb24ccaa2da8cddd6a3a8eb162e6ff62ad4f6e1914d9aa0cacde6772246ca2dd" + }, + "error": "", + "code": "successful" +} +``` + +--- + +🔴 400: Bad request + +```json +{ + "data": null, + "error": "transaction generation failed: invalid chain ID", + "code": "bad_request" +} +``` + + + + +--- + +**Proxy** + +On the Proxy side, if the transaction to simulate is a cross-shard one, then the response format will contain two elements called `senderShard` and `receiverShard` which are of type `SimulationResults` explained above. + +Example response for cross-shard transactions: + +```json +{ + "data": { + "receiverShard": { + "status": "success", + "hash": "bb24ccaa2da8cddd6a3a8eb162e6ff62ad4f6e1914d9aa0cacde6772246ca2dd" + }, + "senderShard": { + "status": "success", + "hash": "bb24ccaa2da8cddd6a3a8eb162e6ff62ad4f6e1914d9aa0cacde6772246ca2dd" + } + }, + "error": "", + "code": "successful" +} +``` + + +## POST Estimate Cost of Transaction {#estimate-cost-of-transaction} + +`https://gateway.multiversx.com/transaction/cost` + +This endpoint is used to estimate the gas cost of a given transaction. + +It performs a read-only simulation of the transaction against the current on-chain state, returning the number of gas units the transaction would consume if executed in that exact state. + +#### How it works: +- The endpoint takes all transaction input fields (value, sender, receiver, data, chainID, etc.). +- It executes the transaction in a sandboxed, non-persistent environment, meaning it simulates execution without affecting the actual blockchain. +- It uses the current state of the network (including smart contract storage, balances, etc.). +- It returns the estimated gas (txGasUnits) and may also return smart contract results and events triggered by the simulation. + +#### How does it apply to smart contracts? + +For smart contracts, the endpoint simulates the contract call exactly as if it were executed live, including processing all logic, branches, and emitted events. +The gas estimate reflects the computation and storage impact that would occur if the state remained unchanged at the time of actual execution. + +#### How does it approximate the cost if the contract logic has variable gas usage? + +If a smart contract’s logic has branches or conditional execution that result in variable gas usage (e.g., depending on internal storage state, previous executions, etc.), +the estimation will only reflect the gas used by the path taken during this particular simulation. + +#### Why is providing the correct nonce important? + +Because the simulation engine mirrors real transaction behavior, it requires a valid and correct nonce to properly simulate the transaction. Using an outdated or incorrect +nonce may lead to simulation failure. + +#### Can the estimated gas differ from the actual cost? + +Yes. Since the blockchain state may change between the moment you call /transaction/cost and when the transaction is actually sent to the network, the real gas usage can differ. +This is especially true for smart contract calls that depend on dynamic or mutable state. + + + + + +Body Parameters + +| Param | Required | Type | Description | +| -------- | ----------------------------------------- | -------- | -------------------------------------------------------------------------------- | +| value | REQUIRED | `string` | The Value to transfer, as a string representation of a Big Integer (can be "0"). | +| receiver | REQUIRED | `string` | The Address (bech32) of the Receiver. | +| sender | REQUIRED | `string` | The Address (bech32) of the Sender. | +| chainID | REQUIRED | `string` | The Chain identifier. | +| version | REQUIRED | `number` | The Version of the Transaction (e.g. 1). | +| nonce | REQUIRED | `number` | The Sender nonce. | +| options | OPTIONAL | `number` | The Options of the Transaction (e.g. 1). | +| data | OPTIONAL | `string` | The base64 string representation of the Transaction's message (data). | + + + + + +🟢 200: OK + +The cost is estimated successfully. + +```json +{ + "data": { + "txGasUnits": "77000" + }, + "error": "", + "code": "successful" +} +``` + + + + +:::tip +- Use the returned `txGasUnits` value as the `gasLimit` in your actual transaction. +- Make sure to provide the correct `nonce` of the transaction +::: + +:::tip +**Best practice:** when sending the transaction, add ~10% extra gas to the estimated value to avoid underestimation and failure due to insufficient gas. +::: + +Here's an example of a request: + +```json +POST https://gateway.multiversx.com/transaction/cost HTTP/1.1 +Content-Type: application/json + +{ + "value": "100000", + "receiver": "erd188nydpkagtpwvfklkl2tn0w6g40zdxkwfgwpjqc2a2m2n7ne9g8q2t22sr", + "sender": "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz", + "data": "dGhpcyBpcyBhbiBleGFtcGxl", #base64 representation of "this is an example" + "chainID": "1", + "version": 1, + "nonce": 1 +} +``` + + +## GET **Get Transaction** {#get-transaction} + +`https://gateway.multiversx.com/transaction/:txHash` + +This endpoint allows one to query the details of a Transaction. + + + + +Path Parameters + +| Param | Required | Type | Description | +| ------ | ----------------------------------------- | -------- | ----------------------------------------- | +| txHash | REQUIRED | `string` | The hash (identifier) of the Transaction. | + +Query Parameters + +| Param | Required | Type | Description | +| ----------- | ----------------------------------------- | -------- | -------------------------------------------------------------------------------------------- | +| sender | OPTIONAL | `string` | The Address of the sender - a hint to optimize the request. | +| withResults | OPTIONAL | `bool` | Boolean parameter to specify if smart contract results and other details should be returned. | + + + + +🟢 200: OK + +Transaction details retrieved successfully. + +```json +{ + "data": { + "transaction": { + "type": "normal", + "nonce": 3, + "round": 186580, + "epoch": 12, + "value": "1000000000000000000", + "receiver": "erd1...", + "sender": "erd1...", + "gasPrice": 1000000000, + "gasLimit": 70000, + "data": "Zm9yIHRlc3Rz", + "signature": "1047...", + "sourceShard": 2, + "destinationShard": 1, + "blockNonce": 186535, + "miniblockHash": "e927...", + "blockHash": "50a1...", + "status": "executed" + } + }, + "error": "", + "code": "successful" +} +``` + + + +Request URL: + +`https://gateway.multiversx.com/transaction/:txHash?withResults=true` + +Response: + +The response can contain additional fields such as `smartContractResults`, or `receipt` + +```json +{ + "data": { + "transaction": { + "type": "normal", + "nonce": 3, + "round": 186580, + "epoch": 12, + "value": "1000000000000000000", + "receiver": "erd1...", + "sender": "erd1...", + "gasPrice": 1000000000, + "gasLimit": 70000, + "data": "Zm9yIHRlc3Rz", + "signature": "1047...", + "sourceShard": 2, + "destinationShard": 1, + "blockNonce": 186535, + "miniblockHash": "e927...", + "blockHash": "50a1...", + "status": "executed", + "receipt": { + "value": 100, + "sender": "erd1...", + "data": "...", + "txHash": "b37..." + }, + "smartContractResults": [ + { + "hash": "...", + "nonce": 5, + "value": 1000, + "receiver": "erd1...", + "sender": "erd1...", + "data": "@6f6b", + "prevTxHash": "3638...", + "originalTxHash": "3638...", + "gasLimit": 0, + "gasPrice": 1000000000, + "callType": 0 + } + ] + } + }, + "error": "", + "code": "successful" +} +``` + + + + +:::important +The optional query parameter **`sender`** is only applicable to requests against the Proxy (not against the Observer Nodes). +::: + + +## GET **Get Transaction Shallow Status** {#get-transaction-status} + +`https://gateway.multiversx.com/transaction/:txHash/status` + +This endpoint allows one to query **the shallow status** of a transaction. For more details, see [this](/integrators/querying-the-blockchain). + + + + +Path Parameters + +| Param | Required | Type | Description | +| ------ | ----------------------------------------- | -------- | ----------------------------------------- | +| txHash | REQUIRED | `string` | The hash (identifier) of the Transaction. | + +Query Parameters + +| Param | Required | Type | Description | +| ------ | ----------------------------------------- | -------- | ----------------------------------------------------------- | +| sender | OPTIONAL | `string` | The Address of the sender - a hint to optimize the request. | + + + + +🟢 200: OK + +Transaction status retrieved successfully. + +```json +{ + "data": { + "status": "success" + }, + "error": "", + "code": "successful" +} +``` + + + + +:::important +The optional query parameter **`sender`** is only applicable to requests against the Proxy (not against the Observer Nodes). +::: + + +## GET **Get Transaction Process Status** {#get-transaction-process-status} + +`https://gateway.multiversx.com/transaction/:txHash/process-status` + +This endpoint allows one to query the **process status** of a transaction. For more details, see [this](/integrators/querying-the-blockchain). + + + + +Path Parameters + +| Param | Required | Type | Description | +| ------ | ----------------------------------------- | -------- | ----------------------------------------- | +| txHash | REQUIRED | `string` | The hash (identifier) of the Transaction. | + + + + +🟢 200: OK + +Transaction status retrieved successfully. + +```json +{ + "data": { + "reason": "" + "status": "success" + }, + "error": "", + "code": "successful" +} +``` + + + + + +## GET **Get Transactions Pool** {#get-transactions-pool} + +`http://local-proxy-instance/transaction/pool` + +:::caution +This endpoint isn't available on public gateway. However, it can be used on a local proxy instance, by setting `AllowEntireTxPoolFetch` to `true` +::: + +This endpoint allows one to fetch the entire transactions pool, merging the pools from each shard. + + +### Default + + + + +Example: + +`http://local-proxy-instance/transaction/pool` + + + + +🟢 200: OK + +Transaction status retrieved successfully. + +```json +{ + "data": { + "txPool": { + "regularTransactions": [ + { + "txFields": { + "hash": "84bb8a..." + } + }, + { + "txFields": { + "hash": "4e2c43..." + } + } + ], + "smartContractResults": [], + "rewards": [] + }, + "error": "", + "code": "successful" +} +``` + + + + + +### Using custom fields + + + + +Query Parameters + +| Param | Required | Type | Description | +| -------- | ----------------------------------------- | -------- | ------------------------------------------------------------- | +| fields | OPTIONAL | `string` | A list of the fields to be included. | +| shard-id | OPTIONAL | `string` | A specific shard id(0, 1, 2 etc. or 4294967295 for Metachain) | + +As seen above, if the `fields` item is empty, only the transaction hash will be displayed. + +If the `shard-id` item is used, only the transactions from that specific shard's pool will be displayed. + +Example request with shard id and fields: + +`https://gateway.multiversx.com/transaction/pool?shard-id=0&fields=sender,receiver,value` + +All possible values for fields item are: + +- hash +- nonce +- sender +- receiver +- gaslimit +- gasprice +- receiverusername +- data +- value + + + + +🟢 200: OK + +Transaction status retrieved successfully. + +```json +{ + "data": { + "txPool": { + "regularTransactions": [ + { + "txFields": { + "gasLimit": 10, + "gasPrice": 1000, + "receiver": "erd1...", + "sender": "erd1...", + "value": "10000000000000000000" + } + } + ], + "smartContractResults": [ + { + "txFields": { + "gasLimit": 10, + "gasPrice": 1000, + "receiver": "erd1...", + "sender": "erd1...", + "value": "10000000000000000000" + } + } + ], + "rewards": [ + { + "txFields": { + "gasLimit": 10, + "gasPrice": 1000, + "receiver": "erd1...", + "sender": "erd1...", + "value": "10000000000000000000" + } + } + ] + } + }, + "error": "", + "code": "successful" +} +``` + + + + + +## GET **Get Transactions Pool for a Sender** {#get-transactions-pool-for-a-sender} + +`https://gateway.multiversx.com/transaction/pool?by-sender=:sender:` + +This endpoint allows one to fetch all the transactions of a sender from the transactions pool. + + +### Default + + + + +Query Parameters + +| Param | Required | Type | Description | +| --------- | ----------------------------------------- | -------- | -------------------------- | +| by-sender | REQUIRED | `string` | The Address of the sender. | + +Example: + +`https://gateway.multiversx.com/transaction/pool?by-sender=erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th` + + + + +🟢 200: OK + +Transaction status retrieved successfully. + +```json +{ + "data": { + "txPool": { + "transactions": [ + { + "txFields": { + "hash": "1daea5..." + } + } + ] + } + }, + "error": "", + "code": "successful" +} +``` + + + + + +### Using custom fields + + + + +Query Parameters + +| Param | Required | Type | Description | +| --------- | ----------------------------------------- | -------- | ------------------------------------ | +| by-sender | REQUIRED | `string` | The Address of the sender. | +| fields | OPTIONAL | `string` | A list of the fields to be included. | + +As seen above, if the `fields` item is empty, only the transaction hash will be displayed. + +Example request with fields: + +`https://gateway.multiversx.com/transaction/pool?by-sender=erd1at9...&fields=sender,receiver,value` + +All possible values for fields item are: + +- hash +- nonce +- sender +- receiver +- gaslimit +- gasprice +- receiverusername +- data +- value + + + + +🟢 200: OK + +Transaction status retrieved successfully. + +```json +{ + "data": { + "txPool": { + "transactions": [ + { + "txFields": { + "hash": "1daea...", + "receiver": "erd1932...", + "sender": "erd1at9ke...", + "value": 0 + } + } + ] + } + }, + "error": "", + "code": "successful" +} +``` + + + + + +## GET **Get the latest nonce of a sender from Tx Pool** {#get-the-latest-nonce-of-a-sender-from-tx-pool} + +`https://gateway.multiversx.com/transaction/pool?by-sender=:sender:&last-nonce=true` + +This endpoint allows one to fetch the latest nonce of a sender from the transactions pool. + + + + +Query Parameters + +| Param | Required | Type | Description | +| ---------- | ----------------------------------------- | -------- | ----------------------------------------------- | +| by-sender | REQUIRED | `string` | The Address of the sender. | +| last-nonce | REQUIRED | `bool` | Specifies if the last nonce has to be returned. | + + + + +🟢 200: OK + +Transaction status retrieved successfully. + +```json +{ + "data": { + "nonce": 38 + }, + "error": "", + "code": "successful" +} +``` + + + + + +## GET **Get the nonce gaps of a sender from Tx Pool** {#get-the-nonce-gaps-of-a-sender-from-tx-pool} + +`https://gateway.multiversx.com/transaction/pool?by-sender=:sender:&nonce-gaps=true` + +This endpoint allows one to fetch the nonce gaps of a sender from the transactions pool. + + + + +Query Parameters + +| Param | Required | Type | Description | +| ---------- | ----------------------------------------- | -------- | ----------------------------------------------- | +| by-sender | REQUIRED | `string` | The Address of the sender. | +| nonce-gaps | REQUIRED | `bool` | Specifies if the nonce gaps should be returned. | + + + + +🟢 200: OK + +Transaction status retrieved successfully. + +```json +{ + "data": { + "nonceGaps": { + "gaps": [ + { + "from": 34, + "to": 35 + }, + { + "from": 37, + "to": 37 + } + ] + } + }, + "error": "", + "code": "successful" +} +``` + + + + +--- + +### Rest API Virtual Machine + +```mdx-code-block +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +``` + + +This component of the REST API allows one to call view functions (pure functions) of Smart Contracts, or, to put it in other words, to query values stored within contracts. + + +## POST Compute Output of Pure Function {#compute-output-of-pure-function} + +`https://gateway.multiversx.com/vm-values/query` + +This endpoint allows one to execute - with no side-effects - a pure function of a Smart Contract and retrieve the execution results (the Virtual Machine Output). + + + + +Body Parameters + +| Param | Required | Type | Description | +| --------- | ----------------------------------------- | -------- | ----------------------------------------------------------------------------------- | +| scAddress | REQUIRED | `string` | The Address (bech32) of the Smart Contract. | +| funcName | REQUIRED | `string` | The name of the Pure Function to execute. | +| args | REQUIRED | `array` | The arguments of the Pure Function, as hex-encoded strings. The array can be empty. | +| caller | OPTIONAL | `string` | The Address (bech32) of the caller. | +| value | OPTIONAL | `string` | The Value to transfer (can be zero). | + + + + +🟢 200: OK + +The VM Output is retrieved successfully. + +```json +{ + "data": { + "data": { + "ReturnData": ["eyJSZ... (base64)"], + "ReturnCode": 0, + "ReturnMessage": "", + "GasRemaining": 1500000000, + "GasRefund": 0, + "OutputAccounts": { + "...": { + "Address": "... (base64)", + "Nonce": 0, + "Balance": null, + "BalanceDelta": 0, + "StorageUpdates": null, + "Code": null, + "CodeMetadata": null, + "Data": null, + "GasLimit": 0, + "CallType": 0 + } + }, + "DeletedAccounts": null, + "TouchedAccounts": null, + "Logs": null + } + }, + "error": "", + "code": "successful" +} +``` + + + + +Here's an example of a request: + +```json +POST https://gateway.multiversx.com/vm-values/query HTTP/1.1 +Content-Type: application/json + +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqllls0lczs7", + "funcName": "get", + "caller": "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", + "value": "0", + "args": ["d98d..."] +} +``` + + +## POST Compute Hex Output of Pure Function {#compute-hex-output-of-pure-function} + +`https://gateway.multiversx.com/vm-values/hex` + +This endpoint allows one to execute - with no side-effects - a pure function of a Smart Contract and retrieve the first output value as a hex-encoded string. + + + + +Body Parameters + +| Param | Required | Type | Description | +| --------- | ----------------------------------------- | -------- | ----------------------------------------------------------------------------------- | +| scAddress | REQUIRED | `string` | The Address (bech32) of the Smart Contract. | +| funcName | REQUIRED | `string` | The name of the Pure Function to execute. | +| args | REQUIRED | `array` | The arguments of the Pure Function, as hex-encoded strings. The array can be empty. | +| caller | OPTIONAL | `string` | The Address (bech32) of the caller. | +| value | OPTIONAL | `string` | The Value to transfer (can be zero). | + + + + +🟢 200: OK + +The output value is retrieved successfully. + +```json +{ + "data": "7b22..." +} +``` + + + + + +## POST Compute String Output of Pure Function {#compute-string-output-of-pure-function} + +`https://gateway.multiversx.com/vm-values/string` + +This endpoint allows one to execute - with no side effects - a pure function of a Smart Contract and retrieve the first output value as a string. + + + + +Body Parameters + +| Param | Required | Type | Description | +| --------- | ----------------------------------------- | -------- | ----------------------------------------------------------------------------------- | +| scAddress | REQUIRED | `string` | The Address (bech32) of the Smart Contract. | +| funcName | REQUIRED | `string` | The name of the Pure Function to execute. | +| args | REQUIRED | `array` | The arguments of the Pure Function, as hex-encoded strings. The array can be empty. | +| caller | OPTIONAL | `string` | The Address (bech32) of the caller. | +| value | OPTIONAL | `string` | The Value to transfer (can be zero). | + + + + +🟢 200: OK + +The output value is retrieved successfully. + +```json +{ + "data": "foobar" +} +``` + + + + + +## POST Get Integer Output of Pure Function {#get-integer-output-of-pure-function} + +`https://gateway.multiversx.com/vm-values/int` + +This endpoint allows one to execute - with no side-effects - a pure function of a Smart Contract and retrieve the first output value as an integer. + + + + +Body Parameters + +| Param | Required | Type | Description | +| --------- | ----------------------------------------- | -------- | ----------------------------------------------------------------------------------- | +| scAddress | REQUIRED | `string` | The Address (bech32) of the Smart Contract. | +| funcName | REQUIRED | `string` | The name of the Pure Function to execute. | +| args | REQUIRED | `array` | The arguments of the Pure Function, as hex-encoded strings. The array can be empty. | +| caller | OPTIONAL | `string` | The Address (bech32) of the caller. | +| value | OPTIONAL | `string` | The Value to transfer (can be zero). | + + -🟢 200: OK +🟢 200: OK + +The output value is retrieved successfully. + +```json +{ + "data": "2020" +} +``` + + + + +--- + +### Result Handlers + +## Overview + +Most of the transaction fields are inputs, or work like inputs. The last one of the fields is the one that deals with the outputs. + +There are 3 types of transactions where it comes to outputs: +1. Transactions where we never receive a result, such as those sent via _transfer-execute_. Result handlers are not needed here, in fact, they are inappropriate. +2. Transactions that must finalize before we can move on. Here, various results might be returned, and we can decode them on the spot. Result handlers will determine what gets decoded and how. +3. Transactions that will finalize at an unknown time in the future, such as cross-shard calls from contracts. Here, the result handler is the callback that we register, to be executed as soon as the VM receives the response from that transaction and passes it on to our code. + +We've had callbacks for a long time, whereas decoders are new. A transaction can have either one, or the other, not both. + +We are going to focus on their usage and at the end, also try to explain how they work. + + + +## Diagram + +The result handler diagram is split into two: the callback side, and the decoder side. + +The decoders are considerably more complex, here we have a simplified version. + +```mermaid +graph LR + subgraph Result Handlers + rh-unit("()") + rh-unit -->|original_type| rh-ot("OriginalTypeMarker") + rh-ot -->|callback| CallbackClosure -->|gas_for_callback| CallbackClosureWithGas + dh[Decode Handler] + rh-unit -->|"returns
with_result"| dh + rh-ot -->|"returns
with_result"| dh + dh -->|"returns
with_result"| dh + end +``` + + + +## No result handlers + +A transaction might have no result handlers attached to it, if: +- The transaction does not return any result data, such as the case for _transfer-execute_ or simple _transfer_ transactions. +- The transaction could return some results, but we are not interested in them. + - If no result handlers are specified, no decoding takes place. + - This is similar to using the `IgnoreValue` return type in the legacy contract call syntax. + + + + +## Original result marker + +Type safety is not only important for inputs, but also for outputs. The first step is signaling what the intended result type is in the original contract, where the endpoint is defined. + +Proxies define this type themselves, since they have access to the ABI. + +If set, the marker will be visible in the transaction type, as an `OriginalResultMarker` result handler. + +:::info +The `OriginalResultMarker` does not do anything by itself, it is a zero-size type, with no methods implemented. + +Having only this marker set is no different from having no result handlers specified. It is only a compile-time artifact for ensuring type safety for outputs. +::: + +Even when we are providing raw data to a transaction, without proxies, we are allowed to specify the original intended result type ourselves. We do this by calling `.original_result()`, with no arguments. If the type cannot be inferred, we need to specify it explicitly, `.original_result::()`. + + + + +## Default result handler + +There is a special case of a default result handler in interactors and in tests. + +Without specifying anything, the framework will check that a transaction is successful. This applies to both interactors and tests (in contracts one cannot recover from a failed sync call, so this mechanism is not necessary). + +If, however, the developer expects the transaction to fail, this system can easily be overridden by adding an error-related result handler, such as [ExpectError](#expecterror), or [ReturnsStatus](#returnsstatus). + + + +## Asynchronous callbacks + + + +## Result decoders + +Result decoders come in handy when defining exact return types from smart contract endpoints. Being part of the unified syntax, they are consistent through the various environments (smart contract, interact, test) and can be used in combination with each other as long as it makes sense for the specific transaction. + +There are two ways to add a result decoder: via `with_result` or `returns`. + + +### `with_result` + +The simpler type of result decoder, it doesn't alter the return type of the transaction run method in any way. + +It registers lambdas or similar constructs, which then react to the results as they come. + + + +### `returns` + +Adding a result handler via `result` causes the transaction run function to return various versions of the result, as indicated by the result handler. + +For instance, [ReturnsResult](#returnsresult) causes the deserialized result of the transaction to be returned. + +If we add multiple result handlers, we will get a tuple with all the requested results. + +:::info +The return type is determined at compile time, with no runtime or bytecode size overhead. +::: + +In the examples below, the return type is stated explicitly, for clarity. Please note that, because of type inference, they barely ever need to be specified like this. + +All examples assume the variable `tx` already has all inputs constructed for it. Let's also assume that the original result type is `MyResult`. + +```rust title="No decoder" +let _: () = tx + .run(); +``` + +The return type here is `()` nothing is deserialized. + +```rust title="One decoder" +let r: MyResult = tx + .returns(ReturnsResult) + .run(); +``` + +Here, we get the transaction result returned. You might see this in a contract, interactor, or test. + +```rust title="Two decoders" +let r: (MyResult, BackTransfers) = tx + .returns(ReturnsResult) + .returns(ReturnsBackTransfers) + .run(); +``` + +This time we want two values out. The framework packs them in a pair, behind the scenes. The order of the values in the tuple is always the same as the order the result handlers were passed. If we pass them in reverse order, we also get the output in reverse order: + +```rust title="Same two decoders, reverse order" +let r: (BackTransfers, MyResult) = tx + .returns(ReturnsBackTransfers) + .returns(ReturnsResult) + .run(); +``` + +This mechanism works with any number of result handlers. _(There is a limit of 16 for now, in the unlikely case that anybody will need more, it can easily be increased.)_ + +```rust title="Three decoders" +let r: (ManagedVec, ManagedAddress, BackTransfers) = tx + .returns(ReturnsRawResult) + .returns(ReturnsNewManagedAddress) + .returns(ReturnsBackTransfers) + .run(); +``` + +There is no limitation that the same decoder cannot be used multiple times, although it makes little sense in practice: + +```rust title="Duplicate decoders" +let r: (MyResult, MyResult, Address, MyResult) = tx + .returns(ReturnsResult) + .returns(ReturnsResult) + .returns(ReturnsNewAddress) + .returns(ReturnsResult) + .run(); +``` + +Methods `returns` and `with_result` can be interspersed in any order. Calls to `with_result` will not affect the return type. + +```rust title="returns + with_result" +let r: Address = tx + .returns(ReturnsAddress) + .with_result(WithResultRaw(|raw|) assert!(raw.is_empty())) + .run(); +``` + +Also adding an example with only `with_result`, something you are likely to see in black-box tests. + +```rust title="No return, with_result" +let r: () = tx + .with_result(ExpectError(4, "sample error")) + .run(); +``` + + + +## List of result decoders + +There are various predefined types of result decoders: + + + + +### `ReturnsRawResult` + +Returns: `ManagedVec>`, representing the raw data result from the call. + +```rust title=contract.rs +#[endpoint] +fn deploy_contract( + &self, + code: ManagedBuffer, + code_metadata: CodeMetadata, + args: MultiValueEncoded, +) -> ManagedVec { + self.tx() + .raw_deploy() + .code(code) + .code_metadata(code_metadata) + .arguments_raw(args.to_arg_buffer()) + .returns(ReturnsRawResult) + .sync_call() + .into() +} +``` + + + +### `ReturnsResult` + +Returns: the original type from the function signature. The exact original type is extracted from the return type of the corresponding function from the proxy. + +```rust title=interact.rs +async fn quorum_reached(&mut self, action_id: usize) -> bool { + self.interactor + .query() + .to(self.state.current_multisig_address()) + .typed(multisig_proxy::MultisigProxy) + .quorum_reached(action_id) + .returns(ReturnsResult) // knows from the original type marker that the expected return type is bool + .prepare_async() + .run() + .await +} +``` + + + +### `ReturnsResultUnmanaged` + +Returns: the unmanaged version of the original result type. This relies on the `Unmanaged` associated type in `TypeAbi`. + +For example: + +| Managed type | Unmanaged version | +| --------------------- | ------------------------------------- | +| Managed `BigUint` | Rust `BigUint` (alias: `RustBigUint`) | +| Managed `BigInt` | Rust `BigInt` (alias: `RustBigInt`) | +| `ManagedBuffer` | `Vec` | +| `ManagedAddress` | `Address` | +| `ManagedVec` | `Vec` | +| `ManagedOption` | `Option` | +| `ManagedByteArray` | `[u8; N]` | +| `BigFloat` | `f64` | + +Also, most generic container types (`Option`, `Vec`, etc.) will point to themselves, but with the unmanaged version of their contents. + +For all other types, it returns the original type, same as `ReturnsResult`. + +It is especially useful in interactor and test environments, as it allows us to avoid performing additional conversions. + +```rust title=interact.rs +async fn get_sum(&mut self) -> RustBigUint { + self + .interactor + .query() + .to(self.state.current_adder_address()) + .typed(adder_proxy::AdderProxy) + .sum() // original return type is multiversx_sc::types::BigUint + .returns(ReturnsResultUnmanaged) // converts into num_bigint::BigUint + .prepare_async() + .run() + .await +} +``` + +In this case, the original return type of the endpoint `sum` is `multiversx_sc::types::BigUint` which is a managed type. `ReturnsResultUnmanaged` automatically provides us with `num_bigint::BigUint`, a much more accessible type for the interactor, where we want to avoid constantly having to specify the API. + + + +### `ReturnsStatus` + +Returns: the transaction status as u64. + +Especially useful in the testing and interactor environments. + +```rust title=blackbox_test.rs +#[test] +fn status_test() { + let mut world = setup(); + + let status = world + .tx() + .from(OWNER_ADDRESS) + .to(SC_ADDRESS) + .typed(proxy::ContractProxy) + .some_endpoint() + .returns(ReturnsStatus) + .run(); + + assert_eq!(status, 4); // status 4 - user error +} +``` + + + +### `ReturnsMessage` + +Returns: the transaction error message as String. + +Especially useful in the testing and interactor environments, + +```rust title=blackbox_test.rs +#[test] +fn status_and_message_test() { + let mut world = setup(); + + let (status, message) = world + .tx() + .from(OWNER_ADDRESS) + .to(SC_ADDRESS) + .typed(proxy::ContractProxy) + .some_endpoint() + .returns(ReturnsStatus) + .returns(ReturnsMessage) + .run(); + + assert_eq!(status, 4); // status 4 - user error + assert_eq!(message, "test"); // error message - test +} +``` + + + +### `ReturnsNewBech32Address` + +Returns: the newly deployed address after a deploy, as `Bech32Address`. + +Used in the testing and interactor environments. + +```rust title=interact.rs +async fn deploy(&mut self) -> Bech32Address { + self + .interactor + .tx() + .from(&self.wallet_address) + .typed(adder_proxy::AdderProxy) + .init(0u32) // deploys adder contract + .code(&self.adder_code) + .returns(ReturnsNewBech32Address) // returns newly deployed address as Bech32Address + .prepare_async() + .run() + .await +} +``` + + + +### `ReturnsNewManagedAddress` + +Returns: the newly deployed address after a deploy, as `Bech32Address`. + +Used in the smart contract environments. + +```rust title=contract.rs +#[endpoint] +fn deploy_from_source( + &self, + source_contract_address: ManagedAddress, + args: MultiValueEncoded, +) -> ManagedAddress { + self.tx() + .raw_deploy() // creates a deploy transaction + .from_source(source_contract_address) + .arguments_raw(args.to_arg_buffer()) + .returns(ReturnsNewManagedAddress) // returns newly deployed address as ManagedAddress + .sync_call() +} +``` + + + +### `ReturnsNewAddress` + +Returns: the newly deployed address after a deploy, as `multiversx_sc::types::heap::Address`. + +```rust title=blackbox_test.rs +#[test] +fn returns_address_test() { + let mut world = ScenarioWorld::new(); + + let new_address = world + .tx() + .from(OWNER_ADDRESS) + .typed(scenario_tester_proxy::ScenarioTesterProxy) + .init(5u32) // deploys contract + .code(CODE_PATH) + .returns(ReturnsNewAddress) // returns newly deployed address as Address + .run(); + + assert_eq!(new_address, SC_TEST_ADDRESS.to_address()); +} +``` + + + +### `ReturnsNewTokenIdentifier` + +Returns: a newly issued token identifier, as String. It will search for it in logs. + +Usable in interactor environments. + +```rust title=interact.rs +async fn issue_token(action_id: usize) -> String { + self.interactor + .tx() + .from(&self.wallet_address) + .to(self.state.current_multisig_address()) + .gas(NumExpr("80,000,000")) + .typed(multisig_proxy::MultisigProxy) + .perform_action_endpoint(action_id) // endpoint that issues token + .returns(ReturnsNewTokenIdentifier) // newly issued token identifier returned as String + .prepare_async() + .run() + .await +} +``` + + + +### `ReturnsBackTransfers` + +Returns the back-transfers of the call, as a specialized structure, called `BackTransfers`. + +Usable in a smart contract environment. + +```rust title=contract.rs +#[endpoint] +fn forward_sync_retrieve_funds_bt( + &self, + to: ManagedAddress, + token: EgldOrEsdtTokenIdentifier, + token_nonce: u64, + amount: BigUint, +) { + let back_transfers = self + .tx() + .to(&to) + .typed(vault_proxy::VaultProxy) + .retrieve_funds(token, token_nonce, amount) + .returns(ReturnsBackTransfers) + .sync_call(); + + require!( + back_transfers.esdt_payments.len() == 1 || back_transfers.total_egld_amount != 0, + "Only one ESDT payment expected" + ); +} +``` + + +### `ReturnsHandledOrError` + +Returns the handled result from a SC call as a `Result`. Can be chained with other result handlers. + +Acting as a wrapper over other result handlers, checks the status of the transaction and returns `Some(HandledType)` or `Err(TxResponseStatus)`. Especially useful in external programs that integrate the interactor in their operations such as microservices. This result handler makes sure that the external program keeps running and opens the door for a more elegant error handling. + +It is usable both in the interactor and testing environments. + +In this example, `ReturnsHandledOrError` checks the status of the transaction. If the status is `success`, the other result handler, `ReturnsNewBech32Address`, will try to extract the newly deployed SC address from the transaction on the blockchain. If successful, the new address is returned as a `Bech32Address`. Otherwise, a `TxResponseStatus` struct is returned, containing the error and the message. + +```rust title=microservice.rs +impl ContractInteract { + pub async fn deploy_paint_harvest( + &mut self, + collection_token_id: String, + is_open: bool, + ) -> Result { + let paint_harvest_code = BytesValue::from(self.contract_code.paint_harvest); + + self.interactor + .tx() + .from(&self.wallet_address) + .gas(60_000_000u64) + .typed(PaintHarvestScProxy) + .init(TokenIdentifier::from(&collection_token_id), is_open) + .code(paint_harvest_code) + .code_metadata(CodeMetadata::UPGRADEABLE) + .returns(ReturnsHandledOrError::new().returns(ReturnsNewBech32Address)) + .run() + .await + } +} +``` + + +### `ExpectError` + +Indicates that the expected return type is error and does an assert on the actual return value. Usable in the testing and interactor environments. + +```rust title=blackbox_test.rs + self.world + .tx() // tx with testing environment + .from(BOARD_MEMBER_ADDRESS) + .to(MULTISIG_ADDRESS) + .typed(multisig_proxy::MultisigProxy) + .perform_action_endpoint(action_id) + .with_result(ExpectError(4, err_message)) // expects error return type + .run(); +``` + +In this example, we expect the returned value to be an error with error code 4 (user error) and a specific error message. If not true, the execution fails. + +However, because `ExpectError` only receives a status and a message, writing `ExpectError(0, "")` is equivalent to success (code 0 - ok, no error message). + + +### `ExpectValue` + +Indicates the expected return type and does an assert on the actual return value. Usable in the testing and interactor environments. + +```rust title=blackbox_test.rs + world + .query() + .to(ADDER_ADDRESS) + .typed(adder_proxy::AdderProxy) + .sum() + .returns(ExpectValue(5u32)) + .run(); +``` + + +### `ExpectMessage` + +Indicates that the expected return type is error and does an assert on the actual error message. Usable in the testing and interactor environments. + +```rust title=blackbox_test.rs + state + .world + .tx() + .from(USER_ADDRESS) + .to(TRANSFER_ROLE_FEATURES_ADDRESS) + .typed(transfer_role_proxy::TransferRoleFeaturesProxy) + .forward_payments(Address::zero(), "", MultiValueVec::>::new()) + .egld_or_single_esdt( + &EgldOrEsdtTokenIdentifier::esdt(TRANSFER_TOKEN), + 0u64, + &multiversx_sc::proxy_imports::BigUint::from(100u64), + ) + .with_result(ExpectMessage("Destination address not whitelisted")) + .run(); +``` + + +### `ExpectStatus` + +Indicates that the expected return type is u64 and does an assert on the actual transaction. Usable in the testing and interactor environments. + +```rust title=blackbox_test.rs + self.world + .tx() + .from(from) + .to(PRICE_AGGREGATOR_ADDRESS) + .typed(price_aggregator_proxy::PriceAggregatorProxy) + .submit( + EGLD_TICKER, + USD_TICKER, + submission_timestamp, + price, + DECIMALS, + ) + .with_result(ExpectStatus(4)) + .run(); +``` + +--- + +### rounds + +This page describes the structure of the `rounds` index (Elasticsearch), and also depicts a few examples of how to query it. + + +## _id + +The `_id` field of this index is composed in this way: `{shardID}_{round}` (example: `2_10905514`) + + +## Fields + + +| Field | Description | +|------------------|------------------------------------------------------------------------------------------------------------------------| +| round | The round field represents the number of the round. | +| signersIndexes | The signersIndexes field is an array that contains the indices of the validators that should sign the block from this round. | +| blockWasProposed | The blockWasProposed field is true if a block was proposed and executed in this round. | +| shardId | The shardId field represents the shard the round belongs to. | +| epoch | The epoch field represents the epoch the round belongs to. | +| timestamp | The timestamp field represents the timestamp of the round. | + + +## Query examples + + +### Fetch the latest rounds for a shard when block was produced + +``` +curl --request GET \ + --url ${ES_URL}/rounds/_search \ + --header 'Content-Type: application/json' \ + --data '{ + "query": { + "match": { + "shardId": 1 + } + }, + "sort": [ + { + "timestamp": { + "order": "desc" + } + } + ], + "size":10 +}' +``` + +--- + +### Run transactions + +## Overview + +As discussed previously, the transaction syntax is consistent through the various transaction environments. However, when sending the transaction across a specific environment, certain conditions apply, depending on the framework's capability of processing the information and the route of the transaction. + +:::note +The transaction itself is not different and will produce the same result, but the way the framework processes the transaction might differ depending on the environment. +::: + + +## Smart contract + +From the smart contract point of view, the transaction ends when specifying the transaction type (sync/async call). + + +### `async_call_and_exit` + +Executes the transaction asynchronously and exits after execution. + +```rust title=contract.rs + self.tx() // tx with sc environment + .to(&marketplace_address) + .typed(nft_marketplace_proxy::NftMarketplaceProxy) + .claim_tokens(token_id, token_nonce, caller) + .async_call_and_exit(); // async call and stop execution +``` + +In this case, the function `async_call_and_exit` marks the end of the transaction and executes it asynchronously. After the transaction is executed, the `never` type is returned, marking the end of the execution. + + +### `sync_call` + +Sends a transaction synchronously. + +```rust title=contract.rs + self + .tx() // tx with sc environment + .to(&to) + .typed(vault_proxy::VaultProxy) + .retrieve_funds(token, token_nonce, amount) + .sync_call(); // synchronous call +``` + + +### `upgrade_async_call_and_exit` + +Upgrades contract asynchronously and exits after execution. + +```rust title=contract.rs + self.tx() // tx with sc environment + .to(child_sc_address) + .typed(vault_proxy::VaultProxy) + .upgrade(opt_arg) // calling the upgrade function + .from_source(source_address) + .upgrade_async_call_and_exit(); // upgrades async and exits +``` + + +### `sync_call_same_context` + +Executes the transaction synchronously on the same context (in the name of the caller). + +```rust title=contract.rs + self + .tx() // tx with sc environment + .to(&to) + .raw_call(endpoint_name) + .arguments_raw(args.to_arg_buffer()) + .sync_call_same_context(); // sync call in the same context +``` + + +### `sync_call_readonly` + +Executes the transaction synchronously, in readonly mode (target contract cannot have its state altered). + +```rust title=contract.rs + let result = self + .tx() // tx with sc environment + .to(&to) + .raw_call(endpoint_name) + .arguments_raw(args.to_arg_buffer()) + .returns(ReturnsRawResult) // result handler - returns raw result data + .sync_call_readonly(); // sync call in readonly mode +``` + +### `transfer_execute` + +Sends transaction asynchronously, and doesn't wait for callback. + +```rust title=contract.rs + self + .tx() + .to(&caller) + .gas(50_000_000u64) + .raw_call(func_name) + .single_esdt(&token, 0u64, &amount) + .transfer_execute(); +``` + + +### `transfer` + +Same as `transfer_execute`, but only allowed for simple transfers. + +```rust title=contract.rs + self + .tx() + .to(&caller_address) + .egld(&collected_fees) + .transfer(); +``` + + + +### `async_call`, `async_call_promise` + +Backwards compatibility only. + +:::important +For the moment, the functions `async_call` and `async_call_promise` exist for backwards compatibility reasons only. These functions do `NOT` execute a transaction, they just return the current object state. Delete them from existing codebases. +::: + + + +## Integration test + +For the Rust testing environment, the only keyword for sending transactions is `run`. Inside an integration test, a developer can build a transaction, choose the return type and then `run` it, marking the end of the transaction and the start of the execution. + +```rust title=blackbox_test.rs + world // ScenarioWorld instance + .query() // query with test environment + .to(ADDER_ADDRESS) + .typed(adder_proxy::AdderProxy) + .sum() + .returns(ExpectValue(6u32)) // result handler - assert return value + .run(); // runs the query step +``` + +In this case, regarding of the type of the transaction (raw call, deploy, upgrade, query), it eventually turns into a scenario `Step` (`ScQueryStep`, `ScCallStep`, `ScDeployStep`) and it is processed as such. + + + +## Interactor + + +### Async Rust + +In the case of the interactor, the processing is similar to the integration test, with the exception that the interactor is using async Rust. First, the transaction is built, then it is turned into a `Step` using the `prepare_async` function, then we can `run` it and `await` the result. + +```rust title=interact.rs + self.interactor // Interactor instance + .tx() // tx with interactor exec environment + .from(&self.wallet_address) + .to(self.state.current_multisig_address()) + .gas(NumExpr("30,000,000")) + .typed(multisig_proxy::MultisigProxy) + .dns_register(dns_address, name) + .prepare_async() // converts tx into step + .run() // runs the step + .await; // awaits the result - async Rust +``` + + +### Sync Rust + +We also have a plan for adding support for a blocking interactor API, but this is currently not available. + + + + +## Feature table + +This table shows what transaction fields are mandatory, optional, or disallowed, in order to run a transaction. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EnvironmentRun methodFromToPaymentGasDataResult Handler
SC: callasync_call_and_exitFC or ()callback only
SC: callregister_promiseFCcallbacks only, with gas for callback
SC: calltransfer_executeFC or ()
SC: calltransfer()
SC: callsync_call🟡FC
SC: callsync_call_same_context🟡FC
SC: callsync_call_readonly🟡FC
SC: deploysync_callimg🟡deploy
SC: upgradeupgrade_async_call_and_exitimg🟡upgradecallback only
Test: tx callrun🟡FC or ()
Test: tx deployrunimg🟡deploy
Test: queryrunFC
Interactor: tx callrun🟡FC or ()
Interactor: tx deployrunimg🟡deploy
Interactor: queryrunFC
+ +Legend: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SymbolMeaning
Mandatory, any allowed value type
Not allowed
🟡Optional
imgEGLD only
FCFunction Call
+ +--- + +### Running scenarios + +Most of the MultiversX smart contract testing infrastructure is built with scenarios in mind, so there are lots of ways to execute them. + +```mermaid +graph TD + json["JSON Scenario + *.scen.json"] + json --> test-go-tool["run-scenarios"] --> vm-go["⚙️ Go VM"] + json --> test-go["Generated + *_scenario_go_test.rs"] --> vm-go + json --> test-rs["Generated + *_scenario_rs_test.rs"] --> vm-rust["⚙️ Rust VM (Debugger)"] +``` + +Assume we have a scenario JSON file. The options for running it are: +- using the `run-scenarios` standalone tool +- generating some tests in a Rust project and running the Rust tests. + + +## Standalone tool + +The only standalone tool for running scenarios is `run-scenarios`, part of the VM tooling. + +The binary is build from [here](https://github.com/multiversx/mx-chain-vm-go/blob/master/cmd/scenariostest/scenariosTest.go). +Most of the code lies [here](https://github.com/multiversx/mx-chain-vm-go/tree/master/scenarioexec), if you're curious. + +To call, simply run `run-scenarios `, where the path can be either a speciific scenario file, or a folder containing scenarios. In the case of a folder, the tool will run all files ending in `*.scen.json`. Results are printed to console. + + +## Integration in Rust + +We normally want to integrate scenario tests in the CI. For this, at the very least we should write Rust tests that run the scenarios. + +We have decided to have a separate Rust test for each scenario file. This way, when running a full test suite, it is easy to see exactly what test has failed, at a glance: + +![console screenshot](/developers/testing/scenario-json-rust-console.png "Example of console with failed scenario tests") + +We also want to have a set of such tests for each of the backends: Go and Rust. + + +### Standard project layout + +The standard for organising tests in a Rust crate is a follows: + +``` +├── Cargo.toml +├── meta +├── multiversx.json +├── output +├── scenarios +│ ├── scenario1.scen.json +│ └── scenario2.scen.json +├── src +│ └── my_contract.rs +├── tests +│ ├── my_contract_scenario_go_test.rs +│ ├── my_contract_scenario_rs_test.rs +│ ├── other_tests.rs +│ └── more_tests.rs +└── wasm +``` + +At a minimum, the project needs to have `src`, `meta`, and `wasm` folders. + +Integration tests go into the `tests` folder. Scenario files, if present, should reside in a folder named `scenarios`. It is fine to also have sub-folders under `scenarios`, if needed. + +The Rust tests should go into 2 files ending in `*scenario_go_test.rs` and `*scenario_rs_test.rs`, respectively. It is also acceptable to simply use file names `scenario_go_test.rs` and `scenario_rs_test.rs` directly, with no prefix, but having a prefix can help developers orient themselves easier in projects with many contracts. + + +### Go backend + +The `*scenario_go_file.rs` should look like this: + +```rust title="adder/tests/my_contract_scenario_go_test.rs" +use multiversx_sc_scenario::*; + +fn world() -> ScenarioWorld { + ScenarioWorld::vm_go() +} + +#[test] +fn adder_go() { + world().run("scenarios/adder.scen.json"); +} + +#[test] +#[ignore = "reason to ignore"] +fn ignored_test_go() { + world().run("scenarios/ignored_test.scen.json"); +} +``` + +The `world()` function sets up the environment, which in this case is very straightforward. + +:::caution +Do not comment out tests! + +Use the `#[ignore]` annotation instead (also writing a reason is nice, but optional). + +The code generator for these files has trouble dealing with commented tests. + +It also helps that ignored tests will also show up in console, unlike the ones that are commented out. +::: + +It is customary to add a `_go` suffix to the test functions, to distinguish them from the ones with the Rust backend. The test-gen tool does the same. + + +### Rust backend + +The `*scenario_rs_file.rs` needs more setup to the environment, but other than that it looks the same: + +```rust title="adder/tests/my_contract_scenario_rs_test.rs" +use multiversx_sc_scenario::*; + +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + blockchain.register_contract("file:output/adder.wasm", adder::ContractBuilder); + blockchain +} + +#[test] +#[ignore] +fn adder_rs() { + world().run("scenarios/adder.scen.json"); +} + +#[test] +fn interactor_trace_rs() { + world().run("scenarios/interactor_trace.scen.json"); +} +``` + +Note that we can have different tests ignored on the different backends. + +Here it is also customary to add a `_rs` suffix to the test functions, to distinguish them from the ones on the Go backend. The test-gen tool does the same. + + +### Rust backend environment minimal setup + +The example above is a great example of a minimal setup. Other than creating the `world: ScenarioWorld` object, this is the line of interest: + +```rust +blockchain.register_contract("file:", ::ContractBuilder); +``` + +The Rust backend doesn't run compiled contracts, instead, it hooks the actual Rust contract code to its engine. This is where we tell the framework how to do that. +The interpretation of this is: +- whenever the framework is asked to deploy or run a contract whose code would normally lie on disk at `` ... +- it should run the code, a prepared by `::ContractBuilder`. + +The path to binary is given as a scenario value expression. [The file syntax](/developers/testing/scenario/values-simple#file-contents) in the example is simply the most common way of loading a large value from file. It is also possible to provide the compiled contract as bytes, (e.g. `"0x0061736d0100000001661160000060017..."`), but hard-coding that is weird. + +The `ContractBuilder` object is generated automatically for every contract, by the `#[multiversx_sc::contract]` procedural macro. That is why you won't see it in code, but it's always there. + + +### Auto-generating the boilerplate + +If you thought "writing these test functions manually is tedious and repetitive", you would be correct. It is also error prone, since it is easy to accidentally leave scenarios out. + +That is why we've build the [`sc-meta test-gen`](/developers/meta/sc-meta-cli#calling-test-gen) tool to help automate this task. + +The tool works as follows: +- If no `*scenario_go_file.rs` or `*scenario_rs_file.rs` are found, they can be created anew, but only if the `--create` flag is passed to the tool. +- The Go VM tests can be fully generated, but the Rust VM environment setup (the `world()` function) cannot be generated automatically. When creating it for the first time, you will get a stub of this function, and will have to fill in the implementation manually. +- If the `scenarios` folder is missing, the tool won't do anything. +- You will always get a test function for each scenario in the `scenarios` folder. +- The tool can be called any number of times, it is easy to update these tests whenever scenarios change. New scenario tests will be added and missing tests will be removed. +- The tool always preserves: + - `#[ignore]` and `#[ignore = "reason"]` annotations; + - Test comments; + - Any additional code that may be written before the tests (not just the `world()` function). + +The test tool can handle multiple contract crates at once. In fact, it will try to update tests for all contracts it can find under a given folder. + +For reference, the tool parameters are: +- `--path` + - Target directory where to call all contract meta crates. + - _default_: current directory. +- `--ignore` + - Ignore all directories with these names. + - _default_: `target`. +- `--create` + - Creates test files if they don't exist. + +--- + +### Rust SDK + +## Rust Interactors + +The Rust SDK for interacting with the blockchain comes in the form of the so-called [**Rust Interactors**](/developers/meta/interactor/interactors-overview). + +Since they use very similar syntax to smart contracts and smart contract tests, their documentation is grouped under the Rust Development Framework section. + + + +## Quick tutorial + +You can also find a quick tutorial [here](/developers/tutorials/interactors-guide). + +--- + +### Rust Version + +## Required Rust version + +Starting with framework version [v0.50.0](https://crates.io/crates/multiversx-sc/0.50.0), MultiversX smart contracts can be built using stable Rust. + +Before this version, nightly Rust was required. + + +## Recommended compiler versions + +For everything after v0.50.0 we recommend running the latest stable version of Rust. Older versions have had compatibility issues with certain framework dependencies, on certain versions of the compiler. + +Also, everything on versions older than v0.50.0 needs to run on nightly Rust. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Application VersionRequired Rust ChannelVersion Requirements
Prior to `v0.50`Nightly`nightly-2023-12-11` or `nightly-2024-05-22`
`v0.50` to `v0.56`**Stable (recommended)** or Nightly≥`1.78` and ≤`1.86`
`v0.57`**Stable (recommended)** or Nightly≥`1.83` and ≤`1.86`
`v0.58` and Higher**Stable (recommended)** or Nightly≥`1.83`*
+ +\* Starting with Rust version `1.89` and higher, there are known runtime issues when using **wasmer 6.0** (`wasmer-experimental`) exclusively on the **Linux platform**. + +:::note +If you are using **wasmer 6.0** on Linux, we recommend pinning your Rust version below `1.89`. +::: + + +## Why Nightly for the older versions? + +There were several nightly features that the framework was using, which we had hoped to see stabilized sooner. + +These are of little relevance to the average developer, but for the record, let's mention a few of them and how we managed to circumvent their usage: + +- `never_type` - avoided by using slightly different syntax; +- `auto_traits` and `negative_impls` - avoided by redesigning the `CodecFrom`/`TypeAbiFrom` trait systems; +- `generic_const_exprs` - replaced with massive amounts of macros; +- `panic_info_message` - replaced by a different method to retrieve the panic message. + +If any of these get stabilized in the future, we might revert the changes enacted in v0.50.0. + +It is in any case our commitment to keep the framework compatible with stable Rust from here on, no matter what. + +--- + +### Save and load a keystore (encrypted JSON) + +The keystore is the safe at-rest wallet format: a JSON file whose key material is +encrypted with your password (scrypt + AES-128-CTR), so the key never touches +disk in plaintext. This recipe saves and reloads both keystore kinds, a single +secret key and a full mnemonic, fully offline. The only I/O is to a throwaway +temp directory it cleans up. + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates its own throwaway keys and writes to a temp + directory it cleans up. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/keystore-save-load +npm install +npm run build +npm start +``` + +## The code + +```ts title="src/index.ts" +// src/index.ts — save a wallet to an encrypted keystore (JSON) and load it +// back, for both keystore kinds: secret-key and mnemonic. Prove the password +// is really required, and show how addressIndex selects an account from a +// mnemonic keystore. +// +// Fully offline. The only I/O is to a throwaway temp directory (cleaned up +// at the end); no devnet, no network. Fresh keys each run, so exact +// addresses differ; the equivalences printed do not. +// +// Grounded in the installed @multiversx/sdk-core v15.4.1: UserWallet +// (wallet/userWallet.d.ts) and Account.newFromKeystore (accounts/account.d.ts). + +import { Account, Mnemonic, UserWallet } from '@multiversx/sdk-core'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +const PASSWORD = 'correct horse battery staple'; + +function main(): void { + const dir = mkdtempSync(join(tmpdir(), 'mvx-keystore-')); + try { + // === 1. Secret-key keystore: encrypt one key. === + const mnemonic = Mnemonic.generate(); + const secretKey = mnemonic.deriveKey(0); + const expectedAddress = secretKey.generatePublicKey().toAddress().toBech32(); + + const wallet = UserWallet.fromSecretKey({ secretKey, password: PASSWORD }); + const keystorePath = join(dir, 'wallet.json'); + wallet.save(keystorePath); + + // The file on disk is encrypted JSON — never plaintext key material. + const onDisk = JSON.parse(readFileSync(keystorePath, 'utf8')) as { + version: number; + kind: string; + crypto: { cipher: string; kdf: string }; + }; + console.log( + `1. Saved keystore: version ${onDisk.version}, kind "${onDisk.kind}", cipher ${onDisk.crypto.cipher}, kdf ${onDisk.crypto.kdf}.`, + ); + + // === 2. Load it back with the password. === + // Account.newFromKeystore handles both keystore kinds and is synchronous. + const loaded = Account.newFromKeystore(keystorePath, PASSWORD); + console.log(`2. Loaded address matches the original: ${loaded.address.toBech32() === expectedAddress}`); + + // UserWallet.loadSecretKey returns the raw key instead of an Account. + const loadedKey = UserWallet.loadSecretKey(keystorePath, PASSWORD); + console.log(` UserWallet.loadSecretKey recovered the same key: ${loadedKey.hex() === secretKey.hex()}`); + + // === 3. The password really is required. === + let wrongPasswordRejected = false; + try { + Account.newFromKeystore(keystorePath, 'wrong password'); + } catch { + wrongPasswordRejected = true; + } + console.log(`3. Loading with the wrong password throws: ${wrongPasswordRejected}`); + + // === 4. Mnemonic keystore: encrypt the whole phrase. === + // A mnemonic keystore holds the seed phrase, so one file yields many + // accounts — addressIndex picks which one on load. + const mnemonicWallet = UserWallet.fromMnemonic({ mnemonic: mnemonic.toString(), password: PASSWORD }); + const mnemonicPath = join(dir, 'mnemonic.json'); + mnemonicWallet.save(mnemonicPath); + + const account0 = Account.newFromKeystore(mnemonicPath, PASSWORD, 0); + const account1 = Account.newFromKeystore(mnemonicPath, PASSWORD, 1); + const derived0 = mnemonic.deriveKey(0).generatePublicKey().toAddress().toBech32(); + const derived1 = mnemonic.deriveKey(1).generatePublicKey().toAddress().toBech32(); + console.log( + `4. Mnemonic keystore, addressIndex 0 and 1 match direct derivation: ${account0.address.toBech32() === derived0 && account1.address.toBech32() === derived1}`, + ); + console.log(` The two indices are different accounts: ${account0.address.toBech32() !== account1.address.toBech32()}`); + + console.log('\nExpected: four "true" confirmations (2, 2b, 3, 4) plus the "different accounts" line.'); + } finally { + // Clean up the temp directory regardless of outcome. + rmSync(dir, { recursive: true, force: true }); + } +} + +main(); +``` + +## Run it + +Keys are generated fresh each run; the pattern of confirmations is stable: + +```text +1. Saved keystore: version 4, kind "secretKey", cipher aes-128-ctr, kdf scrypt. +2. Loaded address matches the original: true + UserWallet.loadSecretKey recovered the same key: true +3. Loading with the wrong password throws: true +4. Mnemonic keystore, addressIndex 0 and 1 match direct derivation: true + The two indices are different accounts: true +``` + +## How it works + +Grounded in the installed `@multiversx/sdk-core` v15.4.1 `UserWallet` +(`wallet/userWallet.d.ts`) and `Account.newFromKeystore`: + +1. **Encrypt a key.** `UserWallet.fromSecretKey({ secretKey, password })` builds + an encrypted wallet; `.save(path)` writes it. On disk it is JSON with + `version: 4`, `kind: "secretKey"`, `cipher: aes-128-ctr`, `kdf: scrypt`. +2. **Load it back.** `Account.newFromKeystore(path, password)` returns an + `Account` (synchronous). `UserWallet.loadSecretKey(path, password)` returns + the raw `UserSecretKey` instead. +3. **The password is required.** Loading with the wrong password throws. +4. **Mnemonic keystore.** `UserWallet.fromMnemonic({ mnemonic, password })` + encrypts the whole phrase, so one file yields many accounts; + `Account.newFromKeystore(path, password, addressIndex)` picks which one. + +## Pitfalls + +:::warning[Pitfall 1: addressIndex only matters for mnemonic keystores] +A `secretKey` keystore holds exactly one key, so `addressIndex` is irrelevant +there. For a `mnemonic` keystore it selects the derived account (0, 1, ...). +`Account.newFromKeystore` handles both kinds with the same call, but only the +mnemonic kind honors the index. +::: + +:::note[Pitfall 2: the JSON's address field is metadata, not proof of the key] +The public `address` field is metadata; the actual key is the encrypted `crypto` +section, recoverable only with the password. Always round-trip (decrypt) to +confirm you hold the right key, as step 2 does. +::: + +:::danger[Pitfall 3: losing the password means losing the key] +There is no recovery path for an encrypted keystore. Scrypt is deliberately slow +to resist brute force, which also means a forgotten password is unrecoverable. +For real keys, back up the mnemonic separately. +::: + +## See also + +- [Save and load a PEM (dev only)](/sdk-and-tools/sdk-js/cookbook/accounts/pem-save-load) + is the unencrypted counterpart; use the keystore for anything real. +- [Create an Account from a KeyPair, secret key, or mnemonic](/sdk-and-tools/sdk-js/cookbook/accounts/account-from-keys) + covers the in-memory constructors. +- [Generate a mnemonic + derive keys](/sdk-and-tools/sdk-js/cookbook/accounts/generate-mnemonic-derive-keys) + is the phrase a mnemonic keystore encrypts. + +--- + +### Save and load a PEM (dev only) + +A PEM file is the quick dev-wallet format: write a key, load it back, done. But +it stores the secret key **unencrypted**, anyone who reads the file has the key. +This recipe covers the full round-trip via both the `Account` API and the +lower-level `UserPem` class, fully offline, while making the dev-only caveat +impossible to miss. + +:::danger[PEM is for development and testing only] +A PEM stores the secret key in plaintext. Never put a mainnet or funded key in a +PEM. For anything real, use an +[encrypted keystore](/sdk-and-tools/sdk-js/cookbook/accounts/keystore-save-load). +::: + +## Prerequisites + +- Node.js >= 20.13.1. +- Nothing else. This recipe generates its own throwaway key and writes to a temp + directory it cleans up. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/pem-save-load +npm install +npm run build +npm start +``` + +## The code + +```ts title="src/index.ts" +// src/index.ts — save a wallet to a PEM file and load it back, via both the +// Account API (saveToPem / newFromPem) and the lower-level UserPem class. +// +// PEM IS FOR DEVELOPMENT AND TESTING ONLY. A PEM file stores the secret key +// UNENCRYPTED — anyone who reads the file has the key. Never put a +// mainnet/funded key in a PEM. For anything real, use an encrypted keystore +// (see the "Save and load a keystore" recipe). +// +// Fully offline. The only I/O is to a throwaway temp directory (cleaned up +// at the end); no devnet, no network. Fresh keys each run. +// +// Grounded in the installed @multiversx/sdk-core v15.4.1: Account.saveToPem / +// Account.newFromPem (accounts/account.d.ts) and UserPem (wallet/userPem.d.ts). + +import { Account, Mnemonic, UserPem } from '@multiversx/sdk-core'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +async function main(): Promise { + const dir = mkdtempSync(join(tmpdir(), 'mvx-pem-')); + try { + // === 1. Write a PEM from an Account. === + const account = Account.newFromMnemonic(Mnemonic.generate().toString(), 0); + const expectedAddress = account.address.toBech32(); + const pemPath = join(dir, 'wallet.pem'); + account.saveToPem(pemPath); + + // A PEM is plain text: a base64 body between BEGIN/END markers, labeled + // with the bech32 address. The secret key sits in that body, unencrypted. + const firstLine = readFileSync(pemPath, 'utf8').split('\n')[0] ?? ''; + console.log(`1. Wrote ${pemPath.split('/').pop() ?? ''}. First line: ${firstLine}`); + + // === 2. Load it back with Account.newFromPem — this one is ASYNC. === + // Unlike newFromMnemonic / newFromKeystore / newFromKeypair (all sync), + // newFromPem returns a Promise and must be awaited. + const loaded = await Account.newFromPem(pemPath); + console.log(`2. Account.newFromPem address matches: ${loaded.address.toBech32() === expectedAddress}`); + + // === 3. UserPem is the lower-level view of the same file. === + // It exposes the label, the secret key, and the public key directly — + // useful when you want the key material rather than a full Account. + const pem = UserPem.fromFile(pemPath); + console.log(`3. UserPem.fromFile label is the address: ${pem.label === expectedAddress}`); + console.log(` UserPem secret key matches the Account's: ${pem.secretKey.hex() === account.secretKey.hex()}`); + + // === 4. Build a PEM from raw key material, no Account needed. === + // new UserPem(label, secretKey).save(path) is the write side of UserPem. + const rebuilt = new UserPem(expectedAddress, account.secretKey); + const rebuiltPath = join(dir, 'rebuilt.pem'); + rebuilt.save(rebuiltPath); + const rebuiltAccount = await Account.newFromPem(rebuiltPath); + console.log(`4. Rebuilt PEM round-trips to the same address: ${rebuiltAccount.address.toBech32() === expectedAddress}`); + + // A PEM file can hold several keys; UserPem.fromFile(path, index) selects + // one, and UserPem.fromFileAll(path) returns them all. Account.newFromPem + // takes the same optional index. + console.log('\nExpected: three "true" confirmations (2, 3, 4) and the matching key line.'); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +Keys are generated fresh each run; the confirmations are stable: + +```text +1. Wrote wallet.pem. First line: -----BEGIN PRIVATE KEY for erd1lss6vxy7pamd6nflt9zuusxdmqutd0mwzu7nrmxvwax3em0kzgjqv63ssn----- +2. Account.newFromPem address matches: true +3. UserPem.fromFile label is the address: true + UserPem secret key matches the Account's: true +4. Rebuilt PEM round-trips to the same address: true +``` + +## How it works + +Grounded in the installed `@multiversx/sdk-core` v15.4.1 `Account.saveToPem` / +`Account.newFromPem` (`accounts/account.d.ts`) and `UserPem` +(`wallet/userPem.d.ts`): + +1. **Write from an Account.** `account.saveToPem(path)`. The file is plain text, + a base64 body between `BEGIN`/`END` markers, labeled with the bech32 address. + The secret key sits in that body, unencrypted. +2. **Load with `Account.newFromPem`, this one is async.** It returns a `Promise` + and must be awaited, unlike the synchronous `newFromMnemonic` / + `newFromKeystore` / `newFromKeypair`. +3. **`UserPem` is the lower-level view.** `UserPem.fromFile(path)` exposes + `label`, `secretKey`, and `publicKey` directly. +4. **Build a PEM from raw material.** `new UserPem(label, secretKey).save(path)` + is the write side of `UserPem`, no `Account` required. + +A PEM can hold several keys; `UserPem.fromFile(path, index)` selects one and +`UserPem.fromFileAll(path)` returns them all. `Account.newFromPem` takes the same +optional index. + +## Pitfalls + +:::danger[Pitfall 1: PEM is unencrypted; treat the file as the key itself] +There is no password. Reading the file is reading the private key. Keep PEMs out +of version control, shared drives, and anything mainnet. This is the whole reason +the keystore format exists. +::: + +:::warning[Pitfall 2: newFromPem is async; saveToPem and the other constructors are not] +Forgetting to `await newFromPem` leaves you holding a `Promise`, whose +`.address` is `undefined`. This is the single most common PEM mistake. +::: + +:::note[Pitfall 3: the label is cosmetic] +The bech32 address in the `BEGIN ... for erd1...` header is a human-readable +hint, not verified against the key on load. The account's real address always +comes from the key, derive it rather than trusting the label. +::: + +## See also + +- [Save and load a keystore](/sdk-and-tools/sdk-js/cookbook/accounts/keystore-save-load) + is the encrypted, production-safe alternative to PEM. +- [Create an Account from a KeyPair, secret key, or mnemonic](/sdk-and-tools/sdk-js/cookbook/accounts/account-from-keys) + covers the in-memory constructors, including the async `newFromPem` note. +- [Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) + is the next step once you have a loaded dev account. + +--- + +### SC to SC Calls + +This guide provides an overview of the different types of smart contract calls that originate from other smart contract calls. + + +## Introduction + +Smart contract calls on MultiversX fall into two main categories: synchronous (`sync`) and asynchronous (`async`), each with distinct usage scenarios based on developer needs, and dApp architecture. + + + +## Overview + + +### Sync calls vs. async calls + + +A `sync` call is similar to regular function call in a program: it relies on a call stack, the current execution is paused, the call is executed immediately, and execution of the caller function resumes immediately after. + +An `async` call is similar to an asynchronous function call in a program, just like launching it on a different thread. The async call is not added to the same stack and does not interrupt the execution of the caller function. + +The main differences between the two are in this table: + +| `sync` calls | `async` calls | +| ------------ | ------------- | +| Are executed immediately, inline. | Are executed after the current transaction is completed. | +| Only work in the same shard. | Work both in-shard and cross-shard. | +| Function results are available immediately. | Function results are only available later, in the callback, if a callback exists. | +| A callee crash causes caller to immediately crash as well. | A callee crash does not cause caller to crash, the error can be caught in the callback, if it exists. | +| Reentrancy issues possible. | Reentrancy not possible. | + + + +### Transfer-execute calls + +Transfer-execute calls are basically async calls without callback. You can think of them as a "fire and forget" mechanism. + +They come in two flavors: +- **Transfer-only** - they are used to move EGLD or ESDT balance, and nothing else. Balance transfers can rarely fail, so they are very convenient to use. +- **Transfer-and-execute** - used when the caller does not care what the result of the callee function is. + +They were very important in async v1, because they were the only mechanism to have more than one call leaving a transaction. + + +### Error recovery + +Error recovery is not possible with sync calls, so sometimes contracts might choose to perform async calls to allow themselves to recover from errors, even if the target contract is in the same shard. + +This also means that no error handling is necessary (or effective) around sync calls. + + +### Reentrancy + +Reentrancy is very unlikely on a MultiversX blockchain, but not impossible, so let's dedicate a chapter to it. + +There are 2 main elements that make it less of an issue than on other blockchain architectures: +1. Native tokens are built into the system, so they cannot become malicious. Most reentrancy attacks are performed via malicious tokens that exploit intermediate states in the middle of function execution. +2. Reentrancy can only be performed in sync calls. Async calls do not interrupt the execution, so they are not vectors for an attack. + +Even so, to avoid vulnerabilities it is best to perform sync calls either at the very beginning, or at the very end of a SC endpoint execution. + +A similar problem to reentrancy is the management of the intermediate state between an async call and the processing of its callback. Great care must be taken, to ensure the state of the contract is not vulnerable to attacks in this interval. + + + +### Async callbacks + +Async calls can optionally register callbacks. The callbacks are called by the system, irrespective of whether the caller completed successfully or failed. + +The callback receives the following inputs: +- The call result, which contains several values: + - In case of success: + - The status code, 0 in this case, in the first position. + - One argument for each result returned by the callee, after that. + - In case of error: + - The status code, will be different from 0. For example, error code "4" indicates a failure in the smart contract execution. It is also in the first position. + - One argument, containing the error message as string. + - Note: it is customary to use type `ManagedAsyncCallResult` since it knows how to conveniently decode this structure. +- The callback closure: + - There might be multiple async calls involved in a smart contract interaction, it can sometimes be hard to figure out which callback came from which call. That is why it is almost always the case that some information needs to be passed directly from the call site to the callback. + - This is done by adding arguments + +:::note +Async call functions do not return values, but may include a `callback` function to handle the response from the destination contract. This is because the results are never available immediately. +::: + +This is an example of a callback function that gets triggered after an `issue_fungible` action: + +```rust + #[callback] + fn esdt_issue_callback( + &self, + caller: &ManagedAddress, + #[call_result] result: ManagedAsyncCallResult<()>, + ) { + let (token_identifier, returned_tokens) = + self.call_value().egld_or_single_fungible_esdt(); + // callback is called with ESDTTransfer of the newly issued token, with the amount requested, + // so we can get the token identifier and amount from the call data + match result { + ManagedAsyncCallResult::Ok(()) => { + self.last_issued_token().set(token_identifier.unwrap_esdt()); + self.last_error_message().clear(); + }, + ManagedAsyncCallResult::Err(message) => { + // return issue cost to the caller + if token_identifier.is_egld() && returned_tokens > 0 { + self.tx().to(caller).egld(&returned_tokens).transfer(); + } + + self.last_error_message().set(&message.err_msg); + }, + } + } +``` + +And this is how it gets called: + +```rust + self.send() + .esdt_system_sc_proxy() + .issue_fungible(/* ... arguments ... */) + .with_callback(self.callbacks().esdt_issue_callback(&caller)) + .async_call_and_exit() +``` + +Notice how the caller gets passed from the call site directly to the callback. + +Also notice how the tokens transferred back to the caller are available in the callback, as _call value_. + + + + +### All call types + +Each of these calls further divide in multiple categories, depending on the mechanism they use, different developer use-cases and expected results. + +- Sync calls + - Sync call + - Sync call same context + - Sync call readonly + - Deploy call +- Async calls + - Async call (V1) + - Register promise (V2) + - Transfer execute + - Upgrade call + +We will now explain each of them in greater depth, and provide some syntax examples. + +For the full syntax specification, visit the [unified transaction syntax](/developers/transactions/tx-overview) documentation. + + + +## Sync calls + + + +### Standard sync call + +A standard sync call performs a direct, synchronous transaction to a contract on the same shard. This type of call is launched with `.sync_call()`. + +```rust + /// Executes transaction synchronously. + /// + /// Only works with contracts from the same shard. + pub fn sync_call(self) -> ::Unpacked +``` + +In this example, we are building a `sync call` to a `destination` smart contract address using the adder contract's proxy: + +```rust title=adder.rs + #[endpoint] + fn sync(&self, destination: ManagedAddress, value: BigUint) { + self.tx() + .to(destination) + .typed(adder_proxy::AdderProxy) + .add(value) + .sync_call(); + } +``` + + + +### Sync call, same context + +This call operates in the same execution context as the source contract. This means that the callee code is executed over the caller's storage and context, so it's just like calling third-party code to deal with your storage. + +It's essential that the code called in such a way is _trusted_, since we are granting it direct access to our entire storage. + +It can be useful for having library-like smart contracts or plug-in systems. It is currently not used often. + +To perform this type of call, use `.sync_call_same_context()`. + +```rust + /// Executes transaction synchronously, in the same context (performed in the name of the caller). + /// + /// Only works with contracts from the same shard. + pub fn sync_call_same_context(self) -> ::Unpacked +``` + +In this example, we are building a `sync call` using the `same execution context` to a `destination` smart contract address using the adder contract's proxy: + +```rust title=adder.rs + #[endpoint] + fn sync_same_context(&self, destination: ManagedAddress, value: BigUint) { + self.tx() + .to(destination) + .typed(adder_proxy::AdderProxy) + .add(value) + .sync_call_same_context(); + } +``` + + + +### Sync call, readonly + +This type of call performs a synchronous call in `readonly` mode, meaning the destination contract's state cannot be altered by this action. This type of call is performed with `.sync_call_readonly()`. + +```rust + /// Executes transaction synchronously, in readonly mode (target contract cannot have its state altered). + /// + /// Only works with contracts from the same shard. + pub fn sync_call_readonly(self) -> ::Unpacked +``` + +In this example, we are building a `sync call` in `readonly mode` to a `destination` smart contract address using the adder contract's proxy: + +```rust title=adder.rs + #[endpoint] + fn sync_readonly(&self, destination: ManagedAddress, _value: BigUint) { + self.tx() + .to(destination) + .typed(adder_proxy::AdderProxy) + .sum() + .sync_call_readonly(); + } +``` + + + +### Contract Deploy + +On MultiversX contracts can currently only be deployed in the same shard as their deployer. The new address will always be generated in such a way that it always lands in the same shard, no matter the shard configuration. + +It therefore makes sense that deploy calls are always synchronous. + +During the deploy, the constructor of the new contract , `init`, is always called. All contracts must have this endpoint. + +There are 2 types of deploy call: +- Deploy with explicit byte code (provided explicitly by the caller contract); +- Deploy from source (using bytecode from an existing source address). This is usually cheaper, since byte codes can be large, and processing or storing this code can incur significant gas costs. + + +Both of these calls are executed using `.sync_call()`, but the transaction setup differs for each type. + +For a simple `raw deploy`, initiate a raw deploy transaction using the `.raw_deploy()` function, as shown below: + +```rust title=adder.rs + #[endpoint] + fn raw_deploy(&self, code: ManagedBuffer) -> ManagedAddress { + self.tx() + .raw_deploy() + .code(code) + .code_metadata(CodeMetadata::UPGRADEABLE) + .returns(ReturnsNewManagedAddress) + .sync_call() + } +``` + +In the example above, only `.code()` is mandatory for a deploy sync call. We need to either pass the code to the transaction as an argument, or to receive it from a specified location on the blockchain (from source). + +In the case of a `deploy from source` transaction, we would use the specific function `.from_source()` instead of `.code()` and pass the source address as a parameter, as such: + +```rust title=adder.rs + #[endpoint] + fn raw_deploy_from_source(&self, source: ManagedAddress) -> ManagedAddress { + self.tx() + .raw_deploy() + .from_source(source) + .code_metadata(CodeMetadata::UPGRADEABLE) + .returns(ReturnsNewManagedAddress) + .sync_call() + } +``` + + + +## Async calls + + + + +### Async call (V1) + +The most common type of async call. This type of call can be executed with `.async_call_and_exit()`. + +:::important +Async call uses the `async V1` mechanism. +::: + +```rust +pub fn async_call_and_exit(self) -> ! +``` + +This type of call always terminates the current transaction immediately. Any code coming after it will not be executed. + +It is therefore only possible to have **one** such call per transaction. + +In this example, we are building an `async V1 call` to a `destination` smart contract address using the adder contract's proxy: + +```rust title=adder.rs + #[endpoint] + fn async_call(&self, destination: ManagedAddress, value: BigUint) { + self.tx() + .to(destination) + .typed(adder_proxy::AdderProxy) + .add(value) + .async_call_and_exit(); + } +``` + + + +### Register promise (V2) + +Register promise performs an asynchronous promise call and allows multiple calls as such in a single transaction. To perform this type of call, use `.register_promise()`. + +:::important +Register promise uses the `async V2` mechanism. +::: + +```rust + /// Launches a transaction as an asynchronous promise (async v2 mechanism). + /// + /// Several such transactions can be launched from a single transaction. + /// + /// Must set: + /// - to + /// - gas + /// - a function call, ideally via a proxy. + /// + /// Value-only promises are not supported. + /// + /// Optionally, can add: + /// - any payment + /// - a promise callback, which also needs explicit gas for callback. + pub fn register_promise(self) +``` + +Unlike the old async call, it is possible to have more than one `register_promise` call in a transaction. Execution is not terminated. + +In this example, we are building an `async V2 call` to a `destination` smart contract address using the adder contract's proxy: + +```rust title=adder.rs + #[endpoint] + fn register_promise(&self, destination: ManagedAddress, value: BigUint) { + self.tx() + .to(destination) + .gas(30_000_000u64) + .typed(adder_proxy::AdderProxy) + .add(value) + .register_promise(); + } +``` + +Just like the old async call, promises allow callbacks. + +:::important +Promises callbacks must be annotated with `#[promises_callback]` instead of `#[callback]`. +::: + + + +### Transfer execute + +This call executes a transaction asynchronously without waiting for a callback. In order to perform this type of call use `.transfer_execute()`. + +```rust + /// Sends transaction asynchronously, and doesn't wait for callback ("fire and forget".) + pub fn transfer_execute(self) +``` + +In this example, we are building an async call that **does not wait for a callback** (fire and forget) to a `destination` smart contract address using the adder contract's proxy: + +```rust title=adder.rs + #[endpoint] + fn transfer_execute(&self, destination: ManagedAddress, value: BigUint) { + self.tx() + .to(destination) + .gas(30_000_000u64) + .typed(adder_proxy::AdderProxy) + .add(value) + .transfer_execute(); + } +``` + + + +### Upgrade call + +If a smart contract is marked as **upgradeable**, its owner is allowed to upgrade the smart contract code to a newer version. + +The upgrade call changes the code and causes the special endpoint `upgrade` to be called, analogous to how a deploy will call the `init` constructor. + +Unlike deploy calls, it is possible to upgrade a contract from another shard. This is because, even though the original owner deployer will always be in the same shard as the contract, contract ownership can be transferred. + +Similar to deploy calls, there are two types of expressing upgrade calls: +- Upgrade with explicit byte code (provided explicitly by the caller contract); +- Upgrade from source (using bytecode from an existing source address). This is usually cheaper, since byte codes can be large, and processing or storing this code can incur significant gas costs. + +Since the upgrade call is an async call (v1), it also terminates execution immediately. It also accepts a callback. + +```rust + /// Launches the upgrade from source async call. + pub fn upgrade_async_call_and_exit(self) +``` + +Syntax-wise, both of these calls are executed using `.upgrade_async_call_and_exit()`, but the transaction setup differs for each type. + +For a simple `raw upgrade` transaction, we could write: +```rust title=adder.rs + #[endpoint] + fn raw_upgrade(&self, address: ManagedAddress, code: ManagedBuffer) { + self.tx() + .to(address) + .raw_upgrade() + .code(code) + .code_metadata(CodeMetadata::UPGRADEABLE) + .upgrade_async_call_and_exit(); + } +``` + +Similar to deploy calls, `.code()` is mandatory. We must either pass the code to the transaction, or to receive it from a specified location on the blockchain (from source). + +In order to change this call into an `upgrade from source` call, replace the provided code with the source address, using `.from_source()`: + +```rust title=adder.rs + #[endpoint] + fn raw_upgrade_from_source(&self, address: ManagedAddress, source: ManagedAddress) { + self.tx() + .to(address) + .raw_upgrade() + .from_source(source) + .code_metadata(CodeMetadata::UPGRADEABLE) + .upgrade_async_call_and_exit(); + } +``` + +--- + +### scdeploys + +This page describes the structure of the `sc-deploys` index (Elasticsearch), and also depicts a few examples of how to query it. + + +## _id + +The `_id` field of this index is represented by a bech32 encoded smart contract address. + + +## Fields + + +| Field | Description | +|---------------|-----------------------------------------------------------------------------------------------------| +| deployTxHash | The deployTxHash holds the hex encoded hash of the transaction that deployed the smart contract. | +| deployer | The address field holds the address in bech32 encoding of the smart contract deployer. | +| timestamp | The timestamp field represents the timestamp of the block in which the smart contract was deployed. | +| upgrades | The upgrades field holds a list with details about the upgrades of the smart contract. | + +The `upgrades` field is populated with the fields below: + + +| upgrades fields | Description | +|-----------------|--------------------------------------------------------------------------------------------------------| +| upgrader | The upgrader field holds the bech32 encoded address of the sender of the contract upgrade transaction. | +| upgradeTxHash | The upgradeTxHash field holds the hex encoded hash of the contract upgrade transaction. | +| timestamp | The timestamp field represents the timestamp of the block in which the smart contract was upgraded. | + + +## Query examples + + +### Fetch details about a smart contract + +``` +curl --request GET \ + --url ${ES_URL}/scdeploys/_search \ + --header 'Content-Type: application/json' \ + --data '{ + "query": { + "match": { + "_id":"erd..." + } + } +}' +``` + +--- + +### Scenario Complex Values + +We already covered representations of simple types [here](/developers/testing/scenario/values-simple). This is enough for arguments of types like `usize`, `BigUint` or `&[u8]`, but we need to also somehow specify complex types like custom structs or lists of items. + + +## **Concatenation** + +It is possible to concatenate multiple expressions using the pipe operator (`|`). The pipe operator takes precedence over everything, so it is not currently possible to concatenate and then apply a function to the whole result. + +This is ideal for short lists or small structs. + +:::note Example + +- a `Vec` can be expressed as `"u32:1|u32:2|u32:3"`. +- a `(BigUint, BigUint)` tuple can be expressed as `"biguint:1|biguint:2"` +- a `SimpleStruct { a: u8, b: BoxedBytes }` can be expressed as `"u8:4|nested:str:value-b"` + ::: + +Please note that the pipe operator only takes care of the concatenation itself. You are responsible for making sure that [nested encoding](/developers/data/serialization-overview/#the-concept-of-top-level-vs-nested-objects) is used where appropriate. + + +## **Using JSON lists as values** + +Scenarios allow using JSON lists to express longer values. This especially makes sense when the value being represented is itself a list in the smart contract. + +:::note Example + +- a `Vec` can also be expressed as `["u32:1", "u32:2", "u32:3"]`. +- a `(BigUint, BigUint)` tuple can also be expressed as `["biguint:1", "biguint:2"]` +- a `SimpleStruct { a: u8, b: BoxedBytes }` can also be expressed as `["u8:4", "nested:str:value-b"]`, although in this case a [JSON map](#using-json-maps-as-values) might be more appropriate. + ::: + +Make sure not to confuse values expressed as JSON lists with other elements of scenario syntax. + +:::note Example + +```json +{ + "step": "scCall", + "txId": "echo_managed_vec_of_managed_vec", + "tx": { + "from": "address:an_account", + "to": "sc:basic-features", + "value": "0", + "function": "echo_managed_vec_of_managed_vec", + "arguments": [ + [ + "u32:3", + [ + "u32:1", + "u32:2", + "u32:3" + ], + "u32:0", + "u32:2", + [ + "u32:5", + "u32:6" + ] + ] + ], + "gasLimit": "50,000,000", + "gasPrice": "0" + } +} +``` + +In the example above, there is in fact a single argument that we are passing to the endpoint. The outer brackets in `"arguments": [ ... ]` are scenario syntax for the list of arguments. The brackets immediately nested signal a JSON list value. Notice how the list itself contains some more lists inside it. They all get concatenated in the end into a single value. + +In this example the only argument is `0x0000000300000001000000020000000300000000000000020000000500000006`. + +::: + +:::tip + +We mentioned above how the developer needs to take care of the serialization of the nested items. This is actually a good example of that. The endpoint `echo_managed_vec_of_managed_vec` takes a list of lists, so we need to serialize the lengths of the lists on the second level. Notice how the lengths are given as JSON strings and the contents as JSON lists; the first `"u32:3"` is the serialized length of the first item, which is `["u32:1", "u32:2", "u32:3"]`, and so forth. + +::: + + +## **Using JSON maps as values** + +JSON lists make sense for representing series of items, but for structs JSON maps are more expressive. + +The rules are as follows: + +- The interpreter will concatenate all JSON map values and leave the keys out. +- The keys need to be in alphanumerical order, so we customarily prefix them with numbers. Map keys in JSON are fundamentally unordered and this is the easiest way to enforce a deterministic order for the values. +- Map values can be either JSON strings, lists or other maps, all scenario value rules apply the same way all the way down. + +:::note Example + +This is an abridged section of the actual lottery contract in the examples. + +```json +{ + "step": "checkState", + "accounts": { + "sc:lottery": { + "storage": { + "str:lotteryInfo|nested:str:lottery_name": { + "0-token_identifier": "nested:str:LOTTERY-123456", + "1-ticket_price": "biguint:100", + "2-tickets-left": "u32:0", + "3-deadline": "u64:123,456", + "4-max_entries_per_user": "u32:1", + "5-prize_distribution": ["u32:2", "u8:75", "u8:25"], + "6-whitelist": [ + "u32:3", + "address:acc1", + "address:acc2", + "address:acc3" + ], + "7-prize_pool": "biguint:500" + } + }, + "code": "file:../output/lottery-esdt.wasm" + } + } +} +``` + +The Rust struct this translates to is: + +```rust +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode, TypeAbi)] +pub struct LotteryInfo { + pub token_identifier: TokenIdentifier, + pub ticket_price: BigUint, + pub tickets_left: u32, + pub deadline: u64, + pub max_entries_per_user: u32, + pub prize_distribution: Vec, + pub whitelist: Vec
, + pub prize_pool: BigUint, +} + +``` + +::: + +:::tip + +Once again, note that all contained values are in [nested encoding format](/developers/data/serialization-overview/#the-concept-of-top-level-vs-nested-objects): + +- the token identifier has its length automatically prepended by the `nested:` prefix, +- big ints are given with the `biguint:` syntax, which prepends their byte length, +- small ints are given in full length, +- lists have their length explicitly encoded at the start, always on 4 bytes (as `u32`). + +::: + + +## **A note about enums** + +There are 2 types of enums that we use in Rust: + +- simple enums are simply encoded as `u8` +- complex enums are encoded just like structures, with an added `u8` discriminant at the beginning. + +Discriminants are not explicit in the Rust code, they get generated automatically. Discriminats start from `0`. + +:::note Example + +This is an abridged section of a Multisig contract test. + +```json +{ + "step": "checkState", + "accounts": { + "sc:multisig": { + "storage": { + "str:action_data.item|u32:3": { + "1-discriminant": "0x06", + "2-amount": "u32:0", + "3-code": "nested:file:../test-contracts/adder.wasm", + "4-code_metadata": "0x0000", + "5-arguments": ["u32:1", "u32:2|1234"] + }, + "+": "" + }, + "code": "file:../output/multisig.wasm" + }, + "+": "" + } +} +``` + +In this example we have a `SCDeploy`: + +```rust +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode, TypeAbi)] +pub enum Action { + Nothing, + AddBoardMember(ManagedAddress), + AddProposer(ManagedAddress), + RemoveUser(ManagedAddress), + ChangeQuorum(usize), + SendEgld { + to: ManagedAddress, + amount: BigUint, + data: BoxedBytes, + }, + SCDeploy { + amount: BigUint, + code: ManagedBuffer, + code_metadata: CodeMetadata, + arguments: Vec, + }, + SCCall { + to: ManagedAddress, + egld_payment: BigUint, + endpoint_name: BoxedBytes, + arguments: Vec, + }, +} +``` + +::: + +--- + +### Scenario Simple Values + +We went through the structure of a scenario, and you might have noticed that in a lot of places values are expressed in diverse ways. + +The VM imposes very few restrictions on its inputs and outputs, most fields are processed as raw bytes. The most straightforward way to write a test that one could think of would be to have the actual raw bytes always expressed in a simple format (e.g. like hexadecimal encoding). Indeed, our first contract tests were like this, but we soon discovered that it took painfully long prepare them and even longer to refactor. So, we gradually came up with increasingly complex formats to represent values in an intuitive human-readable way. + +We chose to create a single universal format to be used everywhere in a scenario file. The same format is used for expressing: + +- addresses, +- balances, +- transaction and block nonces, +- contract code, +- storage keys and values, +- log identifiers, topics and data, +- gas limits, gas costs, +- ESDT metadata, etc. + +The advantage of this unique value format is that it is enough to understand it once to then use it everywhere. + +The scenario value format is closely related to the [MultiversX serialization format](/developers/data/serialization-overview). This is not by accident, Scenarios are designed to make it easy to interact MultiversX contracts and their data. + +Exceptions: `txId`, `comment` and `asyncCallData` are simple strings. `asyncCallData` might be changed to the default value format in the future and/or reworked. + +:::important + +It must be emphasized that no matter how values are expressed in scenarios, the communication with the VM is always done via raw bytes. Of course it is best when the value expression and the types in the smart contract match, but this is not enforced. + +::: + +A note on error messages: whenever we write a test that fails, the test runner tries its best to transform the actual value it found from raw bytes to a more human-readable form. It doesn't really know what format to use, so it tries its best to find something plausible. However, all it has are some heuristics, so it doesn't always get it right. It also displays the raw bytes so that the developer can investigate the proper value. + + +## **A note about the value parser and the use of prefixes** + +The value interpreter is not very complex and uses simple prefixes for most functions. Examples of prefixes are `"str:"` and `"u32:"`. + +The `|` (pipe) operator, which we use for concatenation has the highest priority. More about it [here](/developers/testing/scenario/values-complex#concatenation). + +The arguments of functions start after the prefix (no whitespace) and end either at the first pipe (`|`) or at the end of the string. + +Multiple prefixes evaluated right to left, for instance `"keccak256:keccak256:str:abcd"` will first convert `"abcd"` to bytes, then apply the hashing function on it twice. + +With that being said, the following sections will describe how to express different value types in scenarios. A full list of the prefixes is [at the end of this page](#the-full-list-of-scenario-value-prefixes). + + +## **Empty value** + +Empty strings (`""`) mean empty byte arrays. The number zero can also be represented as an empty byte array. +Other values that translate to an empty byte array are `"0"` and `"0x"`. + + +## **Hexadecimal representation** + +To provide raw hexadecimal representations of the values, use the prefix `0x` and follow it with the base-16 bytes. E.g. `"0x1234567890"`. +After the `0x` prefix an even number of digits is expected, since 2 digits = 1 byte. + +:::note Examples + +- `"0x"` +- `"0x1234567890abcdef"` +- `"0x0000000000000000"` +::: + + +## **Standalone number representations** + +Unprefixed numbers are interpreted as base 10, unsigned. +Unsigned numbers will be represented in the minimum amount of bytes in which they can fit. + +:::note Examples + +- `"0"` +- `"1"` +- `"1000000".` +- `"255"` is the same as `"0xff"` +- `"256"` is the same as `"0x0100"` +- `"0"` is the same as `""` +::: + +:::tip +Digit separators are allowed anywhere, for readability, e.g. `"1,000,000"`. +::: + + +## **Standalone signed numbers** + +:::caution +Only use signed numbers if you absolutely need to. Big signed integer representation has some pitfalls that can lead to subtle and unexpected issues when interacting with the contract. +::: + +Sometimes contract arguments are expected to be signed. These arguments will be transmitted as two’s complement representation. Prefixing any number (base 10 or hex) with a minus sign will convert them to two’s complement. Two’s complement is interpreted as positive or negative based on the first bit. + +Sometimes positive numbers can start with a "1" bit and get accidentally interpreted as negative. To prevent this, we can prefix them with a plus. A few examples should make this clearer: + +:::note Examples + +- `"1"` is represented as `"0x01"`, signed interpretation: `1`, everything OK. +- `"255"` is represented as `"0xff"`, signed interpretation: `"-1",` this might not be what we expected. +- `"+255"` is represented as `"0x00ff"`, signed interpretation: `"255".` The prepended zero byte makes sure the contract interprets it as positive. The `+` makes sure those leading zeroes are added if necessary. +- `"+1"` is still represented as `"0x01"`, here the leading 0 is not necessary. Still, it is good practice adding the `+` if we know the argument is expected to be signed. +- `"-1"` is represented as `"0xff"`. Negative numbers are also represented in the minimum number of bytes possible. +::: + +For more about signed number encoding, see [the big number serialization format](/developers/data/simple-values#arbitrary-width-big-numbers). + + +## **Nested numbers** + +Whenever we nest numbers in larger structures, we need to somehow encode their length. Otherwise, it would become impossible for them to be deserialized. + +The format helps developers to also easily represent nested numbers. These are as follows: + +- `biguint:` is useful for representing a nested BigUint. It outputs the length of the byte representation, followed by the big endian byte representation itself. +- `u64:` `u32:` `u16:` `u8:` interpret the argument as an unsigned int and convert to big endian bytes of respective length (8/4/2/1 bytes) +- `i64:` `i32:` `i16:` `i8:` interpret the argument as a signed int and convert to 2's complement big endian bytes of respective length (8/4/2/1 bytes) + +:::note Examples + +- `"biguint:0"` equals `0x00000000` +- `"biguint:1"` equals `0x0000000101` +- `"biguint:256"` equals `0x00000020100` +- `u64:1` equals `0x0000000000000001` +- `i64:-1` equals `0xFFFFFFFFFFFFFFFF` +- `u32:1` equals `0x00000001` +- `u16:1` equals `0x0001` +- `u8:1` equals `0x01` +::: + + +## **Nested items** + +The `nested:` prefix prepends the length of the argument. It is similar to `biguint:`, but does not expect a number. + +:::note Examples + +- `"nested:str:abc"` equals `0x00000003|str:abc` +- `"nested:0x01020304"` equals `0x0000000401020304` +::: + + +## **Booleans** + +The format offers these 2 constants, for convenience: + +- `"true"` = `"1"` = `"0x01"` +- `"false"` = `"0"` = `""`. + +:::caution +This is the standalone representation. If your boolean is embedded in a structure or list, use `u8:0` instead of `false`. +::: + + +## **ASCII strings** + +The preferred way of representing ASCII strings is with the `str:` prefix. + +:::important +The `''` and ` `` ` prefixes are common in older examples. They are equivalent to `str:`, but considered legacy. We recommend avoiding them because they clash with the syntax of languages where we might want to embed scenario code (Go and Markdown in particular). +::: + + +## **User Addresses** + +`address:` constructs a dummy user address from a word. + +Addresses need to be 32 bytes long, so + +- if the word is longer, it gets chopped off at the end +- if the word is shorter, it gets extended to the right with `0x5f` bytes (the `"_"` character). + +:::note Example +`"address:my_address"` is the same as: + +- `"str:my_address______________________"` or +- `"0x6d795f616464726573735f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f"`. +::: + + +## **Smart Contract Addresses** + +`sc:` constructs a dummy smart contract address. + +On MultiversX, smart contract addresses have a different format than user address - they start with 8 bytes of zero. + +:::important +The format requires that all accounts with addresses in SC format must have non-empty code. + +It is forbidden to have accounts with code but an address that doesn't obey the SC format. +::: + +:::note Example +`"sc:my_address"` is the same as: + +- `"0x0000000000000000|str:my_address______________"` or +- `"0x00000000000000006d795f616464726573735f5f5f5f5f5f5f5f5f5f5f5f5f5f"`. +::: + +Sometimes the last byte of a a SC address is relevant, since it affects which shard the contract will end up in. It can be specified with a hash character `#`, followed by the final byte as hex. + +:::note Example +`"sc:my_address#a3"` is the same as: + +- `"0x0000000000000000|str:my_address_____________|0xa3"` and +- `"0x00000000000000006d795f616464726573735f5f5f5f5f5f5f5f5f5f5f5f5fa3"`. +::: + + +## **File contents** + +`file:` loads an entire file and uses the contents of the entire file as value. + +The path of the file is given relative to the current scenario file. + +Used in the first place for specifying smart contract code. It can, however, be used for specifying any value, anywhere. + +:::note Example +`"file:../output/my-contract.wasm"` +::: + +Example usage: + +- initializing contract code, +- contract code to deploy, +- contract code to be passed to another contract as argument for indirect deploy, +- checking some contract code in storage, +- any large argument + + +## **Hash function** + +`keccak256:` computes the Keccak256 hash of the argument. The result is always 32 bytes in length. + + +## **The full list of scenario value prefixes** + +The prefixes are: + +- `str:` converts from ASCII strings to bytes. +- `address:` dummy user address +- `sc:` dummy smart contract address +- `file:` loads the entire contents of a file +- `keccak256:` computes the hash of the argument +- `u64:` `u32:` `u16:` `u8:` interpret the argument as an unsigned int and convert to big endian bytes of respective length (8/4/2/1 bytes) +- `i64:` `i32:` `i16:` `i8:` interpret the argument as a signed int and convert to 2's complement big endian bytes of respective length (8/4/2/1 bytes) +- `biguint:` big number unsigned byte length followed by big number unsigned bytes themselves +- `nested:` prepends the length of the argument + +--- + +### scresults + +This page describes the structure of the `sc-results` index (Elasticsearch), and also depicts a few examples of how to query it. + + +## _id + +:::warning Important + +**The `scresults` index will be deprecated and removed in the near future.** +We recommend using the [operations](/sdk-and-tools/indices/es-index-operations) index, which contains all the smart contract results data. +The only change required in your queries is to include the `type` field with the value `unsigned` to fetch all smart contract results. + +Please make the necessary updates to ensure a smooth transition. +If you need further assistance, feel free to reach out. +::: + +The `_id` field for this index is composed of hex-encoded smart contract result hash. +(example: `cbd4692a092226d68fde24840586bdf36b30e02dc4bf2a73516730867545d53c`) + + +## Fields + + +| Field | Description | +|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| miniBlockHash | The miniBlockHash field represents the hash of the miniblock in which the smart contract result was included. | +| nonce | The nonce field represents the transaction sequence number. | +| gasLimit | The gasLimit field represents the maximum gas units the sender is willing to pay for. | +| gasPrice | The gasPrice field represents the amount to be paid for each gas unit. | +| value | The value field represents the amount of EGLD to be sent from the sender to the receiver. | +| sender | The sender field represents the address of the smart contract result sender. | +| receiver | The receiver field represents the destination address of the smart contract result. | +| senderShard | The senderShard field represents the shard ID of the sender address. | +| receiverShard | The receiverShard field represents the shard ID of the receiver address. | +| relayerAddr | The relayerAddr field represents the address of the relayer. | +| relayedValue | This relayedValue field represents the amount of EGLD to be transferred via the inner transaction's sender. | +| code | The code holds the code of the smart contract result. | +| data | The data field holds additional information for a smart contract result. It can contain a simple message, a function call, an ESDT transfer payload, and so on. | +| prevTxHash | The prevTxHash holds the hex encoded hash of the previous transaction. | +| originalTxHash | The originalTxHash holds the hex encoded hash of the transaction that generated the smart contract result. | +| callType | The callType field holds the type of smart contract call that is done through the smart contract result. | +| codeMetaData | The codeMetaData field holds the code metadata. | +| returnMessage | The returnMessage field holds the message that is returned by a smart contract in case of an error. | +| timestamp | The timestamp field represents the timestamp of the block in which the smart contract result was executed. | +| status | The status field holds the execution state of the smart contract result. The execution state can be `pending` or `success`. | +| tokens | The tokens field contains a list of ESDT tokens that are transferred based on the data field. The indices from the `tokens` list are linked to the indices from `esdtValues` list. | +| esdtValues | The esdtValues field contains a list of ESDT values that are transferred based on the data field. | +| receivers | The receivers field contains a list of receiver addresses in case of ESDTNFTTransfer or MultiESDTTransfer. | +| receiversShardIDs | The receiversShardIDs field contains a list of receiver addresses' shard IDs. | +| operation | The operation field represents the operation of the smart contract result based on the data field. | +| function | The function field holds the name of the function that is called in case of a smart contract call. | +| originalSender | The originalSender field holds the sender's address of the original transaction. | +| hasLogs | The hasLogs field is true if the transaction has logs. | + + +## Query examples + + +### Fetch all the smart contract results generated by a transaction + +``` +curl --request GET \ + --url ${ES_URL}/scresults/_search \ + --header 'Content-Type: application/json' \ + --data '{ + "query": { + "match": { + "originalTxHash":"d6.." + } + } +}' +``` + +--- + +### sdk-dapp + +Library used to build React dApps on MultiversX Network. + +:::important +The following documentation is for `sdk-dapp v5.0.0` and above. +::: + + +## Introduction + +`sdk-dapp` is a library that holds core functional logic that can be used to create a dApp on MultiversX Network. + +It is built for applications that use any of the following technologies: + +- React (example: [React Template Dapp](https://github.com/multiversx/mx-template-dapp)) +- Angular (example: [Angular Template Dapp](https://github.com/multiversx/mx-template-dapp-angular)) +- Vue (example: [Vue Template Dapp](https://github.com/multiversx/mx-template-dapp-vue)) +- Any other JavaScript framework (e.g. Solid.js etc.) (example: [Solid.js Dapp](https://github.com/multiversx/mx-solidjs-template-dapp)) +- React Native +- Next.js (example: [Next.js Dapp](https://github.com/multiversx/mx-template-dapp-nextjs)) + + +### GitHub project + +The GitHub repository can be found here: [https://github.com/multiversx/mx-sdk-dapp](https://github.com/multiversx/mx-sdk-dapp) + + +### Live demo: template-dapp + +See [Template dApp](https://template-dapp.multiversx.com/) for live demo or checkout usage in the [Github repo](https://github.com/multiversx/mx-template-dapp) + + +### Requirements + +- Node.js version 20.13.1+ +- Npm version 10.5.2+ + + +### Distribution + +[npm](https://www.npmjs.com/package/@multiversx/sdk-dapp) + + +## Installation + +The library can be installed via npm or yarn. + +```bash +npm install @multiversx/sdk-dapp +``` + +or + +```bash +yarn add @multiversx/sdk-dapp +``` + +> **Note:** Make sure you run your app on `https`, not `http`, otherwise some providers will not work. + +If you're transitioning from `@multiversx/sdk-dapp@4.x`, you can check out the [Migration guide](https://github.com/multiversx/mx-template-dapp/blob/main/MIGRATION_GUIDE.md) and [migration PR](https://github.com/multiversx/mx-template-dapp/pull/343) of Template Dapp + + +## Usage + +sdk-dapp aims to abstract and simplify the process of interacting with users' wallets and with the MultiversX blockchain, allowing developers to easily get started with new applications. + +```mermaid +flowchart LR + A["Signing Providers & APIs"] <--> B["sdk-dapp"] <--> C["dApp"] +``` + +The basic concepts you need to understand are configuration, provider interaction, transactions, and presenting data. These are the building blocks of any dApp, and they are abstracted in the `sdk-dapp` library. + +Having this knowledge, we can consider several steps needed to put a dApp together: + +**Table 1.** Steps to build a dApp + +| # | Step | Description | +|----|---------------------|-------------| +| 1 | Configuration | storage configuration (e.g. sessionStorage, localStorage etc.); chain configuration; custom provider configuration (adding / disabling / changing providers) | +| 2 | Provider interaction| logging in and out; signing transactions / messages | +| 3 | Presenting data | get store data (e.g. account balance, account address etc.); use components to display data (e.g. balance, address, transactions list) | +| 4 | Transactions | sending transactions; tracking transactions; displaying transactions history | + +Each of these steps will be explained in more detail in the following sections. + + +### 1. Configuration + +Before your application bootstraps, you need to configure the storage, the network, and the signing providers. This is done by calling the `initApp` method from the `methods` folder. It is recommended to call this method in the `index.tsx` to ensure the sdk-dapp internal functions are initialized before rendering the app. + +```typescript +// index.tsx +import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; +import type { InitAppType } from '@multiversx/sdk-dapp/out/methods/initApp/initApp.types'; +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { App } from "./App"; + +const config: InitAppType = { + storage: { getStorageCallback: () => sessionStorage }, + dAppConfig: { + // nativeAuth: true, // optional + environment: EnvironmentsEnum.devnet, + // network: { // optional + // walletAddress: 'https://devnet-wallet.multiversx.com' // or other props you want to override + // }, + // transactionTracking: { // optional + // successfulToastLifetime: 5000, + // onSuccess: async (sessionId) => { + // console.log('Transaction session successful', sessionId); + // }, + // onFail: async (sessionId) => { + // console.log('Transaction session failed', sessionId); + // } + } + // customProviders: [myCustomProvider] // optional +}; + +initApp(config).then(() => { + render(() => , root!); // render your app +}); +``` + + +### 2. Provider interaction + +Once your dApp has loaded, the first user action is logging in with a chosen provider. There are two ways to perform a login, namely using the `UnlockPanelManager` and programmatic login using the `ProviderFactory`. + +#### 2.1 Using the `UnlockPanelManager` +By using the provided UI, you get the benefit of having all supported providers ready for login in a side panel. You simply need to link the `unlockPanelManager.openUnlockPanel` to a user action. + +```typescript +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager'; +import { ProviderFactory } from '@multiversx/sdk-dapp/out/providers/ProviderFactory'; + +export const ConnectButton = () => { + const unlockPanelManager = UnlockPanelManager.init({ + loginHandler: () => { + navigate('/dashboard'); + }, + onClose: () => { // optional action to be performed when the user closes the Unlock Panel + navigate('/'); + }, + // allowedProviders: [ProviderTypeEnum.walletConnect, 'myCustomProvider'] // optionally, only show specific providers + }); + const handleOpenUnlockPanel = () => { + unlockPanelManager.openUnlockPanel(); + }; + return ; +}; + +``` +Once the user has logged in, if `nativeAuth` is configured in the `initApp` method, an automatic logout will be performed upon native auth expiration. Before the actual logout is performed, the `LogoutManager` will show a warning toast to the user. This toast can be customized by passing a `tokenExpirationToastWarningSeconds` to the `nativeAuth` config. + +```typescript +// in initAoo config +const config: InitAppType = { + // ... + nativeAuth: { + expirySeconds: 30, // test auto logout after 30 seconds + tokenExpirationToastWarningSeconds: 10 // show warning toast 10 seconds before auto logout + }, +} +``` + +You have the option to stop this behavior by calling `LogoutManager.getInstance().stop()` after the user has logged in. + +```typescript +import { LogoutManager } from '@multiversx/sdk-dapp/out/managers/LogoutManager/LogoutManager'; + + loginHandler: () => { + navigate('/dashboard'); + // optional, to stop the automatic logout upon native auth expiration + LogoutManager.getInstance().stop(); + }, +``` + +If you want to perform some actions as soon as the user has logged in, you will need to call `ProviderFactory.create` inside a handler accepting arguments. + +```typescript +export const AdvancedConnectButton = () => { + const unlockPanelManager = UnlockPanelManager.init({ + loginHandler: async ({ type, anchor }) => { + const provider = await ProviderFactory.create({ + type, + anchor + }); + const { address, signature } = await provider.login(); + navigate(`/dashboard?address=${address}`; + }, + }); + const handleOpenUnlockPanel = () => { + unlockPanelManager.openUnlockPanel(); + }; + return ; +}; + +``` + +#### 2.2 Programmatic login using the `ProviderFactory` +If you want to login using your custom UI, you can link user actions to specific providers by calling the `ProviderFactory`. + +```typescript +import { ProviderTypeEnum } from '@multiversx/sdk-dapp/out/providers/types/providerFactory.types'; + +const provider = await ProviderFactory.create({ + type: ProviderTypeEnum.extension +}); +await provider.login(); +``` + +> **Note:** Extension and Ledger login will only work if dApp is served over HTTPS protocol + + +### 3. Displaying app data + +Depending on the framework, you can either use hooks or selectors to get the user details: + +#### 3.1 React hooks + +If you are using React, all hooks can be found under the `/out/react` folder. All store information can be accessed via different hooks but below you will find the main hook related to most common use cases + +```bash +out/react/ +├── account/useGetAccount ### access account data like address, balance and nonce +├── loginInfo/useGetLoginInfo ### access login data like accessToken and provider type +├── network/useGetNetworkConfig ### access network information like chainId and egldLabel +├── store/useSelector ### make use of the useSelector hook for querying the store via selectors +└── transactions/useGetTransactionSessions ### access all current and historic transaction sessions +``` + +Below is an example of using the hooks to display the user address and balance. + +```typescript +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; + +const account = useGetAccount(); +const { + network: { egldLabel } +} = useGetNetworkConfig(); + +console.log(account.address); +console.log(`${account.balance} ${egldLabel}`); +``` + +#### 3.2 Store selector functions: + +If you are not using the React ecosystem, you can use store selectors to get the store data, but note that information will not be reactive unless you subscribe to store changes. + +```typescript +import { getAccount } from '@multiversx/sdk-dapp/out/methods/account/getAccount'; +import { getNetworkConfig } from '@multiversx/sdk-dapp/out/methods/network/getNetworkConfig'; + +const account = getAccount(); +const { egldLabel } = getNetworkConfig(); +``` + +In order to get live updates you may need to subscribe to the store like this: + +```typescript +import { createSignal, onCleanup } from 'solid-js'; +import { getStore } from '@multiversx/sdk-dapp/out/store/store'; + +export function useStore() { + const store = getStore(); + const [state, setState] = createSignal(store.getState()); + + const unsubscribe = store.subscribe((newState) => { + setState(newState); + }); + + onCleanup(() => unsubscribe()); + + return state; +} +``` + + +### 4. Transactions + +#### 4.1 Signing transactions + +To sign transactions, you first need to create the `Transaction` object, then pass it to the initialized provider. + +```typescript +import { Address, Transaction } from '@multiversx/sdk-core'; +import { + GAS_PRICE, + GAS_LIMIT +} from '@multiversx/sdk-dapp/out/constants/mvx.constants'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import { refreshAccount } from '@multiversx/sdk-dapp/out/utils/account/refreshAccount'; + +const pongTransaction = new Transaction({ + value: BigInt(0), + data: Buffer.from('pong'), + receiver: Address.newFromBech32(contractAddress), + gasLimit: BigInt(GAS_LIMIT), + gasPrice: BigInt(GAS_PRICE), + chainID: network.chainId, + nonce: BigInt(account.nonce), + sender: Address.newFromBech32(account.address), + version: 1 +}); + +await refreshAccount(); // optionally, to get the latest nonce +const provider = getAccountProvider(); +const signedTransactions = await provider.signTransactions(transactions); +``` + +#### 4.2 Sending and tracking transactions + +Then, to send the transactions, you need to use the `TransactionManager` class and pass in the `signedTransactions` to the `send` method. You can then track the transactions by using the `track` method. This will create a toast notification with the transaction hash and its status. + +> **Note:** You can set callbacks for the transaction manager to handle the session status, by using the `setCallbacks` method on the `TransactionManager` class. This can also be configured globally in the `initApp` method. + +```typescript +import { TransactionManager } from '@multiversx/sdk-dapp/out/managers/TransactionManager'; +import type { TransactionsDisplayInfoType } from '@multiversx/sdk-dapp/out/types/transactions.types'; + + +const txManager = TransactionManager.getInstance(); + +const sentTransactions = await txManager.send(signedTransactions); + +const toastInformation: TransactionsDisplayInfoType = { + processingMessage: 'Processing transactions', + errorMessage: 'An error has occurred during transaction execution', + successMessage: 'Transactions executed' +} + +const sessionId = await txManager.track(sentTransactions, { + transactionsDisplayInfo: toastInformation, +}); +``` + +#### 4.3 Using the Notifications Feed + +The Notifications Feed is a component that displays **session transactions** in a list. Internally it gets initialized in the `initApp` method. It can be accessed via the `NotificationManager.getInstance()` method. +Once the user logs out of the dApp, all transactions displayed by the Notifications Feed are removed from the store. Note that you can switch between toast notifications and Notifications Feed by pressing the View All button above the current pending transaction toast in the UI. + +```typescript +const notificationManager = NotificationManager.getInstance(); +await notificationManager.openNotificationsFeed(); +``` + +#### 4.4 Inspecting transactions + +You can find both methods and hooks to access transactions data, as seen in the table below. + +**Table 2**. Inspectig transactions +| # | Helper | Description | React hook equivalent | +|---|------|-------------|----| +| | `methods/transactions` | path | `react/transactions` | +| 1 | `getTransactionSessions()` | returns all trabsaction sessions |`useGetTransactionSessions()` | +| 2 | `getPendingTransactionsSessions()` | returns an record of pending sessions | `useGetPendingTransactionsSessions()`| +| 3 | `getPendingTransactions()` | returns an array of signed transactions | `useGetPendingTransactions()` | +| 4 | `getFailedTransactionsSessions()` | returns an record of failed sessions | `useGetFailedTransactionsSessions()`| +| 5 | `getFailedTransactions()` | returns an array of failed transactions | `useGetFailedTransactions()`| +| 6 | `getSuccessfulTransactionsSessions()` | returns an record of successful sessions | `useGetSuccessfulTransactionsSessions()`| +| 7 | `getSuccessfulTransactions()` | returns an array of successful transactions | `useGetSuccessfulTransactions()`| + +There is a way to inspect store information regarding a specific transaction, using the `transactionsSliceSelector`. An example is shown below: + +```typescript +import { + pendingTransactionsSessionsSelector, + transactionsSliceSelector +} from '@multiversx/sdk-dapp/out/store/selectors/transactionsSelector'; +import { getStore } from '@multiversx/sdk-dapp/out/store/store'; + +const store = getStore(); // or use useStore hook for reactivity +const pendingSessions = pendingTransactionsSessionsSelector(store.getState()); +const allTransactionSessions = transactionsSliceSelector(store.getState()); + +const isSessionIdPending = + Object.keys(pendingSessions).includes(sessionId); +const currentSession = allTransactionSessions[sessionId]; +const currentSessionStatus = currentSession?.status; +const currentTransaction = currentSession?.transactions?.[0]; +const currentTransactionStatus = currentTransaction?.status; +``` + +#### 4.5 Logging out +The user journey ends with calling the `provider.logout()` method. + +```typescript +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +const provider = getAccountProvider(); +await provider.logout(); +``` + + +## Internal structure + +We have seen in the previous chapter what are the minimal steps to get up and running with a blockchain interaction using sdk-dapp. Next we will detail each element mentioned above + +**Table 3**. Elements needed to build a dApp +| # | Type | Description | +|---|------|-------------| +| 1 | Network | Chain configuration | +| 2 | Provider | The signing provider for logging in and singing transactions | +| 3 | Account | Inspecting user address and balance | +| 4 | Transactions Manager | Sending and tracking transactions | +| 5 | UI Components | Displaying UI information like balance, public keys etc. | + +Since these are mixtures of business logic and UI components, the library is split into several folders to make it easier to navigate. +When inspecting the package, there is more content under `src`, but the folders of interest are: + + +```bash +src/ +├── apiCalls/ ### methods for interacting with the API +├── constants/ ### useful constants from the ecosystem like ledger error codes, default gas limits for transactions etc. +├── controllers/ ### business logic for UI elements that are not bound to the store +├── managers/ ### business logic for UI elements that are bound to the store +├── providers/ ### signing providers +├── methods/ ### utility functions to query and update the store +├── react/ ### react hooks to query the store +└── store/ ### store initialization, middleware, slices, selectors and actions +``` + +Conceptually, these can be split into 3 main parts: + +- First is the business logic in `apiCalls`, `constants`, `providers` and `methods` +- Then comes the persistence layer hosted in the `store` folder, using [Zustand](https://zustand.docs.pmnd.rs/) under the hood. +- Last are the UI components hosted in [@multiversx/sdk-dapp-ui](https://github.com/multiversx/mx-sdk-dapp-ui) with some components controlled on demand by classes defined in `controlles` and `managers` + +Next, we will take the elements from Table 3 and detail them in the following sections. + + +### 1. Network + +The network configuration is done in the `initApp` method, where you can make several configurations like: + +- specifying the environment (`devnet`, `testnet`, `mainnet`) +- overriding certain network parameters like wallet address, explorer address etc. + +Once the network is configured, the `network` slice in the store will hold the network configuration. + +To query different network parameters, you can use the `getNetworkConfig` method from the `methods/network` folder. + + +### 2. Provider + +The provider is the main class that handles the signing of transactions and messages. It is initialized in the `initApp` method and can be accessed via the `getAccountProvider` method from the `providers/helpers` folder. + +#### Initialization + +An existing provider is initialized on app load (this is take care of by `initApp`), since it restores the session from the store and allows signing transactions without the need of making a new login. + +#### Creating a custom provider + +If you need to create a custom signing provider, make sure to extend the `IProvider` interface and implement all required methods (see example [here](https://github.com/multiversx/mx-template-dapp/tree/main/src/provider)). Next step would be to include it in the `customProviders` array in the `initApp` method or add it to the [window object](https://github.com/multiversx/mx-template-dapp/tree/main/src/initConfig). Last step is to login using the custom provider. + +```typescript +const provider = await ProviderFactory.create({ + type: 'custom-provider' +}); +await provider?.login(); +``` + +#### Accessing provider methods + +Once the provider is initialized, you can get a reference to it using the `getAccountProvider` method. Then you can call the `login`, `logout`, `signTransactions`, `signMessage` methods, or other custom methods depending on the initialized provider (see ledger for example). + + +### 3. Account + +#### Getting account data + +Once the user logs in, a call is made to the API for fetching the account data. This data is persisted in the store and is accessible through helpers found in `methods/account`. These functions are: + +**Table 4**. Getting account data +| # | Helper | Description | React hook equivalent | +|---|------|-------------|----| +| | `methods/account` | path | `react/account` | +| 1 | `getAccount()` | returns all account data |`useGetAccount()` | +| 2 | `getAddress()` | returns just the user's public key | `useGetAddress()`| +| 3 | `getIsLoggedIn()` | returns a login status boolean | `useGetIsLoggedIn()` | +| 4 | `getLatestNonce()` | returns the account nonce | `useGetLatestNonce()` + +#### Nonce management + +`sdk-dapp` has a mechanism that does its best to manage the account nonce. For example, if the user sends a transaction, the nonce gets incremented on the client so that if a new transaction is sent, it will have the correct increased nonce. If you want to make sure the nonce is in sync with the API account, you can call `refreshAccount()` as shown above in the **Signing transactions** section. + + +### 4. Transactions Manager + +#### Overview + +The `TransactionManager` is a class that handles sending and tracking transactions in the MultiversX ecosystem. It provides methods to send either single or batch transactions. It also handles tracking, error management, and toast notifications for user feedback. It is initialized in the `initApp` method and can be accessed via `TransactionManager.getInstance()`. + +#### Features + +- **Supports Single and Batch Transactions:** Handles individual transactions as well as grouped batch transactions. +- **Automatic Tracking:** Monitors transaction status and updates accordingly through a webhook or polling fallback mechanism. +- **Toast Notifications:** Displays status updates for user feedback, with options to disable notifications and customize toast titles. +- **Error Handling:** Catches and processes errors during transaction submission + +#### Transactions Lifecycle + +The transaction lifecycle consists of the following steps: + +1. **Creating** a `Transaction` object from `@multiversx/sdk-core` +2. **Signing** the transaction with the initialized provider and receiving a `SignedTransactionType` object +3. **Sending** the signed transaction using TransactionManager's `send()` function. Signed transactions can be sent in 2 ways: + +**Table 5**. Sending signed transactions +| # | Signature | Method | Description | +|---|------|-------------|-------------| +| 1 | `send([tx1, tx2])` | `POST` to `/transactions` | Transactions are executed in parallel +| 2 | `send([[tx1, tx2], [tx3]])` | `POST` to `/batch` | **a)** 1st batch of two transactions is executed, **b)** the 2nd batch of one transaction waits for the finished results, **c)** and once the 1st batch is finished, the 2nd batch is executed + +4. **Tracking** transactions is made by using `transactionManager.track()`. Since the `send()` function returns the same arguments it has received, the same array payload can be passed into the `track()` method. Under the hood, status updates are received via a WebSocket or polling mechanism. + Once a transaction array is tracked, it gets associated with a `sessionId`, returned by the `track()` method and stored in the `transactions` slice. Depending on the array's type (plain/batch), the session's status varies from initial (`pending`/`invalid`/`sent`) to final (`successful`/`failed`/`timedOut`). + +**Table 6**. Inspecting transaction sessions +| # | Helper | Description | React hook equivalent | +|---|------|-------------|----| +| | `methods/transactions` | path | `react/transactions` | +| 1 | `getTransactionSessions()` | returns all trabsaction sessions |`useGetTransactionSessions()` | +| 2 | `getPendingTransactionsSessions()` | returns an record of pending sessions | `useGetPendingTransactionsSessions()`| +| 3 | `getPendingTransactions()` | returns an array of signed transactions | `useGetPendingTransactions()` | +| 4 | `getFailedTransactionsSessions()` | returns an record of failed sessions | `useGetFailedTransactionsSessions()`| +| 5 | `getFailedTransactions()` | returns an array of failed transactions | `useGetFailedTransactions()`| +| 6 | `getSuccessfulTransactionsSessions()` | returns an record of successful sessions | `useGetSuccessfulTransactionsSessions()`| +| 7 | `getSuccessfulTransactions()` | returns an array of successful transactions | `useGetSuccessfulTransactions()`| + +5. **User feedback** is provided through toast notifications, which are triggered to inform about transactions' progress. Additional tracking details can be optionally displayed in the toast UI. +There is an option to add custom toast messages by using the `createCustomToast` helper. + +```ts +import { createRoot } from 'react-dom/client'; +import { createCustomToast } from '@multiversx/sdk-dapp/out/store/actions/toasts/toastsActions'; + +// by creating a custom toast element containing a component +createCustomToast({ + toastId: 'username-toast', + instantiateToastElement: () => { + const toastBody = document.createElement('div'); + const root = createRoot(toastBody); + root.render(); + return toastBody; + } +}); + +// or by creating a simple custom toast +createCustomToast({ + toastId: 'custom-toast', + icon: 'times', + iconClassName: 'warning', + message: 'This is a custom toast', + title: 'My custom toast' +}); + + +``` + +6. **Error Handling & Recovery** is done through a custom toast that prompts the user to take appropriate action. + +#### Methods +___ + +#### 1. Sending Transactions + +In this way, all transactions are sent simultaneously. There is no limit to the number of transactions contained in the array. + +```typescript +const transactionManager = TransactionManager.getInstance(); +const parallelTransactions: SigendTransactionType[] = [tx1, tx2, tx3, tx4]; +const sentTransactions = await transactionManager.send(parallelTransactions); +``` + +#### 2. Sending Batch Transactions + +In this sequential case, each batch waits for the previous one to complete. + +```typescript +const transactionManager = TransactionManager.getInstance(); +const batchTransactions: SignedTransactionType[][] = [ + [tx1, tx2], + [tx3, tx4] +]; +const sentTransactions = await transactionManager.send(batchTransactions); +``` + +#### 3. Tracking Transactions + +The basic option is to use the built-in tracking, which displays toast notifications with default messages. + +```typescript +import { TransactionManagerTrackOptionsType } from '@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager.types'; + +const options: TransactionManagerTrackOptionsType = { + disableToasts: false, // `false` by default + transactionsDisplayInfo: { // `undefined` by default + errorMessage: 'Failed adding stake', + successMessage: 'Stake successfully added', + processingMessage: 'Staking in progress' + }, + sessionInformation: { // `undefined` by default. Use to perform additional actions based on the session information + stakeAmount: '1000000000000000000000000' + } +}; + +const sessionId = await transactionManager.track( + sentTransactions, + options // optional +); +``` + +If you want to provide more human-friendly messages to your users, you can enable tracking with custom toast messages: + +```typescript +const sessionId = await transactionManager.track(sentTransactions, { + transactionsDisplayInfo: { + errorMessage: 'Failed adding stake', + successMessage: 'Stake successfully added', + processingMessage: 'Staking in progress' + } +}); +``` + +#### 3.1 Tracking transactions without being logged in + +If your application needs to track transactions sent by a server and the user does not need to login to see the outcome of these transactions, there are several steps that you need to do to enable this process. + +Step 1. Enabling the tracking mechanism + +By default the tracking mechanism is enabled only after the user logs in. That is the moment when the WebSocket connection is established. If you want to enable tracking before the user logs in, you need to call the `trackTransactions` method from the `methods/trackTransactions` folder. This method will enable a polling mechanism. + +```typescript +import { trackTransactions } from '@multiversx/sdk-dapp/out/methods/trackTransactions/trackTransactions'; + +initApp(config).then(async () => { + await trackTransactions(); // enable here since by default tracking will be enabled only after login + render(() => , root!); +}); +``` + +Step 2. Tracking transactions + +Then, you can track transactions by calling the `track` method from the `TransactionManager` class with a plain transaction containing the transaction hash. + +```typescript +import { Transaction, TransactionsConverter } from '@multiversx/sdk-core'; + +const tManager = TransactionManager.getInstance(); +const txConverter = new TransactionsConverter(); +const transaction = txConverter.plainObjectToTransaction(signedTx); + +const hash = transaction.getHash().toString(); // get the transaction hash + +const plainTransaction = { ...transaction.toPlainObject(), hash }; +await tManager.track([plainTransaction]); +``` + +#### 3.2 Advanced Usage + +If you need to check the status of the signed transactions, you can query the store directly using the `sessionId` returned by the `track()` method. + +```typescript +import { getStore } from '@multiversx/sdk-dapp/out/store/store'; +import { transactionsSliceSelector } from '@multiversx/sdk-dapp/out/store/selectors/transactionsSelector'; + +const state = transactionsSliceSelector(getStore()); +Object.entries(state).forEach(([sessionKey, data]) => { + if (sessionKey === sessionId) { + console.log(data.status); + } +}); +``` + + +### 5. UI Components + +`sdk-dapp` needs to make use of visual elements for allowing the user to interact with some providers (like the ledger), or to display messages to the user (like idle states or toasts). These visual elements consitst of webcomponents hosted in the `@multiversx/sdk-dapp-ui` package. Thus, `sdk-dapp` does not hold any UI elements, just business logic that controls external components. We can consider two types of UI components: internal and public. They are differentiated by the way they are controlled: internal components are controlled by `sdk-dapp`'s signing or logging in flows, while public components should be controlled by the dApp. + +#### 5.1 Public components + +The business logic for these components is served by a controller. The components are: + +- `MvxTransactionsTable` - used to display the user's transactions + +```tsx +import { TransactionsTableController } from '@multiversx/sdk-dapp/out/controllers/TransactionsTableController'; +import { MvxTransactionsTable } from '@multiversx/sdk-dapp-ui/react'; + +const processedTransactions = await TransactionsTableController.processTransactions({ + address, + egldLabel: network.egldLabel, + explorerAddress: network.explorerAddress, + transactions + }); + +// and use like this: +; + +``` + +- `MvxFormatAmount` - used to format the amount of the user's balance + + +```tsx +import { MvxFormatAmount } from '@multiversx/sdk-dapp-ui/react'; +import { MvxFormatAmountPropsType } from '@multiversx/sdk-dapp-ui/types'; +export { DECIMALS, DIGITS } from '@multiversx/sdk-dapp-utils/out/constants'; +import { FormatAmountController } from '@multiversx/sdk-dapp/out/controllers/FormatAmountController'; +export { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; + + +interface IFormatAmountProps + extends Partial { + value: string; + className?: string; +} + +export const FormatAmount = (props: IFormatAmountProps) => { + const { + network: { egldLabel } + } = useGetNetworkConfig(); + + const { isValid, valueDecimal, valueInteger, label } = + FormatAmountController.getData({ + digits: DIGITS, + decimals: DECIMALS, + egldLabel, + ...props, + input: props.value + }); + + return ( + + ); +}; +``` + + +#### 5.2 Internal components (advanced usage) + +The way internal components are controlled is through a [pub-sub pattern](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) called EventBus. Each webcomponent has a method of exposing its EventBus, thus allowing `sdk-dapp` to get a reference to it and use it for communication. + +```mermaid +flowchart LR + A["Controller"] <--> B["Event Bus"] <--> C["webcomponent"] +``` + +```typescript +import { ComponentFactory } from '@multiversx/sdk-dapp/out/utils/ComponentFactory'; + +const modalElement = await ComponentFactory.create( + 'mvx-ledger-connect-panel' +); +const eventBus = await modalElement.getEventBus(); +eventBus.publish('TRANSACTION_TOAST_DATA_UPDATE', someData); +``` + +If you want to override private components and create your own, you can implement a similar strategy by respecting each webcomponent's API (see an interface example [here](https://github.com/multiversx/mx-sdk-dapp/blob/main/src/providers/strategies/LedgerProviderStrategy/types/ledger.types.ts)). + + +## Debugging your dApp + +> **Note:** For an advanced documentation on how internal flows are implemented, you can check out the [deepwiki](https://deepwiki.com/multiversx/mx-sdk-dapp) diagrams. + +The recommended way to debug your application is by using [lerna](https://lerna.js.org/). Make sure you have the same package version in sdk-dapp's package.json and in your project's package.json. + +If you prefer to use [npm link](https://docs.npmjs.com/cli/v11/commands/npm-link), make sure to use the `preserveSymlinks` option in the server configuration: + +```js + resolve: { + preserveSymlinks: true, // 👈 + alias: { + src: "/src", + }, + }, +``` + +Crome Redux DevTools are by default enabled to help you debug your application. You can find the extension in the Chrome Extensions store. + +To build the library, run: + +```bash +npm run build +``` + +To run the unit tests, run: + +```bash +npm test +``` + +--- + +### sdk-js + +MultiversX SDK for TypeScript and JavaScript + +This SDK consists of TypeScript / JavaScript helpers and utilities for interacting with the Blockchain (in general) and with Smart Contracts (in particular). + -Heartbeat status is retrieved successfully. +## Packages -```json -{ - "data": { - "heartbeats": [ - ... - { - "timeStamp": "2020-06-04T16:02:41.191947208Z", - "publicKey": "006d...", - "versionNumber": "v1.0.125-0-g2164f5f04/go1.13.4/linux-amd64", - "nodeDisplayName": "DrDelphi4", - "identity": "stakingagency", - "totalUpTimeSec": 7367, - "totalDownTimeSec": 0, - "maxInactiveTime": "1m41.148001375s", - "receivedShardID": 4294967295, - "computedShardID": 4294967295, - "peerType": "eligible", - "isActive": true - }, - { - "timeStamp": "2020-06-04T16:02:29.567740999Z", - "publicKey": "667a...", - "versionNumber": "v1.0.125-0-g2164f5f04/go1.13.4/linux-amd64", - "nodeDisplayName": "DrDelphi0", - "identity": "stakingagency", - "totalUpTimeSec": 7367, - "totalDownTimeSec": 0, - "maxInactiveTime": "1m9.537847751s", - "receivedShardID": 4294967295, - "computedShardID": 4294967295, - "peerType": "eligible", - "isActive": true - }, - ... - ] +Base libraries: + +| Package | Source code | Description | +|------------------------------------------------------------------------------------------|---------------------------------------------------------------------|--------------------------------------------------------------------------------| +| [sdk-core](https://www.npmjs.com/package/@multiversx/sdk-core) | [Github](https://github.com/multiversx/mx-sdk-js-core) | The `sdk-core` package is a unification of the previous packages (`multiversx/sdk-wallet` and `multiversx/sdk-network-providers` into `multiversx/sdk-core`). It has basic components for interacting with the blockchain and with smart contracts. | +| [sdk-exchange](https://www.npmjs.com/package/@multiversx/sdk-exchange) | [Github](https://github.com/multiversx/mx-sdk-js-exchange) | Utilities modules for xExchange interactions. | + +Signing providers for dApps: + +| Package | Docs | Source code | Description | +|--------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------| +| [sdk-hw-provider](https://www.npmjs.com/package/@multiversx/sdk-hw-provider) | [documentation](/sdk-and-tools/sdk-js/sdk-js-signing-providers#the-hardware-wallet-provider) | [Github](https://github.com/multiversx/mx-sdk-js-hw-provider) | Sign using the hardware wallet (Ledger). | +| [sdk-wallet-connect-provider](https://www.npmjs.com/package/@multiversx/sdk-wallet-connect-provider) | [documentation](/sdk-and-tools/sdk-js/sdk-js-signing-providers#the-walletconnect-provider) | [Github](https://github.com/multiversx/mx-sdk-js-wallet-connect-provider) | Sign using WalletConnect. | +| [sdk-extension-provider](https://www.npmjs.com/package/@multiversx/sdk-extension-provider) | [documentation](/sdk-and-tools/sdk-js/sdk-js-signing-providers#the-browser-extension-provider) \| [interactive documentation](https://interactive-tutorials.multiversx.com/dashboard/extension-provider) | [Github](https://github.com/multiversx/mx-sdk-js-extension-provider) | Sign using the MultiversX DeFi Wallet (browser extension). | +| [sdk-web-wallet-provider](https://www.npmjs.com/package/@multiversx/sdk-web-wallet-provider) | [documentation](/sdk-and-tools/sdk-js/sdk-js-signing-providers#the-web-wallet-provider) | [Github](https://github.com/multiversx/mx-sdk-js-web-wallet-provider) | Sign using the MultiversX web wallet, using webhooks **(DEPRECATED)**. | +| [mx-sdk-js-web-wallet-cross-window-provider](https://www.npmjs.com/package/@multiversx/sdk-web-wallet-cross-window-provider) | [documentation](/sdk-and-tools/sdk-js/sdk-js-signing-providers#the-web-wallet-cross-window-provider) \| [interactive documentation](https://interactive-tutorials.multiversx.com/dashboard/cross-window-provider) | [Github](https://github.com/multiversx/mx-sdk-js-web-wallet-cross-window-provider) | Sign using the MultiversX web wallet, by opening the wallet in a new tab. | +| [mx-sdk-js-metamask-proxy-provider](https://www.npmjs.com/package/@multiversx/sdk-metamask-proxy-provider) | [documentation](/sdk-and-tools/sdk-js/sdk-js-signing-providers#the-metamask-proxy-provider) \| [interactive documentation](https://interactive-tutorials.multiversx.com/dashboard/iframe-provider) | [Github](https://github.com/multiversx/mx-sdk-js-metamask-proxy-provider) | Sign using the Metamask wallet, by using web wallet as a proxy widget in iframe. | + +For more details about integrating a signing provider into your dApp, please follow [this guide](/sdk-and-tools/sdk-js/sdk-js-signing-providers) or the [mx-sdk-js-examples repository](https://github.com/multiversx/mx-sdk-js-examples). + +Signing SDKs: + +| Package | Source code | Description | +|--------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +| [sdk-dapp](https://www.npmjs.com/package/@multiversx/sdk-dapp) | [Github](https://github.com/multiversx/mx-sdk-dapp) | A library that holds the core functional & signing logic of a dapp on the MultiversX Network. | +| [sdk-guardians-provider](https://www.npmjs.com/package/@multiversx/sdk-guardians-provider) | [Github](https://github.com/multiversx/mx-sdk-js-guardians-provider) | Helper library for integrating a co-signing provider (Guardian) into dApps. | + + +:::important +For all purposes, **we recommend using [sdk-dapp](/sdk-and-tools/sdk-dapp)** instead of integrating the signing providers on your own. +::: + +Native Authenticator libraries: + +| Package | Source code | Description | +|-------------------------------------------------------------------------------------------|----------------------------------------------------------------------|--------------------------------------------------------| +| [sdk-native-auth-client](https://www.npmjs.com/package/@multiversx/sdk-native-auth-client) | [Github](https://github.com/multiversx/mx-sdk-js-native-auth-client) | Native Authenticator - client-side components. | +| [sdk-native-auth-server](https://www.npmjs.com/package/@multiversx/sdk-native-auth-server) | [Github](https://github.com/multiversx/mx-sdk-js-native-auth-server) | Native Authenticator - server-side components. | + +Additional utility packages: + +| Package | Source code | Description | +|------------------------------------------------------------------------------------------|--------------------------------------------------------------------|--------------------------------------------------------| +| [transaction-decoder](https://www.npmjs.com/package/@multiversx/sdk-transaction-decoder) | [Github](https://github.com/multiversx/mx-sdk-transaction-decoder) | Decodes transaction metadata from a given transaction. | + +--- + +### SDKs and Tools - Overview + +## Introduction + +One can (programmatically) interact with the MultiversX Network by leveraging the following SDKs, tools and APIs: + + +### sdk-rs - Rust SDK + +:::important +Note that Rust is also the recommended programming language for writing Smart Contracts on MultiversX. That is, Rust can be used to write both _on-chain software_ (Smart Contracts) and _off-chain software_ (e.g. desktop applications, web applications, microservices). For the on-chain part, please follow [Smart Contracts](/developers/smart-contracts). Here, we refer to the off-chain part. +::: + +| Name | Description | +| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [sdk-rs](https://github.com/multiversx/mx-sdk-rs) | Rust SDK used to interact with the MultiversX Blockchain.
This is the parent repository, also home to the Rust Framework for Smart Contracts. | +| [sdk-rs/core](https://github.com/multiversx/mx-sdk-rs/tree/master/sdk/core) | Core components, accompanied by a set of usage examples. | +| [sdk-rs/snippets](https://github.com/multiversx/mx-sdk-rs/tree/master/framework/snippets) | Smart Contract interaction snippets - base components. Examples of usage: [adder](https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples/adder/interact), [multisig](https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples/multisig/interact). | + + +### sdk-js - Javascript SDK + +| Name | Description | +| -------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | +| [sdk-js](/sdk-and-tools/sdk-js) | High level overview about sdk-js. | +| [sdk-js cookbook](/sdk-and-tools/sdk-js/sdk-js-cookbook) | Learn how to handle common tasks by using sdk-js. | +| [Extending sdk-js](/sdk-and-tools/sdk-js/extending-sdk-js) | How to extend and tailor certain modules of sdk-js. | +| [Writing and testing sdk-js interactions](/sdk-and-tools/sdk-js/writing-and-testing-sdk-js-interactions) | Write sdk-js interactions for Visual Studio Code | +| [sdk-js signing providers](/sdk-and-tools/sdk-js/sdk-js-signing-providers) | Integrate sdk-js signing providers. | + +You might also want to have a look over [**xSuite**](https://xsuite.dev), a toolkit to init, build, test, deploy contracts using JavaScript, made by the [Arda team](https://arda.run). + + +### sdk-dapp - core functional logic of a dApp + +| Name | Description | +| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [sdk-dapp](/sdk-and-tools/sdk-dapp) | React library aimed to help developers create dApps based on MultiversX Network.

It abstracts away all the boilerplate for logging in, signing transactions or messages, and also offers helper functions for common tasks. | + + +### sdk-py - Python SDK + +| Name | Description | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [sdk-py](/sdk-and-tools/sdk-py/#sdk-py-the-python-libraries) | Python SDK that can be used to create wallets, create and send transactions, interact with Smart Contracts and with the MultiversX Network in general. | + + +### mxpy - Python SDK (CLI) + +| Name | Description | +| -------------------------------------------------------------------------------- | ----------------------------------------- | +| [Installing mxpy](/sdk-and-tools/mxpy/installing-mxpy) | How to install and get started with mxpy. | +| [mxpy cli](/sdk-and-tools/mxpy/mxpy-cli) | How to use the Command Line Interface. | + + +### sdk-nestjs - NestJS SDK + +| Name | Description | +| --------------------------------------- | ------------------------------------------------------------------ | +| [sdk-nestjs](/sdk-and-tools/sdk-nestjs) | NestJS SDK commonly used in the MultiversX Microservice ecosystem. | + + +### mx-sdk-go - Golang SDK + +| Name | Description | +| ------------------------------- | -------------------------------------------------------------- | +| [sdk-go](/sdk-and-tools/sdk-go) | Go/Golang SDK used to interact with the MultiversX Blockchain. | + + +### mx-sdk-java - Java SDK + +| Name | Description | +| ------------------------------- | --------------------------------------------------------- | +| [mxjava](/sdk-and-tools/mxjava) | Java SDK used to interact with the MultiversX Blockchain. | + + +### erdcpp - C++ SDK + +| Name | Description | +| ------------------------------- | -------------------------------------------------------- | +| [erdcpp](/sdk-and-tools/erdcpp) | C++ SDK used to interact with the MultiversX Blockchain. | + + +### erdkotlin - Kotlin SDK + +| Name | Description | +| ------------------------------------- | ----------------------------------------------------------- | +| [erdkotlin](/sdk-and-tools/erdkotlin) | Kotlin SDK used to interact with the MultiversX Blockchain. | + + +### nesdtjs-sdk - NestJS SDK + +| Name | Description | +| --------------------------------------- | ----------------------------------------------------------- | +| [sdk-nestjs](/sdk-and-tools/sdk-nestjs) | NestJS SDK used to interact with the MultiversX Blockchain. | + + +### Node Rest API + +| Name | Description | +| ------------------------------------------------------------------------ | ----------------------------------------------------------------- | +| [Rest API](/sdk-and-tools/rest-api/) | High level overview over the MultiversX's Rest API. | +| [api.multiversx.com](/sdk-and-tools/rest-api/multiversx-api) | MultiversX's main API instance. | +| [Gateway overview](/sdk-and-tools/rest-api/gateway-overview) | Gateway overview - public proxy instance. | +| [Addresses](/sdk-and-tools/rest-api/addresses) | Rest API endpoints dedicated to addresses. | +| [Transactions](/sdk-and-tools/rest-api/transactions) | Rest API endpoints dedicated to transactions. | +| [Network](/sdk-and-tools/rest-api/network) | Rest API endpoints dedicated to network status and configuration. | +| [Nodes](/sdk-and-tools/rest-api/nodes) | Rest API endpoints dedicated to nodes. | +| [Blocks](/sdk-and-tools/rest-api/blocks) | Rest API endpoints dedicated to blocks. | +| [Virtual machine](/sdk-and-tools/rest-api/virtual-machine) | Rest API endpoints dedicated to the SC execution VM. | +| [Versions and changelog](/sdk-and-tools/rest-api/versions-and-changelog) | What's new in different versions. | + + +### Proxy + +Proxy is an abstraction layer over the MultiversX Network's sharding. It routes the API request to the desired shard and +merges results when needed. + +| Name | Description | +| ---------------------------------------- | ---------------------------------------------------- | +| [MultiversX Proxy](/sdk-and-tools/proxy) | A Rest API requests handler that abstracts sharding. | + + +### Elasticsearch + +MultiversX Network uses Elasticsearch to index historical data. Find out more about how it can be configured. + +| Name | Description | +| ---------------------------------------------- | --------------------------------------------------------------------------- | +| [Elasticsearch](/sdk-and-tools/elastic-search) | Make use of Elasticsearch near your nodes in order to keep historical data. | + + +### Events notifier + +Events notifier is an external service that can be used to fetch block events and push them to subscribers. + +| Name | Description | +| ------------------------------------------ | ------------------------------------ | +| [Events notifier](/sdk-and-tools/notifier) | A notifier service for block events. | + + +### Chain simulator + +Chain simulator is designed to replicate the behavior of a local testnet. +It can also be pre-initialized / initialized with blockchain state from other networks, such as mainnet or something similar. + +| Name | Description | +| ------------------------------------------------- | ---------------------------- | +| [Chain simulator](/sdk-and-tools/chain-simulator) | A service for local testing. | + + +### Devcontainers (for VSCode or GitHub Codespaces) + +| Name | Description | +| --------------------------------------------- | ----------------------------------------------------------------------- | +| [Devcontainers](/sdk-and-tools/devcontainers) | Overview of MultiversX devcontainers (for VSCode or GitHub Codespaces). | + +--- + +### Send an ESDT + +Send a fungible ESDT token using sdk-core's `TransfersController` and the `Token` / +`TokenTransfer` classes, directly against `DevnetEntrypoint`. Sibling recipe to +[Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld); +same Controller pattern, this time for a custom token instead of the native one. + +If instead you are building a browser dApp where the end user's own wallet should +sign, see +[Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send). +This recipe is for when **your own code holds the private key**. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet with some devnet EGLD (for gas), see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites for how to generate and fund a throwaway one. +- A devnet ESDT token identifier you (or your test wallet) already hold a balance + of, plus **its exact decimal count**, see Pitfall 1. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/send-esdt +npm install +npm run build +npm start -- ./wallet.pem erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th TEST-abcdef 2.5 6 +``` + +(2.5 units of a 6-decimal token, adjust the last argument to match the token you +are actually sending.) + +## Converting the amount + +```ts title="src/amount.ts" +// src/amount.ts — decimal token-amount string <-> smallest-denomination +// bigint, for a token with an arbitrary (not fixed-18) number of decimals. +// +// Unlike EGLD (always 18 decimals), an ESDT's decimal count is per-token — +// set at issuance (the `numDecimals` argument) and readable from the token's +// properties on-chain. This recipe takes it as an explicit parameter rather +// than assuming 18, since assuming EGLD's decimals for an arbitrary ESDT +// would silently send the wrong amount by orders of magnitude. + +import BigNumber from 'bignumber.js'; + +/** + * Converts a decimal token amount (e.g. "2.5") to the smallest-denomination + * bigint the network expects, given the token's own decimal count. + * + * Throws if the input has more precision than `numDecimals` supports. + */ +export function toSmallestDenomination(amount: string, numDecimals: number): bigint { + const value = new BigNumber(amount).multipliedBy(new BigNumber(10).pow(numDecimals)); + if (!value.isFinite() || value.isNegative()) { + throw new Error(`"${amount}" is not a valid non-negative token amount.`); + } + if (!value.isInteger()) { + throw new Error( + `"${amount}" has more precision than this token supports (${numDecimals} decimals).`, + ); + } + return BigInt(value.toFixed(0)); +} +``` + +## Sending + +```ts title="src/sendEsdt.ts" +// src/sendEsdt.ts — the actual subject of this recipe: sending a fungible +// ESDT via sdk-core's TransfersController and the Token/TokenTransfer +// classes. +// +// Sibling recipe to "Send EGLD to an address" — same Controller pattern, +// same "your own code holds the keys" scope (see that recipe for the +// contrast with the browser + connected-wallet flow). The only structural +// difference is which factory method builds the transaction and what +// `resources` input shape it takes. +// +// One easy-to-miss, source-verified naming gotcha this recipe deliberately +// gets right: the CONTROLLER method is +// `createTransactionForEsdtTokenTransfer` (lowercase "sdt"), while the +// underlying FACTORY method it wraps is +// `createTransactionForESDTTokenTransfer` (uppercase "ESDT") — confirmed +// from node_modules/@multiversx/sdk-core/out/transfers/transfersControllers.d.ts +// vs .../transferTransactionsFactory.d.ts. Copying the Factory's casing +// onto a Controller call (or vice versa) is a real `tsc --strict` error, +// not a style nit — TypeScript's property-name matching is exact. + +import { Address, Token, TokenTransfer } from '@multiversx/sdk-core'; +import type { Account, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export interface SendEsdtInput { + /** Bech32 address of the recipient. */ + receiver: string; + /** Token identifier, e.g. "TICKER-abcdef" (fungible — no nonce). */ + tokenIdentifier: string; + /** Amount in the token's own smallest denomination — see src/amount.ts. */ + amountInSmallestDenomination: bigint; +} + +export interface SendEsdtOutput { + txHash: string; +} + +/** + * Builds, signs (via TransfersController), and sends a single fungible ESDT + * transfer. `sender.nonce` must already reflect the network's current + * nonce — see src/account.ts's loadDevnetAccount. + */ +export async function sendEsdt( + entrypoint: DevnetEntrypoint, + sender: Account, + input: SendEsdtInput, +): Promise { + const controller = entrypoint.createTransfersController(); + + const token = new Token({ identifier: input.tokenIdentifier }); + const transfer = new TokenTransfer({ token, amount: input.amountInSmallestDenomination }); + + const transaction = await controller.createTransactionForEsdtTokenTransfer( + sender, + sender.getNonceThenIncrement(), + { + receiver: Address.newFromBech32(input.receiver), + tokenTransfers: [transfer], }, - "error": "", - "code": "successful" + ); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash }; } ``` - - +## Run it +```bash +npm start -- ./wallet.pem +``` -## GET **Get Node Status** {#get-node-status} +Expected output: -`http://localhost:8080/node/status` +```text +Sender: erd1... (nonce 42) +Receiver: erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th +Token: TEST-abcdef +Amount: 2.5 (6 decimals) + +Sent. Transaction hash: <64-char hex hash> +Explorer: https://devnet-explorer.multiversx.com/transactions/ +Status: success (successful: true) +``` + +## How it works + +**`Token` + `TokenTransfer` describe what to send; `TransfersController` builds +and signs the transaction.** `new Token({ identifier })` for a fungible token +needs no `nonce` field (that is only for NFTs/SFTs). +`controller.createTransactionForEsdtTokenTransfer(sender, nonce, { receiver, tokenTransfers: [transfer] })` +builds the ESDT payload, computes gas, sets the nonce, and signs, the same +Controller pattern as +[Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld). + +**Gas for an ESDT transfer is higher than a plain EGLD one, computed for you.** +The factory underneath adds `gasLimitESDTTransfer` (200,000) plus a fixed 100,000 +on top of the base data-movement gas. Verified against a real devnet request: +sending 2.5 units of a 6-decimal token produced a payload +`ESDTTransfer@544553542d616263646566@2625a0` (decodes to `ESDTTransfer` + +hex("TEST-abcdef") + hex(2,500,000)) with `gasLimit: 413000`, exactly +`(50,000 + 1,500 x 42 bytes) + 200,000 + 100,000`. + +## Pitfalls + +:::danger[Pitfall 1: guessing a token's decimal count is a real footgun] +EGLD is always 18 decimals; an ESDT's decimal count is chosen by whoever issued +it and varies token to token, 6, 8, and 18 are all common. Sending +`amount * 10^18` to a 6-decimal token would be off by 10^12. This recipe requires +`numDecimals` as an explicit argument rather than defaulting it, look the real +value up before running this against a token you do not already know the decimals +for. +::: -This endpoint allows one to query all the metrics of the Node. +:::warning[Pitfall 2: the Controller method's casing does not match the Factory it wraps] +It is `createTransactionForEsdtTokenTransfer` (lowercase "sdt") on +`TransfersController`, but `createTransactionForESDTTokenTransfer` (uppercase +"ESDT") on the underlying `TransferTransactionsFactory`, confirmed from both +`.d.ts` files directly. Using the wrong casing for the pattern you are using is a +`tsc --strict` error, not a lint nit. +::: - - +:::note[Pitfall 3: this sends a single fungible token in one transaction] +For NFTs or SFTs, `Token` needs a non-zero `nonce`; for more than one token +transfer in the same transaction, pass multiple entries in `tokenTransfers` (see +[Multi-token transfer in one tx](/sdk-and-tools/sdk-js/cookbook/transactions/multi-token-transfer)). +::: -🟢 200: OK +## See also -Statistics retrieved successfully. +- [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld) + is the native-transfer sibling to this recipe; same Controller pattern. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the browser, connected-wallet equivalent of this recipe. +- [Manage nonces](/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces) + covers the fetch-then-increment pattern in depth, for sending more than one + transaction per process run. -```json -{ - "data": { - "metrics": { - "erd_shard_id": 2, - "erd_nonce": 403, - "erd_min_gas_limit": 50000, - "erd_min_gas_price": 1000000000, - "erd_denomination": 18, - ... - } - }, - "error": "", - "code": "successful" +--- + +### Send EGLD to an address + +Send a native EGLD transfer using sdk-core's `TransfersController` directly +against `DevnetEntrypoint`. No browser, no connected wallet, no sdk-dapp. This is +the pattern for when **your own code holds the private key**: a script, a bot, a +payout job, a backend service. + +If instead you are building a browser dApp where the end user's own wallet should +sign, you want the sdk-dapp flow. Start with +[Read the connected account](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account). +This recipe and that flow build the same kind of transaction two structurally +different ways, for two different trust models. + +## Prerequisites + +- Node.js >= 20.13.1. +- A devnet wallet with some devnet EGLD. If you do not have one, generate a + disposable wallet with only sdk-core: + +```ts +import { Mnemonic, Account } from '@multiversx/sdk-core'; + +const mnemonic = Mnemonic.generate(); +const account = Account.newFromMnemonic(mnemonic.toString()); +account.saveToPem('./wallet.pem'); +console.log('Address:', account.address.toBech32()); +``` + +Then fund the printed address at the [devnet faucet](https://r3d4.fr/faucet) or +the [devnet web wallet](https://devnet-wallet.multiversx.com). `wallet.pem` is +git-ignored, devnet-only, and throwaway. Never reuse a plain PEM like this for +mainnet funds. + +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/send-egld +npm install +npm run build +npm start -- ./wallet.pem erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th 0.01 +``` + +## Loading the account + +```ts title="src/account.ts" +// src/account.ts — load a devnet Account from a PEM file and prime its +// local nonce from the network. Framework-agnostic sdk-core, no sdk-dapp +// involved — this is the "your own code holds the keys" pattern (a script, +// a bot, a backend service), not a browser wallet-connected flow. +// +// The sdk-core nonce rule: fetch the account nonce from the network before +// sending transactions, then increment it locally with +// getNonceThenIncrement(). This helper does exactly the fetch half; the +// increment half matters once you send more than one transaction per run. + +import { Account } from '@multiversx/sdk-core'; +import type { DevnetEntrypoint } from '@multiversx/sdk-core'; + +/** + * Loads an Account from a PEM file and sets its local `nonce` from the + * network's current value — the "fetch" half of the fetch-then-increment + * pattern. + * + * `entrypoint.recallAccountNonce(address)` is a thin convenience wrapper + * around `networkProvider.getAccount(address).nonce`, used here instead of + * constructing a network provider by hand. + */ +export async function loadDevnetAccount( + entrypoint: DevnetEntrypoint, + pemPath: string, +): Promise { + const account = await Account.newFromPem(pemPath); + account.nonce = await entrypoint.recallAccountNonce(account.address); + return account; +} +``` + +## Converting the amount + +```ts title="src/amount.ts" +// src/amount.ts — decimal EGLD string <-> smallest-denomination bigint. +// +// EGLD's denomination is 10^18 (1 EGLD = 1000000000000000000). Getting this +// conversion wrong, or doing it with plain JS `Number` math, is a common +// source of bugs. Use BigNumber (already an sdk-core peer dependency) rather +// than floating point, and reject non-integer results outright instead of +// silently truncating them. + +import BigNumber from 'bignumber.js'; + +export const EGLD_DECIMALS = 18; + +/** + * Converts a decimal EGLD amount (e.g. "0.5") to the smallest-denomination + * bigint the network expects (e.g. 500000000000000000n). + * + * Throws if the input has more precision than 18 decimals — silently + * rounding would mean sending a different amount than what was typed. + */ +export function toSmallestDenomination(amountInEgld: string): bigint { + const value = new BigNumber(amountInEgld).multipliedBy( + new BigNumber(10).pow(EGLD_DECIMALS), + ); + if (!value.isFinite() || value.isNegative()) { + throw new Error(`"${amountInEgld}" is not a valid non-negative EGLD amount.`); + } + if (!value.isInteger()) { + throw new Error( + `"${amountInEgld}" EGLD has more precision than the network supports (${EGLD_DECIMALS} decimals).`, + ); + } + return BigInt(value.toFixed(0)); +} +``` + +## Sending + +```ts title="src/sendEgld.ts" +// src/sendEgld.ts — the actual subject of this recipe: sending EGLD via +// sdk-core's TransfersController, not a hand-built Transaction. +// +// Use this pattern when YOUR code holds the private key directly (a script, +// a bot, a backend payout job): sdk-core's purpose-built transfer +// factory/controller does the gas-limit and payload construction for you. +// When the END USER's own wallet should sign in a browser instead, that is +// the sdk-dapp flow, not this one. +// +// How the pieces fit, verified against the installed @multiversx/sdk-core: +// +// entrypoint.createTransfersController() +// -> TransfersController, a thin wrapper: it calls +// TransferTransactionsFactory.createTransactionForNativeTokenTransfer() +// to build the transaction (which computes a correct gasLimit +// itself — minGasLimit + gasLimitPerByte * data.length, per +// TransactionsFactoryConfig — you never need to hardcode 50000 +// yourself), then BaseController.setupAndSignTransaction() sets the +// nonce and SIGNS it. +// controller.createTransactionForNativeTokenTransfer(sender, nonce, options) +// -> returns an ALREADY-SIGNED Transaction, ready for +// entrypoint.sendTransaction(tx). This is the Controller pattern; +// contrast with the Factory pattern, which returns an UNSIGNED +// transaction and leaves nonce/signing to the caller. +// +// One subtlety worth being explicit about: `Account.signTransaction(tx)` +// returns the raw signature bytes (Promise); it does NOT mutate +// `tx` in place. TransfersController assigns `tx.signature` internally, so it +// is a non-issue here — but if you ever build and sign a transfer by hand +// (for example with the Factory pattern), you must assign +// `tx.signature = await account.signTransaction(tx)` yourself. + +import { Address } from '@multiversx/sdk-core'; +import type { Account, DevnetEntrypoint } from '@multiversx/sdk-core'; + +export interface SendEgldInput { + /** Bech32 address of the recipient. */ + receiver: string; + /** Amount in the smallest denomination (10^18 = 1 EGLD) — see src/amount.ts. */ + amountInSmallestDenomination: bigint; +} + +export interface SendEgldOutput { + txHash: string; +} + +/** + * Builds, signs (via TransfersController), and sends a native EGLD + * transfer. `sender.nonce` must already reflect the network's current + * nonce — see src/account.ts's loadDevnetAccount. + */ +export async function sendEgld( + entrypoint: DevnetEntrypoint, + sender: Account, + input: SendEgldInput, +): Promise { + const controller = entrypoint.createTransfersController(); + + const transaction = await controller.createTransactionForNativeTokenTransfer( + sender, + sender.getNonceThenIncrement(), + { + receiver: Address.newFromBech32(input.receiver), + nativeAmount: input.amountInSmallestDenomination, + }, + ); + + const txHash = await entrypoint.sendTransaction(transaction); + return { txHash }; } ``` - - +## Wiring it together -:::important -This endpoint is not available on the Proxy. Only Nodes (Observers) expose this endpoint. +```ts title="src/index.ts" +// src/index.ts — CLI entry point. Wires together account.ts, amount.ts, +// and sendEgld.ts into a single runnable command. +// +// Usage: +// npm run build && npm start -- [amountInEgld] +// +// Example: +// npm start -- ./wallet.pem erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th 0.01 + +import { DevnetEntrypoint } from '@multiversx/sdk-core'; +import { loadDevnetAccount } from './account'; +import { toSmallestDenomination } from './amount'; +import { sendEgld } from './sendEgld'; + +async function main(): Promise { + const [pemPath, receiver, amountArg] = process.argv.slice(2); + + if (!pemPath || !receiver) { + console.error('Usage: npm start -- [amountInEgld]'); + process.exitCode = 1; + return; + } + + const amountInEgld = amountArg ?? '0.001'; + + // DevnetEntrypoint() with no options defaults to + // https://devnet-api.multiversx.com, chain ID "D". + const entrypoint = new DevnetEntrypoint(); + + const sender = await loadDevnetAccount(entrypoint, pemPath); + console.log(`Sender: ${sender.address.toBech32()} (nonce ${sender.nonce})`); + console.log(`Receiver: ${receiver}`); + console.log(`Amount: ${amountInEgld} EGLD`); + + const { txHash } = await sendEgld(entrypoint, sender, { + receiver, + amountInSmallestDenomination: toSmallestDenomination(amountInEgld), + }); + + console.log(`\nSent. Transaction hash: ${txHash}`); + console.log(`Explorer: https://devnet-explorer.multiversx.com/transactions/${txHash}`); + + // TransactionStatus wraps the raw string status plus boolean helpers. + const outcome = await entrypoint.awaitCompletedTransaction(txHash); + console.log(`Status: ${outcome.status.status} (successful: ${outcome.status.isSuccessful()})`); +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` + +## Run it + +```bash +npm start -- ./wallet.pem 0.01 +``` + +Expected output: + +```text +Sender: erd1... (nonce 42) +Receiver: erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th +Amount: 0.01 EGLD + +Sent. Transaction hash: <64-char hex hash> +Explorer: https://devnet-explorer.multiversx.com/transactions/ +Status: success (successful: true) +``` + +## How it works + +**`TransfersController` computes gas for you.** Unlike hand-building a raw +`Transaction`, where you duplicate the network's own gas formula by hand, +`entrypoint.createTransfersController().createTransactionForNativeTokenTransfer(...)` +sets `gasLimit` to `minGasLimit + gasLimitPerByte * data.length` (50,000 plus +1,500 per byte) automatically, via `TransferTransactionsFactory` internally. That +was confirmed against the installed sdk-core source and against a real devnet +request logged while authoring this recipe (`"gasLimit":50000` for a plain, +data-less transfer). + +**The Controller pattern returns an already-signed transaction.** `sender` (an +`Account`) is passed straight into +`createTransactionForNativeTokenTransfer(sender, nonce, options)`. Internally, +`BaseController.setupAndSignTransaction()` sets the nonce and does +`transaction.signature = await sender.signTransaction(transaction)` for you. That +is what makes the transaction returned by the controller ready to send as-is, +unlike the Factory pattern below. + +## Pitfalls + +:::danger[Pitfall 1: signTransaction returns bytes, it does not mutate the tx] +`Account.signTransaction(transaction)` only computes and *returns* the raw +signature bytes (`Promise`); it does not touch +`transaction.signature` itself. The Controller pattern used in this recipe +assigns it internally, so this does not bite here. But if you ever sign a +transaction by hand, you need `tx.signature = await account.signTransaction(tx);` +explicitly. ::: +:::warning[Pitfall 2: the Factory pattern returns an unsigned transaction] +If you use +`TransferTransactionsFactory.createTransactionForNativeTokenTransfer(senderAddress, options)` +directly instead of `TransfersController`, you get back a transaction with +`nonce: 0n` and no signature. You must set the nonce and sign it yourself before +sending. +::: -## GET **Get P2P Status** {#get-p2p-status} +:::warning[Pitfall 3: the nonce trap applies here too] +This recipe's `loadDevnetAccount()` fetches the nonce once per process run, which +is correct for a single send. Running two instances of this CLI back-to-back +without waiting for the first to confirm produces a stale, duplicate nonce for +the second. Use the fetch-once, increment-locally pattern across multiple sends. +::: -`http://localhost:8080/node/p2pstatus` +:::note[Pitfall 4: amounts beyond 18 decimals throw, on purpose] +`toSmallestDenomination()` rejects (rather than silently rounds) an input with +more precision than EGLD's 18 decimals can represent. Silently truncating would +mean sending a different amount than what was typed. +::: -This endpoint allows one to query the P2P status of the Node. +## See also - - +- [Configure a network provider](/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider) + builds the read-side provider the entrypoint here wraps. +- [Read the connected account with useGetAccount](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + is the browser, connected-wallet counterpart to this backend flow. +- [The versioned sdk-js cookbook](/sdk-and-tools/sdk-js/sdk-js-cookbook) covers + ESDT transfers and the wider transaction API. -🟢 200: OK +--- -P2P status retrieved successfully. +### Sender -```json -{ - "data": { - "metrics": { - "erd_p2p_cross_shard_observers": "...", - "erd_p2p_cross_shard_validators": "...", - "erd_p2p_intra_shard_observers": "...", - "erd_p2p_intra_shard_validators": "...", - "erd_p2p_num_connected_peers_classification": "...", - "erd_p2p_num_receiver_peers_fast_reacting": 2, - "erd_p2p_num_receiver_peers_out_of_specs": 2, - "erd_p2p_num_receiver_peers_slow_reacting": 13, - "erd_p2p_peak_num_receiver_peers_fast_reacting": 8, - "erd_p2p_peak_num_receiver_peers_out_of_specs": 8, - "erd_p2p_peak_num_receiver_peers_slow_reacting": 13, - "erd_p2p_peak_peer_num_processed_messages_fast_reacting": 3, - "erd_p2p_peak_peer_num_processed_messages_out_of_specs": 3, - "erd_p2p_peak_peer_num_processed_messages_slow_reacting": 8, - "erd_p2p_peak_peer_num_received_messages_fast_reacting": 3, - "erd_p2p_peak_peer_num_received_messages_out_of_specs": 3, - "erd_p2p_peak_peer_num_received_messages_slow_reacting": 8, - "erd_p2p_peak_peer_size_processed_messages_fast_reacting": 1291, - "erd_p2p_peak_peer_size_processed_messages_out_of_specs": 1291, - "erd_p2p_peak_peer_size_processed_messages_slow_reacting": 4363, - "erd_p2p_peak_peer_size_received_messages_fast_reacting": 1291, - "erd_p2p_peak_peer_size_received_messages_out_of_specs": 1291, - "erd_p2p_peak_peer_size_received_messages_slow_reacting": 4363, - "erd_p2p_peer_info": "...", - "erd_p2p_peer_num_processed_messages_fast_reacting": 1, - "erd_p2p_peer_num_processed_messages_out_of_specs": 1, - "erd_p2p_peer_num_processed_messages_slow_reacting": 5, - "erd_p2p_peer_num_received_messages_fast_reacting": 1, - "erd_p2p_peer_num_received_messages_out_of_specs": 1, - "erd_p2p_peer_num_received_messages_slow_reacting": 5, - "erd_p2p_peer_size_processed_messages_fast_reacting": 289, - "erd_p2p_peer_size_processed_messages_out_of_specs": 289, - "erd_p2p_peer_size_processed_messages_slow_reacting": 2711, - "erd_p2p_peer_size_received_messages_fast_reacting": 289, - "erd_p2p_peer_size_received_messages_out_of_specs": 289, - "erd_p2p_peer_size_received_messages_slow_reacting": 2711, - "erd_p2p_unknown_shard_peers": "..." - } - }, - "error": "", - "code": "successful" +## Overview + +Within the context of a transaction that comprises seven distinct generics, `From` represents the **second** generic field — **the entity that initiates the transaction**. It is required in three environments: the integration test, the parametric test, and the interactor. + +A transaction originating from a contract environment cannot have a sender in the contract environment. The reason is that the current contract is always the same: the contract that starts the transaction. + + + +## Diagram + +The sender is being set using the `.from(...)` method. Several types can be specified: + +```mermaid +graph LR + subgraph From + from-unit["()"] + from-unit -->|from| from-man-address[ManagedAddress] + from-unit -->|from| from-address[Address] + from-unit -->|from| from-bech32[Bech32Address] + end +``` + + +## No sender + +Transactions initiated in the **contract environment** have no sender specified. There is no need for sender identification because it always refers to the executing contract itself. + +```rust title=contract.rs +#[payable("EGLD")] +#[endpoint] +fn transfer_egld(&self, to: ManagedAddress, amount: BigUint) { + self.tx().to(&to).egld(&amount).transfer(); +} +``` + + +## Explicit sender + +For transactions launched in any other environment than the contract one, it is required to specify the sender. Meaning that, in most cases, no tx can be created without defining the sender. + +In the following section, we will outline the various types of entities that can be designated as the sender in a transaction. + +### ManagedAddress + +The function below is a snippet from an interactor whose purpose is to deploy a contract. The `.from` call, which sets the sender, is the main focus of this example. The sender is a hardcoded ManagedAddress, which illustrates a wallet. + +```rust title=interact.rs +async fn deploy(&mut self) { + let wallet: ManagedAddress = ManagedAddress::new_from_bytes(&[7u8; 32]); + + self.interactor + .tx() + .from(wallet) + .typed(proxy::Proxy) + .init(0u32) + .code(&self.code) + .gas(NumExpr("70,000,000")) + .returns(ReturnsNewBech32Address) +} +``` + +### Address + +The example beneath is a fragment for a blackbox test that runs the add function from a proxy. The sender is received as a parameter in this function. +```rust title=blackbox_test.rs +fn add_one(&mut self, from: &AddressValue) { + self.world + .tx() + .from(from) + .to(self.receiver) + .typed(proxy::Proxy) + .add(1u32) + .run(); +} +``` + +For parametric testing, there is particular address type name: +- **TestAddress** + - encodes a dummy address, equivalent to `"address:{}"`; for the example below it is equivalent to `"address:owner"`; + - contains two functions: + - **`.eval_to_array()`** parse the address into an array of u8 + - **`.eval_to_expr()`** return the address as a String object +```rust title=blackbox_test.rs +const OWNER_ADDRESS: TestAddress = TestAddress::new("owner"); + +fn add_one(&mut self) { + self.world + .tx() + .from(OWNER_ADDRESS) + .to(self.receiver) + .typed(proxy::Proxy) + .add(1u32) + .run(); +} +``` +### Bech32Address +In order to avoid repeated conversions, it keeps the **bech32** representation **inside**. It wraps the address and presents it as a bech32 expression. +```rust title=interact.rs +fn add_one(&mut self, wallet_address: Bech32Address) { + self.world + .tx() + .from(wallet_address) + .to(self.receiver) + .typed(proxy::Proxy) + .add(1u32) + .run(); } ``` - - +## Automatic wallet selection in interactors -:::important -This endpoint is not available on the Proxy. Only Nodes (Observers) expose this endpoint. -::: +Wallets are registered beforehand when it comes to interactor operations. An automated signature is applied to the transaction. More details about interactors here. +--- -## GET **Get Peer Information** {#get-peer-information} +### Set a guardian on an account -`http://localhost:8080/node/peerinfo` +A guardian is a second key that must co-sign an account's transactions once the +account is *guarded*. Turning that protection on is a two-step sequence: -This endpoint allows one to query specific information about its peers. +1. **`SetGuardian`** nominates the guardian (this recipe). +2. **`GuardAccount`** activates guardianship (see + [Guard and unguard an account](/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account)). - - +This recipe builds the `SetGuardian` transaction with sdk-core's +`AccountController` (and the matching `AccountTransactionsFactory`), decodes the +payload, and broadcasts it. -🟢 200: OK +## Prerequisites -Peer info retrieved successfully. +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites. **No devnet EGLD required**, see "How it works". -```json -{ - "data": { - "info": [ - { - "isblacklisted": false, - "pid": "...", - "pk": "", - "peertype": "observer", - "addresses": [ - "/ip4/172.17.0.1/tcp/21100", - "/ip4/127.0.0.1/tcp/21100", - "/ip4/192.168.2.104/tcp/21100", - "/ip4/172.17.0.1/tcp/21100" - ] - }, - { - "isblacklisted": false, - "pid": "...", - "pk": "", - "peertype": "unknown", - "addresses": [ - "/ip4/127.0.0.1/tcp/9999", - "/ip4/127.0.0.1/tcp/9999", - "/ip4/192.168.2.104/tcp/9999", - "/ip4/172.17.0.1/tcp/9999" - ] - }, - { - "isblacklisted": false, - "pid": "...", - "pk": "...", - "peertype": "validator", - "addresses": [ - "/ip4/192.168.2.104/tcp/21504", - "/ip4/192.168.2.104/tcp/21504", - "/ip4/172.17.0.1/tcp/21504", - "/ip4/127.0.0.1/tcp/21504" - ] - }, - { - "isblacklisted": false, - "pid": "...", - "pk": "...", - "peertype": "validator", - "addresses": [ - "/ip4/172.17.0.1/tcp/21503", - "/ip4/172.17.0.1/tcp/21503", - "/ip4/127.0.0.1/tcp/21503", - "/ip4/192.168.2.104/tcp/21503" - ] - }, - { - "isblacklisted": false, - "pid": "...", - "pk": "...", - "peertype": "validator", - "addresses": [ - "/ip4/172.17.0.1/tcp/21502", - "/ip4/127.0.0.1/tcp/21502", - "/ip4/192.168.2.104/tcp/21502", - "/ip4/172.17.0.1/tcp/21502" - ] - }, - { - "isblacklisted": false, - "pid": "...", - "pk": "...", - "peertype": "validator", - "addresses": [ - "/ip4/127.0.0.1/tcp/21507", - "/ip4/127.0.0.1/tcp/21507", - "/ip4/192.168.2.104/tcp/21507", - "/ip4/172.17.0.1/tcp/21507" - ] - }, - { - "isblacklisted": false, - "pid": "...", - "pk": "...", - "peertype": "validator", - "addresses": [ - "/ip4/172.17.0.1/tcp/21501", - "/ip4/192.168.2.104/tcp/21501", - "/ip4/172.17.0.1/tcp/21501", - "/ip4/127.0.0.1/tcp/21501" - ] - }, - { - "isblacklisted": false, - "pid": "...", - "pk": "...", - "peertype": "validator", - "addresses": [ - "/ip4/172.17.0.1/tcp/21508", - "/ip4/127.0.0.1/tcp/21508", - "/ip4/192.168.2.104/tcp/21508", - "/ip4/172.17.0.1/tcp/21508" - ] - }, - { - "isblacklisted": false, - "pid": "...", - "pk": "...", - "peertype": "validator", - "addresses": [ - "/ip4/172.17.0.1/tcp/21506", - "/ip4/127.0.0.1/tcp/21506", - "/ip4/192.168.2.104/tcp/21506", - "/ip4/172.17.0.1/tcp/21506" - ] - }, - { - "isblacklisted": false, - "pid": "...", - "pk": "", - "peertype": "observer", - "addresses": [ - "/ip4/127.0.0.1/tcp/38188", - "/ip4/192.168.2.104/tcp/38188", - "/ip4/172.17.0.1/tcp/38188" - ] - }, - { - "isblacklisted": false, - "pid": "...", - "pk": "...", - "peertype": "validator", - "addresses": [ - "/ip4/172.17.0.1/tcp/21505", - "/ip4/127.0.0.1/tcp/21505", - "/ip4/192.168.2.104/tcp/21505", - "/ip4/172.17.0.1/tcp/21505" - ] - }, - { - "isblacklisted": false, - "pid": "...", - "pk": "", - "peertype": "observer", - "addresses": [ - "/ip4/172.17.0.1/tcp/21102", - "/ip4/127.0.0.1/tcp/21102", - "/ip4/192.168.2.104/tcp/21102", - "/ip4/172.17.0.1/tcp/21102" - ] - }, - { - "isblacklisted": false, - "pid": "...", - "pk": "...", - "peertype": "validator", - "addresses": [ - "/ip4/172.17.0.1/tcp/21500", - "/ip4/127.0.0.1/tcp/21500", - "/ip4/192.168.2.104/tcp/21500", - "/ip4/172.17.0.1/tcp/21500" - ] - }, - { - "isblacklisted": false, - "pid": "...", - "pk": "", - "peertype": "observer", - "addresses": [ - "/ip4/192.168.2.104/tcp/21101", - "/ip4/192.168.2.104/tcp/21101", - "/ip4/172.17.0.1/tcp/21101", - "/ip4/127.0.0.1/tcp/21101" - ] - } - ] - }, - "error": "", - "code": "successful" +## Install + +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/set-guardian +npm install +npm run build +npm start -- ./wallet.pem erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th MyGuardianService +``` + +## The code + +```ts title="src/guardian.ts" +// src/guardian.ts — the subject of this recipe: nominating a guardian for an +// account with the `SetGuardian` builtin, via sdk-core's AccountController +// and AccountTransactionsFactory. +// +// A guardian is a second key that must co-sign your transactions once the +// account is guarded. Setting one is a two-transaction dance: +// 1. SetGuardian — nominate the guardian (this recipe). The nomination +// only becomes active after a cooldown (unless the account is already +// guarded, in which case the current guardian co-signs and it is +// immediate). +// 2. GuardAccount — activate guardianship (see guard-unguard-account). +// +// SetGuardian is a builtin function executed in the caller's own account +// context, so the transaction's receiver is the sender itself. +// +// SetGuardianInput = { guardianAddress: Address; serviceID: string }. The +// serviceID identifies the guardian SERVICE (e.g. a Trusted Co-Signer +// Service). It is written to the wire verbatim as hex; the exact value +// depends on the service you use, so this recipe takes it as a parameter. + +import { Address } from '@multiversx/sdk-core'; +import type { Account, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +export interface SetGuardianInput { + guardianAddress: Address; + serviceID: string; +} + +/** + * Build a SetGuardian transaction. + * + * Controller path: `createTransactionForSettingGuardian` sets the nonce and + * signs (it takes the whole Account). Factory path: it only builds; the + * caller owns nonce + signature. Same method name on both. + */ +export async function setGuardian( + entrypoint: DevnetEntrypoint, + sender: Account, + input: SetGuardianInput, + useFactory: boolean, +): Promise { + if (useFactory) { + const factory = entrypoint.createAccountTransactionsFactory(); + const transaction = await factory.createTransactionForSettingGuardian(sender.address, { + guardianAddress: input.guardianAddress, + serviceID: input.serviceID, + }); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; + } + + const controller = entrypoint.createAccountController(); + return controller.createTransactionForSettingGuardian(sender, sender.getNonceThenIncrement(), { + guardianAddress: input.guardianAddress, + serviceID: input.serviceID, + }); +} + +/** + * Decode a SetGuardian transaction's `data` field: + * `SetGuardian@@`. The guardian argument is + * a raw 32-byte public key (hex), NOT bech32. + */ +export function describeSetGuardianPayload(transaction: Transaction): { + function: string; + receiver: string; + guardian: string; + serviceID: string; + gasLimit: string; +} { + const parts = Buffer.from(transaction.data).toString().split('@'); + const guardianHex = parts[1] ?? ''; + const serviceHex = parts[2] ?? ''; + return { + function: parts[0] ?? '', + receiver: transaction.receiver.toBech32(), + guardian: guardianHex ? Address.newFromHex(guardianHex).toBech32() : '(none)', + serviceID: Buffer.from(serviceHex, 'hex').toString(), + gasLimit: transaction.gasLimit.toString(), + }; } ``` - - +## Run it -:::important -This endpoint is not available on the Proxy. Only Nodes (Observers) expose this endpoint. +```bash +npm start -- [serviceID] [--factory] +``` + +A real captured run (unfunded wallet): + +```text +SetGuardian payload: + function: SetGuardian + receiver: erd18l2cww3g3g6uc35wy60efd87ss85xdqj9gh9qn6ga6halz3jscaqd4dl3k + guardian: erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th + serviceID: MyGuardianService + gasLimit: 466500 + +Broadcasting... +Rejected (expected for an unfunded wallet): ... insufficient funds for address erd18l2c... +``` + +## How it works + +`SetGuardian` is a builtin function on the caller's own account, so the +transaction's **receiver is the sender itself**. Its data is +`SetGuardian@@`. The guardian argument is a +raw 32-byte public key in hex, *not* bech32, this recipe's decoder converts it +back to a readable address. The `serviceID` identifies the guardian **service** +(for example a Trusted Co-Signer Service) and is written verbatim as hex; its +exact value depends on the service you use, so the recipe takes it as a +parameter. + +Gas was `466500`, matching `50,000 + 1,500 × 111 (data bytes) + 250,000 +(gasLimitSetGuardian)` to the unit. The unfunded broadcast is rejected with a +clean `insufficient funds` keyed to the sender, the payload is well-formed. + +:::note[Activation is delayed by design] +A newly set guardian is not usable immediately: on an unguarded account the +nomination becomes active only after a protocol cooldown. If the account is +*already* guarded, the current guardian co-signs the `SetGuardian` and the change +is immediate. Either way, `SetGuardian` only nominates, +[`GuardAccount`](/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account) +is what activates protection. ::: ---- +For built-in function detail, see +[docs.multiversx.com/developers/built-in-functions](https://docs.multiversx.com/developers/built-in-functions/). -### REST API overview +## Pitfalls -## Introduction +:::warning[Pitfall 1: the guardian argument is a public key, not bech32] +On the wire `SetGuardian` carries the guardian's raw 32-byte public key in hex. +The SDK converts your `Address` for you, but if you hand-decode the payload, read +it back with `Address.newFromHex`, not `newFromBech32`. +::: -MultiversX has 2 layers of REST APIs that can be publicly accessed. Both of them can be recreated by anyone that -wants to have the same infrastructure, but self-hosted. +:::note[Pitfall 2: serviceID is service-specific, not a fixed constant] +This recipe uses an illustrative `serviceID`. The real value is defined by the +guardian service you register with; passing the wrong one nominates a guardian +the service cannot co-sign for. +::: -These 2 layers of REST APIs are: +:::note[Pitfall 3: setting a guardian does not guard the account] +`SetGuardian` only nominates. Until you send +[`GuardAccount`](/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account), +transactions still sign with the primary key alone. +::: -- `https://gateway.multiversx.com`: the lower level layer (backed by `MultiversX Proxy`) that handles routing all the requests in accordance to - the sharding mechanism. More details can be found [here](/sdk-and-tools/rest-api/gateway-overview). +## See also -- `https://api.multiversx.com`: the higher level layer (backed by `api.multiversx.com` repository) that uses the gateway level underneath, - but also integrates Elasticsearch (historical) queries, battle-tested caching mechanisms, friendly fields formatting and so on. More details - can be found [here](/sdk-and-tools/rest-api/multiversx-api). +- [Guard and unguard an account](/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account) + activates the guardian you nominated here, and removes it. +- [Apply a guardian to a transaction](/sdk-and-tools/sdk-js/cookbook/accounts/apply-guardian-to-transaction) + co-signs an individual transaction once guardianship is active. +- [Build a relayed v3 transaction](/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction) + is the other multi-signature transaction shape (a relayer paying gas, rather + than a guardian co-signing). --- -### Rest API Transactions +### Set and unset special roles on a fungible ESDT -```mdx-code-block -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; -``` +A freshly issued fungible ESDT can carry three special roles, each granted to a +specific address via the ESDT system contract: +- **`ESDTRoleLocalMint`**, the holder may mint more supply (see + [Local mint and burn](/sdk-and-tools/sdk-js/cookbook/tokens/local-mint-burn-supply)). +- **`ESDTRoleLocalBurn`**, the holder may burn its own supply. +- **`ESDTTransferRole`**, transfers of the token must route through the holder. -This component of the REST API allows one to send (broadcast) Transactions to the Blockchain and query information about them. +This recipe sets and unsets those roles with sdk-core's +`TokenManagementController` (and the matching `TokenManagementTransactionsFactory`). +The method name is identical on both API levels, the only difference is the usual +one: the controller sets the nonce and signs, the factory only builds. +If instead you are building a browser dApp where the token owner's own wallet +should sign, wire the factory method through a connected-wallet flow instead of a +PEM-holding script. -## POST Send Transaction {#send-transaction} +## Prerequisites -`https://gateway.multiversx.com/transaction/send` +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites for a throwaway one. **No devnet EGLD required** to verify the + payload, see "How it works". -This endpoint allows one to send a signed Transaction to the Blockchain. +## Install - - +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/set-special-roles +npm install +npm run build +npm start -- ./wallet.pem COOK-123456 +``` + +## The code + +```ts title="src/roles.ts" +// src/roles.ts — the subject of this recipe: setting and unsetting the +// three special roles a fungible ESDT can carry, both ways (controller and +// factory). +// +// - ESDTRoleLocalMint — the holder may mint more supply locally +// - ESDTRoleLocalBurn — the holder may burn its own supply locally +// - ESDTTransferRole — transfers of the token must go through the holder +// +// Both `createTransactionForSettingSpecialRoleOnFungibleToken` and its +// unset sibling live on BOTH the controller and the factory under the SAME +// name (unlike Nft/NFT and Loca/Local elsewhere in this class — see the +// local-mint-burn-supply and token-lifecycle-operations recipes). The only +// difference is the usual one: the controller sets the nonce and signs; the +// factory only builds. +// +// A CONFIRMED sdk-core v15.4.1 BUG lives in the unset builder — see +// unsetFungibleRoles below and the recipe page's Pitfalls. It is verified +// here by decoding the real wire payload, not asserted from reading types. + +import { Address } from '@multiversx/sdk-core'; +import type { Account, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +/** Which of the three fungible roles to add. */ +export interface SetRolesInput { + tokenIdentifier: string; + /** The address to grant the roles to (often the contract or an operator). */ + user: Address; + localMint: boolean; + localBurn: boolean; + esdtTransfer: boolean; +} + +/** Which of the three fungible roles to remove. */ +export interface UnsetRolesInput { + tokenIdentifier: string; + user: Address; + localMint: boolean; + localBurn: boolean; + esdtTransfer: boolean; +} + +/** + * Build a "set special role" transaction. + * + * Controller path: `createTransactionForSettingSpecialRoleOnFungibleToken` + * sets the nonce and signs (it takes the whole Account). Factory path: it + * only builds — the caller owns nonce + signature. Same method name on both. + */ +export async function setFungibleRoles( + entrypoint: DevnetEntrypoint, + sender: Account, + input: SetRolesInput, + useFactory: boolean, +): Promise { + const options = { + user: input.user, + tokenIdentifier: input.tokenIdentifier, + addRoleLocalMint: input.localMint, + addRoleLocalBurn: input.localBurn, + addRoleESDTTransferRole: input.esdtTransfer, + }; -Body Parameters + if (useFactory) { + const factory = entrypoint.createTokenManagementTransactionsFactory(); + const transaction = await factory.createTransactionForSettingSpecialRoleOnFungibleToken( + sender.address, + options, + ); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; + } -| Param | Required | Type | Description | -| -| ---------------- | ----------------------------------------- | -------- | -------------------------------------------------------------------------------- | -| nonce | REQUIRED | `number` | The Nonce of the Sender. | -| value | REQUIRED | `string` | The Value to transfer, as a string representation of a Big Integer (can be "0"). | -| receiver | REQUIRED | `string` | The Address (bech32) of the Receiver. | -| sender | REQUIRED | `string` | The Address (bech32) of the Sender. | -| guardian | OPTIONAL | `string` | The Address (bech32) of the Guardian. | -| senderUsername | OPTIONAL | `string` | The base64 string representation of the Sender's username. | -| receiverUsername | OPTIONAL | `string` | The base64 string representation of the Receiver's username. | -| gasPrice | REQUIRED | `number` | The desired Gas Price (per Gas Unit). | -| gasLimit | REQUIRED | `number` | The maximum amount of Gas Units to consume. | -| data | OPTIONAL | `string` | The base64 string representation of the Transaction's message (data). | -| signature | REQUIRED | `string` | The Signature (hex-encoded) of the Transaction. | -| guardianSignature| OPTIONAL | `string` | The Guardian's Signature (hex-encoded) of the Transaction. | -| chainID | REQUIRED | `string` | The Chain identifier. | -| version | REQUIRED | `number` | The Version of the Transaction (e.g. 1). | -| options | OPTIONAL | `number` | The Options of the Transaction (e.g. 1). | + const controller = entrypoint.createTokenManagementController(); + return controller.createTransactionForSettingSpecialRoleOnFungibleToken( + sender, + sender.getNonceThenIncrement(), + options, + ); +} - - +/** + * Build an "unset special role" transaction. + * + * WARNING — sdk-core v15.4.1 bug (confirmed in the installed build and in + * the v15.4.1 source tag): the unset builder reads the wrong flags. + * `removeRoleLocalBurn` is NEVER consulted, and `removeRoleESDTTransferRole` + * gates BOTH the `ESDTRoleLocalBurn` and the `ESDTTransferRole` wire parts. + * See describeRolesPayload's output in the recipe page — this function + * passes the input through faithfully; the SDK mis-wires it downstream. + */ +export async function unsetFungibleRoles( + entrypoint: DevnetEntrypoint, + sender: Account, + input: UnsetRolesInput, + useFactory: boolean, +): Promise { + const options = { + user: input.user, + tokenIdentifier: input.tokenIdentifier, + removeRoleLocalMint: input.localMint, + removeRoleLocalBurn: input.localBurn, + removeRoleESDTTransferRole: input.esdtTransfer, + }; -🟢 200: OK + if (useFactory) { + const factory = entrypoint.createTokenManagementTransactionsFactory(); + const transaction = await factory.createTransactionForUnsettingSpecialRoleOnFungibleToken( + sender.address, + options, + ); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; + } -Transaction sent with success. A Transaction Hash is returned. + const controller = entrypoint.createTokenManagementController(); + return controller.createTransactionForUnsettingSpecialRoleOnFungibleToken( + sender, + sender.getNonceThenIncrement(), + options, + ); +} -```json -{ - "data": { - "txHash": "6c41c71946b5b428c2cfb560e3ea425f8a00345de4bb2eb1b784387790914277" - }, - "error": "", - "code": "successful" +/** A fully decoded role transaction, straight from its `data` field. */ +export interface DecodedRolesPayload { + function: string; + receiver: string; + tokenIdentifier: string; + user: string; + roles: string[]; +} + +/** + * Decode a set/unset-role transaction's `data` field into its parts: + * `@@@@...`. Every part + * after the function name is hex; the first two are an ASCII token + * identifier and a 32-byte address, the rest are ASCII role names. + */ +export function describeRolesPayload(transaction: Transaction): DecodedRolesPayload { + const parts = Buffer.from(transaction.data).toString().split('@'); + const tokenHex = parts[1] ?? ''; + const userHex = parts[2] ?? ''; + const roleHexes = parts.slice(3); + + return { + function: parts[0] ?? '', + receiver: transaction.receiver.toBech32(), + tokenIdentifier: Buffer.from(tokenHex, 'hex').toString(), + user: userHex ? Address.newFromHex(userHex).toBech32() : '(none)', + roles: roleHexes.map((hex) => Buffer.from(hex, 'hex').toString()), + }; } ``` -🔴 400: Bad request - -Invalid Transaction signature. +## Run it -```json -{ - "data": null, - "error": "transaction generation failed: ed25519: invalid signature", - "code": "bad_request" -} +```bash +npm start -- [--user ] [--factory] ``` - - +A real captured run (unfunded wallet): -:::caution -For Nodes (Observers or Validators with the HTTP API enabled), this endpoint **only accepts transactions whose sender is in the Node's Shard**. +```text +SET (localMint + localBurn + esdtTransfer): + function: setSpecialRole + receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + tokenIdentifier: COOK-123456 + roles on wire: [ESDTRoleLocalMint, ESDTRoleLocalBurn, ESDTTransferRole] + +UNSET requested { localBurn: true } ONLY (expect ESDTRoleLocalBurn): + function: unSetSpecialRole + roles on wire: [] + +UNSET requested { esdtTransfer: true } ONLY (expect ESDTTransferRole): + function: unSetSpecialRole + roles on wire: [ESDTRoleLocalBurn, ESDTTransferRole] + +Broadcasting the SET transaction... +Rejected (expected for an unfunded wallet): ... insufficient funds for address erd18l2cww3g3g6uc35wy60efd87ss85xdqj9gh9qn6ga6halz3jscaqd4dl3k +``` + +## How it works + +**The set path is correct and hand-verified.** The SET transaction is addressed to +the ESDT system contract (`erd1qqqq…zllls8a5w6u`, the documented ESDT system +contract address), with data `setSpecialRole@@@…`. +The role names travel as ASCII on the wire +(`45534454526f6c654c6f63616c4d696e74` = `ESDTRoleLocalMint`). Gas for a three-role +set on `COOK-123456` was `60357500`, matching +`50,000 + 1,500 x 205 (data bytes) + 60,000,000` to the unit. The unfunded +broadcast is rejected with a clean `insufficient funds` keyed to the exact sender, +the payload is well-formed; only the balance is missing. + +**The unset path has a confirmed sdk-core v15.4.1 bug.** The two UNSET lines above +are the proof, decoded from real transactions this recipe built: + +- Requesting **`localBurn` only** produces an **empty** role list, the local-burn + role is silently *not* removed. +- Requesting **`esdtTransfer` only** removes **both** `ESDTRoleLocalBurn` *and* + `ESDTTransferRole`. + +The cause is in `createTransactionForUnsettingSpecialRoleOnFungibleToken`: the +`ESDTRoleLocalBurn` wire part is gated on `removeRoleESDTTransferRole` instead of +`removeRoleLocalBurn`, and `removeRoleLocalBurn` is never read. Confirmed in both +the installed build and the tagged v15.4.1 source. (The **set** builder is fine, +this is specific to unsetting fungible roles.) + +## Pitfalls + +:::danger[Pitfall 1: unsetting a fungible role is broken in v15.4.1] +`removeRoleLocalBurn` is ignored, and `removeRoleESDTTransferRole` removes the +local-burn role too. Until it is fixed upstream, verify the decoded +`unSetSpecialRole` payload before broadcasting, or build the +`unSetSpecialRole@@@ESDTRoleLocalBurn` data by hand for a burn-only +removal. ::: -Here's an example of a request: +:::note[Pitfall 2: roles are granted per address, not globally] +Each set/unset targets one `user` address. Granting `ESDTRoleLocalMint` to a +contract does not grant it to you, pass the right `--user`. +::: + +:::warning[Pitfall 3: the token must exist and canAddSpecialRoles must be true] +Roles are set through the ESDT system contract against an already-issued token +whose `canAddSpecialRoles` flag was set at issuance. See +[Issue a fungible ESDT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token). +::: + +## See also + +- [Issue a fungible ESDT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token) + issues the token you then grant roles on (set `canAddSpecialRoles`). +- [Local mint and burn](/sdk-and-tools/sdk-js/cookbook/tokens/local-mint-burn-supply) + uses the `ESDTRoleLocalMint` / `ESDTRoleLocalBurn` roles this recipe grants. +- [Token lifecycle operations](/sdk-and-tools/sdk-js/cookbook/tokens/token-lifecycle-operations) + covers freeze, pause, and wipe, the manager-side counterpart to roles. + +--- + +### Set up a Localnet (mxpy) + +This guide describes how to set up a local mini-testnet - also known as **localnet** - using **mxpy**. The purpose of a localnet is to allow developers experiment with and test their Smart Contracts, in addition to writing unit and integration tests. + +The localnet contains: + +- **Validator Nodes** (two, by default) +- **Observer Nodes** (zero, by default) +- A **Seednode** +- A **MultiversX Proxy** + +If not specified otherwise, the localnet starts with two shards plus the metachain (each with one validator). + + +## Prerequisites: mxpy + +In order to install **mxpy**, follow [these instructions](/sdk-and-tools/mxpy/installing-mxpy). + +:::note +This guide assumes you are using `mxpy v9.7.1` or newer. +::: + + +## The easy way to start a localnet + +You can simply setup and start a localnet in a workspace (folder) of your choice by following the steps below. + +Create a new folder (workspace) and navigate to it: ```bash -POST https://gateway.multiversx.com/transaction/send HTTP/1.1 -Content-Type: application/json +mkdir -p ~/my-first-localnet && cd ~/my-first-localnet ``` -```json -{ - "nonce": 42, - "value": "100000000000000000", - "receiver": "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", - "sender": "erd1njqj2zggfup4nl83x0nfgqjkjserm7mjyxdx5vzkm8k0gkh40ezqtfz9lg", - "gasPrice": 1000000000, - "gasLimit": 70000, - "data": "Zm9vZCBmb3IgY2F0cw==", #base64 representation of "food for cats" - "signature": "93207c579bf57be03add632b0e1624a73576eeda8a1687e0fa286f03eb1a17ffb125ccdb008a264c402f074a360442c7a034e237679322f62268b614e926d10f", - "chainId": "1", - "version": 1 -} +Create, build and configure a localnet (in one go): + +```bash +mxpy localnet setup +``` + +Then, start the localnet: + +```bash +mxpy localnet start +``` + +:::tip +Above, the command `mxpy localnet setup` performs the following sub-commands under the hood, in one go (so you don't have to run them individually): + +```bash +mxpy localnet new +mxpy localnet prerequisites +mxpy localnet build +mxpy localnet config ``` -:::info -More information about sending a guarded transaction can be found here: [Guarded Accounts](/developers/guard-accounts#sending-guarded-co-signed-transactions) ::: +If everything goes well, in the terminal you should see logs coming from the nodes and proxy: -## POST Send Multiple Transactions {#send-multiple-transactions} +``` +INFO:cli.localnet:Starting localnet... +... +INFO:localnet:Starting process ['./seednode', ... +... +INFO:localnet:Starting process ['./node', ... +... +INFO:localnet:Starting process ['./proxy', ... +[PID=...] DEBUG[...] [process/block] started committing block ... +``` -`https://gateway.multiversx.com/transaction/send-multiple` +:::tip +The logs from all processes of the localnet can also be found in `~/my-first-localnet/localnet`. Simply look for `*.log` files. +::: -This endpoint allows one to send a bulk of Transactions to the Blockchain. +:::important +Note that the proxy starts with a delay of about 30 seconds. +::: - - -Body Parameters +## Halting and resuming the localnet -Array of: +In order to **halt the localnet**, press `Ctrl+C` in the terminal. This will stop all the processes (nodes, proxy etc.). The localnet can be **resumed at any time** by running again the command `mxpy localnet start` (from within your workspace). -| Param | Required | Type | Description | -| ---------------- | ----------------------------------------- | -------- | -------------------------------------------------------------------------------- | -| nonce | REQUIRED | `number` | The Nonce of the Sender. | -| value | REQUIRED | `string` | The Value to transfer, as a string representation of a Big Integer (can be "0"). | -| receiver | REQUIRED | `string` | The Address (bech32) of the Receiver. | -| sender | REQUIRED | `string` | The Address (bech32) of the Sender. | -| senderUsername | OPTIONAL | `string` | The base64 string representation of the Sender's username. | -| receiverUsername | OPTIONAL | `string` | The base64 string representation of the Receiver's username. | -| gasPrice | REQUIRED | `number` | The desired Gas Price (per Gas Unit). | -| gasLimit | REQUIRED | `number` | The maximum amount of Gas Units to consume. | -| data | OPTIONAL | `string` | The base64 string representation of the Transaction's message (data). | -| signature | REQUIRED | `string` | The Signature (hex-encoded) of the Transaction. | -| chainID | REQUIRED | `string` | The Chain identifier. | -| version | REQUIRED | `number` | The Version of the Transaction (e.g. 1). | -| options | OPTIONAL | `number` | The Options of the Transaction (e.g. 1). | - - +## Removing the localnet -🟢 200: OK +In order to remove the localnet, run the command (from within your workspace): -A bulk of Transactions were successfully sent. +```bash +mxpy localnet clean +``` -```json -{ - "data": { - "numOfSentTxs": 2, - "txsHashes": { - "0": "6c41c71946b5b428c2cfb560e3ea425f8a00345de4bb2eb1b784387790914277", - "1": "fa8195bae93d4609a6fc5972a7a6176feece39a6c4821acae2276701aee12fb0" - } - }, - "error": "", - "code": "successful" -} +This will delete the `~/my-first-localnet/localnet` folder. Note that the configuration file (e.g. `localnet.toml`) will not be deleted automatically. + + +## Gaining more control over the localnet + + +### Perform setup steps individually + +If you want to have more control over the localnet, you can run the setup sub-commands individually, as described below. + +First, let's create a separate workspace (for the sake of this guide): + +```bash +mkdir -p ~/my-second-localnet && cd ~/my-second-localnet +``` + +Then, create a new configuration file for the localnet, as follows: + +```bash +mxpy localnet new +``` + +Upon running this command, a new file called `localnet.toml` will be added in the current directory. This file contains the default configuration of the localnet. **Make sure to open the file and inspect its content**. You should see something like this: + +``` +[general] +... +rounds_per_epoch = 100 +round_duration_milliseconds = 6000 + +[metashard] +... +num_validators = 1 + +[shards] +num_shards = 2 +... + +[networking] +... +port_proxy = 7950 +port_first_validator_rest_api = 10200 + +[software.mx_chain_go] +resolution = "remote" +archive_url = "https://github.com/multiversx/mx-chain-go/archive/refs/heads/master.zip" +... + +[software.mx_chain_proxy_go] +resolution = "remote" +archive_url = "https://github.com/multiversx/mx-chain-proxy-go/archive/refs/heads/master.zip" +... +``` + +:::tip +Generally speaking, it's a good idea to only alter the `localnet.toml` **before first starting a localnet**. Once the localnet is started, the configuration file should not be modified anymore (e.g. when halting and resuming the localnet). +::: + +Now, the following command will fetch the software prerequisites - **mx-chain-go**, **mx-chain-proxy-go** etc. - into `~/multiversx-sdk`: + +```bash +mxpy localnet prerequisites ``` - - +:::tip +The `prerequisites` step is only necessary when the localnet depends on remote source code archives - i.e. at least one software prerequisite has `resolution = remote` in `localnet.toml`. **This is actually the default, and it's the easiest way to get started with the localnet.** Later on, we'll see how to create the localnet from local source code (for advanced use-cases). +::: -:::caution -For Nodes (Observers or Validators with the HTTP API enabled), this endpoint **only accepts transactions whose sender is in the Node's Shard**. +Once the software prerequisites (source code) are fetched, we can build them: + +```bash +mxpy localnet build +``` + +:::tip +The actual build takes place within the download folders of the software prerequisites which, by default, are children of `~/multiversx-sdk/localnet_software_remote`. ::: -Here's an example of a request: +Now let's configure (prepare) the localnet: ```bash -POST https://gateway.multiversx.com/transaction/send-multiple HTTP/1.1 +mxpy localnet config ``` -```json +It is only upon running this command that the localnet subfolders are created. Make sure to inspect them: -Content-Type: application/json +```bash +tree -L 3 ~/my-second-localnet +``` -[ - { - "nonce": 42, - "value": "100000000000000000", - "receiver": "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", - "sender": "erd1njqj2zggfup4nl83x0nfgqjkjserm7mjyxdx5vzkm8k0gkh40ezqtfz9lg", - "gasPrice": 1000000000, - "gasLimit": 70000, - "data": "Zm9vZCBmb3IgY2F0cw==", #base64 representation of "food for cats" - "signature": "93207c579bf57be03add632b0e1624a73576eeda8a1687e0fa286f03eb1a17ffb125ccdb008a264c402f074a360442c7a034e237679322f62268b614e926d10f", - "chainId": "1", - "version": 1 -} - { - "nonce": 43, - "value": "100000000000000000", - "receiver": "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", - "sender": "erd1rhp4q3qlydyrrjt7dgpfzxk8n4f7yrat4wc6hmkmcnmj0vgc543s8h7hyl", - "gasPrice": 1000000000, - "gasLimit": 70000, - "data": "YnVzIHRpY2tldHM=", #base64 representation of "bus tickets" - "signature": "01535fd1d40d98b7178ccfd1729b3f526ee4542482eb9f591d83433f9df97ce7b91db07298b1d14308e020bba80dbe4bba8617a96dd7743f91ee4b03d7f43e00", - "chainID": "1", - "version": 1 - } -] +Example output: + +``` +├── localnet +│   ├── proxy +│   │   ├── config +│   │   └── proxy +│   ├── seednode +│   │   ├── config +│   │   ├── libwasmer_linux_amd64.so +│   | ├── ... +│   │   └── seednode +│   ├── validator00 +│   │   ├── config +│   │   ├── libwasmer_linux_amd64.so +│   | ├── ... +│   │   └── node +│   ├── validator01 +│   │   ├── config +│   │   ├── libwasmer_linux_amd64.so +│   | ├── ... +│   │   └── node +│   └── validator02 +│   ├── config +│   ├── libwasmer_linux_amd64.so +│   ├── ... +│   └── node +└── localnet.toml ``` +We can then start, halt and resume the localnet as previously described. -## POST Simulate Transaction {#simulate-transaction} -**Nodes and observers** +### Altering chronology parameters -`https://gateway.multiversx.com/transaction/simulate` +Let's create a new localnet workspace: -This endpoint allows one to send a signed Transaction to the Blockchain in order to simulate its execution. -This can be useful in order to check if the transaction will be successfully executed before actually sending it. -It receives the same request as the `/transaction/send` endpoint. +```bash +mkdir -p ~/my-localnet-with-altered-chronology && cd ~/my-localnet-with-altered-chronology +mxpy localnet new +``` -Move balance successful transaction simulation +**Before first starting a localnet**, you can alter the chronology parameters in `localnet.toml`. For example, let's have shorter epochs and shorter rounds: - - +``` +[general] +rounds_per_epoch = 50 +round_duration_milliseconds = 4000 +``` -Body Parameters +Then, setup and start the localnet as previously described. -| Param | Required | Type | Description | -| ---------------- | ----------------------------------------- | -------- | -------------------------------------------------------------------------------- | -| nonce | REQUIRED | `number` | The Nonce of the Sender. | -| value | REQUIRED | `string` | The Value to transfer, as a string representation of a Big Integer (can be "0"). | -| receiver | REQUIRED | `string` | The Address (bech32) of the Receiver. | -| sender | REQUIRED | `string` | The Address (bech32) of the Sender. | -| senderUsername | OPTIONAL | `string` | The base64 string representation of the Sender's username. | -| receiverUsername | OPTIONAL | `string` | The base64 string representation of the Receiver's username. | -| gasPrice | REQUIRED | `number` | The desired Gas Price (per Gas Unit). | -| gasLimit | REQUIRED | `number` | The maximum amount of Gas Units to consume. | -| data | OPTIONAL | `string` | The base64 string representation of the Transaction's message (data). | -| signature | REQUIRED | `string` | The Signature (hex-encoded) of the Transaction. | -| chainID | REQUIRED | `string` | The Chain identifier. | -| version | REQUIRED | `number` | The Version of the Transaction (e.g. 1). | -| options | OPTIONAL | `number` | The Options of the Transaction (e.g. 1). | +```bash +mxpy localnet setup +mxpy localnet start +``` - - -A full response contains the fields above: -_SimulationResults_ -| Field | Type | Description | -|------------|---------------------------|-------------------| -| status | string | success, fail ... | -| failReason | string | the error message | -| scResults | []ApiSmartContractResult | an array of smart contract results (if any) | -| receipts | []ApiReceipt | an array of the receipts (if any) | -| hash | string | the hash of the transaction | +### Altering sharding configuration -❕ Note that fields that are empty won't be included in the response. This can be seen in the examples below +Let's create a new localnet workspace: ---- +```bash +mkdir -p ~/my-localnet-with-altered-sharding && cd ~/my-localnet-with-altered-sharding +mxpy localnet new +``` -🟢 200: OK +**Before first starting a localnet**, you can alter the sharding configuration in `localnet.toml`. For example, let's have 3 shards, each with 2 validators and 1 observer (thus, 9 nodes, without the metachain ones): -Transaction would be successful. +``` +[shards] +num_shards = 3 +consensus_size = 2 +num_observers_per_shard = 1 +num_validators_per_shard = 2 +``` -```json -{ - "data": { - "status": "success", - "hash": "bb24ccaa2da8cddd6a3a8eb162e6ff62ad4f6e1914d9aa0cacde6772246ca2dd" - }, - "error": "", - "code": "successful" -} +Then, setup and start the localnet as previously described. + +```bash +mxpy localnet setup +mxpy localnet start ``` ---- -🟢 200: Simulation was successful, but the transaction wouldn't be executed. +### Building from local source code -Invalid Transaction signature. +Let's create a new localnet workspace: -```json -{ - "data": { - "status": "fail", - "failReason": "higher nonce in transaction", - "hash": "bb24ccaa2da8cddd6a3a8eb162e6ff62ad4f6e1914d9aa0cacde6772246ca2dd" - }, - "error": "", - "code": "successful" -} +```bash +mkdir -p ~/my-localnet-from-local-src && cd ~/my-localnet-from-local-src +mxpy localnet new ``` ---- +In order to build the **node** or the **proxy** from local source code (instead of fetching the source code from a remote archive), you can set `resolution = local` in `localnet.toml`. For example, let's build both the node and the proxy from local source code: -🔴 400: Bad request +``` +[software.mx_chain_go] +resolution = "local" +local_path = "~/Desktop/workspace/mx-chain-go" -```json -{ - "data": null, - "error": "transaction generation failed: invalid chain ID", - "code": "bad_request" -} +[software.mx_chain_proxy_go] +resolution = "local" +local_path = "~/Desktop/workspace/mx-chain-proxy-go" ``` - - +Then, setup and start the localnet as previously described. ---- +```bash +mxpy localnet setup +mxpy localnet start +``` -**Proxy** -On the Proxy side, if the transaction to simulate is a cross-shard one, then the response format will contain two elements called `senderShard` and `receiverShard` which are of type `SimulationResults` explained above. +## Test (development) wallets -Example response for cross-shard transactions: +The development wallets **are minted at the genesis of the localnet** and their keys (both PEM files and Wallet JSON files) can be found in `~/multiversx-sdk/testwallets/latest/users`. -```json -{ - "data": { - "receiverShard": { - "status": "success", - "hash": "bb24ccaa2da8cddd6a3a8eb162e6ff62ad4f6e1914d9aa0cacde6772246ca2dd" - }, - "senderShard": { - "status": "success", - "hash": "bb24ccaa2da8cddd6a3a8eb162e6ff62ad4f6e1914d9aa0cacde6772246ca2dd" - } - }, - "error": "", - "code": "successful" -} -``` +:::caution +These wallets (Alice, Bob, Carol, ..., Mike) **are publicly known** - they should only be used for development and testing purpose. +::: -## POST Estimate Cost of Transaction {#estimate-cost-of-transaction} +## Interacting with our localnet -`https://gateway.multiversx.com/transaction/cost` -This endpoint is used to estimate the gas cost of a given transaction. +### Sending transactions -It performs a read-only simulation of the transaction against the current on-chain state, returning the number of gas units the transaction would consume if executed in that exact state. +Let's send a simple transaction using **mxpy**: -#### How it works: -- The endpoint takes all transaction input fields (value, sender, receiver, data, chainID, etc.). -- It executes the transaction in a sandboxed, non-persistent environment, meaning it simulates execution without affecting the actual blockchain. -- It uses the current state of the network (including smart contract storage, balances, etc.). -- It returns the estimated gas (txGasUnits) and may also return smart contract results and events triggered by the simulation. +```bash +mxpy tx new --data="Hello, World" --gas-limit=70000 \ + --receiver=erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --chain=localnet --proxy=http://localhost:7950 \ + --send +``` -#### How does it apply to smart contracts? +You should see the prepared transaction and the **transaction hash** in the `stdout` (or in the `--outfile` of your choice). Using the transaction hash, you can query the status of the transaction against the Proxy: -For smart contracts, the endpoint simulates the contract call exactly as if it were executed live, including processing all logic, branches, and emitted events. -The gas estimate reflects the computation and storage impact that would occur if the state remained unchanged at the time of actual execution. +```bash +curl http://localhost:7950/transaction/8b2cd8e61c12d6f02148537bdef40579c6cbff7ff0aba996f294d34a31992ba4 | jq +``` -#### How does it approximate the cost if the contract logic has variable gas usage? -If a smart contract’s logic has branches or conditional execution that result in variable gas usage (e.g., depending on internal storage state, previous executions, etc.), -the estimation will only reflect the gas used by the path taken during this particular simulation. +### Deploying and interacting with Smart Contracts -#### Why is providing the correct nonce important? +Let's deploy a Smart Contract using **mxpy**. -Because the simulation engine mirrors real transaction behavior, it requires a valid and correct nonce to properly simulate the transaction. Using an outdated or incorrect -nonce may lead to simulation failure. +```bash +mxpy --verbose contract deploy --bytecode=./contract.wasm \ + --gas-limit=5000000 \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --outfile=contract.json \ + --chain=localnet --proxy=http://localhost:7950 \ + --send +``` -#### Can the estimated gas differ from the actual cost? +Upon deployment, you can check the status of the transaction and the existence of the Smart Contract: -Yes. Since the blockchain state may change between the moment you call /transaction/cost and when the transaction is actually sent to the network, the real gas usage can differ. -This is especially true for smart contract calls that depend on dynamic or mutable state. +```bash +curl http://localhost:7950/transaction/4c5bd51ca0a051397bd6b0c89add2a5375106562b005c839f7e9bb113e2a8ce4 | jq +curl http://localhost:7950/address/erd1qqqqqqqqqqqqqpgqj5zftf3ef3gqm3gklcetpmxwg43rh8z2d8ss2e49aq | jq +``` +If everything is fine (transaction status is `executed` and the `code` property of the address is set), you can interact with the deployed contract: - - +```bash +mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqj5zftf3ef3gqm3gklcetpmxwg43rh8z2d8ss2e49aq \ + --gas-limit=1000000 --function=increment \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem --outfile=myCall.json \ + --chain=localnet --proxy=http://localhost:7950 \ + --send +``` -Body Parameters +In order to perform queries against the contract using `mxpy`, do as follows: -| Param | Required | Type | Description | -| -------- | ----------------------------------------- | -------- | -------------------------------------------------------------------------------- | -| value | REQUIRED | `string` | The Value to transfer, as a string representation of a Big Integer (can be "0"). | -| receiver | REQUIRED | `string` | The Address (bech32) of the Receiver. | -| sender | REQUIRED | `string` | The Address (bech32) of the Sender. | -| chainID | REQUIRED | `string` | The Chain identifier. | -| version | REQUIRED | `number` | The Version of the Transaction (e.g. 1). | -| nonce | REQUIRED | `number` | The Sender nonce. | -| options | OPTIONAL | `number` | The Options of the Transaction (e.g. 1). | -| data | OPTIONAL | `string` | The base64 string representation of the Transaction's message (data). | +``` +mxpy --verbose contract query erd1qqqqqqqqqqqqqpgqj5zftf3ef3gqm3gklcetpmxwg43rh8z2d8ss2e49aq --function=get --proxy=http://localhost:7950 +``` - - +### Simulating transactions -🟢 200: OK +At times, you can simulate transactions instead of broadcasting them, by replacing the flag `--send` with the flag `--simulate`. For example: -The cost is estimated successfully. +```bash +# Simulate: Call Contract +mxpy contract call erd1qqqqqqqqqqqqqpgqj5zftf3ef3gqm3gklcetpmxwg43rh8z2d8ss2e49aq \ + --gas-limit=1000000 --function=increment \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --chain=localnet --proxy=http://localhost:7950 \ + --simulate -```json -{ - "data": { - "txGasUnits": "77000" - }, - "error": "", - "code": "successful" -} +# Simulate: Simple Transfer +mxpy tx new --data="Hello, World" --gas-limit=70000 \ + --receiver=erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --chain=localnet --proxy=http://localhost:7950 \ + --simulate ``` - - +--- -:::tip -- Use the returned `txGasUnits` value as the `gasLimit` in your actual transaction. -- Make sure to provide the correct `nonce` of the transaction -::: +### Set up a Localnet (raw) -:::tip -**Best practice:** when sending the transaction, add ~10% extra gas to the estimated value to avoid underestimation and failure due to insufficient gas. -::: +How to set up a local MultiversX Testnet on a workstation. -Here's an example of a request: -```json -POST https://gateway.multiversx.com/transaction/cost HTTP/1.1 -Content-Type: application/json +## **Prerequisites** -{ - "value": "100000", - "receiver": "erd188nydpkagtpwvfklkl2tn0w6g40zdxkwfgwpjqc2a2m2n7ne9g8q2t22sr", - "sender": "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz", - "data": "dGhpcyBpcyBhbiBleGFtcGxl", #base64 representation of "this is an example" - "chainID": "1", - "version": 1, - "nonce": 1 -} +First, clone [mx-chain-go](https://github.com/multiversx/mx-chain-go) and [mx-chain-proxy-go](https://github.com/multiversx/mx-chain-proxy-go) in a directory of your choice. + +```bash +$ mkdir mytestnet && cd mytestnet +$ git clone git@github.com:multiversx/mx-chain-go.git +$ git clone git@github.com:multiversx/mx-chain-proxy-go.git ``` +Then, run the `prerequisites` command. -## GET **Get Transaction** {#get-transaction} +```bash +$ cd mx-chain-go/scripts/testnet +$ ./prerequisites.sh +``` -`https://gateway.multiversx.com/transaction/:txHash` +This will install some packages and also clone the [mx-chain-deploy-go](https://github.com/multiversx/mx-chain-deploy-go) repository, as a sibling of the previously cloned `mx-chain-go`. -This endpoint allows one to query the details of a Transaction. +Depending on your Linux distribution, you may need to run the following commands as well: - - +```bash +sudo apt install tmux +sudo apt install gnome-terminal +``` -Path Parameters -| Param | Required | Type | Description | -| ------ | ----------------------------------------- | -------- | ----------------------------------------- | -| txHash | REQUIRED | `string` | The hash (identifier) of the Transaction. | +## **Configure the Testnet** -Query Parameters +The variables that dictate the structure of the Testnet are located in the file `scripts/testnet/variables.sh`. For example: -| Param | Required | Type | Description | -| ----------- | ----------------------------------------- | -------- | -------------------------------------------------------------------------------------------- | -| sender | OPTIONAL | `string` | The Address of the sender - a hint to optimize the request. | -| withResults | OPTIONAL | `bool` | Boolean parameter to specify if smart contract results and other details should be returned. | +```bash +export TESTNETDIR="$HOME/MultiversX/testnet" +export SHARDCOUNT=2 +... +``` - - +You can override the default variables by creating a new file called `local.sh`, as a sibling of `variables.sh`. For example, in order to use a different directory than the default one: -🟢 200: OK +```bash +local.sh +export TESTNETDIR="$HOME/Desktop/mytestnet/sandbox" +export USETMUX=1 +export NODETERMUI=0 +``` -Transaction details retrieved successfully. +Once ready with overriding the desired parameters, run the `config` command. -```json -{ - "data": { - "transaction": { - "type": "normal", - "nonce": 3, - "round": 186580, - "epoch": 12, - "value": "1000000000000000000", - "receiver": "erd1...", - "sender": "erd1...", - "gasPrice": 1000000000, - "gasLimit": 70000, - "data": "Zm9yIHRlc3Rz", - "signature": "1047...", - "sourceShard": 2, - "destinationShard": 1, - "blockNonce": 186535, - "miniblockHash": "e927...", - "blockHash": "50a1...", - "status": "executed" - } - }, - "error": "", - "code": "successful" -} +```bash +$ ./config.sh +``` + +After that, you can inspect the generated configuration files in the specified folder: + +``` +$HOME/Desktop/mytestnet/sandbox +├── filegen +│ ├── filegen +│ └── output +│ ├── delegationWalletKey.pem +│ ├── delegators.pem +│ ├── genesis.json +│ ├── genesisSmartContracts.json +│ ├── nodesSetup.json +│ ├── validatorKey.pem +│ └── walletKey.pem +├── node +│ └── config +│ ├── api.toml +│ ├── config_observer.toml +│ ├── config_validator.toml +│ ├── delegationWalletKey.pem +│ ├── delegators.pem +│ ├── economics.toml +│ ├── external.toml +│ ├── gasSchedule.toml +│ ├── genesisContracts +│ │ ├── delegation.wasm +│ │ └── dns.wasm +│ ├── genesis.json +│ ├── genesisSmartContracts.json +│ ├── nodesSetup.json +│ ├── p2p.toml +│ ├── prefs.toml +│ ├── ratings.toml +│ ├── systemSmartContractsConfig.toml +│ ├── validatorKey.pem +│ └── walletKey.pem +├── node_working_dirs +├── proxy +│ └── config +│ ├── config.toml +│ ├── economics.toml +│ ├── external.toml +│ └── walletKey.pem +└── seednode + └── config + ├── config.toml + └── p2p.toml +``` + + +## **Starting and stopping the Testnet** + +In order to start the Testnet, run the `start` command. + +```bash +$ ./start.sh debug ``` - - -Request URL: +After waiting about 1 minute, you can inspect the logs of the running nodes in folder `mytestnet/sandbox/node_working_dirs`. -`https://gateway.multiversx.com/transaction/:txHash?withResults=true` +In order to stop the Testnet, run the `stop` command. -Response: +```bash +$ ./stop.sh +``` -The response can contain additional fields such as `smartContractResults`, or `receipt` +If desired, you can also `pause` and `resume` the Testnet (without actually stopping the running nodes): -```json -{ - "data": { - "transaction": { - "type": "normal", - "nonce": 3, - "round": 186580, - "epoch": 12, - "value": "1000000000000000000", - "receiver": "erd1...", - "sender": "erd1...", - "gasPrice": 1000000000, - "gasLimit": 70000, - "data": "Zm9yIHRlc3Rz", - "signature": "1047...", - "sourceShard": 2, - "destinationShard": 1, - "blockNonce": 186535, - "miniblockHash": "e927...", - "blockHash": "50a1...", - "status": "executed", - "receipt": { - "value": 100, - "sender": "erd1...", - "data": "...", - "txHash": "b37..." - }, - "smartContractResults": [ - { - "hash": "...", - "nonce": 5, - "value": 1000, - "receiver": "erd1...", - "sender": "erd1...", - "data": "@6f6b", - "prevTxHash": "3638...", - "originalTxHash": "3638...", - "gasLimit": 0, - "gasPrice": 1000000000, - "callType": 0 - } - ] - } - }, - "error": "", - "code": "successful" -} +```bash +$ ./pause.sh +$ ./resume.sh ``` - - -:::important -The optional query parameter **`sender`** is only applicable to requests against the Proxy (not against the Observer Nodes). +## **Recreating the Testnet** + +In order to destroy the Testnet, run the `clean` command: + +```bash +./stop.sh +./clean.sh +``` + +:::note Run config after clean +After running **clean,** you need to run **config** before **start**, in order to start the Testnet again. ::: +If you need to recreate a Testnet from scratch, use the `reset` command (which also executes `clean` under the hood): -## GET **Get Transaction Shallow Status** {#get-transaction-status} +```bash +$ ./reset.sh +``` -`https://gateway.multiversx.com/transaction/:txHash/status` -This endpoint allows one to query **the shallow status** of a transaction. For more details, see [this](/integrators/querying-the-blockchain). +## **Inspecting the Proxy** - - +By default, the local Testnet also includes a local MultiversX Proxy instance, listening on port **7950**. You can query in a browser or directly in the command line. Also see [REST API](/sdk-and-tools/rest-api/). -Path Parameters +```bash +$ curl http://localhost:7950/network/config +``` -| Param | Required | Type | Description | -| ------ | ----------------------------------------- | -------- | ----------------------------------------- | -| txHash | REQUIRED | `string` | The hash (identifier) of the Transaction. | +Given the request above, extract and save the fields `erd_chain_id` and `erd_min_transaction_version` from the response. You will need them in order to send transactions against your local Testnet. -Query Parameters -| Param | Required | Type | Description | -| ------ | ----------------------------------------- | -------- | ----------------------------------------------------------- | -| sender | OPTIONAL | `string` | The Address of the sender - a hint to optimize the request. | +## **Sending transactions** - - +Let's send a simple transaction using **mxpy:** -🟢 200: OK +```bash +$ mxpy tx new --data="Hello, World" --gas-limit=70000 \ + --receiver=erd1... \ + --pem=./sandbox/node/config/walletKey.pem --pem-index=0 \ + --proxy=http://localhost:7950 \ + --send +``` -Transaction status retrieved successfully. +You should see the prepared transaction and the **transaction hash** in the `stdout` (or in the `--outfile` of your choice). Using the transaction hash, you can query the status of the transaction against the Proxy: -```json -{ - "data": { - "status": "success" - }, - "error": "", - "code": "successful" -} +```bash +$ curl http://localhost:7950/transaction/1363... ``` - - -:::important -The optional query parameter **`sender`** is only applicable to requests against the Proxy (not against the Observer Nodes). -::: +## **Deploying and interacting with Smart Contracts** +Let's deploy a Smart Contract using **mxpy**. -## GET **Get Transaction Process Status** {#get-transaction-process-status} +```bash +Deploy +mxpy --verbose contract deploy --bytecode=./mycontract/output/contract.wasm \ + --gas-limit=5000000 \ + --pem=./sandbox/node/config/walletKey.pem --pem-index=0 \ + --outfile=contract.json \ + --proxy=http://localhost:7950 \ + --send +``` -`https://gateway.multiversx.com/transaction/:txHash/process-status` +Upon deployment, you can check the status of the transaction and the existence of the Smart Contract: -This endpoint allows one to query the **process status** of a transaction. For more details, see [this](/integrators/querying-the-blockchain). +```bash +$ curl http://localhost:7950/transaction/daf2... +$ curl http://localhost:7950/address/erd1qqqqqqqqqqqqqpgql... +``` +If everything is fine (transaction status is `executed` and the `code` property of the address is set), you can interact with or perform queries against the deployed contract: - - +```bash +Call +mxpy --verbose contract call erd1qqqqqqqqqqqqqpgql... \ + --gas-limit=1000000 --function=increment \ + --pem=./sandbox/node/config/walletKey.pem --pem-index=0 --outfile=myCall.json \ + --proxy=http://localhost:7950 \ + --send -Path Parameters +``` -| Param | Required | Type | Description | -| ------ | ----------------------------------------- | -------- | ----------------------------------------- | -| txHash | REQUIRED | `string` | The hash (identifier) of the Transaction. | +```bash +Query +mxpy --verbose contract query erd1qqqqqqqqqqqqqpgqlq... \ + --function=get \ + --proxy=http://localhost:7950 +``` - - +--- -🟢 200: OK +### Setup & Basics -Transaction status retrieved successfully. +Write, build and deploy a simple smart contract written in Rust. -```json -{ - "data": { - "reason": "" - "status": "success" - }, - "error": "", - "code": "successful" -} -``` +This tutorial will guide you through the process of writing, building and deploying a simple smart contract for the MultiversX Network, written in Rust. - - +:::important +The MultiversX Network supports smart contracts written in any programming language compiled into WebAssembly. +::: -## GET **Get Transactions Pool** {#get-transactions-pool} +## Scenario -`http://local-proxy-instance/transaction/pool` +Let's say you need to raise EGLD for a cause you believe in. They will obviously be well spent, but you need to get the EGLD first. For this reason, you decided to run a crowdfunding campaign on the MultiversX Network, which naturally means that you will use a smart contract for the campaign. This tutorial will teach you how to do just that: **write a crowdfunding smart contract, deploy it, and use it**. -:::caution -This endpoint isn't available on public gateway. However, it can be used on a local proxy instance, by setting `AllowEntireTxPoolFetch` to `true` -::: +The idea is simple: the smart contract will accept transfers until a deadline is reached, tracking all contributors. -This endpoint allows one to fetch the entire transactions pool, merging the pools from each shard. +If the deadline is reached and the smart contract has gathered an amount of EGLD above the desired funds, then the smart contract will consider the crowdfunding a success, and it will consequently send all the EGLD to a predetermined account (yours!). +However, if the donations fall short of the target, the contract will return all the all EGLD tokens to the donors. -### Default - - +## Design -Example: +Here is how the smart contract methods are designed: -`http://local-proxy-instance/transaction/pool` +- `init`: automatically triggered when the contract is deployed. It takes two inputs from you: + 1. The target amount of EGLD you want to raise; + 2. The crowdfunding deadline, which is expressed as a block nonce. +- `fund`: used by donors to contribute EGLD to the campaign. It will receive EGLD and save the necessary details so the contract can return funds if the campaign doesn't reach its goal; +- `claim`: if called before the deadline, it does nothing and returns an error. If called after the deadline: + - By you (the campaign creator), it sends all the raised EGLDs to your account if the target amount is met. Otherwise, it returns an error; + - By donor, it refunds their contribution if the target amount is not reached. If the target is met, it does nothing and returns an error; + - By anyone else, it does nothing and returns an error. +- `status`: Provides information about the campaign, such as whether it is still active or completed and how much EGLD has been raised so far. You will likely use this frequently to monitor progress. - - +In this part of the tutorial, we will start with the `init` method to familiarize you with the development process and tools. You will not only implement the init method but also **tests** to ensure it works as expected. -🟢 200: OK +:::important testing +Automated testing is exceptionally important for the development of smart contracts, due to the sensitive nature of the information they must handle. +::: -Transaction status retrieved successfully. -```json -{ - "data": { - "txPool": { - "regularTransactions": [ - { - "txFields": { - "hash": "84bb8a..." - } - }, - { - "txFields": { - "hash": "4e2c43..." - } - } - ], - "smartContractResults": [], - "rewards": [] - }, - "error": "", - "code": "successful" -} -``` +## Prerequisites - - +:::important +Before starting this tutorial, make sure you have the following: +- `stable` **Rust** version `≥ 1.85.0` (install via [rustup](/docs/developers/toolchain-setup.md#installing-rust-and-sc-meta)) +- `sc-meta` (install [multiversx-sc-meta](/docs/developers/meta/sc-meta-cli.md)) -### Using custom fields +::: - - +For contract developers, we generally recommend [**VSCode**](https://code.visualstudio.com) with the following extensions: -Query Parameters +- [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) +- [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) -| Param | Required | Type | Description | -| -------- | ----------------------------------------- | -------- | ------------------------------------------------------------- | -| fields | OPTIONAL | `string` | A list of the fields to be included. | -| shard-id | OPTIONAL | `string` | A specific shard id(0, 1, 2 etc. or 4294967295 for Metachain) | -As seen above, if the `fields` item is empty, only the transaction hash will be displayed. +## Step 1: prepare the workspace -If the `shard-id` item is used, only the transactions from that specific shard's pool will be displayed. +The source code of each smart contract requires its own folder. We will start the development of the **crowdfunding** contract from the **empty** template. To get the development environment ready, simply run the following commands in your terminal: -Example request with shard id and fields: +```bash +sc-meta new --name crowdfunding --template empty +``` -`https://gateway.multiversx.com/transaction/pool?shard-id=0&fields=sender,receiver,value` +You may choose any location you want for your smart contract. Either way, now that you are in the `crowdfunding` folder we can begin. -All possible values for fields item are: +[sc-meta](/docs/developers/meta/sc-meta.md) created your project out of a template. These templates are contracts written and tested by MultiversX, which can be used by anybody as starting points. -- hash -- nonce -- sender -- receiver -- gaslimit -- gasprice -- receiverusername -- data -- value +```toml title=Cargo.toml +[package] +name = "crowdfunding" +version = "0.0.0" +publish = false +edition = "2024" +authors = ["you"] - - +[lib] +path = "src/crowdfunding.rs" -🟢 200: OK +[dependencies.multiversx-sc] +version = "0.64.1" -Transaction status retrieved successfully. +[dev-dependencies.multiversx-sc-scenario] +version = "0.64.1" -```json -{ - "data": { - "txPool": { - "regularTransactions": [ - { - "txFields": { - "gasLimit": 10, - "gasPrice": 1000, - "receiver": "erd1...", - "sender": "erd1...", - "value": "10000000000000000000" - } - } - ], - "smartContractResults": [ - { - "txFields": { - "gasLimit": 10, - "gasPrice": 1000, - "receiver": "erd1...", - "sender": "erd1...", - "value": "10000000000000000000" - } - } - ], - "rewards": [ - { - "txFields": { - "gasLimit": 10, - "gasPrice": 1000, - "receiver": "erd1...", - "sender": "erd1...", - "value": "10000000000000000000" - } - } - ] - } - }, - "error": "", - "code": "successful" -} +[workspace] +members = [ + ".", + "meta", +] ``` - - +Let's inspect the file found at path `crowdfunding/Cargo.toml`: +- `[package]` represents the **project** which is unsurprisingly named `crowdfunding`, and has version `0.0.0`. You can set any version you like, just make sure it has 3 numbers separated by dots. It is a requirement. The `publish` is set to **false** to prevent the package from being published to Rust’s central package registry. It's useful for private or experimental projects; +- `[lib]` declares the source code of the smart contracts, which in our case is `src/crowdfunding.rs`. You can name this file anything you want. The default Rust naming is `lib.rs`, but it can be easier to organize your code when the main code files bear the names of the contracts; +- This project has `dependencies` and `dev-dependencies`. You'll need a few special and very helpful packages: + - `multiversx-sc`: developed by MultiversX, it is the interface that the smart contract sees and can use; + - `multiversx-sc-scenario`: developed by MultiversX, it is the interface that defines and runs blockchain scenarios involving smart contracts; + - `num-bigint`: for working with arbitrarily large integers. +- `[workspace]` is a group of related Rust projects that share common dependencies or build settings; +- The resulting binary will be the name of the project, which in our case is `crowdfunding` (actually, `crowdfunding.wasm`, but the compiler will add the `.wasm` part). -## GET **Get Transactions Pool for a Sender** {#get-transactions-pool-for-a-sender} -`https://gateway.multiversx.com/transaction/pool?by-sender=:sender:` +## Step 2: develop -This endpoint allows one to fetch all the transactions of a sender from the transactions pool. +With the structure in place, you can now write the code and build it. +Open `src/crowdfunding.rs`: -### Default +```rust +#![no_std] // [1] - - +use multiversx_sc::imports::*; // [2] +#[allow(unused_imports)] // [3] -Query Parameters +/// An empty contract. To be used as a template when starting a new contract from scratch. +#[multiversx_sc::contract] // [4] +pub trait Crowdfunding { // [5] + #[init] // [6] + fn init(&self) {} // [7] -| Param | Required | Type | Description | -| --------- | ----------------------------------------- | -------- | -------------------------- | -| by-sender | REQUIRED | `string` | The Address of the sender. | + #[upgrade] // [8] + fn upgrade(&self) {} // [9] +} +``` -Example: +Let's take a look at the code: -`https://gateway.multiversx.com/transaction/pool?by-sender=erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th` +- **[1]**: means that the smart contract **has no access to standard libraries**. That will make the code lean and very light. +- **[2]**: brings imports module from the [multiversx_sc](https://crates.io/crates/multiversx-sc) crate into **Crowdfunding** contract. It effectively grants you access to the [MultiversX framework for Rust smart contracts](https://github.com/multiversx/mx-sdk-rs), which is designed to simplify the code **enormously**. +- **[3]**: since the contract is still in an early stage of development, clippy (Rust's linter) will flag some imports as unused. For now, we will ignore this kind of error. +- **[4]**: processes the **Crowdfunding** trait definition as a **smart contract** that can be deployed on the MultiversX blockchain. +- **[5]**: the contract [trait](https://doc.rust-lang.org/book/ch10-02-traits.html) where all the endpoints will be developed. +- **[6]**: marks the following method (`init`) as the constructor function for the contract. +- **[7]**: this is the constructor itself. It receives the contract's instance as a parameter (_&self_). The method is called once the contract is deployed on the MultiversX blockchain. You can name it any way you wish, but it must be annotated with `#[init]`. For the moment, no initialization logic is defined. +- **[8]**: marks the following method (`upgrade`) as the upgrade function for the contract. It is called when the contract is re-deployed to the same address. +- **[9]**: this is the upgrade method itself. Similar to [7], it takes a reference to the contract instance (_&self_) and performs no specific logic here. - - -🟢 200: OK +## Step 3: build -Transaction status retrieved successfully. +Now go back to the terminal, make sure the current folder is the one containing the Crowdfunding smart contract (`crowdfunding/`), then trigger the **build** command: -```json -{ - "data": { - "txPool": { - "transactions": [ - { - "txFields": { - "hash": "1daea5..." - } - } - ] - } - }, - "error": "", - "code": "successful" -} +```bash +sc-meta all build ``` - - +If this is the first time you build a Rust smart contract with the `sc-meta` command, it will take a little while before it's done. Subsequent builds will be much faster. +When the command completes, a new folder will appear: `crowdfunding/output/`. This folder contains: -### Using custom fields +1. `crowdfunding.abi.json` +2. `crowdfunding.imports.json` +3. `crowdfunding.mxsc.json` +4. `crowdfunding.wasm` - - +We won't be doing anything with these files just yet - wait until we get to the deployment part. Along with `crowdfunding/output/`, there are a few other folders and files generated. You can safely ignore them for now, but do not delete the `/crowdfunding/wasm/` folder - it's what makes the build command faster after the initial run. -Query Parameters +The following can be safely deleted, as they are not important for this contract: + +- The `scenarios/` folder; +- The `crowdfunding/tests/crowdfunding_scenario_go_test.rs` file; +- The `crowdfunding/tests/crowdfunding_scenario_rs_test.rs` file. + +The structure of your folder should be like this (output printed using command `tree -L 2`): + +```bash +. +├── Cargo.lock +├── Cargo.toml +├── meta +│ ├── Cargo.toml +│ └── src +├── multiversx.json +├── output +│ ├── crowdfunding.abi.json +│ ├── crowdfunding.imports.json +│ ├── crowdfunding.mxsc.json +│ └── crowdfunding.wasm +├── src +│ └── crowdfunding.rs +├── target +│ ├── CACHEDIR.TAG +│ ├── debug +│ ├── release +│ ├── tmp +│ └── wasm32-unknown-unknown +├── tests +└── wasm + ├── Cargo.lock + ├── Cargo.toml + └── src +``` + +It's time to add some functionality to the `init` function now. -| Param | Required | Type | Description | -| --------- | ----------------------------------------- | -------- | ------------------------------------ | -| by-sender | REQUIRED | `string` | The Address of the sender. | -| fields | OPTIONAL | `string` | A list of the fields to be included. | -As seen above, if the `fields` item is empty, only the transaction hash will be displayed. +## Step 4: persisting values -Example request with fields: +In this step, you will use the `init` method to persist some values in the storage of the Crowdfunding smart contract. -`https://gateway.multiversx.com/transaction/pool?by-sender=erd1at9...&fields=sender,receiver,value` -All possible values for fields item are: +### Storage mappers -- hash -- nonce -- sender -- receiver -- gaslimit -- gasprice -- receiverusername -- data -- value +Every smart contract can store key-value pairs in a persistent structure, created for the smart contract at its deployment on the MultiversX Network. - - +The storage of a smart contract is, for all intents and purposes, **a generic hash map or dictionary**. When you want to store some arbitrary value, you store it under a specific key. To get the value back, you need to know the key you stored it under. -🟢 200: OK +To help you keep the code clean, the framework enables you to write **setter** and **getter** methods for individual key-value pairs. There are several ways to interact with storage from a contract, but the simplest one is by using [**storage mappers**](/docs/developers/developer-reference/storage-mappers.md). -Transaction status retrieved successfully. +Next, you will declare a [_SingleValueMapper_](/docs/developers/developer-reference/storage-mappers.md#singlevaluemapper) that has the purpose of storing a [_BigUint_](/docs/developers/best-practices/biguint-operations.md) number. This storage mapper is dedicated to storing/retrieving the value stored under the key `target`: -```json -{ - "data": { - "txPool": { - "transactions": [ - { - "txFields": { - "hash": "1daea...", - "receiver": "erd1932...", - "sender": "erd1at9ke...", - "value": 0 - } - } - ] - } - }, - "error": "", - "code": "successful" -} +```rust +#[storage_mapper("target")] +fn target(&self) -> SingleValueMapper; ``` - - +:::important +`BigUint` **type** is a big unsigned number, handled by the VM. There is no need to import any library, big number arithmetic is provided for all contracts out of the box. +::: +Normally, smart contract developers are used to dealing with raw bytes when storing or loading values from storage. The MultiversX framework for Rust smart contracts makes it far easier to manage the storage because it can handle typed values automatically. -## GET **Get the latest nonce of a sender from Tx Pool** {#get-the-latest-nonce-of-a-sender-from-tx-pool} -`https://gateway.multiversx.com/transaction/pool?by-sender=:sender:&last-nonce=true` +### Extend init -This endpoint allows one to fetch the latest nonce of a sender from the transactions pool. +You will now instruct the `init` method to store the amount of tokens that should be gathered upon deployment. - - +The owner of a smart contract is the account that deployed it (you). By design, your Crowdfunding smart contract will send all the donated EGLD to its owner (you), assuming the target amount was reached. Nobody else has this privilege, because there is only one single owner of any given smart contract. -Query Parameters +Here's how the `init` method looks, with the code that saves the target: -| Param | Required | Type | Description | -| ---------- | ----------------------------------------- | -------- | ----------------------------------------------- | -| by-sender | REQUIRED | `string` | The Address of the sender. | -| last-nonce | REQUIRED | `bool` | Specifies if the last nonce has to be returned. | +```Rust +#[init] +fn init(&self, target: BigUint) { + self.target().set(&target); +} +``` - - +We have added an argument to the constructor method. It is called `target` and will need to be supplied when we deploy the contract. The argument then promptly gets saved to storage. -🟢 200: OK +Now note the `self.target()` invocation. This gives us an object that acts like a proxy for a part of the storage. Calling the `.set()` method on it causes the value to be saved to the contract storage. -Transaction status retrieved successfully. +:::important +All of the stored values end up in the storage if the transaction completes successfully. Smart contracts cannot access the protocol directly, it is the VM that intermediates everything. +::: -```json -{ - "data": { - "nonce": 38 - }, - "error": "", - "code": "successful" -} -``` +Whenever you want to make sure your code is in order, run the build command: - - +```bash +sc-meta all build +``` +There's one more thing: by default, none of the `fn` statements declare smart contract methods that are _externally callable_. All the data in the contract is publicly available, but it can be cumbersome to search through the contract storage manually. That is why it is often nice to make getters public, so people can call them to get specific data out. -## GET **Get the nonce gaps of a sender from Tx Pool** {#get-the-nonce-gaps-of-a-sender-from-tx-pool} +Public methods are annotated with either `#[endpoint]` or `#[view]`. There is currently no difference in functionality between them (but there might be at some point in the future). Semantically, `#[view]` indicates readonly methods, while `#[endpoint]` suggests that the method also changes the contract state. -`https://gateway.multiversx.com/transaction/pool?by-sender=:sender:&nonce-gaps=true` +```rust + #[view] + #[storage_mapper("target")] + fn target(&self) -> SingleValueMapper; +``` -This endpoint allows one to fetch the nonce gaps of a sender from the transactions pool. +You can also think of `#[init]` as a special type of endpoint. - - -Query Parameters +## Step 5: testing -| Param | Required | Type | Description | -| ---------- | ----------------------------------------- | -------- | ----------------------------------------------- | -| by-sender | REQUIRED | `string` | The Address of the sender. | -| nonce-gaps | REQUIRED | `bool` | Specifies if the nonce gaps should be returned. | +You must always make sure that the code you write functions as intended. That's what **automated testing** is for. - - +For now, this is how your contract looks: -🟢 200: OK +```rust +#![no_std] -Transaction status retrieved successfully. +#[allow(unused_imports)] +use multiversx_sc::imports::*; -```json -{ - "data": { - "nonceGaps": { - "gaps": [ - { - "from": 34, - "to": 35 - }, - { - "from": 37, - "to": 37 - } - ] +#[multiversx_sc::contract] +pub trait Crowdfunding { + #[init] + fn init(&self, target: BigUint) { + self.target().set(&target); } - }, - "error": "", - "code": "successful" + + #[upgrade] + fn upgrade(&self) {} + + #[view] + #[storage_mapper("target")] + fn target(&self) -> SingleValueMapper; } ``` - - +There are several ways to write [smart contract tests in Rust](/docs/developers/testing/rust/sc-test-overview.md). Now, we will focus on developing a test using [black-box calls](/docs/developers/testing/rust/sc-blackbox-calls.md). ---- +:::important +Blackbox tests execution imitates the blockchain with no access to private contract functions. +::: -### Rest API Virtual Machine +Let's write a test against the `init` method to make sure that it definitely stores the address of the owner under the `target` key at deployment. -```mdx-code-block -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; -``` +### Set up -This component of the REST API allows one to call view functions (pure functions) of Smart Contracts, or, to put it in other words, to query values stored within contracts. +In the folder of the Crowdfunding smart contract, there is a folder called `tests/`. Inside it, create a new Rust file called `crowdfunding_blackbox_test.rs`. +Your folder should look like this (output from the command `tree -L 2`): -## POST Compute Output of Pure Function {#compute-output-of-pure-function} +```bash +. +├── Cargo.lock +├── Cargo.toml +├── meta +│ ├── Cargo.toml +│ └── src +├── multiversx.json +├── output +│ ├── crowdfunding.abi.json +│ ├── crowdfunding.imports.json +│ ├── crowdfunding.mxsc.json +│ └── crowdfunding.wasm +├── src +│ └── crowdfunding.rs +├── target +│ ├── CACHEDIR.TAG +│ ├── debug +│ ├── release +│ ├── tmp +│ └── wasm32-unknown-unknown +├── tests +│ └── crowdfunding_blackbox_test.rs +└── wasm + ├── Cargo.lock + ├── Cargo.toml + └── src +``` -`https://gateway.multiversx.com/vm-values/query` +Before creating the first test, we need to [set up the environment](/docs/developers/testing/rust/sc-test-setup.md). We will: -This endpoint allows one to execute - with no side-effects - a pure function of a Smart Contract and retrieve the execution results (the Virtual Machine Output). +1. Generate the smart contract's proxy; +2. Register the contract; +3. Set up accounts. - - -Body Parameters +### Generate Proxy -| Param | Required | Type | Description | -| --------- | ----------------------------------------- | -------- | ----------------------------------------------------------------------------------- | -| scAddress | REQUIRED | `string` | The Address (bech32) of the Smart Contract. | -| funcName | REQUIRED | `string` | The name of the Pure Function to execute. | -| args | REQUIRED | `array` | The arguments of the Pure Function, as hex-encoded strings. The array can be empty. | -| caller | OPTIONAL | `string` | The Address (bech32) of the caller. | -| value | OPTIONAL | `string` | The Value to transfer (can be zero). | +A smart contract's [proxy](/docs/developers/transactions/tx-proxies.md) is an object that mimics the contract. We will use the proxy to call the endpoints of the Crowdfunding contracts. - - +The proxy contains entirely autogenerated code. However, before running the command to generate the proxy, we need to set up a configuration file. -🟢 200: OK +In the root of the contract, at the path `crowdfunding/`, we will create the configuration file `sc-config.toml`, where we will specify the path to generate the proxy: -The VM Output is retrieved successfully. +```toml title=sc-config.toml +[settings] -```json -{ - "data": { - "data": { - "ReturnData": ["eyJSZ... (base64)"], - "ReturnCode": 0, - "ReturnMessage": "", - "GasRemaining": 1500000000, - "GasRefund": 0, - "OutputAccounts": { - "...": { - "Address": "... (base64)", - "Nonce": 0, - "Balance": null, - "BalanceDelta": 0, - "StorageUpdates": null, - "Code": null, - "CodeMetadata": null, - "Data": null, - "GasLimit": 0, - "CallType": 0 - } - }, - "DeletedAccounts": null, - "TouchedAccounts": null, - "Logs": null - } - }, - "error": "", - "code": "successful" -} +[[proxy]] +path = "src/crowdfunding_proxy.rs" ``` - - +In the terminal, in the root of the contract, we will run the next command that will generate the proxy for the Crowdfunding smart contract: -Here's an example of a request: +```bash +sc-meta all proxy +``` -```json -POST https://gateway.multiversx.com/vm-values/query HTTP/1.1 -Content-Type: application/json +Once the proxy is generated, our work is not over yet. The next thing to do is to import the module in the Crowdfunding smart contract's code: -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqllls0lczs7", - "funcName": "get", - "caller": "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", - "value": "0", - "args": ["d98d..."] -} -``` +```rust +#![no_std] +#[allow(unused_imports)] +use multiversx_sc::imports::*; -## POST Compute Hex Output of Pure Function {#compute-hex-output-of-pure-function} +pub mod crowdfunding_proxy; -`https://gateway.multiversx.com/vm-values/hex` +#[multiversx_sc::contract] +pub trait Crowdfunding { + // Here is the implementation of the crowdfunding contract +} +``` -This endpoint allows one to execute - with no side-effects - a pure function of a Smart Contract and retrieve the first output value as a hex-encoded string. +With each build of the contract executed by the developer, the proxy will be automatically updated with the changes made to the contract. - - -Body Parameters +### Register -| Param | Required | Type | Description | -| --------- | ----------------------------------------- | -------- | ----------------------------------------------------------------------------------- | -| scAddress | REQUIRED | `string` | The Address (bech32) of the Smart Contract. | -| funcName | REQUIRED | `string` | The name of the Pure Function to execute. | -| args | REQUIRED | `array` | The arguments of the Pure Function, as hex-encoded strings. The array can be empty. | -| caller | OPTIONAL | `string` | The Address (bech32) of the caller. | -| value | OPTIONAL | `string` | The Value to transfer (can be zero). | +The Rust backend does not run compiled contracts, instead, it hooks the actual Rust contract code to its engine. You can find more [here](/docs/developers/testing/rust/sc-test-setup.md#registering-contracts). - - +In order to link the smart contract code to the test you are developing, you need to call `register_contract()` in the setup function of the blackbox test. -🟢 200: OK +```rust +use crowdfunding::crowdfunding_proxy; +use multiversx_sc_scenario::imports::*; -The output value is retrieved successfully. +const CODE_PATH: MxscPath = MxscPath::new("output/crowdfunding.mxsc.json"); -```json -{ - "data": "7b22..." +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + + blockchain.set_current_dir_from_workspace("crowdfunding"); + blockchain.register_contract(CODE_PATH, crowdfunding::ContractBuilder); + blockchain } ``` - - +### Account +The environment you're working in is a mocked blockchain. This means you have to create and manage accounts, allowing you to test and verify the behavior of your functions without deploying to a real blockchain. -## POST Compute String Output of Pure Function {#compute-string-output-of-pure-function} +Here's an example to get started in `crowdfunding_blackbox_test.rs`: -`https://gateway.multiversx.com/vm-values/string` +```rust +const OWNER: TestAddress = TestAddress::new("owner"); -This endpoint allows one to execute - with no side effects - a pure function of a Smart Contract and retrieve the first output value as a string. +#[test] +fn crowdfunding_deploy_test() { + let mut world = world(); - - + world.account(OWNER).nonce(0).balance(1000000); +} +``` -Body Parameters +In the snippet above, we've added only one account to the fictional universe of Crowdfunding smart contract. It is an account with the address `owner`, which the testing environment will use to pretend it's you. Note that in this fictional universe, your account nonce is `0` (meaning you've never used this account yet) and your `balance` is `1,000,000`. -| Param | Required | Type | Description | -| --------- | ----------------------------------------- | -------- | ----------------------------------------------------------------------------------- | -| scAddress | REQUIRED | `string` | The Address (bech32) of the Smart Contract. | -| funcName | REQUIRED | `string` | The name of the Pure Function to execute. | -| args | REQUIRED | `array` | The arguments of the Pure Function, as hex-encoded strings. The array can be empty. | -| caller | OPTIONAL | `string` | The Address (bech32) of the caller. | -| value | OPTIONAL | `string` | The Value to transfer (can be zero). | +:::important +No transaction can start if that account does not exist in the mocked blockchain. More explanations can be found [here](/docs/developers/testing/rust/sc-test-setup.md#setting-accounts). +::: - - +### Deploy -🟢 200: OK +The purpose of the account created previously is to act as the owner of the Crowdfunding smart contract. To make this happen, the **OWNER** constant will serve as the transaction **sender**. -The output value is retrieved successfully. +```rust +const CROWDFUNDING_ADDRESS: TestSCAddress = TestSCAddress::new("crowdfunding"); -```json -{ - "data": "foobar" +#[test] +fn crowdfunding_deploy_test() { + /* + Set up account + */ + + let crowdfunding_address = world + .tx() + .from(OWNER) + .typed(crowdfunding_proxy::CrowdfundingProxy) + .init(500_000_000_000u64) + .code(CODE_PATH) + .new_address(CROWDFUNDING_ADDRESS) + .returns(ReturnsNewAddress) + .run(); } ``` - - +The transaction above is a deploy call that stores in `target` value `500,000,000,000`. It was fictionally submitted by "you", using your account with the address `owner`. +`.new_address(CROWDFUNDING_ADDRESS)` marks that the address of the deployed contracts will be the value stored in the **CROWDFUNDING_ADDRESS** constant. -## POST Get Integer Output of Pure Function {#get-integer-output-of-pure-function} +`.code(CODE_PATH)` explicitly sets the deployment Crowdfunding's code source as bytes. -`https://gateway.multiversx.com/vm-values/int` +:::note +Deploy calls are specified by the code source. You can find more details about what data needs a transaction [here](/docs/developers/transactions/tx-data.md). +::: -This endpoint allows one to execute - with no side-effects - a pure function of a Smart Contract and retrieve the first output value as an integer. +:::important +Remember to run `sc-meta all build` before running the test, especially if you made recent changes to the smart contract source code! Code source will be read directly from the file you specify through the **MxscPath** constant, without rebuilding it automatically. +::: - - +### Checks -Body Parameters +What's the purpose of testing if we do not validate the behavior of the entities interacting with the blockchain? Let's take the next step by enhancing the `crowdfunding_deploy_test()` function to include verification operations. -| Param | Required | Type | Description | -| --------- | ----------------------------------------- | -------- | ----------------------------------------------------------------------------------- | -| scAddress | REQUIRED | `string` | The Address (bech32) of the Smart Contract. | -| funcName | REQUIRED | `string` | The name of the Pure Function to execute. | -| args | REQUIRED | `array` | The arguments of the Pure Function, as hex-encoded strings. The array can be empty. | -| caller | OPTIONAL | `string` | The Address (bech32) of the caller. | -| value | OPTIONAL | `string` | The Value to transfer (can be zero). | +Once the deployment is executed, we will verify if: - - +- The **contract address** is **CROWDFUNDING_ADDRESS**; +- The **owner** has no less EGLD than the value with which it was initialized: `1,000,000`; +- `target` contains the value set at deployment: `500,000,000,000`. -🟢 200: OK +```rust +#[test] +fn crowdfunding_deploy_test() { + /* + Set up account + Deploy + */ -The output value is retrieved successfully. + assert_eq!(crowdfunding_address, CROWDFUNDING_ADDRESS.to_address()); -```json -{ - "data": "2020" + world.check_account(OWNER).balance(1_000_000); + + world + .query() + .to(CROWDFUNDING_ADDRESS) + .typed(crowdfunding_proxy::CrowdfundingProxy) + .target() + .returns(ExpectValue(500_000_000_000u64)) + .run(); } ``` - - - ---- - -### Result Handlers - -## Overview - -Most of the transaction fields are inputs, or work like inputs. The last one of the fields is the one that deals with the outputs. - -There are 3 types of transactions where it comes to outputs: -1. Transactions where we never receive a result, such as those sent via _transfer-execute_. Result handlers are not needed here, in fact, they are inappropriate. -2. Transactions that must finalize before we can move on. Here, various results might be returned, and we can decode them on the spot. Result handlers will determine what gets decoded and how. -3. Transactions that will finalize at an unknown time in the future, such as cross-shard calls from contracts. Here, the result handler is the callback that we register, to be executed as soon as the VM receives the response from that transaction and passes it on to our code. +Notice that there are two accounts now, not just one. There's evidently the account `owner` and the new account `crowdfunding`, as a result of the deployment transaction. -We've had callbacks for a long time, whereas decoders are new. A transaction can have either one, or the other, not both. +:::important +Smart contracts _are_ accounts in the MultiversX Network, accounts with associated code, which can be executed when transactions are sent to them. +::: -We are going to focus on their usage and at the end, also try to explain how they work. +The **owner's balance** remains unchanged - the deployment transaction did not cost anything, because the gas price is set to `0` in the **testing environment**. +:::note +The `.check_account(OWNER)` method verifies whether an account exists at the specified address and checks its ownership details. Details available [here](/docs/developers/testing/rust/sc-blackbox-example.md#check-accounts). +::: +:::note +The `.query()` method is used to interact with the smart contract's view functions via the proxy, retrieving information without modifying the blockchain state. -## Diagram +There is no caller, no payment, and gas price/gas limit. On the real blockchain, a smart contract query does not create a transaction on the blockchain, so no account is needed. Details available [here](/docs/developers/testing/rust/sc-blackbox-calls.md#query). +::: -The result handler diagram is split into two: the callback side, and the decoder side. +## Run test -The decoders are considerably more complex, here we have a simplified version. +Do you want to try it out first? Go ahead and issue this command on your terminal at path `/crowdfunding`: -```mermaid -graph LR - subgraph Result Handlers - rh-unit("()") - rh-unit -->|original_type| rh-ot("OriginalTypeMarker") - rh-ot -->|callback| CallbackClosure -->|gas_for_callback| CallbackClosureWithGas - dh[Decode Handler] - rh-unit -->|"returns
with_result"| dh - rh-ot -->|"returns
with_result"| dh - dh -->|"returns
with_result"| dh - end +```bash +cargo test ``` +If everything went well, you should see the following being printed: + +```rust +running 1 test +test crowdfunding_deploy_test ... ok +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s +``` -## No result handlers +You need to understand the contents of this blackbox test - again, the importance of testing your smart contracts cannot be overstated. -A transaction might have no result handlers attached to it, if: -- The transaction does not return any result data, such as the case for _transfer-execute_ or simple _transfer_ transactions. -- The transaction could return some results, but we are not interested in them. - - If no result handlers are specified, no decoding takes place. - - This is similar to using the `IgnoreValue` return type in the legacy contract call syntax. +## Next up +The tutorial will continue with defining of the `fund`, `claim` and `status` function, and will guide you through writing [blackbox tests](/docs/developers//testing/rust/sc-blackbox-calls.md) for them. +--- -## Original result marker +### Sign + verify a transaction (offline) -Type safety is not only important for inputs, but also for outputs. The first step is signaling what the intended result type is in the original contract, where the endpoint is defined. +Sign a `Transaction` offline with a raw secret key and verify the signature three +ways: `Account.verifyTransactionSignature`, a `UserVerifier` built from the +address, and the raw `UserPublicKey`. Then prove the check works by tampering +with the transaction and watching verification fail. This is the +transaction-signing counterpart to +[Sign a message + verify a signature](/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-message): +no devnet, no broadcast, pure local Ed25519. -Proxies define this type themselves, since they have access to the ABI. +## Prerequisites -If set, the marker will be visible in the transaction type, as an `OriginalResultMarker` result handler. +- Node.js >= 20.13.1. +- Nothing else. This recipe generates its own throwaway keys and never touches + the network. -:::info -The `OriginalResultMarker` does not do anything by itself, it is a zero-size type, with no methods implemented. +## Install -Having only this marker set is no different from having no result handlers specified. It is only a compile-time artifact for ensuring type safety for outputs. -::: +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/sign-verify-transaction +npm install +npm run build +npm start +``` + +## The code + +```ts title="src/index.ts" +// src/index.ts — sign arbitrary data with a UserSigner, then sign a +// Transaction OFFLINE with a raw secret key and verify the signature three +// ways (UserVerifier, UserPublicKey.verify, Account.verifyTransactionSignature). +// Prove the check works by tampering with the transaction and watching +// verification fail. +// +// Fully offline. No devnet, no broadcast — signing and verifying are pure +// local Ed25519 cryptography. This is the transaction-signing counterpart to +// the message-signing recipe. Fresh keys each run; the true/false pattern of +// the output is stable. +// +// Grounded in the installed @multiversx/sdk-core v15.4.1: UserSigner / +// UserVerifier (wallet/), TransactionComputer (core/transactionComputer.d.ts). -Even when we are providing raw data to a transaction, without proxies, we are allowed to specify the original intended result type ourselves. We do this by calling `.original_result()`, with no arguments. If the type cannot be inferred, we need to specify it explicitly, `.original_result::()`. +import { + Account, + Address, + Mnemonic, + Transaction, + TransactionComputer, + UserSigner, + UserVerifier, +} from '@multiversx/sdk-core'; + +async function main(): Promise { + const account = Account.newFromMnemonic(Mnemonic.generate().toString(), 0); + const other = Account.newFromMnemonic(Mnemonic.generate().toString(), 0); + const secretKey = account.secretKey; + const publicKey = account.publicKey; + + // === 1. Sign arbitrary bytes with a UserSigner, verify with UserVerifier. === + // UserSigner wraps a secret key; its sign() is ASYNC (returns a Promise). + // UserVerifier needs only the public key to check the signature. (Step 2 + // below uses the raw secretKey.sign, which is SYNC — the contrast matters.) + const data = new Uint8Array(Buffer.from('arbitrary bytes to authenticate')); + const signer = new UserSigner(secretKey); + const dataSignature = await signer.sign(data); + const verifier = new UserVerifier(publicKey); + console.log(`1. UserSigner-signed data verified by UserVerifier: ${await verifier.verify(data, dataSignature)}`); + + // === 2. Build a transaction and sign it offline with the raw key. === + // account.signTransaction would work too, but doing it by hand shows what + // that method does internally: serialize with computeBytesForSigning, then + // sign those bytes. No network, no nonce fetch — this is a local signature. + const transactionComputer = new TransactionComputer(); + const transaction = new Transaction({ + nonce: 42n, + value: 1000000000000000000n, // 1 EGLD + sender: account.address, + receiver: other.address, + gasLimit: 50000n, + chainID: 'D', + }); + transaction.signature = secretKey.sign(transactionComputer.computeBytesForSigning(transaction)); + console.log(`2. Transaction signed. Signature length: ${transaction.signature.length} bytes.`); + + // === 3. Verify the transaction signature three ways. === + // (a) Account convenience method. + const viaAccount = await account.verifyTransactionSignature(transaction, transaction.signature); + // (b) UserVerifier built from the sender's address alone — the path a + // third party uses, needing only the public address. + const viaVerifier = await UserVerifier.fromAddress(account.address).verify( + transactionComputer.computeBytesForVerifying(transaction), + transaction.signature, + ); + // (c) The raw public key. + const viaPublicKey = await publicKey.verify( + transactionComputer.computeBytesForVerifying(transaction), + transaction.signature, + ); + console.log(`3. Verified via account / UserVerifier / publicKey: ${viaAccount} / ${viaVerifier} / ${viaPublicKey}`); + + // === 4. Tamper with the transaction, keep the signature. === + // Changing any signed field (here, the value) makes the recomputed bytes + // no longer match the signature — verification must return false. + transaction.value = 2000000000000000000n; // 2 EGLD, was 1 + const afterTamper = await account.verifyTransactionSignature(transaction, transaction.signature); + console.log(`4. Verify after changing the value: ${afterTamper} (expected false)`); + + // === 5. A different key cannot verify the (untampered) signature. === + // Restore the value, then check the signature against the wrong address. + transaction.value = 1000000000000000000n; + const wrongSigner = await UserVerifier.fromAddress(other.address).verify( + transactionComputer.computeBytesForVerifying(transaction), + transaction.signature, + ); + console.log(`5. Verify against a different address: ${wrongSigner} (expected false)`); + // A convenience helper so the "wrong-key" address is unmistakably distinct. + const distinct = !account.address.equals(Address.newFromBech32(other.address.toBech32())); + console.log(` (signer and other address are distinct: ${distinct})`); + console.log('\nExpected: steps 1 and 3 all true; steps 4 and 5 false.'); +} +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` -## Default result handler +## Run it -There is a special case of a default result handler in interactors and in tests. +Keys are generated fresh each run; the pattern of `true`/`false` is stable: -Without specifying anything, the framework will check that a transaction is successful. This applies to both interactors and tests (in contracts one cannot recover from a failed sync call, so this mechanism is not necessary). +```text +1. UserSigner-signed data verified by UserVerifier: true +2. Transaction signed. Signature length: 64 bytes. +3. Verified via account / UserVerifier / publicKey: true / true / true +4. Verify after changing the value: false (expected false) +5. Verify against a different address: false (expected false) + (signer and other address are distinct: true) +``` + +The two `false` lines are the point: they prove verification actually checks +something, rather than always returning `true`. + +## How it works + +Grounded in the installed `@multiversx/sdk-core` v15.4.1: `UserSigner` / +`UserVerifier` (`wallet/`) and `TransactionComputer` +(`core/transactionComputer.d.ts`): + +1. **The primitive.** A `UserSigner` wraps the secret key and signs arbitrary + data (async); `new UserVerifier(publicKey).verify(bytes, signature)` checks it, + needing only the public key. +2. **Sign a transaction offline.** Build a `Transaction`, serialize it with + `transactionComputer.computeBytesForSigning(tx)`, and sign those bytes with the + raw key. This is exactly what `account.signTransaction` does internally, no + network, no nonce fetch. +3. **Verify three ways.** `account.verifyTransactionSignature`; a + `UserVerifier.fromAddress(sender)` (the path a third party uses, needing only + the public address); and the raw `publicKey.verify`. +4. **Tamper.** Change the transaction's value; verification returns `false`. +5. **Wrong key.** The signature does not verify against a different address. + +## Pitfalls + +:::warning[Pitfall 1: secretKey.sign is synchronous; the verify methods are async] +`UserSecretKey.sign(bytes)` returns a `Uint8Array` directly (no `await`). But +`UserVerifier.verify`, `UserPublicKey.verify`, `account.verifyTransactionSignature`, +and `account.sign` all return `Promise`s. Mixing these up is an easy +`tsc --strict` error or a silently unawaited promise. +::: -If, however, the developer expects the transaction to fail, this system can easily be overridden by adding an error-related result handler, such as [ExpectError](#expecterror), or [ReturnsStatus](#returnsstatus). +:::note[Pitfall 2: computeBytesForSigning and computeBytesForVerifying match only when the transaction is NOT hash-signed] +For an ordinary transaction (options bit unset) they return identical bytes, +which is why signing one and verifying against the other works here. The moment +you set the hash-signing option, they diverge, see +[Hash-signing a transaction](/sdk-and-tools/sdk-js/cookbook/accounts/hash-signing-transaction). +::: +:::tip[Pitfall 3: any signed field is covered] +Changing the value, receiver, nonce, gas, data, or chainID after signing +invalidates the signature. There is no "unsigned metadata" on a transaction, if +it is serialized, it is signed. +::: +## See also -## Asynchronous callbacks +- [Sign a message + verify a signature](/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-message) + is the message-signing counterpart, using `MessageComputer` instead of + `TransactionComputer`. +- [Hash-signing a transaction](/sdk-and-tools/sdk-js/cookbook/accounts/hash-signing-transaction) + is the options-bit variant, where signing and verifying bytes deliberately + differ. +- [Build a relayed v3 transaction](/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction) + is another accounts-and-signing recipe, devnet-verified. +--- +### Sign a message + verify a signature -## Result decoders +Message signing and verification, fully offline. Unlike every other +transaction-shaped recipe in this Cookbook, there is no network call anywhere in +this one: signing and verifying a message is pure local Ed25519 cryptography over +a deterministic byte encoding. No devnet wallet, no funds, no gas. -Result decoders come in handy when defining exact return types from smart contract endpoints. Being part of the unified syntax, they are consistent through the various environments (smart contract, interact, test) and can be used in combination with each other as long as it makes sense for the specific transaction. +Signing a message proves "this address controls this key and endorsed this exact +text", it does not touch the blockchain, cost gas, or change any state. Common +uses: proving wallet ownership to a backend (see +[Native auth](/sdk-and-tools/sdk-js/cookbook/wallets/native-auth) for the +token-based version of this idea), authorizing an off-chain action, or signing +structured data for a service to verify later. If you need to move funds or call +a contract, see +[Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) +instead. -There are two ways to add a result decoder: via `with_result` or `returns`. +## Prerequisites +- Node.js >= 20.13.1. +- Nothing else. This recipe generates its own throwaway keypair and never touches + the network. -### `with_result` +## Install -The simpler type of result decoder, it doesn't alter the return type of the transaction run method in any way. +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/sign-verify-message +npm install +npm run build +npm start +``` + +## The code + +```ts title="src/index.ts" +// src/index.ts — sign a message two ways (Account, then raw SecretKey), +// verify it two ways (UserVerifier, then Account.verifyMessageSignature), +// then prove verification actually checks something by tampering with +// both the message and the signature and showing verify() return false. +// +// Fully offline. No devnet, no network call of any kind — signing and +// verifying a message is pure local cryptography (Ed25519 over a +// deterministic byte encoding), unlike every transaction-sending recipe +// in this Cookbook. A fresh keypair is generated on every run via +// Mnemonic.generate(), so exact addresses/signatures differ run to run; +// the shape of the output (which booleans print true vs false) does not. +// +// Modeled on the mx-sdk-js-core cookbook source (cookbook/signingObjects.ts +// and cookbook/verifySignatures.ts), which shows both the Account-level and +// the raw-SecretKey-level APIs side by side. -It registers lambdas or similar constructs, which then react to the results as they come. +import { + Account, + Message, + MessageComputer, + Mnemonic, + UserVerifier, +} from '@multiversx/sdk-core'; + +async function main(): Promise { + // --- Setup: a fresh, offline keypair. No PEM file, no network. ----- + const mnemonic = Mnemonic.generate(); + const account = await Account.newFromMnemonic(mnemonic.toString(), 0); + console.log(`Address: ${account.address.toBech32()}`); + + const plaintext = 'Hello MultiversX'; + const messageComputer = new MessageComputer(); + + // === 1. Sign using an Account (the common case — cookbook/signingObjects.ts's + // first message example). === + const message = new Message({ + data: new Uint8Array(Buffer.from(plaintext)), + address: account.address, + }); + message.signature = await account.signMessage(message); + console.log( + `\nSigned with Account. Signature (hex): ${Buffer.from(message.signature).toString('hex').slice(0, 24)}…`, + ); + // === 2. Verify using a UserVerifier built from the address — + // cookbook/verifySignatures.ts's "Verifying Message signature + // using a UserVerifier" section. === + const verifier = UserVerifier.fromAddress(account.address); + const bytesToVerify = messageComputer.computeBytesForVerifying(message); + const isValid = await verifier.verify(bytesToVerify, message.signature); + console.log(`UserVerifier.verify() on the untampered message: ${isValid}`); + + // === 3. The same check via Account's own convenience method — no + // separate UserVerifier needed if you already have an Account + // for the signer (cookbook/verifySignatures.ts's "Sending + // messages over boundaries" section uses this exact method + // after an unpack). === + const isValidViaAccount = await account.verifyMessageSignature( + message, + message.signature, + ); + console.log(`account.verifyMessageSignature() on the same message: ${isValidViaAccount}`); + + // === 4. Prove verification actually checks something: tamper with the + // message text, keep the original signature, and verify again. + // A signature scheme that returned true here would be useless — + // this is the check a recipe that only ever shows the "true" + // path can't prove. === + const tamperedMessage = new Message({ + data: new Uint8Array(Buffer.from(`${plaintext} — but edited`)), + address: account.address, + }); + const tamperedBytes = messageComputer.computeBytesForVerifying(tamperedMessage); + const isTamperedValid = await verifier.verify(tamperedBytes, message.signature); + console.log(`\nVerify a tampered message against the original signature: ${isTamperedValid}`); + + // === 5. Same idea, the other direction: original message, corrupted + // signature byte. === + const corruptedSignature = new Uint8Array(message.signature); + const firstByte = corruptedSignature[0] ?? 0; + corruptedSignature[0] = firstByte ^ 0xff; + const isCorruptedValid = await verifier.verify(bytesToVerify, corruptedSignature); + console.log(`Verify the original message against a corrupted signature: ${isCorruptedValid}`); + + // === 6. Signing directly with a SecretKey derived from the mnemonic, + // no Account wrapper — cookbook/signingObjects.ts's "Signing a + // Message using an SecretKey" section (that source uses + // UserSecretKey.fromString(hex); this recipe derives from the + // same fresh mnemonic at a second index instead, to stay + // offline without a hardcoded key). Useful when you're holding + // raw key material rather than a PEM/keystore-backed Account. === + const secretKey = mnemonic.deriveKey(1); + const publicKey = secretKey.generatePublicKey(); + const rawMessage = new Message({ + data: new Uint8Array(Buffer.from(plaintext)), + address: publicKey.toAddress(), + }); + const serialized = messageComputer.computeBytesForSigning(rawMessage); + rawMessage.signature = await secretKey.sign(serialized); + const rawVerifier = new UserVerifier(publicKey); + const rawIsValid = await rawVerifier.verify( + messageComputer.computeBytesForVerifying(rawMessage), + rawMessage.signature, + ); + console.log(`\nSigned + verified via raw SecretKey/UserVerifier (no Account): ${rawIsValid}`); + + // === 7. Packing/unpacking for sending across a boundary (a service + // call, a file, a QR code) — cookbook/verifySignatures.ts's + // "Sending messages over boundaries" section. === + const packed = messageComputer.packMessage(message); + const unpacked = messageComputer.unpackMessage(packed); + // Message.signature is typed Uint8Array | undefined (a Message can + // exist unsigned, before signing) — a real tsc --strict error if you + // pass it straight through without narrowing first. + if (!unpacked.signature) { + throw new Error('Unpacked message has no signature.'); + } + const isUnpackedValid = await account.verifyMessageSignature( + unpacked, + unpacked.signature, + ); + console.log(`\nPack -> unpack -> verify round-trip: ${isUnpackedValid}`); + console.log( + '\nExpected: five "true" lines above (2, 3, 6, 7) and two "false" lines (4, 5).', + ); +} -### `returns` +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` -Adding a result handler via `result` causes the transaction run function to return various versions of the result, as indicated by the result handler. +## Run it -For instance, [ReturnsResult](#returnsresult) causes the deserialized result of the transaction to be returned. +Addresses and signatures are freshly generated every run; the pattern of +`true`/`false` lines is not: -If we add multiple result handlers, we will get a tuple with all the requested results. +```text +Address: erd1xg4xglsa43epnp5v2k45anqcejzdqwug86kug52tt3mq7k7vtt2qm0spu4 -:::info -The return type is determined at compile time, with no runtime or bytecode size overhead. -::: +Signed with Account. Signature (hex): e997e384490ad843899b1047… +UserVerifier.verify() on the untampered message: true +account.verifyMessageSignature() on the same message: true -In the examples below, the return type is stated explicitly, for clarity. Please note that, because of type inference, they barely ever need to be specified like this. +Verify a tampered message against the original signature: false +Verify the original message against a corrupted signature: false -All examples assume the variable `tx` already has all inputs constructed for it. Let's also assume that the original result type is `MyResult`. +Signed + verified via raw SecretKey/UserVerifier (no Account): true -```rust title="No decoder" -let _: () = tx - .run(); +Pack -> unpack -> verify round-trip: true ``` -The return type here is `()` nothing is deserialized. +The two `false` lines are the important ones: they are the proof this recipe's +verification step actually checks something, rather than always returning `true`. -```rust title="One decoder" -let r: MyResult = tx - .returns(ReturnsResult) - .run(); -``` +## How it works -Here, we get the transaction result returned. You might see this in a contract, interactor, or test. +Modeled on the mx-sdk-js-core cookbook source (`cookbook/signingObjects.ts` and +`cookbook/verifySignatures.ts`), which shows both an Account-level and a +raw-SecretKey-level path side by side: -```rust title="Two decoders" -let r: (MyResult, BackTransfers) = tx - .returns(ReturnsResult) - .returns(ReturnsBackTransfers) - .run(); -``` +1. **Generate a fresh keypair.** `Mnemonic.generate()` + `Account.newFromMnemonic()`. +2. **Sign with the Account.** `account.signMessage(message)`, the address is part + of what gets signed, via `MessageComputer`. +3. **Verify with a `UserVerifier`.** Built from the address alone, the path a + *different* party (a backend, a service) uses, never needing the key. +4. **Verify with `account.verifyMessageSignature()`.** A shortcut when the + verifying code already holds the same `Account`. +5. **Tamper with the message, then the signature.** Both correctly return `false`. +6. **Sign directly with a `SecretKey`**, no `Account` wrapper, the lower-level + path for raw key material. +7. **Pack, unpack, verify.** Round-trips a message for transmission across a + boundary (a service call, a QR code, a file). -This time we want two values out. The framework packs them in a pair, behind the scenes. The order of the values in the tuple is always the same as the order the result handlers were passed. If we pass them in reverse order, we also get the output in reverse order: +## In a dApp (browser wallet, not verified live here) -```rust title="Same two decoders, reverse order" -let r: (BackTransfers, MyResult) = tx - .returns(ReturnsBackTransfers) - .returns(ReturnsResult) - .run(); -``` +`mx-template-dapp`'s `SignMessage` widget shows the same call from a connected +browser wallet: `provider.signMessage(messageToSign)` opens the wallet's own +confirmation UI instead of signing silently. This recipe doesn't exercise that +path end-to-end, it needs a real wallet extension or xPortal session. The +verification side (`UserVerifier`) is identical regardless of which side signed. -This mechanism works with any number of result handlers. _(There is a limit of 16 for now, in the unlikely case that anybody will need more, it can easily be increased.)_ +## Pitfalls -```rust title="Three decoders" -let r: (ManagedVec, ManagedAddress, BackTransfers) = tx - .returns(ReturnsRawResult) - .returns(ReturnsNewManagedAddress) - .returns(ReturnsBackTransfers) - .run(); -``` +:::warning[Pitfall 1: Message.signature is Uint8Array | undefined, not always present] +A `Message` can exist unsigned. Passing an unsigned message's `.signature` +straight into a function expecting `Uint8Array` is a real `tsc --strict` failure, +narrow with a null check first. +::: -There is no limitation that the same decoder cannot be used multiple times, although it makes little sense in practice: +:::note[Pitfall 2: the address is part of the signed payload, not a side channel] +`MessageComputer` folds the message's `address` field into the bytes that get +signed/verified. A verifier built from the wrong address reports `false` even +against an untampered message + signature pair. +::: -```rust title="Duplicate decoders" -let r: (MyResult, MyResult, Address, MyResult) = tx - .returns(ReturnsResult) - .returns(ReturnsResult) - .returns(ReturnsNewAddress) - .returns(ReturnsResult) - .run(); -``` +:::tip[Pitfall 3: a UserVerifier only needs the address, never the secret key] +That's the entire point of asymmetric signing, don't design a verification flow +that requires shipping key material to the verifying party. +::: -Methods `returns` and `with_result` can be interspersed in any order. Calls to `with_result` will not affect the return type. +:::danger[Pitfall 4: this recipe doesn't sign a transaction] +Message signing and transaction signing use related but distinct primitives +(`MessageComputer` vs `TransactionComputer`), see +[Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) +for the transaction case. +::: -```rust title="returns + with_result" -let r: Address = tx - .returns(ReturnsAddress) - .with_result(WithResultRaw(|raw|) assert!(raw.is_empty())) - .run(); -``` +## See also -Also adding an example with only `with_result`, something you are likely to see in black-box tests. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the transaction-signing counterpart to this recipe. +- [Native auth: token issuance, expiry, auto-logout](/sdk-and-tools/sdk-js/cookbook/wallets/native-auth) + is a productized use of message-adjacent signing. +- [Build a relayed v3 transaction](/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction) + is another accounts-and-signing recipe, devnet-verified instead of offline. -```rust title="No return, with_result" -let r: () = tx - .with_result(ExpectError(4, "sample error")) - .run(); -``` +--- +### Sign and perform a multisig action +Once an action is proposed (see +[Propose a multisig action](/sdk-and-tools/sdk-js/cookbook/multisig/propose-action)), +it sits pending until board members **sign** it up to the contract's **quorum**, +and then anyone can **perform** it, the moment it actually executes and funds move +or the board changes. Both `sign` and `performAction` take only the action id. +This recipe shows each via the controller and factory, plus the read helpers you +use to decide whether an action is ready. -## List of result decoders +The default `npm start` reads the live signer-versus-quorum state of a real devnet +multisig's first pending action, so you see the readiness check work without a +funded wallet. -There are various predefined types of result decoders: +## Prerequisites +- Node.js >= 20.13.1. +- For the default read and `payload` demos: devnet network access only. +- For an actual sign or perform: a devnet PEM whose address is a board member on + the target multisig, with a little EGLD for gas. +## Install +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/multisig-sign-perform-action +npm install +npm run build +``` -### `ReturnsRawResult` +## Signing and performing + +```ts title="src/signPerform.ts" +// src/signPerform.ts - the subject of this recipe: the second half of the +// multisig lifecycle. Once an action has been proposed (see the propose +// recipe), board members must SIGN it until it reaches quorum, and then anyone +// can PERFORM it - which is the moment it actually executes. +// +// propose -> action id N created, 1 signer (the proposer, if a board member) +// sign -> each board member adds their signature to id N +// perform -> once signers >= quorum, execute id N (the funds move / the +// board changes / the call fires) +// +// Both `sign` and `performAction` take just the action id. This file shows +// each via the controller and the factory, plus the read helpers you use to +// decide whether an action is ready to perform. + +import { Account, Address } from '@multiversx/sdk-core'; +import type { Abi, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +// Signing is cheap; performing must also fund the wrapped call, so give it +// more. Gas is REQUIRED for both (see Pitfalls) - never rely on a default. +const SIGN_GAS_LIMIT = 8_000_000n; +const PERFORM_GAS_LIMIT = 12_000_000n; + +/** Sign (approve) an action, path 1 - the controller. */ +export async function signViaController( + entrypoint: DevnetEntrypoint, + abi: Abi, + signer: Account, + multisigContract: string, + actionId: number, +): Promise { + const controller = entrypoint.createMultisigController(abi); + return controller.createTransactionForSignAction(signer, signer.getNonceThenIncrement(), { + multisigContract: Address.newFromBech32(multisigContract), + actionId, + gasLimit: SIGN_GAS_LIMIT, + }); +} -Returns: `ManagedVec>`, representing the raw data result from the call. +/** Sign (approve) an action, path 2 - the factory (you set nonce + signature). */ +export async function signViaFactory( + entrypoint: DevnetEntrypoint, + abi: Abi, + signer: Account, + multisigContract: string, + actionId: number, +): Promise { + const factory = entrypoint.createMultisigTransactionsFactory(abi); + const transaction = await factory.createTransactionForSignAction(signer.address, { + multisigContract: Address.newFromBech32(multisigContract), + actionId, + gasLimit: SIGN_GAS_LIMIT, + }); + transaction.nonce = signer.getNonceThenIncrement(); + transaction.signature = await signer.signTransaction(transaction); + return transaction; +} + +/** Perform (execute) an action that has reached quorum, path 1 - the controller. */ +export async function performViaController( + entrypoint: DevnetEntrypoint, + abi: Abi, + performer: Account, + multisigContract: string, + actionId: number, +): Promise { + const controller = entrypoint.createMultisigController(abi); + return controller.createTransactionForPerformAction(performer, performer.getNonceThenIncrement(), { + multisigContract: Address.newFromBech32(multisigContract), + actionId, + gasLimit: PERFORM_GAS_LIMIT, + }); +} -```rust title=contract.rs -#[endpoint] -fn deploy_contract( - &self, - code: ManagedBuffer, - code_metadata: CodeMetadata, - args: MultiValueEncoded, -) -> ManagedVec { - self.tx() - .raw_deploy() - .code(code) - .code_metadata(code_metadata) - .arguments_raw(args.to_arg_buffer()) - .returns(ReturnsRawResult) - .sync_call() - .into() +/** Perform (execute) an action that has reached quorum, path 2 - the factory. */ +export async function performViaFactory( + entrypoint: DevnetEntrypoint, + abi: Abi, + performer: Account, + multisigContract: string, + actionId: number, +): Promise { + const factory = entrypoint.createMultisigTransactionsFactory(abi); + const transaction = await factory.createTransactionForPerformAction(performer.address, { + multisigContract: Address.newFromBech32(multisigContract), + actionId, + gasLimit: PERFORM_GAS_LIMIT, + }); + transaction.nonce = performer.getNonceThenIncrement(); + transaction.signature = await performer.signTransaction(transaction); + return transaction; +} + +export interface SignPerformPayload { + function: string; + actionIdHex: string; + receiver: string; + gasLimit: bigint; +} + +/** Decode a sign/perform transaction: the function name plus the action id. */ +export function describeSignPerformPayload(transaction: Transaction): SignPerformPayload { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + function: parts[0] ?? '', + actionIdHex: parts[1] ?? '', + receiver: transaction.receiver.toBech32(), + gasLimit: transaction.gasLimit, + }; } -``` +export interface ActionReadiness { + quorum: number; + validSigners: number; + signers: string[]; + quorumReached: boolean; +} + +/** + * Read whether an action is ready to perform: how many valid board-member + * signatures it has versus the quorum. This is exactly what you check before + * spending gas on a perform that would revert. + */ +export async function readActionReadiness( + entrypoint: DevnetEntrypoint, + abi: Abi, + multisigAddress: string, + actionId: number, +): Promise { + const controller = entrypoint.createMultisigController(abi); + const [quorum, validSigners, signers, quorumReached] = await Promise.all([ + controller.getQuorum({ multisigAddress }), + controller.getActionValidSignerCount({ multisigAddress, actionId }), + controller.getActionSigners({ multisigAddress, actionId }), + controller.quorumReached({ multisigAddress, actionId }), + ]); + // SDK trap: getActionSigners is declared `Promise` but actually + // returns `Address[]` at runtime (v15.4.1). Handle both so this stays correct + // whichever way the SDK settles it. See Pitfalls on the recipe page. + const signerAddresses = signers as Array; + return { + quorum, + validSigners, + signers: signerAddresses.map((address) => (typeof address === 'string' ? address : address.toBech32())), + quorumReached, + }; +} +``` +## Run it -### `ReturnsResult` +```bash +# Read the first pending action's signers vs quorum - no wallet, no funds: +npm start -Returns: the original type from the function signature. The exact original type is extracted from the return type of the corresponding function from the proxy. +# Inspect the sign and perform wire payloads offline: +npm start -- payload -```rust title=interact.rs -async fn quorum_reached(&mut self, action_id: usize) -> bool { - self.interactor - .query() - .to(self.state.current_multisig_address()) - .typed(multisig_proxy::MultisigProxy) - .quorum_reached(action_id) - .returns(ReturnsResult) // knows from the original type marker that the expected return type is bool - .prepare_async() - .run() - .await -} +# Actually sign action 4 (needs a board-member devnet PEM); --perform to perform, +# --factory for the factory path: +npm start -- sign ./wallet.pem ``` +Output of the default read mode, and of `payload`: +```text +Reading pending actions of erd1qqqqqqqqqqqqqpgqvjcswsav2rw4ccvguyuk0f0u6qycpsuvu3dq9rse9w ... + first pending action: id 4 (SendTransferExecuteEgld) + valid signers / quorum: 1 / 2 + quorum reached (ready to perform): false + signers so far: erd1d4q9a3qx5695gwd02rap2qqad3hv7lga7s06y5e6m3shuy6pu3dqagj7qe + +sign: sign@04 (actionId 4 = 0x04) gasLimit 8000000 +performAction: performAction@04 (actionId 4 = 0x04) gasLimit 12000000 +receiver: erd1qqqqqqqqqqqqqpgqvjcswsav2rw4ccvguyuk0f0u6qycpsuvu3dq9rse9w (the multisig contract, for both) +``` + +## How it works + +**Controller vs factory.** +`controller.createTransactionForSignAction(account, nonce, { multisigContract, actionId, gasLimit })` +builds, sets the nonce, and signs; the factory form only builds and you sign. +`performAction` is identical in shape. Both need the multisig ABI at construction. + +**Check readiness before performing.** `getActionValidSignerCount` and +`quorumReached` tell you whether an action can be performed. Performing below quorum +reverts and wastes gas, so the recipe reads `validSigners / quorum` first. Note the +proposer's own signature usually counts as the first one when they are a board +member. + +**Perform is the moment of execution.** Signing only records approval; +`performAction` is what fires the wrapped call, moves the EGLD, or applies the board +change. Give perform enough gas to also run whatever it wraps (this recipe uses +12M). + +## Pitfalls + +:::warning[Pitfall 1: getActionSigners is typed as a string array but returns Address objects] +`MultisigController.getActionSigners` is declared `Promise`, but at +runtime (sdk-core v15.4.1) it returns an array of `Address` objects, not bech32 +strings. TypeScript will let you write `.length` and index it, then `String(x)` +yields `[object Object]`. The recipe handles both forms defensively. +`getPendingActionFullInfo(...).signers`, by contrast, is correctly typed +`Address[]`. +::: -### `ReturnsResultUnmanaged` +:::warning[Pitfall 2: gas is required; the controller does NOT protect you] +As with proposing, both `sign` and `performAction` need an explicit `gasLimit`. The +factory throws without one; the **controller silently coerces a missing gasLimit to +`0n`** and builds a `gasLimit: 0` transaction the network rejects. Confirmed against +sdk-core v15.4.1. Always pass `gasLimit`. +::: -Returns: the unmanaged version of the original result type. This relies on the `Unmanaged` associated type in `TypeAbi`. +:::note[Pitfall 3: performing below quorum reverts] +`performAction` succeeds only when valid signers >= quorum. Calling it early reverts +on-chain (the SDK builds it happily). Read `quorumReached` first. For the final +signer, `createTransactionForSignAndPerform` signs and performs in one transaction, +saving a round trip. +::: -For example: +:::note[Pitfall 4: only current board members' signatures count] +`getActionValidSignerCount` counts only signers who are still board members; a +signer removed from the board no longer counts toward quorum. That is why it can be +lower than the raw `getActionSigners` length. +::: -| Managed type | Unmanaged version | -| --------------------- | ------------------------------------- | -| Managed `BigUint` | Rust `BigUint` (alias: `RustBigUint`) | -| Managed `BigInt` | Rust `BigInt` (alias: `RustBigInt`) | -| `ManagedBuffer` | `Vec` | -| `ManagedAddress` | `Address` | -| `ManagedVec` | `Vec` | -| `ManagedOption` | `Option` | -| `ManagedByteArray` | `[u8; N]` | -| `BigFloat` | `f64` | +## See also -Also, most generic container types (`Option`, `Vec`, etc.) will point to themselves, but with the unmanaged version of their contents. +- [Propose a multisig action](/sdk-and-tools/sdk-js/cookbook/multisig/propose-action) + creates the action id this recipe signs and performs. +- [Read a multisig's state](/sdk-and-tools/sdk-js/cookbook/multisig/read-multisig-state) + is the full read surface behind the readiness check here. +- [Delegate (stake) EGLD](/sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake) + is another sdk-core controller/factory write, for comparison. -For all other types, it returns the original type, same as `ReturnsResult`. +--- -It is especially useful in interactor and test environments, as it allows us to avoid performing additional conversions. +### Sign and send a transaction (the working path) -```rust title=interact.rs -async fn get_sum(&mut self) -> RustBigUint { - self - .interactor - .query() - .to(self.state.current_adder_address()) - .typed(adder_proxy::AdderProxy) - .sum() // original return type is multiversx_sc::types::BigUint - .returns(ReturnsResultUnmanaged) // converts into num_bigint::BigUint - .prepare_async() - .run() - .await -} -``` +This is the canonical "I have a wallet, now what?" recipe, the foundation every +other transaction recipe links from. Build a `Transaction` with sdk-core, sign +with the connected provider, send and track with `TransactionManager`. The whole +pattern is three calls. -In this case, the original return type of the endpoint `sum` is `multiversx_sc::types::BigUint` which is a managed type. `ReturnsResultUnmanaged` automatically provides us with `num_bigint::BigUint`, a much more accessible type for the interactor, where we want to avoid constantly having to specify the API. +In v4, signing was a React hook (`useSignTransactions`). In v5, signing is on the +provider itself (`provider.signTransactions(txs)`), a deliberate change because +the provider already knows the user's signing surface (extension popup, Ledger +USB, WalletConnect mobile prompt). This recipe is the v5-current pattern. +If you do not yet have a working sdk-dapp v5 setup, start with +[Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) +or +[Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal), +both of which end where this recipe begins. +## Prerequisites -### `ReturnsStatus` +- A working sdk-dapp v5 setup (Next.js or Vite, see the start-here recipes). +- A logged-in account on devnet. +- A few devnet EGLD ([faucet](https://r3d4.fr/faucet) or + [devnet wallet](https://devnet-wallet.multiversx.com)). -Returns: the transaction status as u64. +## Install -Especially useful in the testing and interactor environments. +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/sign-and-send +npm install +npm run dev +# open https://localhost:3000 +``` + +The recipe is a Next.js app, but the load-bearing file (`lib/transactions.ts`) is +framework-agnostic. Copy it into a Vite app, a Remix loader, a CLI script, or +anywhere the sdk-dapp store has been initialized. + +## The three-step pattern + +The whole flow lives in `lib/transactions.ts`. Read this once and you have the v5 +transaction pattern. + +```ts title="lib/transactions.ts" +// lib/transactions.ts — the canonical sign / send / track flow for sdk-dapp v5. +// +// This file demonstrates the three-step pattern that every transaction in a +// v5 dApp follows: +// +// 1. BUILD with sdk-core's Transaction constructor. +// Pull sender, nonce, and chainID from the sdk-dapp +// store via getAccount() and getNetworkConfig() — both are non-React +// synchronous reads. +// 2. SIGN via the connected provider. +// In v5 signing is on the provider, not a hook — this is the v4 → v5 +// semantic change documented in the v5 changelog. +// 3. SEND + TRACK with TransactionManager. +// The returned sessionId is the React-hook key for +// useGetPendingTransactions(sessionId). +// +// The function below is intentionally framework-agnostic. It reads from the +// sdk-dapp store directly (no hooks), so you can call it from a React +// onClick, a server action, a CLI script, or anywhere else. -```rust title=blackbox_test.rs -#[test] -fn status_test() { - let mut world = setup(); +import { Address, Transaction } from '@multiversx/sdk-core'; +import { GAS_PRICE, GAS_LIMIT } from '@multiversx/sdk-dapp/out/constants/mvx.constants'; +import { TransactionManager } from '@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager'; +import { getAccount } from '@multiversx/sdk-dapp/out/methods/account/getAccount'; +import { getNetworkConfig } from '@multiversx/sdk-dapp/out/methods/network/getNetworkConfig'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import type { SignedTransactionType } from '@multiversx/sdk-dapp/out/types/transactions.types'; + +/** + * Inputs for an EGLD transfer. Amount is in the smallest denomination — + * 1 EGLD = 10^18. + * + * Use bigint everywhere. Never `Number` — JavaScript loses precision past + * 2^53 and EGLD amounts go higher than that all the time. + */ +export interface SendEgldInput { + /** Bech32 address of the recipient. */ + receiver: string; + /** Amount in the smallest denomination (10^18 = 1 EGLD). */ + amountInSmallestDenomination: bigint; + /** Optional UTF-8 data field. Note: data adds gas. */ + data?: string; +} + +/** + * Output of a successful sign + send + track call. The sessionId is the key + * the React hooks (useGetPendingTransactions, etc.) use to render UI state. + */ +export interface SendEgldOutput { + sessionId: string; + transactionHash: string; +} + +/** + * Build, sign, send, and start tracking an EGLD transfer. + * + * Throws if no account is connected. The caller is responsible for ensuring + * useGetIsLoggedIn() is true before invoking — the function does not + * gracefully wait for login. + */ +export async function sendEgld(input: SendEgldInput): Promise { + const account = getAccount(); + const { network } = getNetworkConfig(); + + if (!account.address) { + throw new Error('No account connected. Call after a successful login.'); + } - let status = world - .tx() - .from(OWNER_ADDRESS) - .to(SC_ADDRESS) - .typed(proxy::ContractProxy) - .some_endpoint() - .returns(ReturnsStatus) - .run(); + // Step 1 — BUILD. + // Note: account.nonce is the network's last-known nonce. If you sent any + // transactions earlier in this session you must increment locally — see + // Pitfall 1 on this page. + // + // The canonical Transaction constructor shape: value/gasLimit/nonce are + // bigint, data is Uint8Array. + const tx = new Transaction({ + sender: Address.newFromBech32(account.address), + receiver: Address.newFromBech32(input.receiver), + value: input.amountInSmallestDenomination, + gasLimit: BigInt(computeGasLimit(input.data)), + gasPrice: BigInt(GAS_PRICE), + chainID: network.chainId, + nonce: BigInt(account.nonce), + // Version 1 is the unsigned-transaction default; sdk-dapp managers handle + // hash-signing version bumps internally where needed. + version: 1, + // data is omitted via spread when undefined to play nicely with + // exactOptionalPropertyTypes (strict mode rejects passing + // `undefined` to a property that is `Uint8Array` not `Uint8Array | undefined`). + ...(input.data ? { data: new Uint8Array(Buffer.from(input.data)) } : {}), + }); - assert_eq!(status, 4); // status 4 - user error -} -``` + // Step 2 — SIGN. + // The provider knows the connected wallet's signing surface (extension + // popup / Ledger USB / WalletConnect mobile prompt). signTransactions + // returns the same array shape it was given, with each tx now carrying a + // .signature field. + const provider = getAccountProvider(); + const [signed] = await provider.signTransactions([tx]); + + if (!signed) { + // Defensive: if the user cancelled the wallet popup, signTransactions + // throws or returns an empty array depending on the provider. + throw new Error('User cancelled signing.'); + } + // Step 3 — SEND + TRACK. + // TransactionManager.send()'s type signature is shape-symmetric: pass a + // flat Transaction[] (one batch), get a flat SignedTransactionType[] back; + // pass Transaction[][] (multiple batches), get SignedTransactionType[][] + // back (node_modules/@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager.d.ts). + // We always pass a single flat batch, so the nested variant cannot occur + // here — narrow explicitly (isFlatSentTransactions below) rather than + // casting, so a real shape change fails loudly instead of miscompiling. + // + // track() takes that SAME array (not a single unwrapped element) — see the + // TransactionManager.d.ts JSDoc example, which calls + // `txManager.track(sentTransactions, {...})` on the whole array. + const txManager = TransactionManager.getInstance(); + const sentTransactions = await txManager.send([signed]); + + if (!isFlatSentTransactions(sentTransactions)) { + throw new Error('Unexpected response shape from TransactionManager.send().'); + } + const [sent] = sentTransactions; + if (!sent) { + throw new Error('Send failed: TransactionManager returned no transactions.'); + } -### `ReturnsMessage` + const sessionId = await txManager.track(sentTransactions, { + transactionsDisplayInfo: { + processingMessage: 'Sending EGLD…', + successMessage: 'Transfer complete.', + errorMessage: 'Transfer failed.', + }, + }); -Returns: the transaction error message as String. + // `SignedTransactionType` already carries `hash: string` (see + // node_modules/@multiversx/sdk-dapp/out/types/transactions.types.d.ts). + // sdk-dapp's own status-polling joins on this same field internally, so + // it is the authoritative hash — no separate computation needed. + // + // Earlier drafts of this recipe called + // `new TransactionComputer().computeTransactionHash(sent)` and hex-encoded + // the result via `Buffer.from(...)`. Both are wrong for this SDK version: + // computeTransactionHash() expects an sdk-core `Transaction` instance, not + // the `SignedTransactionType` plain object `send()` returns here (that's a + // real tsc --strict error, not just a style nit) — and its return value is + // already a hex `string`, not a `Uint8Array` + // (node_modules/@multiversx/sdk-core/out/core/transactionComputer.js), so + // re-wrapping it in `Buffer.from(str).toString('hex')` would silently + // double-encode it into a wrong, longer hex string. + const transactionHash = sent.hash; + + return { + sessionId, + transactionHash, + }; +} -Especially useful in the testing and interactor environments, +/** + * Type guard narrowing TransactionManager.send()'s union return type down to + * the flat (single-batch) variant. We only ever call send() with a single + * flat Transaction[] batch, so this should always hold at runtime; it exists + * so the compiler (not an assertion) enforces that invariant. + */ +function isFlatSentTransactions( + value: SignedTransactionType[] | SignedTransactionType[][], +): value is SignedTransactionType[] { + return value.length === 0 || !Array.isArray(value[0]); +} + +/** + * Compute a reasonable gas limit for a plain or data-bearing EGLD transfer. + * + * The default GAS_LIMIT (50_000) covers the empty-data case. For data + * transactions, the network charges 1500 gas per byte. + */ +function computeGasLimit(data: string | undefined): number { + const baseLimit = GAS_LIMIT; + if (!data) { + return baseLimit; + } + const dataBytes = Buffer.byteLength(data, 'utf8'); + // 1500 gas per byte of data. Add some headroom (10%) so a slight protocol + // change doesn't immediately invalidate every recipe — CI catches drift. + return Math.ceil((baseLimit + dataBytes * 1500) * 1.1); +} +``` + +The three steps: + +1. **Build** with sdk-core. `Transaction` is the value type; + `Address.newFromBech32()` parses the bech32 strings. `getAccount()` and + `getNetworkConfig()` are non-React reads of the sdk-dapp store; they work from + any context as long as `initApp()` has resolved. +2. **Sign** with the provider. `getAccountProvider()` returns the connected + provider; calling `.signTransactions([tx])` opens the wallet UI and resolves + with the same array, now signature-bearing. +3. **Send and track** with `TransactionManager`. The singleton's `.send()` POSTs + to the network; `.track()` starts the WebSocket-or-polling tracker that updates + the Zustand store as the tx transitions pending to success/fail. The returned + `sessionId` is the lookup key for the React hooks. + +## The React hook + +A thin wrapper around `sendEgld()` with status / error state for `onClick` use: + +```ts title="lib/useSendEgld.ts" +// lib/useSendEgld.ts — React hook around the framework-agnostic sendEgld(). +// +// Wraps lib/transactions.ts in a stateful hook so a component can render a +// "Send" button, drive it from local state, and surface in-flight / success / +// error UI. +// +// Why a hook? sendEgld() is callable from anywhere, but most UI calls want: +// - a loading flag while the wallet popup is open +// - the sessionId, so other parts of the page can use +// useGetPendingTransactions(sessionId) to render per-tx state +// - a stable reference to the send function for use in onClick + +import { useCallback, useState } from 'react'; +import { sendEgld, type SendEgldInput, type SendEgldOutput } from './transactions'; + +export interface UseSendEgldState { + status: 'idle' | 'signing' | 'sending' | 'tracking' | 'done' | 'error'; + sessionId: string | null; + transactionHash: string | null; + error: string | null; +} + +const initialState: UseSendEgldState = { + status: 'idle', + sessionId: null, + transactionHash: null, + error: null, +}; -```rust title=blackbox_test.rs -#[test] -fn status_and_message_test() { - let mut world = setup(); +export interface UseSendEgld { + state: UseSendEgldState; + send: (input: SendEgldInput) => Promise; + reset: () => void; +} + +export function useSendEgld(): UseSendEgld { + const [state, setState] = useState(initialState); + + const send = useCallback( + async (input: SendEgldInput): Promise => { + setState({ ...initialState, status: 'signing' }); + try { + // The status doesn't get a chance to update between signing and + // sending in any visually meaningful way; we keep it simple. + const out = await sendEgld(input); + setState({ + status: 'done', + sessionId: out.sessionId, + transactionHash: out.transactionHash, + error: null, + }); + return out; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + setState({ + status: 'error', + sessionId: null, + transactionHash: null, + error: message, + }); + return null; + } + }, + [], + ); - let (status, message) = world - .tx() - .from(OWNER_ADDRESS) - .to(SC_ADDRESS) - .typed(proxy::ContractProxy) - .some_endpoint() - .returns(ReturnsStatus) - .returns(ReturnsMessage) - .run(); + const reset = useCallback((): void => { + setState(initialState); + }, []); - assert_eq!(status, 4); // status 4 - user error - assert_eq!(message, "test"); // error message - test + return { state, send, reset }; } ``` +## The page +The form: receiver address, amount in EGLD (decimal, converted internally to the +smallest denomination), submit button. It pulls login state and pending-transaction +count from the sdk-dapp hooks, so the UI reflects the same store the manager writes to. -### `ReturnsNewBech32Address` - -Returns: the newly deployed address after a deploy, as `Bech32Address`. +```tsx title="app/page.tsx" +'use client'; -Used in the testing and interactor environments. +// app/page.tsx — sign + send + track demo page. +// +// Three states: +// 1. Logged out → "Connect wallet" button (delegates to UnlockPanelManager). +// 2. Logged in, idle → form: receiver address + amount in EGLD. +// 3. Logged in, post-send → status display: hash, sessionId, success/error. +// +// The form's amount input is in EGLD (decimal), but lib/transactions.ts +// expects the smallest denomination (10^18). We do the conversion inline — +// see toSmallestDenomination() below — so the user can think in EGLD while +// the SDK still gets a precise bigint. -```rust title=interact.rs -async fn deploy(&mut self) -> Bech32Address { - self - .interactor - .tx() - .from(&self.wallet_address) - .typed(adder_proxy::AdderProxy) - .init(0u32) // deploys adder contract - .code(&self.adder_code) - .returns(ReturnsNewBech32Address) // returns newly deployed address as Bech32Address - .prepare_async() - .run() - .await -} -``` +import { useState } from 'react'; +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/out/react/account/useGetIsLoggedIn'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +import { useGetPendingTransactions } from '@multiversx/sdk-dapp/out/react/transactions/useGetPendingTransactions'; +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import { useSendEgld } from '../lib/useSendEgld'; + +export default function SendPage(): JSX.Element { + const isLoggedIn = useGetIsLoggedIn(); + const account = useGetAccount(); + const { network } = useGetNetworkConfig(); + const { state, send, reset } = useSendEgld(); + + // useGetPendingTransactions() returns SignedTransactionType[] — a flat + // array of currently-pending transactions, NOT a sessionId-keyed map (see + // node_modules/@multiversx/sdk-dapp/out/react/transactions/useGetPendingTransactions.d.ts). + // If you need session-keyed + // lookups instead, use useGetPendingTransactionsSessions(), which returns + // Record. + const pending = useGetPendingTransactions(); + const pendingCount = pending.length; + + const [receiver, setReceiver] = useState(''); + const [amountEgld, setAmountEgld] = useState('0.001'); + + const handleConnect = (): void => { + // openUnlockPanel() returns Promise (see + // node_modules/@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager.d.ts). + // This handler is a sync onClick, so we explicitly discard the promise — + // matching the eslint no-floating-promises gate. + void UnlockPanelManager.getInstance().openUnlockPanel(); + }; + const handleLogout = async (): Promise => { + const provider = getAccountProvider(); + await provider.logout(); + }; + const handleSubmit = async (e: React.FormEvent): Promise => { + e.preventDefault(); + let amountInSmallest: bigint; + try { + amountInSmallest = toSmallestDenomination(amountEgld); + } catch (err) { + // eslint-disable-next-line no-console + console.error('Bad amount:', err); + return; + } + await send({ + receiver, + amountInSmallestDenomination: amountInSmallest, + }); + }; -### `ReturnsNewManagedAddress` + return ( +
+

Sign and send a transaction

+

+ Network: {network.chainId} +

+ + {!isLoggedIn ? ( + + ) : ( + <> +

+ Connected as {account.address} +

+ + +
{ + void handleSubmit(e); + }} + style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }} + > + + + +
+ + + + )} +
+ ); +} -Returns: the newly deployed address after a deploy, as `Bech32Address`. +interface ResultProps { + state: ReturnType['state']; + reset: () => void; + pending: number; +} -Used in the smart contract environments. +function Result({ state, reset, pending }: ResultProps): JSX.Element | null { + if (state.status === 'idle') { + return null; + } + return ( +
+

Result

+

+ Status: {state.status} · Pending in store:{' '} + {pending} +

+ {state.transactionHash && ( +

+ Hash: {state.transactionHash} +

+ )} + {state.sessionId && ( +

+ Session ID: {state.sessionId} +

+ )} + {state.error && ( +

+ Error: {state.error} +

+ )} + +
+ ); +} -```rust title=contract.rs -#[endpoint] -fn deploy_from_source( - &self, - source_contract_address: ManagedAddress, - args: MultiValueEncoded, -) -> ManagedAddress { - self.tx() - .raw_deploy() // creates a deploy transaction - .from_source(source_contract_address) - .arguments_raw(args.to_arg_buffer()) - .returns(ReturnsNewManagedAddress) // returns newly deployed address as ManagedAddress - .sync_call() +/** + * Convert a decimal EGLD string to the smallest denomination (10^18). + * + * Avoid `parseFloat` + `* 1e18` — it loses precision for any meaningful + * balance. We do the multiplication on the integer part with bigint, then + * pad the fractional part with zeros to 18 digits. + */ +function toSmallestDenomination(decimalEgld: string): bigint { + const trimmed = decimalEgld.trim(); + if (trimmed === '' || !/^\d+(\.\d+)?$/.test(trimmed)) { + throw new Error(`Invalid amount: "${decimalEgld}"`); + } + const dotIndex = trimmed.indexOf('.'); + const integerPart = dotIndex === -1 ? trimmed : trimmed.slice(0, dotIndex); + const rawFraction = dotIndex === -1 ? '' : trimmed.slice(dotIndex + 1); + if (rawFraction.length > 18) { + throw new Error('Amount has more than 18 decimal places.'); + } + const paddedFraction = rawFraction.padEnd(18, '0'); + return BigInt(integerPart) * 10n ** 18n + BigInt(paddedFraction || '0'); } -``` +const buttonStyle: React.CSSProperties = { + padding: '0.6rem 1.1rem', + fontSize: '1rem', + cursor: 'pointer', +}; +const inputStyle: React.CSSProperties = { + display: 'block', + width: '100%', + marginTop: '0.25rem', + padding: '0.5rem', + fontSize: '0.95rem', + fontFamily: 'monospace', +}; +``` -### `ReturnsNewAddress` +## How it works -Returns: the newly deployed address after a deploy, as `multiversx_sc::types::heap::Address`. +**Why signing is on the provider, not a hook.** v4 had `useSignTransactions()`, a +hook that paid the cost of being a hook (Rules of Hooks, render-phase identity) +without using any of the benefits. The signing call is fundamentally imperative: +"user clicks Send, wallet pops up". Putting it on the provider makes the call site +honest about that. -```rust title=blackbox_test.rs -#[test] -fn returns_address_test() { - let mut world = ScenarioWorld::new(); +**Why sending is via TransactionManager, not the provider.** Sending is a separate +concern from signing. `TransactionManager` handles batching, gas-bumping retries, +and integrates with the WebSocket-or-polling tracker. The provider is just a +signature surface; the manager is the orchestrator. - let new_address = world - .tx() - .from(OWNER_ADDRESS) - .typed(scenario_tester_proxy::ScenarioTesterProxy) - .init(5u32) // deploys contract - .code(CODE_PATH) - .returns(ReturnsNewAddress) // returns newly deployed address as Address - .run(); +**What `sessionId` is for.** `useGetPendingTransactions()`, +`useGetSuccessfulTransactions()`, and `useGetFailedTransactions()` take **no +arguments**. Each returns the flat `SignedTransactionType[]` for *every* +transaction in that state, not just yours: - assert_eq!(new_address, SC_TEST_ADDRESS.to_address()); -} +```ts +const pending = useGetPendingTransactions(); // every pending tx, all sessions +const successful = useGetSuccessfulTransactions(); +const failed = useGetFailedTransactions(); ``` +To look up **one** session (the one `sessionId` from `send()` refers to), use the +`...Sessions()` variants instead. They return +`Record`: +```ts +const sessions = useGetPendingTransactionsSessions(); +const mySession = sessions[sessionId]; // SessionTransactionType | undefined +``` -### `ReturnsNewTokenIdentifier` - -Returns: a newly issued token identifier, as String. It will search for it in logs. +## Pitfalls -Usable in interactor environments. +:::warning[Pitfall 1: the nonce trap (silent rejection)] +`getAccount()` returns the network's last-known nonce. If you fire two +transactions in a session, the second is rejected unless you increment the nonce +locally: -```rust title=interact.rs -async fn issue_token(action_id: usize) -> String { - self.interactor - .tx() - .from(&self.wallet_address) - .to(self.state.current_multisig_address()) - .gas(NumExpr("80,000,000")) - .typed(multisig_proxy::MultisigProxy) - .perform_action_endpoint(action_id) // endpoint that issues token - .returns(ReturnsNewTokenIdentifier) // newly issued token identifier returned as String - .prepare_async() - .run() - .await -} +```ts +const nonce = BigInt(getAccount().nonce); +// First transaction with nonce, signed and sent. +// For the second: +const tx2 = new Transaction({ ...rest, nonce: nonce + 1n }); ``` +The store updates the nonce after a confirmed transaction, but if you fire two +before the first confirms you must manage the nonce yourself. +::: +:::warning[Pitfall 2: amount is in the smallest denomination (10^18)] +`value: 1n` means **0.000000000000000001** EGLD, not 1 EGLD. The recipe's UI takes +a decimal string and converts via `toSmallestDenomination()`. Avoid +`parseFloat * 1e18`, JavaScript loses precision past 2^53. Use bigint arithmetic. +::: -### `ReturnsBackTransfers` - -Returns the back-transfers of the call, as a specialized structure, called `BackTransfers`. +:::warning[Pitfall 3: gas limit scales with data] +The default `GAS_LIMIT` (50,000) covers an empty-data transfer. Each byte of +`data` adds 1500 gas. If you hand-roll a `Transaction` and skip the math, the +network rejects with "insufficient gas". The recipe's `computeGasLimit()` does +this, copy it. +::: -Usable in a smart contract environment. +:::warning[Pitfall 4: chain ID mismatch] +The signed transaction's `chainID` must match the network you are sending to. +Pulling from `getNetworkConfig().network.chainId` ensures consistency. Never +hard-code `"D"`, `"T"`, or `"1"`; it makes the recipe environment-bound and +silently breaks when someone switches via a config change. +::: -```rust title=contract.rs -#[endpoint] -fn forward_sync_retrieve_funds_bt( - &self, - to: ManagedAddress, - token: EgldOrEsdtTokenIdentifier, - token_nonce: u64, - amount: BigUint, -) { - let back_transfers = self - .tx() - .to(&to) - .typed(vault_proxy::VaultProxy) - .retrieve_funds(token, token_nonce, amount) - .returns(ReturnsBackTransfers) - .sync_call(); +:::warning[Pitfall 5: sender mismatch] +`tx.sender` MUST equal the logged-in account's bech32 address. The wallet refuses +to sign otherwise (it would sign with a different key than the tx's sender field, +then the network rejects on mismatch). +::: - require!( - back_transfers.esdt_payments.len() == 1 || back_transfers.total_egld_amount != 0, - "Only one ESDT payment expected" - ); -} -``` +:::warning[Pitfall 6: useGetPendingTransactions() has no per-session overload] +It is tempting to assume `useGetPendingTransactions()` accepts the `sessionId` +from `send()` and filters to just that transaction. It does not; the signature is +`(): SignedTransactionType[]`, no parameters, verified against +`node_modules/@multiversx/sdk-dapp/out/react/transactions/useGetPendingTransactions.d.ts` +on `@multiversx/sdk-dapp@5.6.23`. It returns *every* pending transaction across +every session. For a single session's status, index into +`useGetPendingTransactionsSessions()` by `sessionId` instead. +::: +## See also -### `ReturnsHandledOrError` +- [Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) + is the wallet-connect setup this recipe builds on. +- [Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) + is the same pattern, Vite path. +- [Migration: useSendTransactions to TransactionManager.send](/sdk-and-tools/sdk-js/cookbook/migration/send-transactions-to-transaction-manager) + extends this single-transaction flow to grouped/batch sends. +- [Read the connected account](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + is how to read the account this recipe signs for. -Returns the handled result from a SC call as a `Result`. Can be chained with other result handlers. +--- -Acting as a wrapper over other result handlers, checks the status of the transaction and returns `Some(HandledType)` or `Err(TxResponseStatus)`. Especially useful in external programs that integrate the interactor in their operations such as microservices. This result handler makes sure that the external program keeps running and opens the door for a more elegant error handling. +### Signing programmatically -It is usable both in the interactor and testing environments. +In order to sign a transaction (or a message) using one of the SDKs, follow: -In this example, `ReturnsHandledOrError` checks the status of the transaction. If the status is `success`, the other result handler, `ReturnsNewBech32Address`, will try to extract the newly deployed SC address from the transaction on the blockchain. If successful, the new address is returned as a `Bech32Address`. Otherwise, a `TxResponseStatus` struct is returned, containing the error and the message. +- [Signing objects using **sdk-js**](/sdk-and-tools/sdk-js/sdk-js-cookbook#signing-objects) +- [Signing objects using **sdk-py**](/sdk-and-tools/sdk-py#signing-objects) -```rust title=microservice.rs -impl ContractInteract { - pub async fn deploy_paint_harvest( - &mut self, - collection_token_id: String, - is_open: bool, - ) -> Result { - let paint_harvest_code = BytesValue::from(self.contract_code.paint_harvest); +--- - self.interactor - .tx() - .from(&self.wallet_address) - .gas(60_000_000u64) - .typed(PaintHarvestScProxy) - .init(TokenIdentifier::from(&collection_token_id), is_open) - .code(paint_harvest_code) - .code_metadata(CodeMetadata::UPGRADEABLE) - .returns(ReturnsHandledOrError::new().returns(ReturnsNewBech32Address)) - .run() - .await - } -} -``` +### Signing Providers for dApps +This page will guide you through the process of integrating the **sdk-js signing providers** in a dApp which isn't based on `sdk-dapp`. -### `ExpectError` +:::important +Note that for most purposes, **we recommend using [sdk-dapp](https://github.com/multiversx/mx-sdk-dapp)** instead of integrating the signing providers on your own. +::: -Indicates that the expected return type is error and does an assert on the actual return value. Usable in the testing and interactor environments. -```rust title=blackbox_test.rs - self.world - .tx() // tx with testing environment - .from(BOARD_MEMBER_ADDRESS) - .to(MULTISIG_ADDRESS) - .typed(multisig_proxy::MultisigProxy) - .perform_action_endpoint(action_id) - .with_result(ExpectError(4, err_message)) // expects error return type - .run(); -``` +## Anatomy of a signing provider -In this example, we expect the returned value to be an error with error code 4 (user error) and a specific error message. If not true, the execution fails. +Generally speaking, a signing provider is a component that supports the following use-cases: -However, because `ExpectError` only receives a status and a message, writing `ExpectError(0, "")` is equivalent to success (code 0 - ok, no error message). +- **Log in (trivial flow, not recommended)**: the user of a dApp is asked her MultiversX identity. The user reaches the wallet, unlocks it, and confirms the login. The flow continues back to the dApp, which is now informed about the user's blockchain address. Note, though, that this piece of information is not authenticated: the dApp receives a _hint_ about the user's address, not a _guarantee_ (proof). Sometimes (though rarely), this is enough. If in doubt, always have your users login using the **native authentication** flow (see below). +- **Log in using native authentication (recommended)**: once the user decides to log in, the dApp crafts a special piece of data called _the native authentication initial part_ - a shortly-lived artifact that contains, among others, a marker of the originating dApp and a marker of the target Network. The user is given this piece of data and she is asked to sign it, to prove her MultiversX identity. The user then reaches the wallet, which unwraps and (partly) displays the payload of _the native authentication initial part_. The user unlocks the wallet and confirms the login - under the hood, the _part_ is signed with the user's secret key. The flow continues back to the dApp, which now receives the user's blockchain address, along with a proof (signature). Then, the dApp (e.g. maybe a server-side component) can verify the signature to make sure that the user is indeed the owner of the address. +- **Log out**: once the user decides to log out from the dApp, the latter should ask the wallet to do so. Once the user is signed out, the flow continues back to the dApp. +- **Sign transactions**: while interacting with the dApp, the user might be asked to sign one or more transactions. The user reaches the wallet, unlocks it again if necessary, and confirms the signing. The flow continues back to the dApp, which receives the signed transactions, ready to be broadcasted to the Network. +- **Sign messages**: while interacting with the dApp, the user might be asked to sign an arbitrary message. The user reaches the wallet, unlocks it again if necessary, and confirms the signing. The flow continues back to the dApp, which receives the signed message. -### `ExpectValue` +## Implementations (available providers) -Indicates the expected return type and does an assert on the actual return value. Usable in the testing and interactor environments. +For MultiversX dApps, the following signing providers are available: -```rust title=blackbox_test.rs - world - .query() - .to(ADDER_ADDRESS) - .typed(adder_proxy::AdderProxy) - .sum() - .returns(ExpectValue(5u32)) - .run(); -``` +- [Web wallet provider](https://github.com/multiversx/mx-sdk-js-web-wallet-provider), compatible with the [Web Wallet](/wallet/web-wallet) and [xAlias](/wallet/xalias) +- [Browser extension provider](https://github.com/multiversx/mx-sdk-js-extension-provider), compatible with the [MultiversX DeFi Wallet](/wallet/wallet-extension) +- [WalletConnect provider](https://github.com/multiversx/mx-sdk-js-wallet-connect-provider), compatible with [xPortal App](/wallet/xportal) +- [Hardware wallet provider](https://github.com/multiversx/mx-sdk-js-hw-provider), compatible with [Ledger](/wallet/ledger) +:::important +The code samples depicted on this page are written in JavaScript, and can also be found [on GitHub](https://github.com/multiversx/mx-sdk-js-examples/tree/main/signing-providers). There, the signing providers are integrated into a basic web page (for example purposes). +::: -### `ExpectMessage` -Indicates that the expected return type is error and does an assert on the actual error message. Usable in the testing and interactor environments. +## The Web Wallet Provider -```rust title=blackbox_test.rs - state - .world - .tx() - .from(USER_ADDRESS) - .to(TRANSFER_ROLE_FEATURES_ADDRESS) - .typed(transfer_role_proxy::TransferRoleFeaturesProxy) - .forward_payments(Address::zero(), "", MultiValueVec::>::new()) - .egld_or_single_esdt( - &EgldOrEsdtTokenIdentifier::esdt(TRANSFER_TOKEN), - 0u64, - &multiversx_sc::proxy_imports::BigUint::from(100u64), - ) - .with_result(ExpectMessage("Destination address not whitelisted")) - .run(); -``` +:::note +Make sure you have a look over the [webhooks](/wallet/webhooks), in advance. +::: +[`@multiversx/sdk-web-wallet-provider`](https://github.com/multiversx/mx-sdk-js-web-wallet-provider) allows the users of a dApp to log in and sign data using the [Web Wallet](/wallet/web-wallet) or [xAlias](/wallet/xalias). -### `ExpectStatus` +:::important +Remember that [xAlias](/wallet/xalias) exposes **the same [URL hooks and callbacks](/wallet/webhooks)** as the [Web Wallet](/wallet/web-wallet). Therefore, integrating xAlias is **identical to integrating the Web Wallet** - with one trivial exception: the configuration of the URL base (see below). +::: -Indicates that the expected return type is u64 and does an assert on the actual transaction. Usable in the testing and interactor environments. +In order to create an instance of the provider that talks to the **Web Wallet**, do as follows: -```rust title=blackbox_test.rs - self.world - .tx() - .from(from) - .to(PRICE_AGGREGATOR_ADDRESS) - .typed(price_aggregator_proxy::PriceAggregatorProxy) - .submit( - EGLD_TICKER, - USD_TICKER, - submission_timestamp, - price, - DECIMALS, - ) - .with_result(ExpectStatus(4)) - .run(); +```js +import { WalletProvider, WALLET_PROVIDER_DEVNET } from "@multiversx/sdk-web-wallet-provider"; + +const provider = new WalletProvider(WALLET_PROVIDER_DEVNET); ``` ---- +Or, for **xAlias**: -### rounds +```js +import { WalletProvider, XALIAS_PROVIDER_DEVNET } from "@multiversx/sdk-web-wallet-provider"; -This page describes the structure of the `rounds` index (Elasticsearch), and also depicts a few examples of how to query it. +const provider = new WalletProvider(XALIAS_PROVIDER_DEVNET); +``` +The following provider URLs [are defined](https://github.com/multiversx/mx-sdk-js-web-wallet-provider/blob/main/src/constants.ts) by the package: -## _id +- `WALLET_PROVIDER_TESTNET`, `WALLET_PROVIDER_DEVNET`, `WALLET_PROVIDER_MAINNET` +- `XALIAS_PROVIDER_MAINNET`, `XALIAS_PROVIDER_DEVNET`, `XALIAS_PROVIDER_TESTNET` -The `_id` field of this index is composed in this way: `{shardID}_{round}` (example: `2_10905514`) +### Login and logout {#wallet-login-and-logout} -## Fields +Then, ask the user to log in: +```js +const callbackUrl = encodeURIComponent("http://my-dapp"); +await provider.login({ callbackUrl }); +``` -| Field | Description | -|------------------|------------------------------------------------------------------------------------------------------------------------| -| round | The round field represents the number of the round. | -| signersIndexes | The signersIndexes field is an array that contains the indices of the validators that should sign the block from this round. | -| blockWasProposed | The blockWasProposed field is true if a block was proposed and executed in this round. | -| shardId | The shardId field represents the shard the round belongs to. | -| epoch | The epoch field represents the epoch the round belongs to. | -| timestamp | The timestamp field represents the timestamp of the round. | +Once the user opens her wallet, the web wallet issues a redirected back to `callbackUrl`, along with the **address** of the user. You can get the address as follows: +```js +import qs from "qs"; -## Query examples +const queryString = window.location.search.slice(1); +const queryStringParams = qs.parse(queryString); +console.log(queryStringParams.address); +``` +In order to log out, do as follows: -### Fetch the latest rounds for a shard when block was produced +```js +const callbackUrl = window.location.href.split("?")[0]; +await provider.logout({ callbackUrl: callbackUrl }); +``` + +Though, most often, the dApps (or their server-side components) want to **reliably assign an off-chain user identity to a MultiversX address**. For this, the signing providers support an extra parameter to the `login()` method: **the initial part of the native authentication token**. That piece of data, generally crafted with the aid of [`sdk-native-auth-client`](https://www.npmjs.com/package/@multiversx/sdk-native-auth-client), is signed with the user's wallet at login-time, and the signature is made available to the dApp, which, in turn, packs it into the actual **native authentication token**. + +:::important +We always recommend using the **native authentication** flow, instead of the trivial one. That is, always pass the `token` parameter to the `login()` method. This is applicable to all signing providers. +::: + +```js +import { NativeAuthClient } from "@multiversx/sdk-native-auth-client"; + +const nativeAuthClient = new NativeAuthClient({ ... }); +const nativeAuthInitialPart = await nativeAuthClient.initialize(); +const callbackUrl = encodeURIComponent("https://my-dapp/on-wallet-login"); +await provider.login({ callbackUrl, token: nativeAuthInitialPart }); ``` -curl --request GET \ - --url ${ES_URL}/rounds/_search \ - --header 'Content-Type: application/json' \ - --data '{ - "query": { - "match": { - "shardId": 1 - } - }, - "sort": [ - { - "timestamp": { - "order": "desc" - } - } - ], - "size":10 -}' + +Once the flow returns to the dApp, the `address` and `signature` parameters are available. The actual **native authentication token** is obtained upon an additional processing step - a re-packing, by means of `NativeAuthClient.getToken()`: + +```js +const address = queryStringParams.address; +const signature = queryStringParams.signature; +const nativeAuthToken = nativeAuthClient.getToken(address, nativeAuthInitialPart, signature); ``` ---- -### Run transactions +### Signing transactions {#wallet-signing-transactions} -## Overview +Transactions can be signed as follows: -As discussed previously, the transaction syntax is consistent through the various transaction environments. However, when sending the transaction across a specific environment, certain conditions apply, depending on the framework's capability of processing the information and the route of the transaction. +```js +import { Transaction } from "@multiversx/sdk-core"; -:::note -The transaction itself is not different and will produce the same result, but the way the framework processes the transaction might differ depending on the environment. -::: +const firstTransaction = new Transaction({ ... }); +const secondTransaction = new Transaction({ ... }); +await provider.signTransactions( + [firstTransaction, secondTransaction], + { callbackUrl: callbackUrl } +); +``` -## Smart contract +Upon signing the transactions, the user is redirected back to `callbackUrl`, while the _query string_ contains information about the transactions, including their signatures. The information can be used to reconstruct `Transaction` objects using `getTransactionsFromWalletUrl()`: -From the smart contract point of view, the transaction ends when specifying the transaction type (sync/async call). +```js +const plainSignedTransactions = provider.getTransactionsFromWalletUrl(); +``` +:::important +The following workaround is subject to change. +::: -### `async_call_and_exit` +As of January 2024, this signing provider returns the data field as a plain string. However, sdk-js' `Transaction.fromPlainObject()` expects it to be base64-encoded. Therefore, we need to apply a workaround (an additional conversion) on the results of `getTransactionsFromWalletUrl()`. -Executes the transaction asynchronously and exits after execution. +```js +for (const plainTransaction of plainSignedTransactions) { + const plainTransactionClone = structuredClone(plainTransaction); + plainTransactionClone.data = Buffer.from(plainTransactionClone.data).toString("base64"); + const transaction = Transaction.fromPlainObject(plainTransactionClone); -```rust title=contract.rs - self.tx() // tx with sc environment - .to(&marketplace_address) - .typed(nft_marketplace_proxy::NftMarketplaceProxy) - .claim_tokens(token_id, token_nonce, caller) - .async_call_and_exit(); // async call and stop execution + // "transaction" can now be broadcasted. +} ``` -In this case, the function `async_call_and_exit` marks the end of the transaction and executes it asynchronously. After the transaction is executed, the `never` type is returned, marking the end of the execution. +### Signing messages {#wallet-signing-messages} -### `sync_call` +Messages can be signed as follows: -Sends a transaction synchronously. +```js +import { SignableMessage } from "@multiversx/sdk-core"; -```rust title=contract.rs - self - .tx() // tx with sc environment - .to(&to) - .typed(vault_proxy::VaultProxy) - .retrieve_funds(token, token_nonce, amount) - .sync_call(); // synchronous call +const message = new SignableMessage({ message: "hello" }); +await provider.signMessage(message, { callbackUrl }); ``` +Upon signing the transactions, the user is redirected back to `callbackUrl`, while the _query string_ includes the signature of the message. The signature can be retrieved as follows: -### `upgrade_async_call_and_exit` +```js +const signature = provider.getMessageSignatureFromWalletUrl(); +``` -Upgrades contract asynchronously and exits after execution. -```rust title=contract.rs - self.tx() // tx with sc environment - .to(child_sc_address) - .typed(vault_proxy::VaultProxy) - .upgrade(opt_arg) // calling the upgrade function - .from_source(source_address) - .upgrade_async_call_and_exit(); // upgrades async and exits -``` +## The browser extension provider +:::note +Make sure you have a look over [this page](/wallet/wallet-extension), in advance. +::: -### `sync_call_same_context` +[`@multiversx/sdk-js-extension-provider`](https://github.com/multiversx/mx-sdk-sdk-js-extension-provider) allows the users of a dApp to log in and sign transactions using the [MultiversX DeFi Wallet](/wallet/wallet-extension). -Executes the transaction synchronously on the same context (in the name of the caller). +In order to acquire the instance (singleton) of the provider, do as follows: -```rust title=contract.rs - self - .tx() // tx with sc environment - .to(&to) - .raw_call(endpoint_name) - .arguments_raw(args.to_arg_buffer()) - .sync_call_same_context(); // sync call in the same context +```js +import { ExtensionProvider } from "@multiversx/sdk-extension-provider"; + +const provider = ExtensionProvider.getInstance(); ``` +Before performing any operation, make sure to initialize the provider: -### `sync_call_readonly` +```js +await provider.init(); +``` -Executes the transaction synchronously, in readonly mode (target contract cannot have its state altered). -```rust title=contract.rs - let result = self - .tx() // tx with sc environment - .to(&to) - .raw_call(endpoint_name) - .arguments_raw(args.to_arg_buffer()) - .returns(ReturnsRawResult) // result handler - returns raw result data - .sync_call_readonly(); // sync call in readonly mode -``` +### Login and logout {#extension-login-and-logout} -### `transfer_execute` +Then, ask the user to log in: -Sends transaction asynchronously, and doesn't wait for callback. +```js +const address = await provider.login(); -```rust title=contract.rs - self - .tx() - .to(&caller) - .gas(50_000_000u64) - .raw_call(func_name) - .single_esdt(&token, 0u64, &amount) - .transfer_execute(); +console.log(address); +console.log(provider.account); ``` +In order to log out, do as follows: -### `transfer` +```js +await provider.logout(); +``` -Same as `transfer_execute`, but only allowed for simple transfers. +The `login()` method supports the `token` parameter, for **the native authentication flow** (always recommended, see above): -```rust title=contract.rs - self - .tx() - .to(&caller_address) - .egld(&collected_fees) - .transfer(); +```js +const nativeAuthInitialPart = await nativeAuthClient.initialize(); +await provider.login({ token: nativeAuthInitialPart }); + +const address = provider.account.address; +const signature = provider.account.signature; +const nativeAuthToken = nativeAuthClient.getToken(address, nativeAuthInitialPart, signature); ``` +### Signing transactions {#extension-signing-transactions} -### `async_call`, `async_call_promise` +Transactions can be signed as follows: -Backwards compatibility only. +```js +import { Transaction } from "@multiversx/sdk-core"; -:::important -For the moment, the functions `async_call` and `async_call_promise` exist for backwards compatibility reasons only. These functions do `NOT` execute a transaction, they just return the current object state. Delete them from existing codebases. -::: +const firstTransaction = new Transaction({ ... }); +const secondTransaction = new Transaction({ ... }); +await provider.signTransactions([firstTransaction, secondTransaction]); +// "firstTransaction" and "secondTransaction" can now be broadcasted. +``` -## Integration test -For the Rust testing environment, the only keyword for sending transactions is `run`. Inside an integration test, a developer can build a transaction, choose the return type and then `run` it, marking the end of the transaction and the start of the execution. +### Signing messages {#extension-signing-messages} -```rust title=blackbox_test.rs - world // ScenarioWorld instance - .query() // query with test environment - .to(ADDER_ADDRESS) - .typed(adder_proxy::AdderProxy) - .sum() - .returns(ExpectValue(6u32)) // result handler - assert return value - .run(); // runs the query step -``` +Arbitrary messages can be signed as follows: -In this case, regarding of the type of the transaction (raw call, deploy, upgrade, query), it eventually turns into a scenario `Step` (`ScQueryStep`, `ScCallStep`, `ScDeployStep`) and it is processed as such. +```js +import { SignableMessage } from "@multiversx/sdk-core"; +const message = new SignableMessage({ + message: Buffer.from("hello") +}); +await provider.signMessage(message); -## Interactor +console.log(message.toJSON()); +``` -### Async Rust +## The WalletConnect provider -In the case of the interactor, the processing is similar to the integration test, with the exception that the interactor is using async Rust. First, the transaction is built, then it is turned into a `Step` using the `prepare_async` function, then we can `run` it and `await` the result. +[`@multiversx/sdk-js-wallet-connect-provider`](https://github.com/multiversx/mx-sdk-js-wallet-connect-provider) allows the users of a dApp to log in and sign transactions using [xPortal](https://xportal.com/) (the mobile application). -```rust title=interact.rs - self.interactor // Interactor instance - .tx() // tx with interactor exec environment - .from(&self.wallet_address) - .to(self.state.current_multisig_address()) - .gas(NumExpr("30,000,000")) - .typed(multisig_proxy::MultisigProxy) - .dns_register(dns_address, name) - .prepare_async() // converts tx into step - .run() // runs the step - .await; // awaits the result - async Rust -``` +For this example we will use the WalletConnect 2.0 provider since 1.0 is no longer maintained and it is [deprecated](https://medium.com/walletconnect/weve-reset-the-clock-on-the-walletconnect-v1-0-shutdown-now-scheduled-for-june-28-2023-ead2d953b595) +First, let's see a (simple) way to build a QR dialog using [`qrcode`](https://www.npmjs.com/package/qrcode) (and bootstrap): -### Sync Rust +```js +import QRCode from "qrcode"; -We also have a plan for adding support for a blocking interactor API, but this is currently not available. +async function openModal(connectorUri) { + const svg = await QRCode.toString(connectorUri, { type: "svg" }); + // The referenced elements must be added to your page, in advance + $("#MyWalletConnectQRContainer").html(svg); + $("#MyWalletConnectModal").modal("show"); +} +function closeModal() { + $("#MyWalletConnectModal").modal("hide"); +} +``` +In order to create an instance of the provider, do as follows: -## Feature table +```js +import { WalletConnectV2Provider } from "@multiversx/sdk-wallet-connect-provider"; -This table shows what transaction fields are mandatory, optional, or disallowed, in order to run a transaction. +// Generate your own WalletConnect 2 ProjectId here: +// https://cloud.walletconnect.com/app +const projectId = "9b1a9564f91cb659ffe21b73d5c4e2d8"; +// The default WalletConnect V2 Cloud Relay +const relayUrl = "wss://relay.walletconnect.com"; +// T for Testnet, D for Devnet and 1 for Mainnet +const chainId = "T" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
EnvironmentRun methodFromToPaymentGasDataResult Handler
SC: callasync_call_and_exitFC or ()callback only
SC: callregister_promiseFCcallbacks only, with gas for callback
SC: calltransfer_executeFC or ()
SC: calltransfer()
SC: callsync_call🟡FC
SC: callsync_call_same_context🟡FC
SC: callsync_call_readonly🟡FC
SC: deploysync_callimg🟡deploy
SC: upgradeupgrade_async_call_and_exitimg🟡upgradecallback only
Test: tx callrun🟡FC or ()
Test: tx deployrunimg🟡deploy
Test: queryrunFC
Interactor: tx callrun🟡FC or ()
Interactor: tx deployrunimg🟡deploy
Interactor: queryrunFC
+const callbacks = { + onClientLogin: async function () { + // closeModal() is defined above + closeModal(); + const address = await provider.getAddress(); + console.log("Address:", address); + }, + onClientLogout: async function () { + console.log("onClientLogout()"); + }, + onClientEvent: async function (event) { + console.log("onClientEvent()", event); + } +}; + +const provider = new WalletConnectProvider(callbacks, chainId, relayUrl, projectId); +``` -Legend: +:::note +You can customize the Core WalletConnect functionality by passing `WalletConnectProvider` an optional 5th parameter: `options` +For example `metadata` and `storage` for [React Native](https://docs.walletconnect.com/2.0/javascript/guides/react-native) or `{ logger: 'debug' }` for a detailed under the hood logging +::: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SymbolMeaning
Mandatory, any allowed value type
Not allowed
🟡Optional
imgEGLD only
FCFunction Call
+Before performing any operation, make sure to initialize the provider: ---- +```js +await provider.init(); +``` -### Running scenarios -Most of the MultiversX smart contract testing infrastructure is built with scenarios in mind, so there are lots of ways to execute them. +### Login and logout {#walletconnect-login-and-logout} -```mermaid -graph TD - json["JSON Scenario - *.scen.json"] - json --> test-go-tool["run-scenarios"] --> vm-go["⚙️ Go VM"] - json --> test-go["Generated - *_scenario_go_test.rs"] --> vm-go - json --> test-rs["Generated - *_scenario_rs_test.rs"] --> vm-rust["⚙️ Rust VM (Debugger)"] +Then, ask the user to log in using xPortal on her phone: + +```js +const { uri, approval } = await provider.connect(); +// connect will provide the uri required for the qr code display +// and an approval Promise that will return the connection session +// once the user confirms the login + +// openModal() is defined above +openModal(uri); + +// pass the approval Promise +await provider.login({ approval }); ``` -Assume we have a scenario JSON file. The options for running it are: -- using the `run-scenarios` standalone tool -- generating some tests in a Rust project and running the Rust tests. +The `login()` method supports the `token` parameter, for **the native authentication flow** (always recommended, see above): +```js +const nativeAuthInitialPart = await nativeAuthClient.initialize(); +await provider.login({ approval, token: nativeAuthInitialPart }); -## Standalone tool +const address = await provider.getAddress(); +const signature = await provider.getSignature(); +const nativeAuthToken = nativeAuthClient.getToken(address, nativeAuthInitialPart, signature); +``` -The only standalone tool for running scenarios is `run-scenarios`, part of the VM tooling. +:::important +The pairing proposal between a wallet and a dapp is made using an [URI](https://docs.walletconnect.com/2.0/specs/clients/core/pairing/pairing-uri). In WalletConnect v2.0 the session and pairing are decoupled from each other. This means that a URI is shared to construct a pairing proposal, and only after settling the pairing the dapp can propose a session using that pairing. In simpler words, the dapp generates an URI that can be used by the wallet for pairing. +::: -The binary is build from [here](https://github.com/multiversx/mx-chain-vm-go/blob/master/cmd/scenariostest/scenariosTest.go). -Most of the code lies [here](https://github.com/multiversx/mx-chain-vm-go/tree/master/scenarioexec), if you're curious. +Once the user confirms the login, the `onClientLogin()` callback (declared above) is executed. -To call, simply run `run-scenarios `, where the path can be either a speciific scenario file, or a folder containing scenarios. In the case of a folder, the tool will run all files ending in `*.scen.json`. Results are printed to console. +In order to log out, do as follows: +```js +await provider.logout(); +``` -## Integration in Rust -We normally want to integrate scenario tests in the CI. For this, at the very least we should write Rust tests that run the scenarios. +### Signing transactions {#walletconnect-signing-transactions} -We have decided to have a separate Rust test for each scenario file. This way, when running a full test suite, it is easy to see exactly what test has failed, at a glance: +Transactions can be signed as follows: -![console screenshot](/developers/testing/scenario-json-rust-console.png "Example of console with failed scenario tests") +```js +import { Transaction } from "@multiversx/sdk-core"; -We also want to have a set of such tests for each of the backends: Go and Rust. +const firstTransaction = new Transaction({ ... }); +const secondTransaction = new Transaction({ ... }); +await provider.signTransactions([firstTransaction, secondTransaction]); -### Standard project layout +// "firstTransaction" and "secondTransaction" can now be broadcasted. +``` -The standard for organising tests in a Rust crate is a follows: +Alternatively, one can sign a single transaction using the method `signTransaction()`. + + +### Signing messages {#walletconnect-signing-messages} + +Arbitrary messages can be signed as follows: + +```js +import { SignableMessage } from "@multiversx/sdk-core"; + +const message = new SignableMessage({ + message: Buffer.from("hello") +}); +await provider.signMessage(message); + +console.log(message.toJSON()); ``` -├── Cargo.toml -├── meta -├── multiversx.json -├── output -├── scenarios -│ ├── scenario1.scen.json -│ └── scenario2.scen.json -├── src -│ └── my_contract.rs -├── tests -│ ├── my_contract_scenario_go_test.rs -│ ├── my_contract_scenario_rs_test.rs -│ ├── other_tests.rs -│ └── more_tests.rs -└── wasm + + +## The hardware wallet provider + +:::note +Make sure you have a look over [this page](/wallet/ledger), in advance. +::: + +[`@multiversx/sdk-hw-provider`](https://github.com/multiversx/mx-sdk-js-hw-provider) allows the users of a dApp to log in and sign transactions using a [Ledger device](/wallet/ledger). + +In order to create an instance of the provider, do as follows: + +```js +import { HWProvider } from "@multiversx/sdk-hw-provider"; + +const provider = new HWProvider(); ``` -At a minimum, the project needs to have `src`, `meta`, and `wasm` folders. +Before performing any operation, make sure to initialize the provider (also, the MultiversX application has to be open on the device): -Integration tests go into the `tests` folder. Scenario files, if present, should reside in a folder named `scenarios`. It is fine to also have sub-folders under `scenarios`, if needed. +```js +await provider.init(); +``` -The Rust tests should go into 2 files ending in `*scenario_go_test.rs` and `*scenario_rs_test.rs`, respectively. It is also acceptable to simply use file names `scenario_go_test.rs` and `scenario_rs_test.rs` directly, with no prefix, but having a prefix can help developers orient themselves easier in projects with many contracts. +### Login -### Go backend +Before asking the user to log in using the Ledger, you may want to get all the available addresses on the device, display them, and let the user choose one of them: -The `*scenario_go_file.rs` should look like this: +```js +const addresses = await provider.getAccounts(); +console.log(addresses); +``` -```rust title="adder/tests/my_contract_scenario_go_test.rs" -use multiversx_sc_scenario::*; +The login looks like this: -fn world() -> ScenarioWorld { - ScenarioWorld::vm_go() -} +```js +const chosenAddressIndex = 3; +await provider.login({ addressIndex: chosenAddressIndex }); +alert(`Logged in. Address: ${await provider.getAddress()}`); +``` -#[test] -fn adder_go() { - world().run("scenarios/adder.scen.json"); -} +Alternatively, in order to select a specific address on the device after login, call `setAddressIndex()`: -#[test] -#[ignore = "reason to ignore"] -fn ignored_test_go() { - world().run("scenarios/ignored_test.scen.json"); -} +```js +const addressIndex = 3; +await provider.setAddressIndex(addressIndex); +console.log(`Address has been set: ${await provider.getAddress()}.`); ``` -The `world()` function sets up the environment, which in this case is very straightforward. +The Ledger provider does not support a _logout_ operation per se (not applicable in this context). -:::caution -Do not comment out tests! +The **the native authentication flow** (always recommended, see above) is supported using the `tokenLogin()` method: -Use the `#[ignore]` annotation instead (also writing a reason is nice, but optional). +```js +const nativeAuthInitialPart = await nativeAuthClient.initialize(); +const nativeAuthInitialPartAsBuffer = Buffer.from(nativeAuthInitialPart); +const { address, signature } = await provider.tokenLogin({ addressIndex: 0, token: nativeAuthInitialPartAsBuffer }); -The code generator for these files has trouble dealing with commented tests. +const nativeAuthToken = nativeAuthClient.getToken(address, nativeAuthInitialPart, signature.toString("hex")); +``` -It also helps that ignored tests will also show up in console, unlike the ones that are commented out. -::: -It is customary to add a `_go` suffix to the test functions, to distinguish them from the ones with the Rust backend. The test-gen tool does the same. +### Signing transactions {#hw-signing-transactions} +Transactions can be signed as follows: -### Rust backend +```js +import { Transaction } from "@multiversx/sdk-core"; -The `*scenario_rs_file.rs` needs more setup to the environment, but other than that it looks the same: +const firstTransaction = new Transaction({ ... }); +const secondTransaction = new Transaction({ ... }); -```rust title="adder/tests/my_contract_scenario_rs_test.rs" -use multiversx_sc_scenario::*; +await provider.signTransactions([firstTransaction, secondTransaction]); -fn world() -> ScenarioWorld { - let mut blockchain = ScenarioWorld::new(); - blockchain.register_contract("file:output/adder.wasm", adder::ContractBuilder); - blockchain -} +// "firstTransaction" and "secondTransaction" can now be broadcasted. +``` -#[test] -#[ignore] -fn adder_rs() { - world().run("scenarios/adder.scen.json"); -} +Alternatively, one can sign a single transaction using the method `signTransaction()`. -#[test] -fn interactor_trace_rs() { - world().run("scenarios/interactor_trace.scen.json"); -} + +### Signing messages {#hw-signing-messages} + +Arbitrary messages can be signed as follows: + +```js +import { SignableMessage } from "@multiversx/sdk-core"; + +const message = new SignableMessage({ + message: Buffer.from("hello") +}); + +await provider.signMessage(message); + +console.log(message.toJSON()); ``` -Note that we can have different tests ignored on the different backends. -Here it is also customary to add a `_rs` suffix to the test functions, to distinguish them from the ones on the Go backend. The test-gen tool does the same. +## The Web Wallet Cross Window Provider +[`@multiversx/sdk-web-wallet-cross-window-provider`](https://github.com/multiversx/mx-sdk-js-web-wallet-cross-window-provider) allows the users of a dApp to log in and sign transactions using the MultiversX Web Wallet. -### Rust backend environment minimal setup +In order to acquire the instance (singleton) of the provider, do as follows: -The example above is a great example of a minimal setup. Other than creating the `world: ScenarioWorld` object, this is the line of interest: +```js +import { CrossWindowProvider } from "@multiversx/sdk-web-wallet-cross-window-provider"; -```rust -blockchain.register_contract("file:", ::ContractBuilder); +const provider = CrossWindowProvider.getInstance(); ``` -The Rust backend doesn't run compiled contracts, instead, it hooks the actual Rust contract code to its engine. This is where we tell the framework how to do that. -The interpretation of this is: -- whenever the framework is asked to deploy or run a contract whose code would normally lie on disk at `` ... -- it should run the code, a prepared by `::ContractBuilder`. +Before performing any operation, make sure to initialize the provider and web wallet address: -The path to binary is given as a scenario value expression. [The file syntax](/developers/testing/scenario/values-simple#file-contents) in the example is simply the most common way of loading a large value from file. It is also possible to provide the compiled contract as bytes, (e.g. `"0x0061736d0100000001661160000060017..."`), but hard-coding that is weird. +```js +await provider.init(); +provider.setWalletUrl("https://wallet.multiversx.com"); +``` -The `ContractBuilder` object is generated automatically for every contract, by the `#[multiversx_sc::contract]` procedural macro. That is why you won't see it in code, but it's always there. +### Login and logout {#cross-window-login-and-logout} -### Auto-generating the boilerplate +Then, ask the user to log in: -If you thought "writing these test functions manually is tedious and repetitive", you would be correct. It is also error prone, since it is easy to accidentally leave scenarios out. +```js +const address = await provider.login(); -That is why we've build the [`sc-meta test-gen`](/developers/meta/sc-meta-cli#calling-test-gen) tool to help automate this task. +console.log(address); +console.log(provider.account); +``` -The tool works as follows: -- If no `*scenario_go_file.rs` or `*scenario_rs_file.rs` are found, they can be created anew, but only if the `--create` flag is passed to the tool. -- The Go VM tests can be fully generated, but the Rust VM environment setup (the `world()` function) cannot be generated automatically. When creating it for the first time, you will get a stub of this function, and will have to fill in the implementation manually. -- If the `scenarios` folder is missing, the tool won't do anything. -- You will always get a test function for each scenario in the `scenarios` folder. -- The tool can be called any number of times, it is easy to update these tests whenever scenarios change. New scenario tests will be added and missing tests will be removed. -- The tool always preserves: - - `#[ignore]` and `#[ignore = "reason"]` annotations; - - Test comments; - - Any additional code that may be written before the tests (not just the `world()` function). +In order to log out, do as follows: -The test tool can handle multiple contract crates at once. In fact, it will try to update tests for all contracts it can find under a given folder. +```js +await provider.logout(); +``` -For reference, the tool parameters are: -- `--path` - - Target directory where to call all contract meta crates. - - _default_: current directory. -- `--ignore` - - Ignore all directories with these names. - - _default_: `target`. -- `--create` - - Creates test files if they don't exist. +The `login()` method supports the `token` parameter, for **the native authentication flow** (always recommended, see above): ---- +```js +const nativeAuthInitialPart = await nativeAuthClient.initialize(); +await provider.login({ token: nativeAuthInitialPart }); -### Rust SDK +const address = provider.account.address; +const signature = provider.account.signature; +const nativeAuthToken = nativeAuthClient.getToken(address, nativeAuthInitialPart, signature); +``` -## Rust Interactors -The Rust SDK for interacting with the blockchain comes in the form of the so-called [**Rust Interactors**](/developers/meta/interactor/interactors-overview). +### Signing transactions {#cross-window-signing-transactions} -Since they use very similar syntax to smart contracts and smart contract tests, their documentation is grouped under the Rust Development Framework section. +Transactions can be signed as follows: +```js +import { Transaction } from "@multiversx/sdk-core"; +const firstTransaction = new Transaction({ ... }); +const secondTransaction = new Transaction({ ... }); -## Quick tutorial +await provider.signTransactions([firstTransaction, secondTransaction]); -You can also find a quick tutorial [here](/developers/tutorials/interactors-guide). +// "firstTransaction" and "secondTransaction" can now be broadcasted. +``` ---- -### Rust Version +### Signing messages {#cross-window-signing-messages} -## Required Rust version +Arbitrary messages can be signed as follows: -Starting with framework version [v0.50.0](https://crates.io/crates/multiversx-sc/0.50.0), MultiversX smart contracts can be built using stable Rust. +```js +import { SignableMessage } from "@multiversx/sdk-core"; -Before this version, nightly Rust was required. +const message = new SignableMessage({ + message: Buffer.from("hello") +}); +await provider.signMessage(message); -## Recommended compiler versions +console.log(message.toJSON()); +``` -For everything after v0.50.0 we recommend running the latest stable version of Rust. Older versions have had compatibility issues with certain framework dependencies, on certain versions of the compiler. -Also, everything on versions older than v0.50.0 needs to run on nightly Rust. +## The Metamask Proxy Provider - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Application VersionRequired Rust ChannelVersion Requirements
Prior to `v0.50`Nightly`nightly-2023-12-11` or `nightly-2024-05-22`
`v0.50` to `v0.56`**Stable (recommended)** or Nightly≥`1.78` and ≤`1.86`
`v0.57`**Stable (recommended)** or Nightly≥`1.83` and ≤`1.86`
`v0.58` and Higher**Stable (recommended)** or Nightly≥`1.83`*
+[`@multiversx/sdk-metamask-proxy-provider`](https://github.com/multiversx/mx-sdk-js-metamask-proxy-provider) allows the users of a dApp to log in and sign transactions using the Metamask wallet by using MultiversX Web Wallet as a proxy widget in iframe. -\* Starting with Rust version `1.89` and higher, there are known runtime issues when using **wasmer 6.0** (`wasmer-experimental`) exclusively on the **Linux platform**. +In order to acquire the instance (singleton) of the provider, do as follows: + +```js +import { MetamaskProxyProvider } from "@multiversx/sdk-metamask-proxy-provider"; + +const provider = MetamaskProxyProvider.getInstance(); +``` + +Before performing any operation, make sure to initialize the provider and metamask snap web wallet address: + +```js +await provider.init(); +provider.setWalletUrl("https://snap-wallet.multiversx.com"); +``` + + +### Login and logout {#metamask-proxy-login-and-logout} + +Then, ask the user to log in: + +```js +const address = await provider.login(); + +console.log(address); +console.log(provider.account); +``` + +In order to log out, do as follows: + +```js +await provider.logout(); +``` + +The `login()` method supports the `token` parameter, for **the native authentication flow** (always recommended, see above): + +```js +const nativeAuthInitialPart = await nativeAuthClient.initialize(); +await provider.login({ token: nativeAuthInitialPart }); + +const address = provider.account.address; +const signature = provider.account.signature; +const nativeAuthToken = nativeAuthClient.getToken(address, nativeAuthInitialPart, signature); +``` + + +### Signing transactions {#metamask-proxy-signing-transactions} + +Transactions can be signed as follows: + +```js +import { Transaction } from "@multiversx/sdk-core"; + +const firstTransaction = new Transaction({ ... }); +const secondTransaction = new Transaction({ ... }); + +await provider.signTransactions([firstTransaction, secondTransaction]); + +// "firstTransaction" and "secondTransaction" can now be broadcasted. +``` + + +### Signing messages {#metamask-proxy-signing-messages} + +Arbitrary messages can be signed as follows: + +```js +import { SignableMessage } from "@multiversx/sdk-core"; + +const message = new SignableMessage({ + message: Buffer.from("hello") +}); + +await provider.signMessage(message); + +console.log(message.toJSON()); +``` + + +## Verifying the signature of a login token :::note -If you are using **wasmer 6.0** on Linux, we recommend pinning your Rust version below `1.89`. +It's recommended to use the libraries [`sdk-native-auth-client`](https://www.npmjs.com/package/@multiversx/sdk-native-auth-client) and [`sdk-native-auth-server`](https://www.npmjs.com/package/@multiversx/sdk-native-auth-server) to handle the **native authentication** flow. ::: +Please follow [this](https://github.com/multiversx/mx-sdk-js-native-auth-server) for more details about verifying the signature of a login token (i.e. within a server-side component of the dApp). -## Why Nightly for the older versions? +--- -There were several nightly features that the framework was using, which we had hoped to see stabilized sooner. +### Signing Transactions -These are of little relevance to the average developer, but for the record, let's mention a few of them and how we managed to circumvent their usage: +By reading this page you will find out how to serialize and sign the Transaction payload. -- `never_type` - avoided by using slightly different syntax; -- `auto_traits` and `negative_impls` - avoided by redesigning the `CodecFrom`/`TypeAbiFrom` trait systems; -- `generic_const_exprs` - replaced with massive amounts of macros; -- `panic_info_message` - replaced by a different method to retrieve the panic message. +Transactions must be **signed** with the Sender's Private Key before submitting them to the MultiversX Network. Signing is performed with the [Ed25519](https://ed25519.cr.yp.to/) algorithm. -If any of these get stabilized in the future, we might revert the changes enacted in v0.50.0. -It is in any case our commitment to keep the framework compatible with stable Rust from here on, no matter what. +## **General structure** ---- +An _unsigned transaction_ has the following fields: -### SC to SC Calls +| Field | Type | Required | Description | +| ---------- | ------ | ------------------ | ------------------------------------------------------------------------------ | +| `nonce` | number | Yes | The account sequence number | +| `value` | string | Yes (can be `"0"`) | The value to transfer, represented in atomic units:`EGLD` times `denomination` | +| `receiver` | string | Yes | The address of the receiver (bech32 format) | +| `sender` | string | Yes | The address of the sender (bech32 format) | +| `gasPrice` | number | Yes | The gas price to be used in the scope of the transaction | +| `gasLimit` | number | Yes | The maximum number of gas units allocated for the transaction | +| `data` | string | No | Arbitrary information about the transaction, **base64-encoded**. | +| `chainID` | string | Yes | The chain identifier. | +| `version` | number | Yes | The version of the transaction (e.g. `1`). | + +A signed transaction has the additional **`signature`** field: + +| Field | Type | Description | +| --------- | ------ | ---------------------------------------------------------------------------------------------- | +| signature | string | The digital signature consisting of 128 hex-characters (thus 64 bytes in a raw representation) | + + +## **Serialization for signing** + +Before signing a transaction, one has to **serialize** it, that is, to obtain its raw binary representation - as a sequence of bytes. This is achieved through the following steps: + +1. order the fields of the transaction with respect to their appearance order in the table above (`nonce` is first, `version` is last). +2. discard the `data` field if it's empty. +3. convert the `data` payload to its **base64** representation. +4. obtain a JSON representation (UTF-8 string) of the transaction, maintaining the order of the fields. This JSON representation must contain **no indentation** and **no separating spaces**. +5. encode the resulted JSON (UTF-8) string as a sequence of bytes. -This guide provides an overview of the different types of smart contract calls that originate from other smart contract calls. +For example, given the transaction: +```js +nonce = 7 +value = "10000000000000000000" # 10 EGLD +receiver = "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r" +sender = "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz" +gasPrice = 1000000000 +gasLimit = 70000 +data = "for the book" +chainID = "1" +version = 1 +``` -## Introduction +By applying steps 1-3 (step 4 is omitted in this example), one obtains: -Smart contract calls on MultiversX fall into two main categories: synchronous (`sync`) and asynchronous (`async`), each with distinct usage scenarios based on developer needs, and dApp architecture. +```js +{"nonce":7,"value":"10000000000000000000","receiver":"erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r","sender":"erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz","gasPrice":1000000000,"gasLimit":70000,"data":"Zm9yIHRoZSBib29r","chainID":"1","version":1} +``` +If the transaction has an empty **no data field**: +```js +nonce = 8 +value = "10000000000000000000" # 10 ERD +receiver = "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r" +sender = "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz" +gasPrice = 1000000000 +gasLimit = 50000 +data = "" +chainID = "1" +version = 1 +``` -## Overview +Then it's serialized form (step 5 is omitted in this example) is as follows: +```js +{"nonce":8,"value":"10000000000000000000","receiver":"erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r","sender":"erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz","gasPrice":1000000000,"gasLimit":50000,"chainID":"1","version":1} +``` -### Sync calls vs. async calls +## **Ed25519 signature** -A `sync` call is similar to regular function call in a program: it relies on a call stack, the current execution is paused, the call is executed immediately, and execution of the caller function resumes immediately after. +MultiversX uses the [Ed25519](https://ed25519.cr.yp.to/) algorithm to sign transactions. In order to obtain the signature, one can use generic software libraries such as [PyNaCl](https://pynacl.readthedocs.io/en/stable/signing/), [tweetnacl-js](https://github.com/dchest/tweetnacl-js#signatures) or components of MultiversX SDK such as [mx-sdk-js-core](https://github.com/multiversx/mx-sdk-js-core), [mx-sdk-py](https://github.com/multiversx/mx-sdk-py), [sdk-go](https://github.com/multiversx/mx-sdk-go), [mxjava](https://github.com/multiversx/mx-sdk-java) etc. -An `async` call is similar to an asynchronous function call in a program, just like launching it on a different thread. The async call is not added to the same stack and does not interrupt the execution of the caller function. +The raw signature consisting of 64 bytes has to be **hex-encoded** afterwards and placed in the transaction object. -The main differences between the two are in this table: -| `sync` calls | `async` calls | -| ------------ | ------------- | -| Are executed immediately, inline. | Are executed after the current transaction is completed. | -| Only work in the same shard. | Work both in-shard and cross-shard. | -| Function results are available immediately. | Function results are only available later, in the callback, if a callback exists. | -| A callee crash causes caller to immediately crash as well. | A callee crash does not cause caller to crash, the error can be caught in the callback, if it exists. | -| Reentrancy issues possible. | Reentrancy not possible. | +## **Ready to broadcast** +Once the `signature` field is set as well, the transaction is ready to be broadcasted. Following the examples above, their ready-to-broadcast form is as follows: +```js +# With data field +nonce = 7 +value = "10000000000000000000" # 10 EGLD +receiver = "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r" +sender = "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz" +gasPrice = 1000000000 +gasLimit = 70000 +data = "Zm9yIHRoZSBib29r" +chainID = "1" +version = 1 +signature = "1702bb7696f992525fb77597956dd74059b5b01e88c813066ad1f6053c6afca97d6eaf7039b2a21cccc7d73b3e5959be4f4c16f862438c7d61a30c91e3d16c01" +``` -### Transfer-execute calls +```js +# Without data field +nonce = 8 +value = "10000000000000000000" # 10 EGLD +receiver = "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r" +sender = "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz" +gasPrice = 1000000000 +gasLimit = 50000 +data = "" +chainID = "1" +version = 1 +signature = "4a6d8186eae110894e7417af82c9bf9592696c0600faf110972e0e5310d8485efc656b867a2336acec2b4c1e5f76c9cc70ba1803c6a46455ed7f1e2989a90105" +``` -Transfer-execute calls are basically async calls without callback. You can think of them as a "fire and forget" mechanism. +--- -They come in two flavors: -- **Transfer-only** - they are used to move EGLD or ESDT balance, and nothing else. Balance transfers can rarely fail, so they are very convenient to use. -- **Transfer-and-execute** - used when the caller does not care what the result of the callee function is. +### Simple Values -They were very important in async v1, because they were the only mechanism to have more than one call leaving a transaction. +We will start by going through the basic types used in smart contracts: +- Fixed-width numbers +- Arbitrary width (big) numbers +- Boolean values -### Error recovery -Error recovery is not possible with sync calls, so sometimes contracts might choose to perform async calls to allow themselves to recover from errors, even if the target contract is in the same shard. +### Fixed-width numbers -This also means that no error handling is necessary (or effective) around sync calls. +Small numbers can be stored in variables of up to 64 bits. We use big endian encoding for all numbers in our projects. +**Rust types**: `u8`, `u16`, `u32`, `usize`, `u64`, `i8`, `i16`, `i32`, `isize`, `i64`. -### Reentrancy +**Top-encoding**: The same as for all numerical types, the minimum number of bytes that +can fit their 2's complement, big endian representation. -Reentrancy is very unlikely on a MultiversX blockchain, but not impossible, so let's dedicate a chapter to it. +**Nested encoding**: Fixed width big endian encoding of the type, using 2's complement. -There are 2 main elements that make it less of an issue than on other blockchain architectures: -1. Native tokens are built into the system, so they cannot become malicious. Most reentrancy attacks are performed via malicious tokens that exploit intermediate states in the middle of function execution. -2. Reentrancy can only be performed in sync calls. Async calls do not interrupt the execution, so they are not vectors for an attack. +:::important +A note about the types `usize` and `isize`: these Rust-specific types have the width of the underlying architecture, +i.e. 32 on 32-bit systems and 64 on 64-bit systems. However, smart contracts always run on a wasm32 architecture, so +these types will always be identical to `u32` and `i32` respectively. +Even when simulating smart contract execution on 64-bit systems, they must still be serialized on 32 bits. +::: -Even so, to avoid vulnerabilities it is best to perform sync calls either at the very beginning, or at the very end of a SC endpoint execution. +**Examples** -A similar problem to reentrancy is the management of the intermediate state between an async call and the processing of its callback. Great care must be taken, to ensure the state of the contract is not vulnerable to attacks in this interval. +| Type | Number | Top-level encoding | Nested encoding | +| +| ------- | --------------------- | -------------------- | -------------------- | +| `u8` | `0` | `0x` | `0x00` | +| `u8` | `1` | `0x01` | `0x01` | +| `u8` | `0x11` | `0x11` | `0x11` | +| `u8` | `255` | `0xFF` | `0xFF` | +| `u16` | `0` | `0x` | `0x0000` | +| `u16` | `0x11` | `0x11` | `0x0011` | +| `u16` | `0x1122` | `0x1122` | `0x1122` | +| `u32` | `0` | `0x` | `0x00000000` | +| `u32` | `0x11` | `0x11` | `0x00000011` | +| `u32` | `0x1122` | `0x1122` | `0x00001122` | +| `u32` | `0x112233` | `0x112233` | `0x00112233` | +| `u32` | `0x11223344` | `0x11223344` | `0x11223344` | +| `u64` | `0` | `0x` | `0x0000000000000000` | +| `u64` | `0x11` | `0x11` | `0x0000000000000011` | +| `u64` | `0x1122` | `0x1122` | `0x0000000000001122` | +| `u64` | `0x112233` | `0x112233` | `0x0000000000112233` | +| `u64` | `0x11223344` | `0x11223344` | `0x0000000011223344` | +| `u64` | `0x1122334455` | `0x1122334455` | `0x0000001122334455` | +| `u64` | `0x112233445566` | `0x112233445566` | `0x0000112233445566` | +| `u64` | `0x11223344556677` | `0x11223344556677` | `0x0011223344556677` | +| `u64` | `0x1122334455667788` | `0x1122334455667788` | `0x1122334455667788` | +| `usize` | `0` | `0x` | `0x00000000` | +| `usize` | `0x11` | `0x11` | `0x00000011` | +| `usize` | `0x1122` | `0x1122` | `0x00001122` | +| `usize` | `0x112233` | `0x112233` | `0x00112233` | +| `usize` | `0x11223344` | `0x11223344` | `0x11223344` | +| `i8` | `0` | `0x` | `0x00` | +| `i8` | `1` | `0x01` | `0x01` | +| `i8` | `-1` | `0xFF` | `0xFF` | +| `i8` | `127` | `0x7F` | `0x7F` | +| `i8` | `-0x11` | `0xEF` | `0xEF` | +| `i8` | `-128` | `0x80` | `0x80` | +| `i16` | `-1` | `0xFF` | `0xFFFF` | +| `i16` | `-0x11` | `0xEF` | `0xFFEF` | +| `i16` | `-0x1122` | `0xEEDE` | `0xEEDE` | +| `i32` | `-1` | `0xFF` | `0xFFFFFFFF` | +| `i32` | `-0x11` | `0xEF` | `0xFFFFFFEF` | +| `i32` | `-0x1122` | `0xEEDE` | `0xFFFFEEDE` | +| `i32` | `-0x112233` | `0xEEDDCD` | `0xFFEEDDCD` | +| `i32` | `-0x11223344` | `0xEEDDCCBC` | `0xEEDDCCBC` | +| `i64` | `-1` | `0xFF` | `0xFFFFFFFFFFFFFFFF` | +| `i64` | `-0x11` | `0xEF` | `0xFFFFFFFFFFFFFFEF` | +| `i64` | `-0x1122` | `0xEEDE` | `0xFFFFFFFFFFFFEEDE` | +| `i64` | `-0x112233` | `0xEEDDCD` | `0xFFFFFFFFFFEEDDCD` | +| `i64` | `-0x11223344` | `0xEEDDCCBC` | `0xFFFFFFFFEEDDCCBC` | +| `i64` | `-0x1122334455` | `0xEEDDCCBBAB` | `0xFFFFFFEEDDCCBBAB` | +| `i64` | `-0x112233445566` | `0xEEDDCCBBAA9A` | `0xFFFFEEDDCCBBAA9A` | +| `i64` | `-0x11223344556677` | `0xEEDDCCBBAA9989` | `0xFFEEDDCCBBAA9989` | +| `i64` | `-0x1122334455667788` | `0xEEDDCCBBAA998878` | `0xEEDDCCBBAA998878` | +| `isize` | `0` | `0x` | `0x00000000` | +| `isize` | `-1` | `0xFF` | `0xFFFFFFFF` | +| `isize` | `-0x11` | `0xEF` | `0xFFFFFFEF` | +| `isize` | `-0x1122` | `0xEEDE` | `0xFFFFEEDE` | +| `isize` | `-0x112233` | `0xEEDDCD` | `0xFFEEDDCD` | +| `isize` | `-0x11223344` | `0xEEDDCCBC` | `0xEEDDCCBC` | +--- -### Async callbacks +### Arbitrary width (big) numbers -Async calls can optionally register callbacks. The callbacks are called by the system, irrespective of whether the caller completed successfully or failed. +For most smart contracts applications, number larger than the maximum uint64 value are needed. +EGLD balances for instance are represented as fixed-point decimal numbers with 18 decimals. +This means that to represent even just 100 EGLD we use the number 100*1018, which already exceeds the capacity of a regular 64-bit integer. -The callback receives the following inputs: -- The call result, which contains several values: - - In case of success: - - The status code, 0 in this case, in the first position. - - One argument for each result returned by the callee, after that. - - In case of error: - - The status code, will be different from 0. For example, error code "4" indicates a failure in the smart contract execution. It is also in the first position. - - One argument, containing the error message as string. - - Note: it is customary to use type `ManagedAsyncCallResult` since it knows how to conveniently decode this structure. -- The callback closure: - - There might be multiple async calls involved in a smart contract interaction, it can sometimes be hard to figure out which callback came from which call. That is why it is almost always the case that some information needs to be passed directly from the call site to the callback. - - This is done by adding arguments +**Rust types**: `BigUint`, `BigInt`, -:::note -Async call functions do not return values, but may include a `callback` function to handle the response from the destination contract. This is because the results are never available immediately. +:::important +These types are managed by MultiversX VM, in many cases the contract never sees the data, only a handle. +This is to reduce the burden on the smart contract. ::: -This is an example of a callback function that gets triggered after an `issue_fungible` action: - -```rust - #[callback] - fn esdt_issue_callback( - &self, - caller: &ManagedAddress, - #[call_result] result: ManagedAsyncCallResult<()>, - ) { - let (token_identifier, returned_tokens) = - self.call_value().egld_or_single_fungible_esdt(); - // callback is called with ESDTTransfer of the newly issued token, with the amount requested, - // so we can get the token identifier and amount from the call data - match result { - ManagedAsyncCallResult::Ok(()) => { - self.last_issued_token().set(token_identifier.unwrap_esdt()); - self.last_error_message().clear(); - }, - ManagedAsyncCallResult::Err(message) => { - // return issue cost to the caller - if token_identifier.is_egld() && returned_tokens > 0 { - self.tx().to(caller).egld(&returned_tokens).transfer(); - } - - self.last_error_message().set(&message.err_msg); - }, - } - } -``` +**Top-encoding**: The same as for all numerical types, the minimum number of bytes that +can fit their 2's complement, big endian representation. -And this is how it gets called: +**Nested encoding**: Since these types are variable length, we need to encode their length, so that the decodes knows when to stop decoding. +The length of the encoded number always comes first, on 4 bytes (`usize`/`u32`). +Next we encode: -```rust - self.send() - .esdt_system_sc_proxy() - .issue_fungible(/* ... arguments ... */) - .with_callback(self.callbacks().esdt_issue_callback(&caller)) - .async_call_and_exit() -``` +- For `BigUint` the big endian bytes +- For `BigInt` the shortest 2's complement number that can unambiguously represent the number. Positive numbers must always have the most significant bit `0`, while the negative ones `1`. See examples below. -Notice how the caller gets passed from the call site directly to the callback. +**Examples** -Also notice how the tokens transferred back to the caller are available in the callback, as _call value_. +| Type | Number | Top-level encoding | Nested encoding | Explanation | +| --------- | ------ | ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------ | +| `BigUint` | `0` | `0x` | `0x00000000` | The length of `0` is considered `0`. | +| `BigUint` | `1` | `0x01` | `0x0000000101` | `1` can be represented on 1 byte, so the length is 1. | +| `BigUint` | `256` | `0x0100` | `0x000000020100` | `256` is the smallest number that takes 2 bytes. | +| `BigInt` | `0` | `0x` | `0x00000000` | Signed `0` is also represented as zero-length bytes. | +| `BigInt` | `1` | `0x01` | `0x0000000101` | Signed `1` is also represented as 1 byte. | +| `BigInt` | `-1` | `0xFF` | `0x00000001FF` | The shortest 2's complement representation of `-1` is `FF`. The most significant bit is 1. | +| `BigUint` | `127` | `0x7F` | `0x000000017F` | | +| `BigInt` | `127` | `0x7F` | `0x000000017F` | | +| `BigUint` | `128` | `0x80` | `0x0000000180` | | +| `BigInt` | `128` | `0x0080` | `0x000000020080` | The most significant bit of this number is 1, so to avoid ambiguity an extra `0` byte needs to be prepended. | +| `BigInt` | `255` | `0x00FF` | `0x0000000200FF` | Same as above. | +| `BigInt` | `256` | `0x0100` | `0x000000020100` | `256` requires 2 bytes to represent, of which the MSB is 0, no more need to prepend a `0` byte. | +--- +### Boolean values -### All call types +Booleans are serialized the same as a byte (`u8`) that can take values `1` or `0`. -Each of these calls further divide in multiple categories, depending on the mechanism they use, different developer use-cases and expected results. +**Rust type**: `bool` -- Sync calls - - Sync call - - Sync call same context - - Sync call readonly - - Deploy call -- Async calls - - Async call (V1) - - Register promise (V2) - - Transfer execute - - Upgrade call +**Values** -We will now explain each of them in greater depth, and provide some syntax examples. +| Type | Value | Top-level encoding | Nested encoding | +| ------ | ------- | ------------------ | --------------- | +| `bool` | `true` | `0x01` | `0x01` | +| `bool` | `false` | `0x` | `0x00` | -For the full syntax specification, visit the [unified transaction syntax](/developers/transactions/tx-overview) documentation. +--- +### Byte slices and ASCII strings -## Sync calls +Byte slices are technically a special case of the [list types](/developers/data/composite-values#lists-of-items), but they are usually thought of as basic types. Their encoding is, in any case, consistent with the rules for lists of "byte items". +:::important +Strings are treated from the point of view of serialization as series of bytes. Using Unicode strings, while often a good practice in programming, tends to add unnecessary overhead to smart contracts. The difference is that Unicode strings get validated on input and concatenation. +We consider best practice to use Unicode on the frontend, but keep all messages and error messages in ASCII format on smart contract level. +::: -### Standard sync call +**Rust types**: `ManagedBuffer`, `BoxedBytes`, `&[u8]`, `Vec`, `String`, `&str`. -A standard sync call performs a direct, synchronous transaction to a contract on the same shard. This type of call is launched with `.sync_call()`. +**Top-encoding**: The byte slice, as-is. -```rust - /// Executes transaction synchronously. - /// - /// Only works with contracts from the same shard. - pub fn sync_call(self) -> ::Unpacked -``` +**Nested encoding**: The length of the byte slice on 4 bytes, followed by the byte slice as-is. -In this example, we are building a `sync call` to a `destination` smart contract address using the adder contract's proxy: +**Examples** -```rust title=adder.rs - #[endpoint] - fn sync(&self, destination: ManagedAddress, value: BigUint) { - self.tx() - .to(destination) - .typed(adder_proxy::AdderProxy) - .add(value) - .sync_call(); - } -``` +| Type | Value | Top-level encoding | Nested encoding | Explanation | +| --------------- | --------------------------- | ------------------ | ------------------ | ----------------------------------------------------------------- | +| `&'static [u8]` | `b"abc"` | `0x616263` | `0x00000003616263` | ASCII strings are regular byte slices of buffers. | +| `ManagedBuffer` | `ManagedBuffer::from("abc")`| `0x616263` | `0x00000003616263` | Use `Vec` for a buffer that can grow. | +| `BoxedBytes` | `BoxedBytes::from( b"abc")` | `0x616263` | `0x00000003616263` | BoxedBytes are just optimized owned byte slices that cannot grow. | +| `Vec` | `b"abc".to_vec()` | `0x616263` | `0x00000003616263` | Use `Vec` for a buffer that can grow. | +| `&'static str` | `"abc"` | `0x616263` | `0x00000003616263` | Unicode string (slice). | +| `String` | `"abc".to_string()` | `0x616263` | `0x00000003616263` | Unicode string (owned). | +:::info Note +Inside contracts, `ManagedBuffer` is [the only recommended type for generic bytes](/developers/best-practices/the-dynamic-allocation-problem). +::: +--- -### Sync call, same context -This call operates in the same execution context as the source contract. This means that the callee code is executed over the caller's storage and context, so it's just like calling third-party code to deal with your storage. +### Address -It's essential that the code called in such a way is _trusted_, since we are granting it direct access to our entire storage. +MultiversX addresses are 32 byte arrays, so they get serialized as such, both in top and nested encodings. -It can be useful for having library-like smart contracts or plug-in systems. It is currently not used often. +--- -To perform this type of call, use `.sync_call_same_context()`. -```rust - /// Executes transaction synchronously, in the same context (performed in the name of the caller). - /// - /// Only works with contracts from the same shard. - pub fn sync_call_same_context(self) -> ::Unpacked -``` +### Token identifiers -In this example, we are building a `sync call` using the `same execution context` to a `destination` smart contract address using the adder contract's proxy: +MultiversX ESDT token identifiers are of the form `XXXXXX-123456`, where the first part is the token ticker, 3 to 20 characters in length, and the last is a random generated number. -```rust title=adder.rs - #[endpoint] - fn sync_same_context(&self, destination: ManagedAddress, value: BigUint) { - self.tx() - .to(destination) - .typed(adder_proxy::AdderProxy) - .add(value) - .sync_call_same_context(); - } -``` +They are top-encoded as is, the exact bytes and nothing else. +Because of their variable length, they need to be serialized like variable length byte slices when nested, so the length is explicitly encoded at the start. +| Type | Value | Top-level encoding | Nested encoding | +| --------------- | --------------------------- | ------------------ | ------------------ | +| `TokenIdentifier` | `ABC-123456` | `0x4142432d313233343536` | `0x0000000A4142432d313233343536` | -### Sync call, readonly +--- -This type of call performs a synchronous call in `readonly` mode, meaning the destination contract's state cannot be altered by this action. This type of call is performed with `.sync_call_readonly()`. +--- -```rust - /// Executes transaction synchronously, in readonly mode (target contract cannot have its state altered). - /// - /// Only works with contracts from the same shard. - pub fn sync_call_readonly(self) -> ::Unpacked -``` +### Simulate and estimate a transaction -In this example, we are building a `sync call` in `readonly mode` to a `destination` smart contract address using the adder contract's proxy: +Before you spend real gas, ask the network two questions about a transaction, +without broadcasting it, without moving funds: -```rust title=adder.rs - #[endpoint] - fn sync_readonly(&self, destination: ManagedAddress, _value: BigUint) { - self.tx() - .to(destination) - .typed(adder_proxy::AdderProxy) - .sum() - .sync_call_readonly(); - } -``` +- **`estimateTransactionCost(tx)`**, how much gas will this need? Returns + `{ gasLimit, status }`. +- **`simulateTransaction(tx)`**, what would this actually do? Returns the full + `TransactionOnNetwork` it would produce: status, smart-contract results, logs, + and a `failReason` if it would fail. +Both are read-only. This recipe builds a plain transfer from a **throwaway +account** that is generated in-process and never funded or broadcast, so there is +nothing to set up, no PEM, no faucet. +## Prerequisites -### Contract Deploy +- Node.js >= 20.13.1. +- Network access to devnet. No wallet, no PEM, no EGLD. -On MultiversX contracts can currently only be deployed in the same shard as their deployer. The new address will always be generated in such a way that it always lands in the same shard, no matter the shard configuration. +## Install -It therefore makes sense that deploy calls are always synchronous. +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/simulate-estimate-transaction +npm install +npm run build +npm start +``` + +## The two dry-run reads + +```ts title="src/simulateEstimate.ts" +// src/simulateEstimate.ts — the subject of this recipe: two "dry run" reads +// that ask the network about a transaction WITHOUT broadcasting it. Neither +// changes state or spends funds. +// +// - estimateTransactionCost(tx) → how much gas this transaction needs. +// Returns a TransactionCostResponse { gasLimit, status }. Works on an +// UNSIGNED transaction: the SDK fills in a dummy signature and fetches the +// sender nonce for you internally. +// - simulateTransaction(tx) → what this transaction WOULD do: the full +// TransactionOnNetwork it would produce (status, smart-contract results, +// logs). Runs in a sandbox. Unlike estimate, it requires a signature to be +// PRESENT on the transaction (a throwaway one is fine — nothing is sent). +// +// Both are on INetworkProvider, so an Api or Proxy provider both work. + +import type { + INetworkProvider, + Transaction, + TransactionOnNetwork, + TransactionCostResponse, +} from '@multiversx/sdk-core'; + +/** How much gas will this cost? Works on an unsigned transaction. */ +export async function estimateGas( + provider: INetworkProvider, + tx: Transaction, +): Promise { + return provider.estimateTransactionCost(tx); +} + +/** What would this transaction do? Requires a signature to be present. */ +export async function simulate( + provider: INetworkProvider, + tx: Transaction, +): Promise { + return provider.simulateTransaction(tx); +} +``` + +## Wiring it up + +```ts title="src/index.ts" +// src/index.ts — build a plain EGLD transfer from a THROWAWAY account (no +// funds, never broadcast), then ask devnet to estimate its gas and simulate its +// execution. No real wallet, no real funds. +// +// Usage: +// npm run build && npm start + +import { DevnetEntrypoint, Account, Mnemonic, Transaction } from '@multiversx/sdk-core'; +import { estimateGas, simulate } from './simulateEstimate'; + +async function main(): Promise { + const entrypoint = new DevnetEntrypoint(); + const provider = entrypoint.createNetworkProvider(); + + // A throwaway account: generated in-process, never funded, never broadcast. + // It only needs to be a valid sender address for the transaction we probe. + const account = Account.newFromMnemonic(Mnemonic.generate().toString()); + const config = await provider.getNetworkConfig(); + + // Send to SELF so the sender and receiver are always in the same shard — + // simulate runs on the sender's shard, so a same-shard tx gives a definitive + // execution status every run (a cross-shard one would only report routing). + const tx = new Transaction({ + sender: account.address, + receiver: account.address, + gasLimit: 50000n, + chainID: config.chainID, + value: 1000000000000000000n, // 1 EGLD — this account does NOT have it; that is the point of a dry run + nonce: 0n, + }); -During the deploy, the constructor of the new contract , `init`, is always called. All contracts must have this endpoint. + console.log(`Throwaway sender/receiver: ${account.address.toBech32()}`); + + // ESTIMATE — runs against the UNSIGNED transaction. + const cost = await estimateGas(provider, tx); + console.log('\nestimateTransactionCost (unsigned):'); + console.log(` gasLimit ${cost.gasLimit}`); + + // SIMULATE — needs a signature present. A throwaway signature from the + // ephemeral key is enough; nothing is broadcast. + tx.signature = await account.signTransaction(tx); + const simulated = await simulate(provider, tx); + const failReason = typeof simulated.raw.failReason === 'string' ? simulated.raw.failReason : ''; + console.log('\nsimulateTransaction (signed with the throwaway key, not broadcast):'); + console.log(` status ${simulated.status.toString()} (the sandbox execution outcome)`); + if (failReason) { + console.log(` failReason: ${failReason}`); + } + console.log(' (an unfunded sender means "fail" here — simulate caught it for free, no gas spent)'); +} -There are 2 types of deploy call: -- Deploy with explicit byte code (provided explicitly by the caller contract); -- Deploy from source (using bytecode from an existing source address). This is usually cheaper, since byte codes can be large, and processing or storing this code can incur significant gas costs. +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); +``` +## Run it -Both of these calls are executed using `.sync_call()`, but the transaction setup differs for each type. +```bash +npm start +``` -For a simple `raw deploy`, initiate a raw deploy transaction using the `.raw_deploy()` function, as shown below: +Expected output (the throwaway address changes every run; everything else is +stable): -```rust title=adder.rs - #[endpoint] - fn raw_deploy(&self, code: ManagedBuffer) -> ManagedAddress { - self.tx() - .raw_deploy() - .code(code) - .code_metadata(CodeMetadata::UPGRADEABLE) - .returns(ReturnsNewManagedAddress) - .sync_call() - } -``` +```text +Throwaway sender/receiver: erd1u34zst7xxnkj2f097mw7j3xqx75mxnfd2tut5nyvl7usfewtjgps6yzv0d + +estimateTransactionCost (unsigned): + gasLimit 50000 + +simulateTransaction (signed with the throwaway key, not broadcast): + status fail (the sandbox execution outcome) + failReason: insufficient balance for fees, has: 0, wanted: 50000000000000 + (an unfunded sender means "fail" here — simulate caught it for free, no gas spent) +``` + +## How it works + +**The failure is the point.** The throwaway sender has no funds, so simulate +returns `status: fail` with `failReason: insufficient balance for fees`, the exact +error you would otherwise have paid gas to discover on-chain. Simulate is how you +catch that first. On a funded sender the same call would return `status: success` +and the real smart-contract results. + +**Estimate needs no signature; simulate does.** `estimateTransactionCost` works on +the raw, unsigned transaction, the SDK injects a dummy 64-byte signature and +recalls the nonce for you internally (confirmed in +`node_modules/@multiversx/sdk-core/out/networkProviders/proxyNetworkProvider.js`). +`simulateTransaction` rejects a nil signature even though `checkSignature` +defaults to `false`, so the transaction must carry one, and a throwaway key's +signature is enough because nothing is broadcast. + +**Send to self to keep the simulation same-shard.** Simulate runs on the sender's +shard. A cross-shard transaction's simulate reports only routing (`senderShard` / +`receiverShard`) with no execution status; a same-shard one (sender === receiver +here) always returns a definitive status. That is why `failReason` is populated on +every run. + +## Pitfalls + +:::warning[Pitfall 1: simulate needs a signature, estimate does not] +`simulateTransaction` throws `nil signature while trying to simulate` on a +transaction with no signature, even with `checkSignature=false`. Sign it first (a +throwaway key is fine, nothing is sent). `estimateTransactionCost` has no such +requirement. +::: -In the example above, only `.code()` is mandatory for a deploy sync call. We need to either pass the code to the transaction as an argument, or to receive it from a specified location on the blockchain (from source). +:::note[Pitfall 2: a cross-shard simulate returns routing, not a status] +Simulate executes on the sender's shard only. If sender and receiver are in +different shards, the response is just `{ senderShard, receiverShard }` and +`status` is empty, not a bug, just the limit of a single-shard sandbox. Send to +self (or keep both endpoints in one shard) for a definitive status. +::: -In the case of a `deploy from source` transaction, we would use the specific function `.from_source()` instead of `.code()` and pass the source address as a parameter, as such: +:::note[Pitfall 3: estimate's status field is not the execution status] +`TransactionCostResponse.status` is empty for a plain transfer, the `/cost` +endpoint returns a `gasLimit`, not an execution outcome. For "would this succeed", +use `simulateTransaction`, not the `status` on the estimate response. +::: -```rust title=adder.rs - #[endpoint] - fn raw_deploy_from_source(&self, source: ManagedAddress) -> ManagedAddress { - self.tx() - .raw_deploy() - .from_source(source) - .code_metadata(CodeMetadata::UPGRADEABLE) - .returns(ReturnsNewManagedAddress) - .sync_call() - } -``` +## See also +- [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld) + is how you actually send it once simulate says it will succeed. +- [Fetch network config and status](/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-network-config-status) + provides the `gasPerDataByte` / `minGasLimit` formula that estimate spares you + from applying by hand. +- [Configure a network provider](/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider) + builds the provider both dry-run reads run on. +--- -## Async calls +### Smart contract annotations +## Introduction +Annotations (also known as Rust "attributes") are the bread and butter of the `multiversx-sc` smart contract development framework. While contracts can in principle be written without any annotations or code generation macros in place, it is infinitely more difficult to do so. +One of the main purposes of the framework is to make the code as readable and concise as possible, and annotations are the path to get there. -### Async call (V1) +For an introduction, check out [the Crowdfunding tutorial](/developers/tutorials/crowdfunding-p1). This page is supposed to be a complete index of all annotations that can be encountered in smart contracts. -The most common type of async call. This type of call can be executed with `.async_call_and_exit()`. -:::important -Async call uses the `async V1` mechanism. -::: +## Trait annotations -```rust -pub fn async_call_and_exit(self) -> ! -``` -This type of call always terminates the current transaction immediately. Any code coming after it will not be executed. +### `#[multiversx_sc::contract]` -It is therefore only possible to have **one** such call per transaction. +The `contract` annotation must always be placed on a trait and will automatically make that trait the main container for the smart contract endpoints and logic. There should be only one such trait defined per crate. -In this example, we are building an `async V1 call` to a `destination` smart contract address using the adder contract's proxy: +Note that the annotation takes no additional arguments. -```rust title=adder.rs - #[endpoint] - fn async_call(&self, destination: ManagedAddress, value: BigUint) { - self.tx() - .to(destination) - .typed(adder_proxy::AdderProxy) - .add(value) - .async_call_and_exit(); - } -``` +--- +### `#[multiversx_sc::module]` -### Register promise (V2) +The `module` annotation must always be placed on a trait and will automatically make that trait a smart contract module. -Register promise performs an asynchronous promise call and allows multiple calls as such in a single transaction. To perform this type of call, use `.register_promise()`. +Note that the annotation takes no additional arguments. -:::important -Register promise uses the `async V2` mechanism. +:::caution +Only one contract, module or proxy annotation is allowed per Rust module. If they are in separate files there is no problem, but if several share a file, explicit `mod module_name { ... }` must enclose the module. ::: -```rust - /// Launches a transaction as an asynchronous promise (async v2 mechanism). - /// - /// Several such transactions can be launched from a single transaction. - /// - /// Must set: - /// - to - /// - gas - /// - a function call, ideally via a proxy. - /// - /// Value-only promises are not supported. - /// - /// Optionally, can add: - /// - any payment - /// - a promise callback, which also needs explicit gas for callback. - pub fn register_promise(self) -``` +--- -Unlike the old async call, it is possible to have more than one `register_promise` call in a transaction. Execution is not terminated. -In this example, we are building an `async V2 call` to a `destination` smart contract address using the adder contract's proxy: +### `#[multiversx_sc::proxy]` -```rust title=adder.rs - #[endpoint] - fn register_promise(&self, destination: ManagedAddress, value: BigUint) { - self.tx() - .to(destination) - .gas(30_000_000u64) - .typed(adder_proxy::AdderProxy) - .add(value) - .register_promise(); - } -``` +The `proxy` annotation must always be placed on a trait and will automatically make that trait a smart contract call proxy. More about smart contract proxies in [the contract calls reference](/developers/transactions/tx-legacy-calls). -Just like the old async call, promises allow callbacks. +In short, contracts always get an auto-generated proxy. However, if such an auto-generated proxy of another contract is not available, it is possible to define such a "contract interface" by hand, using the `proxy` attribute. -:::important -Promises callbacks must be annotated with `#[promises_callback]` instead of `#[callback]`. -::: +Note that the annotation takes no additional arguments. +:::caution +Only one contract, module or proxy annotation is allowed per Rust module. If they are in separate files there is no problem, but if several share a file, explicit `mod proxy_name { ... }` must enclose the module. +::: -### Transfer execute +## Method annotations -This call executes a transaction asynchronously without waiting for a callback. In order to perform this type of call use `.transfer_execute()`. -```rust - /// Sends transaction asynchronously, and doesn't wait for callback ("fire and forget".) - pub fn transfer_execute(self) -``` +### `#[init]` -In this example, we are building an async call that **does not wait for a callback** (fire and forget) to a `destination` smart contract address using the adder contract's proxy: +Every smart contract needs one constructor that only gets called once when the contract is deployed. The method annotated with init is the constructor. -```rust title=adder.rs - #[endpoint] - fn transfer_execute(&self, destination: ManagedAddress, value: BigUint) { - self.tx() - .to(destination) - .gas(30_000_000u64) - .typed(adder_proxy::AdderProxy) - .add(value) - .transfer_execute(); +```rust +#[multiversx_sc::contract] +pub trait Example { + #[init] + fn this_is_the_constructor( + constructor_arg_1: u32, + constructor_arg_2: BigUint) { + // ... } +} ``` +:::note +When upgrading a smart contract, the constructor in the new code is called. It is also called only once, and it can also never be called again. +::: -### Upgrade call - -If a smart contract is marked as **upgradeable**, its owner is allowed to upgrade the smart contract code to a newer version. +### `#[endpoint]` and `#[view]` -The upgrade call changes the code and causes the special endpoint `upgrade` to be called, analogous to how a deploy will call the `init` constructor. +Endpoints are the public methods of contracts, which can be called in transactions. A contract can define any number of methods, but only those annotated with `#[endpoint]` or `#[view]` are visible to the outside world. -Unlike deploy calls, it is possible to upgrade a contract from another shard. This is because, even though the original owner deployer will always be in the same shard as the contract, contract ownership can be transferred. +`#[view]` is meant to indicate readonly methods, but this is currently not enforced in any way. Functionally, `#[view]` and `#[endpoint]` are currently perfectly synonymous. However, there are plans for the future to enforce views to be verified at compile time to be readonly. When that happens, smart contracts that will already have been correctly annotated will be easier to migrate. Until then, there is still value in having 2 annotations, since they indicate intent. -Similar to deploy calls, there are two types of expressing upgrade calls: -- Upgrade with explicit byte code (provided explicitly by the caller contract); -- Upgrade from source (using bytecode from an existing source address). This is usually cheaper, since byte codes can be large, and processing or storing this code can incur significant gas costs. +If no arguments are provided to the attribute, the name of the Rust method will be the name of the endpoint. Alternatively, an explicit endpoint name can be provided in brackets. -Since the upgrade call is an async call (v1), it also terminates execution immediately. It also accepts a callback. +Example: ```rust - /// Launches the upgrade from source async call. - pub fn upgrade_async_call_and_exit(self) -``` - -Syntax-wise, both of these calls are executed using `.upgrade_async_call_and_exit()`, but the transaction setup differs for each type. - -For a simple `raw upgrade` transaction, we could write: -```rust title=adder.rs +#[multiversx_sc::contract] +pub trait Example { #[endpoint] - fn raw_upgrade(&self, address: ManagedAddress, code: ManagedBuffer) { - self.tx() - .to(address) - .raw_upgrade() - .code(code) - .code_metadata(CodeMetadata::UPGRADEABLE) - .upgrade_async_call_and_exit(); + fn example(&self) { } -``` - -Similar to deploy calls, `.code()` is mandatory. We must either pass the code to the transaction, or to receive it from a specified location on the blockchain (from source). -In order to change this call into an `upgrade from source` call, replace the provided code with the source address, using `.from_source()`: + #[endpoint(camelCaseEndpointName)] + fn snake_case_method_name(&self, value: BigUint) { + } -```rust title=adder.rs - #[endpoint] - fn raw_upgrade_from_source(&self, address: ManagedAddress, source: ManagedAddress) { - self.tx() - .to(address) - .raw_upgrade() - .from_source(source) - .code_metadata(CodeMetadata::UPGRADEABLE) - .upgrade_async_call_and_exit(); + fn private_method(&self, value: &BigUint) { + } + + #[view(getData)] + fn get_data(&self) -> u32 { + 0 } +} ``` ---- +In this example, 3 methods are public endpoints. They are named `example`, `camelCaseEndpointName` and `getData`. All other names are internal and do not show up in the resulting contract. -### scdeploys +:::note +All endpoint arguments and results must be either serializable or special endpoint argument types such as `MultiValueEncoded`. They must also all implement the `TypeAbi` trait. There is no such restriction for private methods. +::: -This page describes the structure of the `sc-deploys` index (Elasticsearch), and also depicts a few examples of how to query it. +### Callbacks -## _id +There are 2 annotations for callbacks: `#[callback]` and `#[callback_raw]`. The second is only used in extreme cases. -The `_id` field of this index is represented by a bech32 encoded smart contract address. +Callbacks are special methods that get called automatically when the response comes after an asynchronous contract call. They give the contract the possibility to react to the result of a cross-shard call, but for consistency they get called the same way if the asynchronous call happens in the same shard. +They also act as closures, since they can retain some of the context of the transaction that performed the asynchronous call in the first place. -## Fields +A more detailed explanation on how they work in [the contract calls reference](/developers/transactions/tx-legacy-calls). -| Field | Description | -|---------------|-----------------------------------------------------------------------------------------------------| -| deployTxHash | The deployTxHash holds the hex encoded hash of the transaction that deployed the smart contract. | -| deployer | The address field holds the address in bech32 encoding of the smart contract deployer. | -| timestamp | The timestamp field represents the timestamp of the block in which the smart contract was deployed. | -| upgrades | The upgrades field holds a list with details about the upgrades of the smart contract. | +### Storage -The `upgrades` field is populated with the fields below: +It is possible for a developer to access storage manually in a contract, but this is error-prone and involves a lot of boilerplate code. For this reason, `multiversx-sc` offers storage annotations that manage and serialize the keys and values behind the scenes. +Each contract has a storage where arbitrary data can be stored on-chain. This storage is organized as a map of arbitrary length keys and values. The blockchain has no concept of storage key or value types, they are all stored as raw bytes. It is the job of the contract to interpret these values. -| upgrades fields | Description | -|-----------------|--------------------------------------------------------------------------------------------------------| -| upgrader | The upgrader field holds the bech32 encoded address of the sender of the contract upgrade transaction. | -| upgradeTxHash | The upgradeTxHash field holds the hex encoded hash of the contract upgrade transaction. | -| timestamp | The timestamp field represents the timestamp of the block in which the smart contract was upgraded. | +All trait methods annotated for storage handling must have no implementation. -## Query examples +#### `#[storage_get("key")]` +This is the simplest way to retrieve data from the storage. Let's start with an example of usage: -### Fetch details about a smart contract +```rust +#[multiversx_sc::contract] +pub trait Adder { + #[view(getSum)] + #[storage_get("sum")] + fn get_sum(&self) -> BigUint; + #[storage_get("example_map")] + fn get_value(&self, key_1: u32, key_2: u32) -> SerializableType; +} ``` -curl --request GET \ - --url ${ES_URL}/scdeploys/_search \ - --header 'Content-Type: application/json' \ - --data '{ - "query": { - "match": { - "_id":"erd..." - } - } -}' -``` - ---- -### Scenario Complex Values +First off, please note that a storage method can also be annotated with `#[view]` or `#[endpoint]`. The endpoint annotations refer to the role of the method in the contract, while the storage annotation refers to its implementation, so there is no overlap. -We already covered representations of simple types [here](/developers/testing/scenario/values-simple). This is enough for arguments of types like `usize`, `BigUint` or `&[u8]`, but we need to also somehow specify complex types like custom structs or lists of items. +Then, also note that there are 2 ways to use this annotation. In the first example, we simply specify the key in the annotation and from here on the method will always read from the same storage key, `"sum"` in this case. +In the second example the get method also takes some arguments. Any number of arguments is allowed. These get concatenated to the base key to form a composite key, effectively turning a section of the contract storage into a dictionary or map. -## **Concatenation** +For instance calling `self.get_value(1, 2)` will retrieve from the storage key `"example_map\x00\x00\x00\x01\x00\x00\x00\x02"` or `0x6578616d706c655f6d61700000000100000002`. `self.get_value(1, 3)` will read from a different place in storage, and so on. -It is possible to concatenate multiple expressions using the pipe operator (`|`). The pipe operator takes precedence over everything, so it is not currently possible to concatenate and then apply a function to the whole result. +This is the easiest way to get the equivalent of a HashMap in a smart contract. -This is ideal for short lists or small structs. +Lastly, storage getters must always return a deserializable type. The framework will automatically deserialize the object from whatever bytes it finds in the storage value. -:::note Example -- a `Vec` can be expressed as `"u32:1|u32:2|u32:3"`. -- a `(BigUint, BigUint)` tuple can be expressed as `"biguint:1|biguint:2"` -- a `SimpleStruct { a: u8, b: BoxedBytes }` can be expressed as `"u8:4|nested:str:value-b"` - ::: +#### `#[storage_set("key")]` -Please note that the pipe operator only takes care of the concatenation itself. You are responsible for making sure that [nested encoding](/developers/data/serialization-overview/#the-concept-of-top-level-vs-nested-objects) is used where appropriate. +This is the simplest way to write data to storage. Example: +```rust +#[multiversx_sc::contract] +pub trait Adder { + #[storage_set("sum")] + fn set_sum(&self, sum: &BigUint); -## **Using JSON lists as values** + #[storage_set("example_map")] + fn set_value(&self, key_1: u32, key_2: u32, value: &SerializableType); +} +``` -Scenarios allow using JSON lists to express longer values. This especially makes sense when the value being represented is itself a list in the smart contract. +It works very similarly to `storage_get`, with the notable difference that instead of returning a value, the value must be provided as an argument. The value to store is always the last argument. -:::note Example +Again, just like for the getter, an arbitrary number of additional map keys can be specified, as for `set_value` in the example. This is how we can write values to a section of our storage that behaves like a map. -- a `Vec` can also be expressed as `["u32:1", "u32:2", "u32:3"]`. -- a `(BigUint, BigUint)` tuple can also be expressed as `["biguint:1", "biguint:2"]` -- a `SimpleStruct { a: u8, b: BoxedBytes }` can also be expressed as `["u8:4", "nested:str:value-b"]`, although in this case a [JSON map](#using-json-maps-as-values) might be more appropriate. - ::: +:::caution +There is no mechanism in place to ensure that there is no overlap between storage keys. Nothing prevents a developer from writing: -Make sure not to confuse values expressed as JSON lists with other elements of scenario syntax. +```rust +#[storage_set("sum")] +fn set_sum(&self, sum: &BigUint); -:::note Example +#[storage_set("sum")] +fn set_another_sum(&self, another_sum: &BigUint); -```json -{ - "step": "scCall", - "txId": "echo_managed_vec_of_managed_vec", - "tx": { - "from": "address:an_account", - "to": "sc:basic-features", - "value": "0", - "function": "echo_managed_vec_of_managed_vec", - "arguments": [ - [ - "u32:3", - [ - "u32:1", - "u32:2", - "u32:3" - ], - "u32:0", - "u32:2", - [ - "u32:5", - "u32:6" - ] - ] - ], - "gasLimit": "50,000,000", - "gasPrice": "0" - } -} +#[storage_set("s")] +fn set_value(&self, key: u16, value: &SerializableType); ``` -In the example above, there is in fact a single argument that we are passing to the endpoint. The outer brackets in `"arguments": [ ... ]` are scenario syntax for the list of arguments. The brackets immediately nested signal a JSON list value. Notice how the list itself contains some more lists inside it. They all get concatenated in the end into a single value. +The first problem is easy to spot: we have 2 setters with the same key. -In this example the only argument is `0x0000000300000001000000020000000300000000000000020000000500000006`. +The second is harder to notice. Calling `self.set_value(0x756d, value)` or `self.set_value(30061, value)` will also overwrite `"sum"`. This is because `"um"` = `"\x75\6d"`, which gets concatenated to the `"s"`, forming `"sum"`. + +To avoid this vulnerability, **never have a key that is the prefix of another key!** ::: -:::tip -We mentioned above how the developer needs to take care of the serialization of the nested items. This is actually a good example of that. The endpoint `echo_managed_vec_of_managed_vec` takes a list of lists, so we need to serialize the lengths of the lists on the second level. Notice how the lengths are given as JSON strings and the contents as JSON lists; the first `"u32:3"` is the serialized length of the first item, which is `["u32:1", "u32:2", "u32:3"]`, and so forth. +#### `#[storage_mapper("key")]` -::: +Storage mappers are objects that can manage multiple storage keys at once. They are in charge with both reading and writing values. Some of them read and write values to multiple storage keys at once. +There are many storage mappers in the framework and more can be custom-defined. -## **Using JSON maps as values** +Example: -JSON lists make sense for representing series of items, but for structs JSON maps are more expressive. +```rust +#[storage_mapper("user_status")] +fn user_status(&self) -> SingleValueMapper; -The rules are as follows: +#[storage_mapper("list_mapper")] +fn list_mapper(&self, sub_key: usize) -> LinkedListMapper; +``` -- The interpreter will concatenate all JSON map values and leave the keys out. -- The keys need to be in alphanumerical order, so we customarily prefix them with numbers. Map keys in JSON are fundamentally unordered and this is the easiest way to enforce a deterministic order for the values. -- Map values can be either JSON strings, lists or other maps, all scenario value rules apply the same way all the way down. +The `SingleValueMapper` is the simplest of them all, since it only manages one storage key. Even though it only works with one storage entry, its syntax is more compact than `storage_get`/`storage_set` so it is used quite a lot. -:::note Example +In the `LinkedListMapper` we are dealing with a list of items, each with its own key. -This is an abridged section of the actual lottery contract in the examples. +Also note that additional sub-keys are also allowed for storage mappers, the same as for `storage_get` and `storage_set`. -```json -{ - "step": "checkState", - "accounts": { - "sc:lottery": { - "storage": { - "str:lotteryInfo|nested:str:lottery_name": { - "0-token_identifier": "nested:str:LOTTERY-123456", - "1-ticket_price": "biguint:100", - "2-tickets-left": "u32:0", - "3-deadline": "u64:123,456", - "4-max_entries_per_user": "u32:1", - "5-prize_distribution": ["u32:2", "u8:75", "u8:25"], - "6-whitelist": [ - "u32:3", - "address:acc1", - "address:acc2", - "address:acc3" - ], - "7-prize_pool": "biguint:500" - } - }, - "code": "file:../output/lottery-esdt.wasm" - } - } -} -``` -The Rust struct this translates to is: +#### `#[storage_is_empty("key")]` -```rust -#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode, TypeAbi)] -pub struct LotteryInfo { - pub token_identifier: TokenIdentifier, - pub ticket_price: BigUint, - pub tickets_left: u32, - pub deadline: u64, - pub max_entries_per_user: u32, - pub prize_distribution: Vec, - pub whitelist: Vec
, - pub prize_pool: BigUint, -} +This is very similar to `storage_get`, but instead of retrieving the value, it returns a boolean indicating whether the serialized value is empty or not. It does not attempt to deserialize the value, so it can be faster and more resilient than `storage_get`, depending on type. +```rust +#[storage_is_empty("opt_addr")] +fn is_empty_opt_addr(&self) -> bool; ``` -::: - -:::tip +Nowadays, it is more common to use storage mappers. The `SingleValueMapper` has an `is_empty()` method that does the same. -Once again, note that all contained values are in [nested encoding format](/developers/data/serialization-overview/#the-concept-of-top-level-vs-nested-objects): -- the token identifier has its length automatically prepended by the `nested:` prefix, -- big ints are given with the `biguint:` syntax, which prepends their byte length, -- small ints are given in full length, -- lists have their length explicitly encoded at the start, always on 4 bytes (as `u32`). +#### `#[storage_clear("key")]` -::: +This is very similar to `storage_set`, but instead of serializing and writing the storage value, it simply clears the raw bytes. +It does not do any serializing, so it can be faster than `storage_set`, depending on type. +```rust +#[storage_clear("field_to_clear")] +fn clear_storage_value(&self); +``` -## **A note about enums** +Nowadays, it is more common to use storage mappers. The `SingleValueMapper` has an `clear()` method that does the same. -There are 2 types of enums that we use in Rust: -- simple enums are simply encoded as `u8` -- complex enums are encoded just like structures, with an added `u8` discriminant at the beginning. +### Events -Discriminants are not explicit in the Rust code, they get generated automatically. Discriminats start from `0`. +Events are a way of returning data from smart contract, by leaving a trace of what happened during the execution. Event logs are not saved on the blockchain, but a hash of them is. This means that we can always check whether certain events were emitted by a transactions or not. -:::note Example +Because they are not saved on the chain in full, they are also a lot cheaper than storage. -This is an abridged section of a Multisig contract test. +In smart contracts we define them as trait methods with no implementation, as follows: -```json -{ - "step": "checkState", - "accounts": { - "sc:multisig": { - "storage": { - "str:action_data.item|u32:3": { - "1-discriminant": "0x06", - "2-amount": "u32:0", - "3-code": "nested:file:../test-contracts/adder.wasm", - "4-code_metadata": "0x0000", - "5-arguments": ["u32:1", "u32:2|1234"] - }, - "+": "" - }, - "code": "file:../output/multisig.wasm" - }, - "+": "" - } -} +```rust +#[event("transfer")] +fn transfer_event( + &self, + #[indexed] from: &ManagedAddress, + #[indexed] to: &ManagedAddress, + #[indexed] token_id: u32, + data: ManagedBuffer, +); ``` -In this example we have a `SCDeploy`: +The annotation always requires the name of the event to be specified explicitly in brackets. -```rust -#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode, TypeAbi)] -pub enum Action { - Nothing, - AddBoardMember(ManagedAddress), - AddProposer(ManagedAddress), - RemoveUser(ManagedAddress), - ChangeQuorum(usize), - SendEgld { - to: ManagedAddress, - amount: BigUint, - data: BoxedBytes, - }, - SCDeploy { - amount: BigUint, - code: ManagedBuffer, - code_metadata: CodeMetadata, - arguments: Vec, - }, - SCCall { - to: ManagedAddress, - egld_payment: BigUint, - endpoint_name: BoxedBytes, - arguments: Vec, - }, -} -``` +Events have 2 types of arguments: -::: +- "Topics" are annotated with `#[indexed]`. When saving event logs to a database, indexes will be created for all these fields, so they can be searched for efficiently. +- The "data" argument has no annotation. There can be only one data field in an event, and it cannot be indexed later. ---- +Event arguments (fields) can be of any serializable type. There is no return value for events. -### Scenario Simple Values -We went through the structure of a scenario, and you might have noticed that in a lot of places values are expressed in diverse ways. +### Events (legacy) -The VM imposes very few restrictions on its inputs and outputs, most fields are processed as raw bytes. The most straightforward way to write a test that one could think of would be to have the actual raw bytes always expressed in a simple format (e.g. like hexadecimal encoding). Indeed, our first contract tests were like this, but we soon discovered that it took painfully long prepare them and even longer to refactor. So, we gradually came up with increasingly complex formats to represent values in an intuitive human-readable way. +There is a legacy annotation, `#[legacy_event]` still used by some older contracts. It is deprecated and should no longer be used. -We chose to create a single universal format to be used everywhere in a scenario file. The same format is used for expressing: -- addresses, -- balances, -- transaction and block nonces, -- contract code, -- storage keys and values, -- log identifiers, topics and data, -- gas limits, gas costs, -- ESDT metadata, etc. +### `#[proxy]` -The advantage of this unique value format is that it is enough to understand it once to then use it everywhere. +This is a simple getter, which provides a convenient instance of a contract proxy. It is used when wanting to call another contract. -The scenario value format is closely related to the [MultiversX serialization format](/developers/data/serialization-overview). This is not by accident, Scenarios are designed to make it easy to interact MultiversX contracts and their data. +```rust +#[multiversx_sc::module] +pub trait ForwarderAsyncCallModule { + #[proxy] + fn vault_proxy(&self, to: Address) -> vault::Proxy; -Exceptions: `txId`, `comment` and `asyncCallData` are simple strings. `asyncCallData` might be changed to the default value format in the future and/or reworked. + // ... +} +``` + +There is no need for arguments, the annotation will figure out the contract to call by the provided return type. :::important +Proxy types need to be specified with an explicit module. In the example `vault::` is compulsory. +::: -It must be emphasized that no matter how values are expressed in scenarios, the communication with the VM is always done via raw bytes. Of course it is best when the value expression and the types in the smart contract match, but this is not enforced. -::: +### `#[output_names]` -A note on error messages: whenever we write a test that fails, the test runner tries its best to transform the actual value it found from raw bytes to a more human-readable form. It doesn't really know what format to use, so it tries its best to find something plausible. However, all it has are some heuristics, so it doesn't always get it right. It also displays the raw bytes so that the developer can investigate the proper value. +This one is used for ABI result names. In Rust, it is impossible to write Rust Docs for method returns, so we are using this annotation to optionally name the outputs of an endpoint. +--- -## **A note about the value parser and the use of prefixes** +### Smart Contract API Functions -The value interpreter is not very complex and uses simple prefixes for most functions. Examples of prefixes are `"str:"` and `"u32:"`. +## Introduction -The `|` (pipe) operator, which we use for concatenation has the highest priority. More about it [here](/developers/testing/scenario/values-complex#concatenation). +The Rust framework provides a wrapper over the MultiversX VM API functions and over account-level built-in functions. They are split into multiple modules, grouped by category: -The arguments of functions start after the prefix (no whitespace) and end either at the first pipe (`|`) or at the end of the string. +- BlockchainApi: Provides general blockchain information, which ranges from account balances, NFT metadata/roles to information about the current and previous block (nonce, epoch, etc.) +- CallValueApi: Used in payable endpoints, providing information about the tokens received as payment (token type, nonce, amount) +- CryptoApi: Provides support for cryptographic functions like hashing and signature checking +- SendApi: Handles all types of transfers to accounts and smart contract calls/deploys/upgrades, as well as support for ESDT local built-in functions -Multiple prefixes evaluated right to left, for instance `"keccak256:keccak256:str:abcd"` will first convert `"abcd"` to bytes, then apply the hashing function on it twice. +The base trait for the APi is: https://docs.rs/multiversx-sc/0.39.0/multiversx_sc/api/trait.VMApi.html -With that being said, the following sections will describe how to express different value types in scenarios. A full list of the prefixes is [at the end of this page](#the-full-list-of-scenario-value-prefixes). +The source code for the APIs can be found here: https://github.com/multiversx/mx-sdk-rs/tree/master/framework/base/src/api -## **Empty value** +## Blockchain API -Empty strings (`""`) mean empty byte arrays. The number zero can also be represented as an empty byte array. -Other values that translate to an empty byte array are `"0"` and `"0x"`. +This API is accessible through `self.blockchain()`. Available functions: -## **Hexadecimal representation** +### get_sc_address -To provide raw hexadecimal representations of the values, use the prefix `0x` and follow it with the base-16 bytes. E.g. `"0x1234567890"`. -After the `0x` prefix an even number of digits is expected, since 2 digits = 1 byte. +```rust +get_sc_address() -> ManagedAddress +``` -:::note Examples +Returns the smart contract's own address. -- `"0x"` -- `"0x1234567890abcdef"` -- `"0x0000000000000000"` -::: +### get_owner_address -## **Standalone number representations** +```rust +get_owner_address() -> ManagedAddress +``` -Unprefixed numbers are interpreted as base 10, unsigned. -Unsigned numbers will be represented in the minimum amount of bytes in which they can fit. +Returns the owner's address. -:::note Examples -- `"0"` -- `"1"` -- `"1000000".` -- `"255"` is the same as `"0xff"` -- `"256"` is the same as `"0x0100"` -- `"0"` is the same as `""` -::: +### check_caller_is_owner -:::tip -Digit separators are allowed anywhere, for readability, e.g. `"1,000,000"`. -::: +```rust +check_caller_is_owner() +``` +Terminates the execution and signals an error if the caller is not the owner. -## **Standalone signed numbers** +Use `#[only_owner]` endpoint annotation instead of directly calling this function. -:::caution -Only use signed numbers if you absolutely need to. Big signed integer representation has some pitfalls that can lead to subtle and unexpected issues when interacting with the contract. -::: -Sometimes contract arguments are expected to be signed. These arguments will be transmitted as two’s complement representation. Prefixing any number (base 10 or hex) with a minus sign will convert them to two’s complement. Two’s complement is interpreted as positive or negative based on the first bit. +### get_shard_of_address -Sometimes positive numbers can start with a "1" bit and get accidentally interpreted as negative. To prevent this, we can prefix them with a plus. A few examples should make this clearer: +```rust +get_shard_of_address(address: &ManagedAddress) -> u32 +``` -:::note Examples +Returns the shard of the address passed as argument. -- `"1"` is represented as `"0x01"`, signed interpretation: `1`, everything OK. -- `"255"` is represented as `"0xff"`, signed interpretation: `"-1",` this might not be what we expected. -- `"+255"` is represented as `"0x00ff"`, signed interpretation: `"255".` The prepended zero byte makes sure the contract interprets it as positive. The `+` makes sure those leading zeroes are added if necessary. -- `"+1"` is still represented as `"0x01"`, here the leading 0 is not necessary. Still, it is good practice adding the `+` if we know the argument is expected to be signed. -- `"-1"` is represented as `"0xff"`. Negative numbers are also represented in the minimum number of bytes possible. -::: -For more about signed number encoding, see [the big number serialization format](/developers/data/simple-values#arbitrary-width-big-numbers). +### is_smart_contract +```rust +is_smart_contract(address: &ManagedAddress) -> bool +``` -## **Nested numbers** +Returns `true` if the address passed as parameter is a Smart Contract address, `false` for simple accounts. + + +### get_caller + +```rust +get_caller() -> ManagedAddress +``` -Whenever we nest numbers in larger structures, we need to somehow encode their length. Otherwise, it would become impossible for them to be deserialized. +Returns the current caller. -The format helps developers to also easily represent nested numbers. These are as follows: +Keep in mind that for SC Queries, this function will return the SC's own address, so having a view function that uses this API function will not have the expected behaviour. -- `biguint:` is useful for representing a nested BigUint. It outputs the length of the byte representation, followed by the big endian byte representation itself. -- `u64:` `u32:` `u16:` `u8:` interpret the argument as an unsigned int and convert to big endian bytes of respective length (8/4/2/1 bytes) -- `i64:` `i32:` `i16:` `i8:` interpret the argument as a signed int and convert to 2's complement big endian bytes of respective length (8/4/2/1 bytes) -:::note Examples +### get_balance -- `"biguint:0"` equals `0x00000000` -- `"biguint:1"` equals `0x0000000101` -- `"biguint:256"` equals `0x00000020100` -- `u64:1` equals `0x0000000000000001` -- `i64:-1` equals `0xFFFFFFFFFFFFFFFF` -- `u32:1` equals `0x00000001` -- `u16:1` equals `0x0001` -- `u8:1` equals `0x01` -::: +```rust +get_balance(address: &ManagedAddress) -> BigUint +``` +Returns the EGLD balance of the given address. -## **Nested items** +This only works for addresses that are in the same shard as the smart contract. -The `nested:` prefix prepends the length of the argument. It is similar to `biguint:`, but does not expect a number. -:::note Examples +### get_sc_balance -- `"nested:str:abc"` equals `0x00000003|str:abc` -- `"nested:0x01020304"` equals `0x0000000401020304` -::: +```rust +get_sc_balance(token: &EgldOrEsdtTokenIdentifier, nonce: u64) -> BigUint +``` +Returns the EGLD/ESDT/NFT balance of the smart contract. -## **Booleans** +For fungible ESDT, nonce should be 0. To get the EGLD balance, you can simply pass `EgldOrEsdtTokenIdentifier::egld()` as parameter. -The format offers these 2 constants, for convenience: -- `"true"` = `"1"` = `"0x01"` -- `"false"` = `"0"` = `""`. +### get_tx_hash -:::caution -This is the standalone representation. If your boolean is embedded in a structure or list, use `u8:0` instead of `false`. -::: +```rust +get_tx_hash() -> ManagedByteArray +``` +Returns the current tx hash. -## **ASCII strings** +In case of asynchronous calls, the tx hash is the same both in the original call and in the associated callback. -The preferred way of representing ASCII strings is with the `str:` prefix. -:::important -The `''` and ` `` ` prefixes are common in older examples. They are equivalent to `str:`, but considered legacy. We recommend avoiding them because they clash with the syntax of languages where we might want to embed scenario code (Go and Markdown in particular). -::: +### get_gas_left +```rust +get_gas_left() - > u64 +``` -## **User Addresses** +Returns the remaining gas, at the time of the call. -`address:` constructs a dummy user address from a word. +This is useful for expensive operations, like iterating over an array of users in storage and sending rewards. -Addresses need to be 32 bytes long, so +A smart contract call that runs out of gas will revert all operations, so this function can be used to return _before_ running out of gas, saving a checkpoint, and continuing on a second call. -- if the word is longer, it gets chopped off at the end -- if the word is shorter, it gets extended to the right with `0x5f` bytes (the `"_"` character). -:::note Example -`"address:my_address"` is the same as: +### get_block_timestamp_seconds -- `"str:my_address______________________"` or -- `"0x6d795f616464726573735f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f"`. -::: +```rust +get_block_timestamp_millis() -> TimestampSeconds +``` +Returns the timestamp of the current block, in seconds (UNIX timestamp). -## **Smart Contract Addresses** -`sc:` constructs a dummy smart contract address. -On MultiversX, smart contract addresses have a different format than user address - they start with 8 bytes of zero. +### get_block_timestamp_millis -:::important -The format requires that all accounts with addresses in SC format must have non-empty code. +```rust +get_block_timestamp_millis() -> TimestampMillis +``` -It is forbidden to have accounts with code but an address that doesn't obey the SC format. -::: +Returns the timestamp of the current block, in milliseconds (UNIX timestamp). -:::note Example -`"sc:my_address"` is the same as: -- `"0x0000000000000000|str:my_address______________"` or -- `"0x00000000000000006d795f616464726573735f5f5f5f5f5f5f5f5f5f5f5f5f5f"`. -::: -Sometimes the last byte of a a SC address is relevant, since it affects which shard the contract will end up in. It can be specified with a hash character `#`, followed by the final byte as hex. +### get_block_nonce -:::note Example -`"sc:my_address#a3"` is the same as: +```rust +get_block_nonce() -> u64 +``` -- `"0x0000000000000000|str:my_address_____________|0xa3"` and -- `"0x00000000000000006d795f616464726573735f5f5f5f5f5f5f5f5f5f5f5f5fa3"`. -::: +Returns the unique nonce of the block that includes the current transaction. -## **File contents** +### get_block_round -`file:` loads an entire file and uses the contents of the entire file as value. +```rust +get_block_round() -> u64 +``` -The path of the file is given relative to the current scenario file. +Returns the round number of the current block. Each epoch consists of a fixed number of rounds. The round number resets to 1 at the start of every new epoch. -Used in the first place for specifying smart contract code. It can, however, be used for specifying any value, anywhere. -:::note Example -`"file:../output/my-contract.wasm"` -::: +### get_block_epoch -Example usage: +```rust +get_block_epoch() -> u64 +``` -- initializing contract code, -- contract code to deploy, -- contract code to be passed to another contract as argument for indirect deploy, -- checking some contract code in storage, -- any large argument +Returns the epoch of the current block. +These functions are mostly used for setting up deadlines, so they've been grouped together. -## **Hash function** -`keccak256:` computes the Keccak256 hash of the argument. The result is always 32 bytes in length. +### get_block_random_seed +```rust +get_block_random_seed() -> ManagedByteArray +``` -## **The full list of scenario value prefixes** +Returns the block random seed, which can be used for generating random numbers. -The prefixes are: +This will be the same for all the calls in the current block, so it can be predicted by someone calling this at the start of the round and only then calling your contract. -- `str:` converts from ASCII strings to bytes. -- `address:` dummy user address -- `sc:` dummy smart contract address -- `file:` loads the entire contents of a file -- `keccak256:` computes the hash of the argument -- `u64:` `u32:` `u16:` `u8:` interpret the argument as an unsigned int and convert to big endian bytes of respective length (8/4/2/1 bytes) -- `i64:` `i32:` `i16:` `i8:` interpret the argument as a signed int and convert to 2's complement big endian bytes of respective length (8/4/2/1 bytes) -- `biguint:` big number unsigned byte length followed by big number unsigned bytes themselves -- `nested:` prepends the length of the argument ---- +### get_prev_block_timestamp_seconds -### scresults +```rust +get_prev_block_timestamp_seconds() -> TimestampSeconds +``` -This page describes the structure of the `sc-results` index (Elasticsearch), and also depicts a few examples of how to query it. +Returns the timestamp of the previous block, in seconds (UNIX timestamp). -## _id -:::warning Important +### get_prev_block_timestamp_millis -**The `scresults` index will be deprecated and removed in the near future.** -We recommend using the [operations](/sdk-and-tools/indices/es-index-operations) index, which contains all the smart contract results data. -The only change required in your queries is to include the `type` field with the value `unsigned` to fetch all smart contract results. +```rust +get_prev_block_timestamp_millis() -> TimestampMillis +``` -Please make the necessary updates to ensure a smooth transition. -If you need further assistance, feel free to reach out. -::: +Returns the timestamp of the previous block, in milliseconds (UNIX timestamp). -The `_id` field for this index is composed of hex-encoded smart contract result hash. -(example: `cbd4692a092226d68fde24840586bdf36b30e02dc4bf2a73516730867545d53c`) -## Fields +### get_prev_block_nonce +```rust +get_prev_block_nonce() -> u64 +``` -| Field | Description | -|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| miniBlockHash | The miniBlockHash field represents the hash of the miniblock in which the smart contract result was included. | -| nonce | The nonce field represents the transaction sequence number. | -| gasLimit | The gasLimit field represents the maximum gas units the sender is willing to pay for. | -| gasPrice | The gasPrice field represents the amount to be paid for each gas unit. | -| value | The value field represents the amount of EGLD to be sent from the sender to the receiver. | -| sender | The sender field represents the address of the smart contract result sender. | -| receiver | The receiver field represents the destination address of the smart contract result. | -| senderShard | The senderShard field represents the shard ID of the sender address. | -| receiverShard | The receiverShard field represents the shard ID of the receiver address. | -| relayerAddr | The relayerAddr field represents the address of the relayer. | -| relayedValue | This relayedValue field represents the amount of EGLD to be transferred via the inner transaction's sender. | -| code | The code holds the code of the smart contract result. | -| data | The data field holds additional information for a smart contract result. It can contain a simple message, a function call, an ESDT transfer payload, and so on. | -| prevTxHash | The prevTxHash holds the hex encoded hash of the previous transaction. | -| originalTxHash | The originalTxHash holds the hex encoded hash of the transaction that generated the smart contract result. | -| callType | The callType field holds the type of smart contract call that is done through the smart contract result. | -| codeMetaData | The codeMetaData field holds the code metadata. | -| returnMessage | The returnMessage field holds the message that is returned by a smart contract in case of an error. | -| timestamp | The timestamp field represents the timestamp of the block in which the smart contract result was executed. | -| status | The status field holds the execution state of the smart contract result. The execution state can be `pending` or `success`. | -| tokens | The tokens field contains a list of ESDT tokens that are transferred based on the data field. The indices from the `tokens` list are linked to the indices from `esdtValues` list. | -| esdtValues | The esdtValues field contains a list of ESDT values that are transferred based on the data field. | -| receivers | The receivers field contains a list of receiver addresses in case of ESDTNFTTransfer or MultiESDTTransfer. | -| receiversShardIDs | The receiversShardIDs field contains a list of receiver addresses' shard IDs. | -| operation | The operation field represents the operation of the smart contract result based on the data field. | -| function | The function field holds the name of the function that is called in case of a smart contract call. | -| originalSender | The originalSender field holds the sender's address of the original transaction. | -| hasLogs | The hasLogs field is true if the transaction has logs. | +### get_prev_block_round -## Query examples +```rust +get_prev_block_round() -> u64 +``` -### Fetch all the smart contract results generated by a transaction +### get_prev_block_epoch -``` -curl --request GET \ - --url ${ES_URL}/scresults/_search \ - --header 'Content-Type: application/json' \ - --data '{ - "query": { - "match": { - "originalTxHash":"d6.." - } - } -}' +```rust +get_prev_block_epoch() -> u64 ``` ---- -### sdk-dapp +### get_prev_block_random_seed -Library used to build React dApps on MultiversX Network. +```rust +get_prev_block_random_seed() -> ManagedByteArray +``` -:::important -The following documentation is for `sdk-dapp v5.0.0` and above. -::: +### get_block_round_time_millis -## Introduction +```rust +get_block_round_time_millis(&self) -> DurationMillis +``` -`sdk-dapp` is a library that holds core functional logic that can be used to create a dApp on MultiversX Network. +The block round time, in milliseconds, i.e the time between consecutive blocks. -It is built for applications that use any of the following technologies: -- React (example: [React Template Dapp](https://github.com/multiversx/mx-template-dapp)) -- Angular (example: [Angular Template Dapp](https://github.com/multiversx/mx-template-dapp-angular)) -- Vue (example: [Vue Template Dapp](https://github.com/multiversx/mx-template-dapp-vue)) -- Any other JavaScript framework (e.g. Solid.js etc.) (example: [Solid.js Dapp](https://github.com/multiversx/mx-solidjs-template-dapp)) -- React Native -- Next.js (example: [Next.js Dapp](https://github.com/multiversx/mx-template-dapp-nextjs)) +### get_current_esdt_nft_nonce -### GitHub project +```rust +get_current_esdt_nft_nonce(address: &ManagedAddress, token_id: &TokenIdentifier) -> u64 +``` -The GitHub repository can be found here: [https://github.com/multiversx/mx-sdk-dapp](https://github.com/multiversx/mx-sdk-dapp) +Gets the last nonce for an SFT/NFT. Nonces are incremented after every ESDTNFTCreate operation. + +This only works for accounts that have the ESDTNFTCreateRole set and only for accounts in the same shard as the smart contract. +This function is usually used with `self.blockchain().get_sc_address()` for smart contracts that create SFT/NFTs themselves. -### Live demo: template-dapp -See [Template dApp](https://template-dapp.multiversx.com/) for live demo or checkout usage in the [Github repo](https://github.com/multiversx/mx-template-dapp) +### get_esdt_balance +```rust +get_esdt_balance(address: &ManagedAddress, token_id: &TokenIdentifier, nonce: u64) -> BigUint +``` -### Requirements +Gets the ESDT/SFT/NFT balance for the specified address. -- Node.js version 20.13.1+ -- Npm version 10.5.2+ +This only works for addresses that are in the same shard as the smart contract. +For fungible ESDT, nonce should be 0. For EGLD balance, use the `get_balance` instead. -### Distribution -[npm](https://www.npmjs.com/package/@multiversx/sdk-dapp) +### get_esdt_token_data +```rust +get_esdt_token_data(address: &ManagedAddress, token_id: &TokenIdentifier, nonce: u64) -> EsdtTokenData +``` -## Installation +Gets the ESDT token properties for the specific token type, owned by the specified address. -The library can be installed via npm or yarn. +`EsdtTokenData` has the following format: -```bash -npm install @multiversx/sdk-dapp +```rust +pub struct EsdtTokenData { + pub token_type: EsdtTokenType, + pub amount: BigUint, + pub frozen: bool, + pub hash: ManagedBuffer, + pub name: ManagedBuffer, + pub attributes: ManagedBuffer, + pub creator: ManagedAddress, + pub royalties: BigUint, + pub uris: ManagedVec>, +} ``` -or +`token_type` is an enum, which can have one of the following values: -```bash -yarn add @multiversx/sdk-dapp +```rust +pub enum EsdtTokenType { + Fungible, + NonFungible, + SemiFungible, + Meta, + Invalid, +} ``` -> **Note:** Make sure you run your app on `https`, not `http`, otherwise some providers will not work. - -If you're transitioning from `@multiversx/sdk-dapp@4.x`, you can check out the [Migration guide](https://github.com/multiversx/mx-template-dapp/blob/main/MIGRATION_GUIDE.md) and [migration PR](https://github.com/multiversx/mx-template-dapp/pull/343) of Template Dapp +You will only receive basic distinctions for the token type, i.e. only `Fungible` and `NonFungible` (The smart contract has no way of telling the difference between non-fungible, semi-fungible and meta tokens). +`amount` is the current owned balance of the account. -## Usage +`frozen` is a boolean indicating if the account is frozen or not. -sdk-dapp aims to abstract and simplify the process of interacting with users' wallets and with the MultiversX blockchain, allowing developers to easily get started with new applications. +`hash` is the hash of the NFT. Generally, this will be the hash of the `attributes`, but this is not enforced in any way. Also, the hash length is not fixed either. -```mermaid -flowchart LR - A["Signing Providers & APIs"] <--> B["sdk-dapp"] <--> C["dApp"] -``` +`name` is the name of the NFT, often used as display name in front-end applications. -The basic concepts you need to understand are configuration, provider interaction, transactions, and presenting data. These are the building blocks of any dApp, and they are abstracted in the `sdk-dapp` library. +`attributes` can contain any user-defined data. If you know the format, you can use the `EsdtTokenData::decode_attributes` method to deserialize them. -Having this knowledge, we can consider several steps needed to put a dApp together: +`creator` is the creator's address. -**Table 1.** Steps to build a dApp +`royalties` a number between 0 and 10,000, meaning a percentage of any selling price the creator receives. This is used in the ESDT NFT marketplace, but is not enforced in any other way. (The way these percentages work is 5,444 would be 54.44%, which you would calculate: price \* 5,444 / 10,000. This convention is used to grant some additional precision) -| # | Step | Description | -|----|---------------------|-------------| -| 1 | Configuration | storage configuration (e.g. sessionStorage, localStorage etc.); chain configuration; custom provider configuration (adding / disabling / changing providers) | -| 2 | Provider interaction| logging in and out; signing transactions / messages | -| 3 | Presenting data | get store data (e.g. account balance, account address etc.); use components to display data (e.g. balance, address, transactions list) | -| 4 | Transactions | sending transactions; tracking transactions; displaying transactions history | +`uris` list of URIs to an image/audio/video, which represents the given NFT. -Each of these steps will be explained in more detail in the following sections. +This only works for addresses that are in the same shard as the smart contract. +Most of the time, this function is used with `self.blockchain().get_sc_address()` as address to get the properties of a token that is owned by the smart contract, or was transferred to the smart contract in the current executing call. -### 1. Configuration -Before your application bootstraps, you need to configure the storage, the network, and the signing providers. This is done by calling the `initApp` method from the `methods` folder. It is recommended to call this method in the `index.tsx` to ensure the sdk-dapp internal functions are initialized before rendering the app. +### get_esdt_local_roles -```typescript -// index.tsx -import { initApp } from '@multiversx/sdk-dapp/out/methods/initApp/initApp'; -import type { InitAppType } from '@multiversx/sdk-dapp/out/methods/initApp/initApp.types'; -import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; -import { App } from "./App"; +```rust +get_esdt_local_roles(token_id: &TokenIdentifier) -> EsdtLocalRoleFlags +``` -const config: InitAppType = { - storage: { getStorageCallback: () => sessionStorage }, - dAppConfig: { - // nativeAuth: true, // optional - environment: EnvironmentsEnum.devnet, - // network: { // optional - // walletAddress: 'https://devnet-wallet.multiversx.com' // or other props you want to override - // }, - // transactionTracking: { // optional - // successfulToastLifetime: 5000, - // onSuccess: async (sessionId) => { - // console.log('Transaction session successful', sessionId); - // }, - // onFail: async (sessionId) => { - // console.log('Transaction session failed', sessionId); - // } - } - // customProviders: [myCustomProvider] // optional -}; +Gets the ESDTLocalRoles set for the smart contract, as a bitflag. The returned type contains methods of checking if a role exists and iterating over all the roles. -initApp(config).then(() => { - render(() => , root!); // render your app -}); -``` +This is done by simply reading protected storage, but this is a convenient function to use. -### 2. Provider interaction +## Call Value API -Once your dApp has loaded, the first user action is logging in with a chosen provider. There are two ways to perform a login, namely using the `UnlockPanelManager` and programmatic login using the `ProviderFactory`. +This API is accessible through `self.call_value()`. The alternative is to use the `#[payment]` annotations, but we no longer recommend them. They have a history of creating confusion, especially for new users. -#### 2.1 Using the `UnlockPanelManager` -By using the provided UI, you get the benefit of having all supported providers ready for login in a side panel. You simply need to link the `unlockPanelManager.openUnlockPanel` to a user action. +Available functions: -```typescript -import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager'; -import { ProviderFactory } from '@multiversx/sdk-dapp/out/providers/ProviderFactory'; -export const ConnectButton = () => { - const unlockPanelManager = UnlockPanelManager.init({ - loginHandler: () => { - navigate('/dashboard'); - }, - onClose: () => { // optional action to be performed when the user closes the Unlock Panel - navigate('/'); - }, - // allowedProviders: [ProviderTypeEnum.walletConnect, 'myCustomProvider'] // optionally, only show specific providers - }); - const handleOpenUnlockPanel = () => { - unlockPanelManager.openUnlockPanel(); - }; - return ; -}; +### egld_value +```rust +egld_value() -> BigUint ``` -Once the user has logged in, if `nativeAuth` is configured in the `initApp` method, an automatic logout will be performed upon native auth expiration. Before the actual logout is performed, the `LogoutManager` will show a warning toast to the user. This toast can be customized by passing a `tokenExpirationToastWarningSeconds` to the `nativeAuth` config. -```typescript -// in initAoo config -const config: InitAppType = { - // ... - nativeAuth: { - expirySeconds: 30, // test auto logout after 30 seconds - tokenExpirationToastWarningSeconds: 10 // show warning toast 10 seconds before auto logout - }, -} -``` +Returns the amount of EGLD transferred in the current transaction. Will return 0 for ESDT transfers. -You have the option to stop this behavior by calling `LogoutManager.getInstance().stop()` after the user has logged in. -```typescript -import { LogoutManager } from '@multiversx/sdk-dapp/out/managers/LogoutManager/LogoutManager'; +### all_esdt_transfers - loginHandler: () => { - navigate('/dashboard'); - // optional, to stop the automatic logout upon native auth expiration - LogoutManager.getInstance().stop(); - }, +```rust +all_esdt_transfers() -> ManagedVec ``` -If you want to perform some actions as soon as the user has logged in, you will need to call `ProviderFactory.create` inside a handler accepting arguments. +Returns all ESDT transfers. Useful when you're expecting a variable number of transfers. -```typescript -export const AdvancedConnectButton = () => { - const unlockPanelManager = UnlockPanelManager.init({ - loginHandler: async ({ type, anchor }) => { - const provider = await ProviderFactory.create({ - type, - anchor - }); - const { address, signature } = await provider.login(); - navigate(`/dashboard?address=${address}`; - }, - }); - const handleOpenUnlockPanel = () => { - unlockPanelManager.openUnlockPanel(); - }; - return ; -}; +Returns the payments into a `ManagedVec` of structs, that contain the token type, token ID, token nonce and the amount being transferred: +```rust +pub struct EsdtTokenPayment { + pub token_identifier: TokenIdentifier, + pub token_nonce: u64, + pub amount: BigUint, +} ``` -#### 2.2 Programmatic login using the `ProviderFactory` -If you want to login using your custom UI, you can link user actions to specific providers by calling the `ProviderFactory`. -```typescript -import { ProviderTypeEnum } from '@multiversx/sdk-dapp/out/providers/types/providerFactory.types'; +### multi_esdt -const provider = await ProviderFactory.create({ - type: ProviderTypeEnum.extension -}); -await provider.login(); +```rust +multi_esdt() -> [EsdtTokenPayment; N] ``` -> **Note:** Extension and Ledger login will only work if dApp is served over HTTPS protocol - - -### 3. Displaying app data +Returns a fixed number of ESDT transfers as an array. Will signal an error if the number of ESDT transfers differs from `N`. -Depending on the framework, you can either use hooks or selectors to get the user details: +For example, if you always expect exactly 3 payments in your endpoint, you can use this function like so: +`let [payment_a, payment_b, payment_c] = self.call_value().multi_esdt();` -#### 3.1 React hooks -If you are using React, all hooks can be found under the `/out/react` folder. All store information can be accessed via different hooks but below you will find the main hook related to most common use cases +### single_esdt -```bash -out/react/ -├── account/useGetAccount ### access account data like address, balance and nonce -├── loginInfo/useGetLoginInfo ### access login data like accessToken and provider type -├── network/useGetNetworkConfig ### access network information like chainId and egldLabel -├── store/useSelector ### make use of the useSelector hook for querying the store via selectors -└── transactions/useGetTransactionSessions ### access all current and historic transaction sessions +```rust +single_esdt() -> EsdtTokenPayment ``` -Below is an example of using the hooks to display the user address and balance. +Returns the received ESDT token payment if exactly one was received. Will signal an error in case of multi-transfer or no transfer. -```typescript -import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; -import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; -const account = useGetAccount(); -const { - network: { egldLabel } -} = useGetNetworkConfig(); +### single_fungible_esdt -console.log(account.address); -console.log(`${account.balance} ${egldLabel}`); +```rust +single_fungible_esdt(&self) -> (TokenIdentifier, BigUint) ``` -#### 3.2 Store selector functions: - -If you are not using the React ecosystem, you can use store selectors to get the store data, but note that information will not be reactive unless you subscribe to store changes. - -```typescript -import { getAccount } from '@multiversx/sdk-dapp/out/methods/account/getAccount'; -import { getNetworkConfig } from '@multiversx/sdk-dapp/out/methods/network/getNetworkConfig'; +Similar to the function above, but also enforces the payment to be a fungible ESDT. -const account = getAccount(); -const { egldLabel } = getNetworkConfig(); -``` -In order to get live updates you may need to subscribe to the store like this: +### egld_or_single_fungible_esdt -```typescript -import { createSignal, onCleanup } from 'solid-js'; -import { getStore } from '@multiversx/sdk-dapp/out/store/store'; +```rust +egld_or_single_fungible_esdt(&self) -> (EgldOrEsdtTokenIdentifier, BigUint) +``` -export function useStore() { - const store = getStore(); - const [state, setState] = createSignal(store.getState()); +Same as the function above, but also allows EGLD to be received. - const unsubscribe = store.subscribe((newState) => { - setState(newState); - }); - onCleanup(() => unsubscribe()); +### egld_or_single_esdt - return state; -} +```rust +egld_or_single_esdt() -> EgldOrEsdtTokenPayment ``` +Allows EGLD or any single ESDT token to be received. -### 4. Transactions -#### 4.1 Signing transactions +## Crypto API -To sign transactions, you first need to create the `Transaction` object, then pass it to the initialized provider. +This API is accessible through `self.crypto()`. It provides hashing functions and signature verification. Since those functions are widely known and have their own pages of documentation, we will not go into too much detail in this section. -```typescript -import { Address, Transaction } from '@multiversx/sdk-core'; -import { - GAS_PRICE, - GAS_LIMIT -} from '@multiversx/sdk-dapp/out/constants/mvx.constants'; -import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; -import { refreshAccount } from '@multiversx/sdk-dapp/out/utils/account/refreshAccount'; +Hashing functions: -const pongTransaction = new Transaction({ - value: BigInt(0), - data: Buffer.from('pong'), - receiver: Address.newFromBech32(contractAddress), - gasLimit: BigInt(GAS_LIMIT), - gasPrice: BigInt(GAS_PRICE), - chainID: network.chainId, - nonce: BigInt(account.nonce), - sender: Address.newFromBech32(account.address), - version: 1 -}); -await refreshAccount(); // optionally, to get the latest nonce -const provider = getAccountProvider(); -const signedTransactions = await provider.signTransactions(transactions); +### sha256 + +```rust +sha256(data: &ManagedBuffer) -> ManagedByteArray ``` -#### 4.2 Sending and tracking transactions -Then, to send the transactions, you need to use the `TransactionManager` class and pass in the `signedTransactions` to the `send` method. You can then track the transactions by using the `track` method. This will create a toast notification with the transaction hash and its status. +### keccak256 -> **Note:** You can set callbacks for the transaction manager to handle the session status, by using the `setCallbacks` method on the `TransactionManager` class. This can also be configured globally in the `initApp` method. +```rust +keccak256(data: &ManagedBuffer) -> ManagedByteArray +``` -```typescript -import { TransactionManager } from '@multiversx/sdk-dapp/out/managers/TransactionManager'; -import type { TransactionsDisplayInfoType } from '@multiversx/sdk-dapp/out/types/transactions.types'; +### ripemd160 -const txManager = TransactionManager.getInstance(); +```rust +ripemd160(data: &ManagedBuffer) -> ManagedByteArray +``` -const sentTransactions = await txManager.send(signedTransactions); +Signature verification functions: -const toastInformation: TransactionsDisplayInfoType = { - processingMessage: 'Processing transactions', - errorMessage: 'An error has occurred during transaction execution', - successMessage: 'Transactions executed' -} -const sessionId = await txManager.track(sentTransactions, { - transactionsDisplayInfo: toastInformation, -}); +### verify_ed25519_legacy_managed + +```rust +verify_ed25519_legacy_managed(key: &ManagedByteArray, message: &ManagedBuffer, signature: &ManagedByteArray) -> bool ``` -#### 4.3 Using the Notifications Feed -The Notifications Feed is a component that displays **session transactions** in a list. Internally it gets initialized in the `initApp` method. It can be accessed via the `NotificationManager.getInstance()` method. -Once the user logs out of the dApp, all transactions displayed by the Notifications Feed are removed from the store. Note that you can switch between toast notifications and Notifications Feed by pressing the View All button above the current pending transaction toast in the UI. +### verify_bls -```typescript -const notificationManager = NotificationManager.getInstance(); -await notificationManager.openNotificationsFeed(); +```rust +verify_bls(key: &[u8], message: &[u8], signature: &[u8]) -> bool ``` -#### 4.4 Inspecting transactions - -You can find both methods and hooks to access transactions data, as seen in the table below. -**Table 2**. Inspectig transactions -| # | Helper | Description | React hook equivalent | -|---|------|-------------|----| -| | `methods/transactions` | path | `react/transactions` | -| 1 | `getTransactionSessions()` | returns all trabsaction sessions |`useGetTransactionSessions()` | -| 2 | `getPendingTransactionsSessions()` | returns an record of pending sessions | `useGetPendingTransactionsSessions()`| -| 3 | `getPendingTransactions()` | returns an array of signed transactions | `useGetPendingTransactions()` | -| 4 | `getFailedTransactionsSessions()` | returns an record of failed sessions | `useGetFailedTransactionsSessions()`| -| 5 | `getFailedTransactions()` | returns an array of failed transactions | `useGetFailedTransactions()`| -| 6 | `getSuccessfulTransactionsSessions()` | returns an record of successful sessions | `useGetSuccessfulTransactionsSessions()`| -| 7 | `getSuccessfulTransactions()` | returns an array of successful transactions | `useGetSuccessfulTransactions()`| +### verify_secp256k1 -There is a way to inspect store information regarding a specific transaction, using the `transactionsSliceSelector`. An example is shown below: +```rust +verify_secp256k1(key: &[u8], message: &[u8], signature: &[u8]) -> bool +``` -```typescript -import { - pendingTransactionsSessionsSelector, - transactionsSliceSelector -} from '@multiversx/sdk-dapp/out/store/selectors/transactionsSelector'; -import { getStore } from '@multiversx/sdk-dapp/out/store/store'; -const store = getStore(); // or use useStore hook for reactivity -const pendingSessions = pendingTransactionsSessionsSelector(store.getState()); -const allTransactionSessions = transactionsSliceSelector(store.getState()); +### verify_custom_secp256k1 -const isSessionIdPending = - Object.keys(pendingSessions).includes(sessionId); -const currentSession = allTransactionSessions[sessionId]; -const currentSessionStatus = currentSession?.status; -const currentTransaction = currentSession?.transactions?.[0]; -const currentTransactionStatus = currentTransaction?.status; +```rust +verify_custom_secp256k1(key: &[u8], message: &[u8], signature: &[u8], hash_type: MessageHashType) -> bool ``` -#### 4.5 Logging out -The user journey ends with calling the `provider.logout()` method. +`MessageHashType` is an enum, representing the hashing algorithm that was used to create the `message` argument. Use `ECDSAPlainMsg` if the message is in "plain text". -```typescript -import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; -const provider = getAccountProvider(); -await provider.logout(); +```rust +pub enum MessageHashType { + ECDSAPlainMsg, + ECDSASha256, + ECDSADoubleSha256, + ECDSAKeccak256, + ECDSARipemd160, +} ``` +To be able to use the hashing functions without dynamic allocations, we use a concept in Rust known as `const generics`. This allows the function to have a constant value as a generic instead of the usual trait types you'd see in generics. The value is used to allocate a static buffer in which the data is copied temporarily, to then be passed to the legacy API. -## Internal structure +To call such a function, the call would look like this: -We have seen in the previous chapter what are the minimal steps to get up and running with a blockchain interaction using sdk-dapp. Next we will detail each element mentioned above +```rust +let hash = self.crypto().sha256_legacy_managed::<200>(&data); +``` -**Table 3**. Elements needed to build a dApp -| # | Type | Description | -|---|------|-------------| -| 1 | Network | Chain configuration | -| 2 | Provider | The signing provider for logging in and singing transactions | -| 3 | Account | Inspecting user address and balance | -| 4 | Transactions Manager | Sending and tracking transactions | -| 5 | UI Components | Displaying UI information like balance, public keys etc. | +Where `200` is the max expected byte length of `data`. -Since these are mixtures of business logic and UI components, the library is split into several folders to make it easier to navigate. -When inspecting the package, there is more content under `src`, but the folders of interest are: +### encode_secp256k1_der_signature -```bash -src/ -├── apiCalls/ ### methods for interacting with the API -├── constants/ ### useful constants from the ecosystem like ledger error codes, default gas limits for transactions etc. -├── controllers/ ### business logic for UI elements that are not bound to the store -├── managers/ ### business logic for UI elements that are bound to the store -├── providers/ ### signing providers -├── methods/ ### utility functions to query and update the store -├── react/ ### react hooks to query the store -└── store/ ### store initialization, middleware, slices, selectors and actions +```rust +encode_secp256k1_der_signature(r: &[u8], s: &[u8]) -> BoxedBytes ``` -Conceptually, these can be split into 3 main parts: - -- First is the business logic in `apiCalls`, `constants`, `providers` and `methods` -- Then comes the persistence layer hosted in the `store` folder, using [Zustand](https://zustand.docs.pmnd.rs/) under the hood. -- Last are the UI components hosted in [@multiversx/sdk-dapp-ui](https://github.com/multiversx/mx-sdk-dapp-ui) with some components controlled on demand by classes defined in `controlles` and `managers` - -Next, we will take the elements from Table 3 and detail them in the following sections. +Creates a signature from the corresponding elliptic curve parameters provided. -### 1. Network +## Send API -The network configuration is done in the `initApp` method, where you can make several configurations like: +This API is accessible through `self.send()`. It provides functionalities like sending tokens, performing smart contract calls, calling built-in functions and much more. -- specifying the environment (`devnet`, `testnet`, `mainnet`) -- overriding certain network parameters like wallet address, explorer address etc. +We will not describe every single function in the API, as that would create confusion. We will only describe those that are recommended to be used (as they're mostly wrappers around more complicated low-level functions). -Once the network is configured, the `network` slice in the store will hold the network configuration. +For Smart Contract to Smart Contract calls, use the Proxies, as described in the [contract calls](/developers/transactions/tx-legacy-calls) section. -To query different network parameters, you can use the `getNetworkConfig` method from the `methods/network` folder. +Without further ado, let's take a look at the available functions: -### 2. Provider +### direct -The provider is the main class that handles the signing of transactions and messages. It is initialized in the `initApp` method and can be accessed via the `getAccountProvider` method from the `providers/helpers` folder. +```rust +direct(to: &ManagedAddress, token: &EgldOrEsdtTokenIdentifier, nonce: u64, amount: &BigUint) +``` -#### Initialization +Performs a simple EGLD/ESDT/NFT transfer to the target address, with some optional additional data. If you want to send EGLD, simply pass `EgldOrEsdtTokenIdentifier::egld()`. For both EGLD and fungible ESDT, `nonce` should be 0. -An existing provider is initialized on app load (this is take care of by `initApp`), since it restores the session from the store and allows signing transactions without the need of making a new login. +This will fail if the destination is a non-payable smart contract, but the current executing transaction will only fail if the destination SC is in the same shard, and as such, any changes done to the storage will persist. The tokens will not be lost though, as they will be automatically returned. -#### Creating a custom provider +Even though an invalid destination will not revert, an illegal transfer will return an error and revert. An illegal transfer is any transfer that would leave the SC with a negative balance for the specific token. -If you need to create a custom signing provider, make sure to extend the `IProvider` interface and implement all required methods (see example [here](https://github.com/multiversx/mx-template-dapp/tree/main/src/provider)). Next step would be to include it in the `customProviders` array in the `initApp` method or add it to the [window object](https://github.com/multiversx/mx-template-dapp/tree/main/src/initConfig). Last step is to login using the custom provider. +If you're unsure about the destination's account type, you can use the `is_smart_contract` function from `Blockchain API`. -```typescript -const provider = await ProviderFactory.create({ - type: 'custom-provider' -}); -await provider?.login(); -``` +If you need a bit more control, use the `direct_with_gas_limit` function instead. -#### Accessing provider methods -Once the provider is initialized, you can get a reference to it using the `getAccountProvider` method. Then you can call the `login`, `logout`, `signTransactions`, `signMessage` methods, or other custom methods depending on the initialized provider (see ledger for example). +### direct_egld +```rust +direct_egld(to: &ManagedAddress, amount: &BigUint) +``` -### 3. Account +The EGLD-transfer version for the `direct` function. -#### Getting account data -Once the user logs in, a call is made to the API for fetching the account data. This data is persisted in the store and is accessible through helpers found in `methods/account`. These functions are: +### direct_esdt -**Table 4**. Getting account data -| # | Helper | Description | React hook equivalent | -|---|------|-------------|----| -| | `methods/account` | path | `react/account` | -| 1 | `getAccount()` | returns all account data |`useGetAccount()` | -| 2 | `getAddress()` | returns just the user's public key | `useGetAddress()`| -| 3 | `getIsLoggedIn()` | returns a login status boolean | `useGetIsLoggedIn()` | -| 4 | `getLatestNonce()` | returns the account nonce | `useGetLatestNonce()` +```rust +direct_esdt(to: &ManagedAddress, token_id: &TokenIdentifier, token_nonce: u64, amount: &BigUint) +``` -#### Nonce management +The ESDT-only version for the `direct` function. Used so you don't have to wrap `TokenIdentifier` into an `EgldOrEsdtTokenIdentifier`. -`sdk-dapp` has a mechanism that does its best to manage the account nonce. For example, if the user sends a transaction, the nonce gets incremented on the client so that if a new transaction is sent, it will have the correct increased nonce. If you want to make sure the nonce is in sync with the API account, you can call `refreshAccount()` as shown above in the **Signing transactions** section. +### direct_multi -### 4. Transactions Manager +```rust +direct_multi(to: &ManagedAddress, payments: &ManagedVec) +``` -#### Overview +The multi-transfer version for the `direct_esdt` function. Keep in mind you cannot transfer EGLD with this function, only ESDTs. -The `TransactionManager` is a class that handles sending and tracking transactions in the MultiversX ecosystem. It provides methods to send either single or batch transactions. It also handles tracking, error management, and toast notifications for user feedback. It is initialized in the `initApp` method and can be accessed via `TransactionManager.getInstance()`. -#### Features +### change_owner_address -- **Supports Single and Batch Transactions:** Handles individual transactions as well as grouped batch transactions. -- **Automatic Tracking:** Monitors transaction status and updates accordingly through a webhook or polling fallback mechanism. -- **Toast Notifications:** Displays status updates for user feedback, with options to disable notifications and customize toast titles. -- **Error Handling:** Catches and processes errors during transaction submission +```rust +change_owner_address(child_sc_address: &ManagedAddress, new_owner: &ManagedAddress) +``` -#### Transactions Lifecycle +Changes the ownership of target child contract to another address. This will fail if the current contract is not the owner of the `child_sc_address` contract. -The transaction lifecycle consists of the following steps: +This also has the implication that the current contract will not be able to call `#[only_owner]` functions of the child contract, upgrade, or change owner again. -1. **Creating** a `Transaction` object from `@multiversx/sdk-core` -2. **Signing** the transaction with the initialized provider and receiving a `SignedTransactionType` object -3. **Sending** the signed transaction using TransactionManager's `send()` function. Signed transactions can be sent in 2 ways: -**Table 5**. Sending signed transactions -| # | Signature | Method | Description | -|---|------|-------------|-------------| -| 1 | `send([tx1, tx2])` | `POST` to `/transactions` | Transactions are executed in parallel -| 2 | `send([[tx1, tx2], [tx3]])` | `POST` to `/batch` | **a)** 1st batch of two transactions is executed, **b)** the 2nd batch of one transaction waits for the finished results, **c)** and once the 1st batch is finished, the 2nd batch is executed +### esdt_local_mint -4. **Tracking** transactions is made by using `transactionManager.track()`. Since the `send()` function returns the same arguments it has received, the same array payload can be passed into the `track()` method. Under the hood, status updates are received via a WebSocket or polling mechanism. - Once a transaction array is tracked, it gets associated with a `sessionId`, returned by the `track()` method and stored in the `transactions` slice. Depending on the array's type (plain/batch), the session's status varies from initial (`pending`/`invalid`/`sent`) to final (`successful`/`failed`/`timedOut`). +```rust +esdt_local_mint(token: &TokenIdentifier, nonce: u64, amount: &BigUint) +``` -**Table 6**. Inspecting transaction sessions -| # | Helper | Description | React hook equivalent | -|---|------|-------------|----| -| | `methods/transactions` | path | `react/transactions` | -| 1 | `getTransactionSessions()` | returns all trabsaction sessions |`useGetTransactionSessions()` | -| 2 | `getPendingTransactionsSessions()` | returns an record of pending sessions | `useGetPendingTransactionsSessions()`| -| 3 | `getPendingTransactions()` | returns an array of signed transactions | `useGetPendingTransactions()` | -| 4 | `getFailedTransactionsSessions()` | returns an record of failed sessions | `useGetFailedTransactionsSessions()`| -| 5 | `getFailedTransactions()` | returns an array of failed transactions | `useGetFailedTransactions()`| -| 6 | `getSuccessfulTransactionsSessions()` | returns an record of successful sessions | `useGetSuccessfulTransactionsSessions()`| -| 7 | `getSuccessfulTransactions()` | returns an array of successful transactions | `useGetSuccessfulTransactions()`| +Allows synchronous minting of ESDT/SFT (depending on nonce). Execution is resumed afterwards. Note that the SC must have the `ESDTLocalMint` or `ESDTNftAddQuantity` roles set, or this will fail with "action is not allowed". -5. **User feedback** is provided through toast notifications, which are triggered to inform about transactions' progress. Additional tracking details can be optionally displayed in the toast UI. -There is an option to add custom toast messages by using the `createCustomToast` helper. +For SFTs, you must use `esdt_nft_create` before adding additional quantity. -```ts -import { createRoot } from 'react-dom/client'; -import { createCustomToast } from '@multiversx/sdk-dapp/out/store/actions/toasts/toastsActions'; +This function cannot be used for NFTs. -// by creating a custom toast element containing a component -createCustomToast({ - toastId: 'username-toast', - instantiateToastElement: () => { - const toastBody = document.createElement('div'); - const root = createRoot(toastBody); - root.render(); - return toastBody; - } -}); -// or by creating a simple custom toast -createCustomToast({ - toastId: 'custom-toast', - icon: 'times', - iconClassName: 'warning', - message: 'This is a custom toast', - title: 'My custom toast' -}); - +### esdt_local_burn +```rust +esdt_local_burn(token: &TokenIdentifier, nonce: u64, amount: &BigUint) ``` -6. **Error Handling & Recovery** is done through a custom toast that prompts the user to take appropriate action. +The inverse operation of `esdt_local_mint`, which permanently removes the tokens. Note that the SC must have the `ESDTLocalBurn` or `ESDTNftBurn` roles set, or this will fail with "action is not allowed". -#### Methods -___ +Unlike the mint function, this can be used for NFTs. -#### 1. Sending Transactions -In this way, all transactions are sent simultaneously. There is no limit to the number of transactions contained in the array. +### esdt_nft_create -```typescript -const transactionManager = TransactionManager.getInstance(); -const parallelTransactions: SigendTransactionType[] = [tx1, tx2, tx3, tx4]; -const sentTransactions = await transactionManager.send(parallelTransactions); +```rust +esdt_nft_create(token: &TokenIdentifier, amount: &BigUint, name: &ManagedBuffer, royalties: &BigUint, hash: &ManagedBuffer, attributes: &T, uris: &ManagedVec< ManagedBuffer>) -> u64 ``` -#### 2. Sending Batch Transactions +Creates a new SFT/NFT, and returns its nonce. -In this sequential case, each batch waits for the previous one to complete. +Must have `ESDTNftCreate` role set, or this will fail with "action is not allowed". -```typescript -const transactionManager = TransactionManager.getInstance(); -const batchTransactions: SignedTransactionType[][] = [ - [tx1, tx2], - [tx3, tx4] -]; -const sentTransactions = await transactionManager.send(batchTransactions); -``` +`token` is identifier of the SFT/NFT brand. -#### 3. Tracking Transactions +`amount` is the amount of tokens to be minted. For NFTs, this should be "1". -The basic option is to use the built-in tracking, which displays toast notifications with default messages. +`name` is the display name of the token, which will be used in explorers, marketplaces, etc. -```typescript -import { TransactionManagerTrackOptionsType } from '@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager.types'; +`royalties` is a number between 0 and 10,000, which represents the percentage of any selling amount the creator receives. This representation is used to be able to have more precision. For example, a percentage like `55.66%` is stored as `5566`. These royalties are not enforced, and will mostly be used in "official" NFT marketplaces. -const options: TransactionManagerTrackOptionsType = { - disableToasts: false, // `false` by default - transactionsDisplayInfo: { // `undefined` by default - errorMessage: 'Failed adding stake', - successMessage: 'Stake successfully added', - processingMessage: 'Staking in progress' - }, - sessionInformation: { // `undefined` by default. Use to perform additional actions based on the session information - stakeAmount: '1000000000000000000000000' - } -}; +`hash` is a user-defined hash for the token. Recommended value is sha256(attributes), but it can be anything. -const sessionId = await transactionManager.track( - sentTransactions, - options // optional -); -``` +`attributes` can be any serializable user-defined struct, more specifically, any type that implements the `TopEncode` trait. There is no real standard for attributes format at the point of writing this document, but that might change in the future. -If you want to provide more human-friendly messages to your users, you can enable tracking with custom toast messages: +`uris` is a list of links to the NFTs visual/audio representation, most of the time, these will be links to images, videos or songs. If empty, the framework will automatically add an "empty" URI. -```typescript -const sessionId = await transactionManager.track(sentTransactions, { - transactionsDisplayInfo: { - errorMessage: 'Failed adding stake', - successMessage: 'Stake successfully added', - processingMessage: 'Staking in progress' - } -}); -``` -#### 3.1 Tracking transactions without being logged in +### esdt_nft_create_compact -If your application needs to track transactions sent by a server and the user does not need to login to see the outcome of these transactions, there are several steps that you need to do to enable this process. +```rust +esdt_nft_create_compact(token: &TokenIdentifier, amount: &BigUint, attributes: &T) -> u64 +``` -Step 1. Enabling the tracking mechanism +Same as `esdt_nft_create`, but fills most arguments with default values. Mostly used in contracts that use NFTs as a means of information rather than for display purposes. -By default the tracking mechanism is enabled only after the user logs in. That is the moment when the WebSocket connection is established. If you want to enable tracking before the user logs in, you need to call the `trackTransactions` method from the `methods/trackTransactions` folder. This method will enable a polling mechanism. -```typescript -import { trackTransactions } from '@multiversx/sdk-dapp/out/methods/trackTransactions/trackTransactions'; +### sell_nft -initApp(config).then(async () => { - await trackTransactions(); // enable here since by default tracking will be enabled only after login - render(() => , root!); -}); +```rust +sell_nft(nft_id: &TokenIdentifier, nft_nonce: u64, nft_amount: &BigUint, buyer: &ManagedAddress, payment_token: &EgldOrEsdtTokenIdentifier, payment_nonce: u64, payment_amount: &BigUint) -> BigUint ``` -Step 2. Tracking transactions +Sends the SFTs/NFTs to target address, while also automatically calculating and sending NFT royalties to the creator. Returns the amount left after deducting royalties. -Then, you can track transactions by calling the `track` method from the `TransactionManager` class with a plain transaction containing the transaction hash. +`(nft_id, nft_nonce, nft_amount)` are the SFTs/NFTs that are going to be sent to the `buyer` address. -```typescript -import { Transaction, TransactionsConverter } from '@multiversx/sdk-core'; +`(payment_token, payment_nonce, payment_amount)` are the tokens that are used to pay the creator royalties. -const tManager = TransactionManager.getInstance(); -const txConverter = new TransactionsConverter(); -const transaction = txConverter.plainObjectToTransaction(signedTx); +This function's purpose is mostly to be used in marketplace-like smart contracts, where the contract sells NFTs to users. -const hash = transaction.getHash().toString(); // get the transaction hash -const plainTransaction = { ...transaction.toPlainObject(), hash }; -await tManager.track([plainTransaction]); +### nft_add_uri + +```rust +nft_add_uri(token_id: &TokenIdentifier, nft_nonce: u64, new_uri: ManagedBuffer) ``` -#### 3.2 Advanced Usage +Adds an URI to the selected NFT. The SC must own the NFT and have the `ESDTRoleNFTAddURI` to be able to use this function. -If you need to check the status of the signed transactions, you can query the store directly using the `sessionId` returned by the `track()` method. +If you need to add multiple URIs at once, you can use `nft_add_multiple_uri` function, which takes a `ManagedVec` as argument instead. -```typescript -import { getStore } from '@multiversx/sdk-dapp/out/store/store'; -import { transactionsSliceSelector } from '@multiversx/sdk-dapp/out/store/selectors/transactionsSelector'; -const state = transactionsSliceSelector(getStore()); -Object.entries(state).forEach(([sessionKey, data]) => { - if (sessionKey === sessionId) { - console.log(data.status); - } -}); +### nft_update_attributes + +```rust +nft_update_attributes(token_id: &TokenIdentifier, nft_nonce: u64, new_attributes: &T) ``` +Updates the attributes of the selected NFT to the provided value. The SC must own the NFT and have the `ESDTRoleNFTUpdateAttributes` to be able to update the attributes. -### 5. UI Components -`sdk-dapp` needs to make use of visual elements for allowing the user to interact with some providers (like the ledger), or to display messages to the user (like idle states or toasts). These visual elements consitst of webcomponents hosted in the `@multiversx/sdk-dapp-ui` package. Thus, `sdk-dapp` does not hold any UI elements, just business logic that controls external components. We can consider two types of UI components: internal and public. They are differentiated by the way they are controlled: internal components are controlled by `sdk-dapp`'s signing or logging in flows, while public components should be controlled by the dApp. +## Conclusion -#### 5.1 Public components +While there are still other various APIs in multiversx-sc, they are mostly hidden from the user. These are the ones you're going to be using in your day-to-day smart contract development. -The business logic for these components is served by a controller. The components are: +--- -- `MvxTransactionsTable` - used to display the user's transactions +### Smart Contract Call Events -```tsx -import { TransactionsTableController } from '@multiversx/sdk-dapp/out/controllers/TransactionsTableController'; -import { MvxTransactionsTable } from '@multiversx/sdk-dapp-ui/react'; - -const processedTransactions = await TransactionsTableController.processTransactions({ - address, - egldLabel: network.egldLabel, - explorerAddress: network.explorerAddress, - transactions - }); -// and use like this: -; +--- -``` +### Smart Contract Calls Data Format -- `MvxFormatAmount` - used to format the amount of the user's balance +This page provides an in-depth examination of the Smart Contract Calls Data Format. -```tsx -import { MvxFormatAmount } from '@multiversx/sdk-dapp-ui/react'; -import { MvxFormatAmountPropsType } from '@multiversx/sdk-dapp-ui/types'; -export { DECIMALS, DIGITS } from '@multiversx/sdk-dapp-utils/out/constants'; -import { FormatAmountController } from '@multiversx/sdk-dapp/out/controllers/FormatAmountController'; -export { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; +## Introduction +Besides regular move-balance transactions (address A sends the amount X to address B, while optionally including a note in the `data` field), +MultiversX transactions can trigger a Smart Contract call, or a [built-in function call](/developers/built-in-functions). -interface IFormatAmountProps - extends Partial { - value: string; - className?: string; +This can happen in the following situations: + +- the receiver of the transaction is a Smart Contract Address and the data field begins with a valid function of the contract. +- the data field of the transaction begins with a valid built-in function name. + +Calls to Smart Contracts functions (or built-in functions) on MultiversX have the following format: + +```rust +ScCallTransaction { + Sender: + Receiver: # can be a SC, or other address in case of built in functions + Value: X # to be determined for each case + GasLimit: Y # to be determined for each case + Data: "functionName" + + "@" + + + "@" + + + ... } +``` -export const FormatAmount = (props: IFormatAmountProps) => { - const { - network: { egldLabel } - } = useGetNetworkConfig(); +The number of arguments is specific to each function. - const { isValid, valueDecimal, valueInteger, label } = - FormatAmountController.getData({ - digits: DIGITS, - decimals: DECIMALS, - egldLabel, - ...props, - input: props.value - }); +_Example_. We have a smart contract A with the address `erd1qqqqqqqqqqqqqpgqrchxzx5uu8sv3ceg8nx8cxc0gesezure5awqn46gtd`. The contract +has a function `add(numberToAdd numeric)` which adds the `numberToAdd` to an internally managed sum. If we want to call the +function and add `15` to the internal sum, the transaction would look like: - return ( - - ); -}; +```rust +ExampleScCallTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqpgqrchxzx5uu8sv3ceg8nx8cxc0gesezure5awqn46gtd + Value: 0 # no value needed for this call + GasLimit: 1_000_000 # let's suppose we need this much gas for calling the function + Data: "add@0f" # call the function add with the argument 15, hex-encoded +} ``` -#### 5.2 Internal components (advanced usage) +### Constraints -The way internal components are controlled is through a [pub-sub pattern](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) called EventBus. Each webcomponent has a method of exposing its EventBus, thus allowing `sdk-dapp` to get a reference to it and use it for communication. +Focusing only on the data field of a Smart Contract call / Built-In Function Call, there are some limitation for the function name and the arguments: -```mermaid -flowchart LR - A["Controller"] <--> B["Event Bus"] <--> C["webcomponent"] -``` +- `function name` has to be the plain text name of the function to be called. +- `arguments` must be hexadecimal encoded with an **even number of characters** (eq: `7` - invalid, `07` - valid; `f6f` - invalid, `6f6b` - valid). +- the `function name` and the `arguments` must be separated by the `@` character. -```typescript -import { ComponentFactory } from '@multiversx/sdk-dapp/out/utils/ComponentFactory'; +The next section of this page will focus on how different data types have to be encoded in order to be compliant with the desired format. -const modalElement = await ComponentFactory.create( - 'mvx-ledger-connect-panel' -); -const eventBus = await modalElement.getEventBus(); -eventBus.publish('TRANSACTION_TOAST_DATA_UPDATE', someData); -``` -If you want to override private components and create your own, you can implement a similar strategy by respecting each webcomponent's API (see an interface example [here](https://github.com/multiversx/mx-sdk-dapp/blob/main/src/providers/strategies/LedgerProviderStrategy/types/ledger.types.ts)). +## How to convert arguments for Smart Contract calls +There are multiple ways of converting arguments from their original format to the hexadecimal encoding. -## Debugging your dApp +For manually created transactions, arguments can be encoded by using tools that can be found online. For example, `hex to string`, `hex to decimal` and so on. -> **Note:** For an advanced documentation on how internal flows are implemented, you can check out the [deepwiki](https://deepwiki.com/multiversx/mx-sdk-dapp) diagrams. +For programmatically created transactions, arguments can be encoded by using one of our SDKs (`sdk-js`, `mxpy`, `sdk-go`, `sdk-java`, and so on) or by using built-in components or other libraries +of the language the transaction is created in. -The recommended way to debug your application is by using [lerna](https://lerna.js.org/). Make sure you have the same package version in sdk-dapp's package.json and in your project's package.json. +There are multiple ways of formatting the data field: -If you prefer to use [npm link](https://docs.npmjs.com/cli/v11/commands/npm-link), make sure to use the `preserveSymlinks` option in the server configuration: +- manually convert each argument, and then join the function name, alongside the argument via the `@` character. +- use a pre-defined arguments serializer, such as [the one found in sdk-js](https://github.com/multiversx/mx-sdk-js-core/blob/main/src/smartcontracts/argSerializer.ts). +- use sdk-js's [contract calls](/sdk-and-tools/sdk-js/sdk-js-cookbook/#smart-contracts). +- use sdk-cpp's [contract calls](https://github.com/multiversx/mx-sdk-cpp/blob/main/src/smartcontracts/contract_call.cpp). +- and so on -```js - resolve: { - preserveSymlinks: true, // 👈 - alias: { - src: "/src", - }, - }, -``` -Crome Redux DevTools are by default enabled to help you debug your application. You can find the extension in the Chrome Extensions store. +## Converting bech32 addresses (erd1) -To build the library, run: +MultiversX uses `bech32` addresses with the HRP `erd`. Therefore, an address would look like: -```bash -npm run build -``` +`erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th` -To run the unit tests, run: +:::caution +Converting a bech32 address into hexadecimal encoding _is not_ a simple `string to hex` operation, but requires specialized +tools or helpers. +::: -```bash -npm test -``` +There are many smart contract calls (or built-in function calls) that receive an address as one of their arguments. Obviously, +they have to be hexadecimal encoded. ---- -### sdk-js +### Examples -MultiversX SDK for TypeScript and JavaScript +bech32 --> hex -This SDK consists of TypeScript / JavaScript helpers and utilities for interacting with the Blockchain (in general) and with Smart Contracts (in particular). +``` +erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th +--> +0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1 +erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r +--> +c70cf50b238372fffaf7b7c5723b06b57859d424a2da621bcc1b2f317543aa36 +``` -## Packages -Base libraries: +### Converting addresses using online tools -| Package | Source code | Description | -|------------------------------------------------------------------------------------------|---------------------------------------------------------------------|--------------------------------------------------------------------------------| -| [sdk-core](https://www.npmjs.com/package/@multiversx/sdk-core) | [Github](https://github.com/multiversx/mx-sdk-js-core) | The `sdk-core` package is a unification of the previous packages (`multiversx/sdk-wallet` and `multiversx/sdk-network-providers` into `multiversx/sdk-core`). It has basic components for interacting with the blockchain and with smart contracts. | -| [sdk-exchange](https://www.npmjs.com/package/@multiversx/sdk-exchange) | [Github](https://github.com/multiversx/mx-sdk-js-exchange) | Utilities modules for xExchange interactions. | +There are multiple tools that one can use in order to convert an address into hexadecimal encoding: -Signing providers for dApps: +- [https://utils.multiversx.com/converters#addresses-bech32-to-hexadecimal](https://utils.multiversx.com/converters#addresses-bech32-to-hexadecimal) -| Package | Docs | Source code | Description | -|--------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------| -| [sdk-hw-provider](https://www.npmjs.com/package/@multiversx/sdk-hw-provider) | [documentation](/sdk-and-tools/sdk-js/sdk-js-signing-providers#the-hardware-wallet-provider) | [Github](https://github.com/multiversx/mx-sdk-js-hw-provider) | Sign using the hardware wallet (Ledger). | -| [sdk-wallet-connect-provider](https://www.npmjs.com/package/@multiversx/sdk-wallet-connect-provider) | [documentation](/sdk-and-tools/sdk-js/sdk-js-signing-providers#the-walletconnect-provider) | [Github](https://github.com/multiversx/mx-sdk-js-wallet-connect-provider) | Sign using WalletConnect. | -| [sdk-extension-provider](https://www.npmjs.com/package/@multiversx/sdk-extension-provider) | [documentation](/sdk-and-tools/sdk-js/sdk-js-signing-providers#the-browser-extension-provider) \| [interactive documentation](https://interactive-tutorials.multiversx.com/dashboard/extension-provider) | [Github](https://github.com/multiversx/mx-sdk-js-extension-provider) | Sign using the MultiversX DeFi Wallet (browser extension). | -| [sdk-web-wallet-provider](https://www.npmjs.com/package/@multiversx/sdk-web-wallet-provider) | [documentation](/sdk-and-tools/sdk-js/sdk-js-signing-providers#the-web-wallet-provider) | [Github](https://github.com/multiversx/mx-sdk-js-web-wallet-provider) | Sign using the MultiversX web wallet, using webhooks **(DEPRECATED)**. | -| [mx-sdk-js-web-wallet-cross-window-provider](https://www.npmjs.com/package/@multiversx/sdk-web-wallet-cross-window-provider) | [documentation](/sdk-and-tools/sdk-js/sdk-js-signing-providers#the-web-wallet-cross-window-provider) \| [interactive documentation](https://interactive-tutorials.multiversx.com/dashboard/cross-window-provider) | [Github](https://github.com/multiversx/mx-sdk-js-web-wallet-cross-window-provider) | Sign using the MultiversX web wallet, by opening the wallet in a new tab. | -| [mx-sdk-js-metamask-proxy-provider](https://www.npmjs.com/package/@multiversx/sdk-metamask-proxy-provider) | [documentation](/sdk-and-tools/sdk-js/sdk-js-signing-providers#the-metamask-proxy-provider) \| [interactive documentation](https://interactive-tutorials.multiversx.com/dashboard/iframe-provider) | [Github](https://github.com/multiversx/mx-sdk-js-metamask-proxy-provider) | Sign using the Metamask wallet, by using web wallet as a proxy widget in iframe. | +- [https://slowli.github.io/bech32-buffer](https://slowli.github.io/bech32-buffer) (go to `Data`, select `erd` as Tag and `Bech32` as Encoding) -For more details about integrating a signing provider into your dApp, please follow [this guide](/sdk-and-tools/sdk-js/sdk-js-signing-providers) or the [mx-sdk-js-examples repository](https://github.com/multiversx/mx-sdk-js-examples). +- [http://207.244.241.38/elrond-converters/#bech32-to-hex](http://207.244.241.38/elrond-converters/#bech32-to-hex) -Signing SDKs: -| Package | Source code | Description | -|--------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| -| [sdk-dapp](https://www.npmjs.com/package/@multiversx/sdk-dapp) | [Github](https://github.com/multiversx/mx-sdk-dapp) | A library that holds the core functional & signing logic of a dapp on the MultiversX Network. | -| [sdk-guardians-provider](https://www.npmjs.com/package/@multiversx/sdk-guardians-provider) | [Github](https://github.com/multiversx/mx-sdk-js-guardians-provider) | Helper library for integrating a co-signing provider (Guardian) into dApps. | +### Converting addresses using mxpy +Make sure you have `mxpy` [installed](/sdk-and-tools/mxpy/installing-mxpy). -:::important -For all purposes, **we recommend using [sdk-dapp](/sdk-and-tools/sdk-dapp)** instead of integrating the signing providers on your own. -::: +```bash +mxpy wallet bech32 --decode erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th +``` -Native Authenticator libraries: +will output `0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1`. -| Package | Source code | Description | -|-------------------------------------------------------------------------------------------|----------------------------------------------------------------------|--------------------------------------------------------| -| [sdk-native-auth-client](https://www.npmjs.com/package/@multiversx/sdk-native-auth-client) | [Github](https://github.com/multiversx/mx-sdk-js-native-auth-client) | Native Authenticator - client-side components. | -| [sdk-native-auth-server](https://www.npmjs.com/package/@multiversx/sdk-native-auth-server) | [Github](https://github.com/multiversx/mx-sdk-js-native-auth-server) | Native Authenticator - server-side components. | +Additionally, hex addresses can be converted to bech32 as follows: -Additional utility packages: +```bash +mxpy wallet bech32 --encode 0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1 +``` -| Package | Source code | Description | -|------------------------------------------------------------------------------------------|--------------------------------------------------------------------|--------------------------------------------------------| -| [transaction-decoder](https://www.npmjs.com/package/@multiversx/sdk-transaction-decoder) | [Github](https://github.com/multiversx/mx-sdk-transaction-decoder) | Decodes transaction metadata from a given transaction. | +will output `erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th`. ---- +The encoding algorithm that handles these conversions can be found [here](https://github.com/multiversx/mx-sdk-py-core/blob/main/multiversx_sdk_core/bech32.py). -### SDKs and Tools - Overview -## Introduction +### Converting addresses using sdk-js -One can (programmatically) interact with the MultiversX Network by leveraging the following SDKs, tools and APIs: +Find more about `sdk-js` [here](/sdk-and-tools/sdk-js/). +```js +import { Address } from "@multiversx/sdk-core"; +... -### sdk-rs - Rust SDK +const address = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); +console.log(address.hex()); +``` -:::important -Note that Rust is also the recommended programming language for writing Smart Contracts on MultiversX. That is, Rust can be used to write both _on-chain software_ (Smart Contracts) and _off-chain software_ (e.g. desktop applications, web applications, microservices). For the on-chain part, please follow [Smart Contracts](/developers/smart-contracts). Here, we refer to the off-chain part. -::: +will output `0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1`. -| Name | Description | -| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [sdk-rs](https://github.com/multiversx/mx-sdk-rs) | Rust SDK used to interact with the MultiversX Blockchain.
This is the parent repository, also home to the Rust Framework for Smart Contracts. | -| [sdk-rs/core](https://github.com/multiversx/mx-sdk-rs/tree/master/sdk/core) | Core components, accompanied by a set of usage examples. | -| [sdk-rs/snippets](https://github.com/multiversx/mx-sdk-rs/tree/master/framework/snippets) | Smart Contract interaction snippets - base components. Examples of usage: [adder](https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples/adder/interact), [multisig](https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples/multisig/interact). | +Additionally, hex addresses can be converted to bech32 as follows: +```js +import { Address } from "@multiversx/sdk-core"; +... -### sdk-js - Javascript SDK +const address = Address.fromHex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1"); +console.log(address.bech32()); +``` -| Name | Description | -| -------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -| [sdk-js](/sdk-and-tools/sdk-js) | High level overview about sdk-js. | -| [sdk-js cookbook](/sdk-and-tools/sdk-js/sdk-js-cookbook) | Learn how to handle common tasks by using sdk-js. | -| [Extending sdk-js](/sdk-and-tools/sdk-js/extending-sdk-js) | How to extend and tailor certain modules of sdk-js. | -| [Writing and testing sdk-js interactions](/sdk-and-tools/sdk-js/writing-and-testing-sdk-js-interactions) | Write sdk-js interactions for Visual Studio Code | -| [sdk-js signing providers](/sdk-and-tools/sdk-js/sdk-js-signing-providers) | Integrate sdk-js signing providers. | +will output `erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th`. -You might also want to have a look over [**xSuite**](https://xsuite.dev), a toolkit to init, build, test, deploy contracts using JavaScript, made by the [Arda team](https://arda.run). +The encoding algorithm that handles these conversions can be found [here](https://github.com/multiversx/mx-sdk-js-core/blob/main/src/address.ts). -### sdk-dapp - core functional logic of a dApp +### Converting addresses using sdk-go -| Name | Description | -| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [sdk-dapp](/sdk-and-tools/sdk-dapp) | React library aimed to help developers create dApps based on MultiversX Network.

It abstracts away all the boilerplate for logging in, signing transactions or messages, and also offers helper functions for common tasks. | +Find more about `sdk-go` [here](/sdk-and-tools/sdk-go/). +```js +import ( + ... + "github.com/multiversx/mx-sdk-go/data" + ... +) -### sdk-py - Python SDK +addressObj, err := data.NewAddressFromBech32String("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") +if err != nil { + return err +} -| Name | Description | -| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [sdk-py](/sdk-and-tools/sdk-py/#sdk-py-the-python-libraries) | Python SDK that can be used to create wallets, create and send transactions, interact with Smart Contracts and with the MultiversX Network in general. | +fmt.Println(hex.EncodeToString(addressObj.AddressBytes())) +``` +will output `0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1`. -### mxpy - Python SDK (CLI) +Additionally, hex addresses can be converted to bech32 as follows: -| Name | Description | -| -------------------------------------------------------------------------------- | ----------------------------------------- | -| [Installing mxpy](/sdk-and-tools/mxpy/installing-mxpy) | How to install and get started with mxpy. | -| [mxpy cli](/sdk-and-tools/mxpy/mxpy-cli) | How to use the Command Line Interface. | +```js +import ( + ... + "ggithub.com/multiversx/mx-sdk-go/data" + ... +) +addressBytes, err := hex.DecodeString("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") +if err != nil { + return err +} +addressObj := data.NewAddressFromBytes(addressBytes) -### sdk-nestjs - NestJS SDK +fmt.Println(addressObj.AddressAsBech32String()) +``` -| Name | Description | -| --------------------------------------- | ------------------------------------------------------------------ | -| [sdk-nestjs](/sdk-and-tools/sdk-nestjs) | NestJS SDK commonly used in the MultiversX Microservice ecosystem. | +will output `erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th`. +The encoding algorithm that handles these conversions can be found [here](https://github.com/multiversx/mx-chain-core-go/blob/main/core/pubkeyConverter/bech32PubkeyConverter.go). -### mx-sdk-go - Golang SDK -| Name | Description | -| ------------------------------- | -------------------------------------------------------------- | -| [sdk-go](/sdk-and-tools/sdk-go) | Go/Golang SDK used to interact with the MultiversX Blockchain. | +### Converting addresses using sdk-java +Find more about `sdk-java` [here](/sdk-and-tools/mxjava). -### mx-sdk-java - Java SDK +```java +System.out.println(Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th").hex()); +``` -| Name | Description | -| ------------------------------- | --------------------------------------------------------- | -| [mxjava](/sdk-and-tools/mxjava) | Java SDK used to interact with the MultiversX Blockchain. | +will output `0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1`. +Additionally, hex addresses can be converted to bech32 as follows: -### erdcpp - C++ SDK +```java +System.out.println(Address.fromHex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1").bech32()); +``` -| Name | Description | -| ------------------------------- | -------------------------------------------------------- | -| [erdcpp](/sdk-and-tools/erdcpp) | C++ SDK used to interact with the MultiversX Blockchain. | +will output `erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th`. +The encoding algorithm that handles these conversions can be found [here](https://github.com/multiversx/mx-sdk-java/blob/main/src/main/java/multiversx/Address.java). -### erdkotlin - Kotlin SDK -| Name | Description | -| ------------------------------------- | ----------------------------------------------------------- | -| [erdkotlin](/sdk-and-tools/erdkotlin) | Kotlin SDK used to interact with the MultiversX Blockchain. | +## Converting string values +For situations when a string argument is desired for a smart contract call, it can be simply obtained by using +built-in libraries to convert them into hexadecimal format. -### nesdtjs-sdk - NestJS SDK +:::important +Make sure that the result has an even number of characters. +::: -| Name | Description | -| --------------------------------------- | ----------------------------------------------------------- | -| [sdk-nestjs](/sdk-and-tools/sdk-nestjs) | NestJS SDK used to interact with the MultiversX Blockchain. | +Below you can find some examples: +:::note +By no means, these code snippets provide a coding guideline; they are more of simple examples on how to perform the necessary actions. +::: -### Node Rest API -| Name | Description | -| ------------------------------------------------------------------------ | ----------------------------------------------------------------- | -| [Rest API](/sdk-and-tools/rest-api/) | High level overview over the MultiversX's Rest API. | -| [api.multiversx.com](/sdk-and-tools/rest-api/multiversx-api) | MultiversX's main API instance. | -| [Gateway overview](/sdk-and-tools/rest-api/gateway-overview) | Gateway overview - public proxy instance. | -| [Addresses](/sdk-and-tools/rest-api/addresses) | Rest API endpoints dedicated to addresses. | -| [Transactions](/sdk-and-tools/rest-api/transactions) | Rest API endpoints dedicated to transactions. | -| [Network](/sdk-and-tools/rest-api/network) | Rest API endpoints dedicated to network status and configuration. | -| [Nodes](/sdk-and-tools/rest-api/nodes) | Rest API endpoints dedicated to nodes. | -| [Blocks](/sdk-and-tools/rest-api/blocks) | Rest API endpoints dedicated to blocks. | -| [Virtual machine](/sdk-and-tools/rest-api/virtual-machine) | Rest API endpoints dedicated to the SC execution VM. | -| [Versions and changelog](/sdk-and-tools/rest-api/versions-and-changelog) | What's new in different versions. | +### Examples +string --> hex -### Proxy +``` +ok --> 6f6b +MEX-455c57 --> 4d45582d343535633537 +``` -Proxy is an abstraction layer over the MultiversX Network's sharding. It routes the API request to the desired shard and -merges results when needed. -| Name | Description | -| ---------------------------------------- | ---------------------------------------------------- | -| [MultiversX Proxy](/sdk-and-tools/proxy) | A Rest API requests handler that abstracts sharding. | +### Converting string values in javascript +```js +console.log(Buffer.from("ok").toString("hex")); // 6f6b +``` -### Elasticsearch +for converting hex-encoded string to regular string: -MultiversX Network uses Elasticsearch to index historical data. Find out more about how it can be configured. +```js +console.log(Buffer.from("6f6b", "hex").toString()); // ok +``` -| Name | Description | -| ---------------------------------------------- | --------------------------------------------------------------------------- | -| [Elasticsearch](/sdk-and-tools/elastic-search) | Make use of Elasticsearch near your nodes in order to keep historical data. | +### Converting string values in java -### Events notifier +```java +String inputHex = Hex.encodeHexString("ok".getBytes(StandardCharsets.UTF_8)); +if (inputHex.length() % 2 != 0) { + inputHex = "0" + inputex; +} -Events notifier is an external service that can be used to fetch block events and push them to subscribers. +System.out.println(inputHex); // 6f6b +``` -| Name | Description | -| ------------------------------------------ | ------------------------------------ | -| [Events notifier](/sdk-and-tools/notifier) | A notifier service for block events. | +for converting hex-encoded string to regular string: +```java +byte[] bytes = Hex.decodeHex("6f6b".toCharArray()); -### Chain simulator +String result = new String(bytes, StandardCharsets.UTF_8); // ok +``` -Chain simulator is designed to replicate the behavior of a local testnet. -It can also be pre-initialized / initialized with blockchain state from other networks, such as mainnet or something similar. -| Name | Description | -| ------------------------------------------------- | ---------------------------- | -| [Chain simulator](/sdk-and-tools/chain-simulator) | A service for local testing. | +### Converting string values in go +```go +fmt.Println(hex.EncodeToString([]byte("ok"))) // 6f6b +``` -### Devcontainers (for VSCode or GitHub Codespaces) +for converting hex-encoded string to regular string: -| Name | Description | -| --------------------------------------------- | ----------------------------------------------------------------------- | -| [Devcontainers](/sdk-and-tools/devcontainers) | Overview of MultiversX devcontainers (for VSCode or GitHub Codespaces). | +```go +decodedInput, err := hex.DecodeString("6f6b") +if err != nil { + return err +} ---- +fmt.Println(string(decodedInput)) // ok +``` -### Sender -## Overview +## Converting numeric values -Within the context of a transaction that comprises seven distinct generics, `From` represents the **second** generic field — **the entity that initiates the transaction**. It is required in three environments: the integration test, the parametric test, and the interactor. +For situations when a numeric argument is desired for a smart contract call, it can be simply obtained by using +built-in libraries to convert them into hexadecimal format. -A transaction originating from a contract environment cannot have a sender in the contract environment. The reason is that the current contract is always the same: the contract that starts the transaction. +:::important +Make sure that the result has an even number of characters. +::: +Below you can find some examples. They use big integer / number libraries to ensure the code works for large values as well: +:::note +By no means, these code snippets provide a coding guideline; they are more of simple examples on how to perform the necessary actions. +::: -## Diagram -The sender is being set using the `.from(...)` method. Several types can be specified: +### Examples -```mermaid -graph LR - subgraph From - from-unit["()"] - from-unit -->|from| from-man-address[ManagedAddress] - from-unit -->|from| from-address[Address] - from-unit -->|from| from-bech32[Bech32Address] - end -``` +numeric --> hex +``` +7 --> 07 +10 --> 0a +35 --> 23 +``` -## No sender -Transactions initiated in the **contract environment** have no sender specified. There is no need for sender identification because it always refers to the executing contract itself. +### Converting numeric values in javascript -```rust title=contract.rs -#[payable("EGLD")] -#[endpoint] -fn transfer_egld(&self, to: ManagedAddress, amount: BigUint) { - self.tx().to(&to).egld(&amount).transfer(); +```js +const intValue = 37; +const bn = new BigNumber(intValue, 10); +let bnStr = bn.toString(16); +if (bnStr.length % 2 != 0) { + bnStr = "0" + bnStr; } +console.log(bnStr); // 25 ``` +for converting hex-encoded string to regular number: -## Explicit sender - -For transactions launched in any other environment than the contract one, it is required to specify the sender. Meaning that, in most cases, no tx can be created without defining the sender. +```js +const hexValue = "25"; +let bn = new BigNumber(hexValue, 16); +console.log(bn.toString()); // 37 +``` -In the following section, we will outline the various types of entities that can be designated as the sender in a transaction. +Also, `sdk-js` includes some [utility functions](https://github.com/multiversx/mx-sdk-js-core/blob/main/src/utils.codec.ts) for padding the results. -### ManagedAddress -The function below is a snippet from an interactor whose purpose is to deploy a contract. The `.from` call, which sets the sender, is the main focus of this example. The sender is a hardcoded ManagedAddress, which illustrates a wallet. +### Converting numeric values in go -```rust title=interact.rs -async fn deploy(&mut self) { - let wallet: ManagedAddress = ManagedAddress::new_from_bytes(&[7u8; 32]); +```go +inputNum := int64(37) - self.interactor - .tx() - .from(wallet) - .typed(proxy::Proxy) - .init(0u32) - .code(&self.code) - .gas(NumExpr("70,000,000")) - .returns(ReturnsNewBech32Address) -} +bi := big.NewInt(inputNum) +fmt.Println(hex.EncodeToString(bi.Bytes())) // 25 ``` -### Address - -The example beneath is a fragment for a blackbox test that runs the add function from a proxy. The sender is received as a parameter in this function. -```rust title=blackbox_test.rs -fn add_one(&mut self, from: &AddressValue) { - self.world - .tx() - .from(from) - .to(self.receiver) - .typed(proxy::Proxy) - .add(1u32) - .run(); -} -``` +for converting hex-encoded number to regular number: -For parametric testing, there is particular address type name: -- **TestAddress** - - encodes a dummy address, equivalent to `"address:{}"`; for the example below it is equivalent to `"address:owner"`; - - contains two functions: - - **`.eval_to_array()`** parse the address into an array of u8 - - **`.eval_to_expr()`** return the address as a String object -```rust title=blackbox_test.rs -const OWNER_ADDRESS: TestAddress = TestAddress::new("owner"); +```go +hexString := "25" -fn add_one(&mut self) { - self.world - .tx() - .from(OWNER_ADDRESS) - .to(self.receiver) - .typed(proxy::Proxy) - .add(1u32) - .run(); -} -``` -### Bech32Address -In order to avoid repeated conversions, it keeps the **bech32** representation **inside**. It wraps the address and presents it as a bech32 expression. -```rust title=interact.rs -fn add_one(&mut self, wallet_address: Bech32Address) { - self.world - .tx() - .from(wallet_address) - .to(self.receiver) - .typed(proxy::Proxy) - .add(1u32) - .run(); +decodedHex, err := hex.DecodeString(hexString) +if err != nil { + return err } +bi := big.NewInt(0).SetBytes(decodedHex) +fmt.Println(bi.String()) // 37 ``` -## Automatic wallet selection in interactors +--- -Wallets are registered beforehand when it comes to interactor operations. An automated signature is applied to the transaction. More details about interactors here. +### Smart Contract Debugging ---- +## Introduction -### Set up a Localnet (mxpy) +Debugging smart contracts is possible with the integrated debugger in Visual Studio Code. You will be able to debug your contract just like you would debug a regular program. -This guide describes how to set up a local mini-testnet - also known as **localnet** - using **mxpy**. The purpose of a localnet is to allow developers experiment with and test their Smart Contracts, in addition to writing unit and integration tests. -The localnet contains: +## Prerequisites -- **Validator Nodes** (two, by default) -- **Observer Nodes** (zero, by default) -- A **Seednode** -- A **MultiversX Proxy** +For this tutorial, you will need: +- Visual Studio Code +- the [rust-analyser](https://marketplace.visualstudio.com/items?itemName=matklad.rust-analyzer) extension. +- the [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) extension. +- A [Rust test](rust/sc-blackbox-example) -If not specified otherwise, the localnet starts with two shards plus the metachain (each with one validator). +If you want to follow along, you can clone the [mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs) repository and use the [crowdfunding](https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples/crowdfunding) example. -## Prerequisites: mxpy +## Step by step debugging -In order to install **mxpy**, follow [these instructions](/sdk-and-tools/mxpy/installing-mxpy). +In VSCode, you can put breakpoints anywhere in your code, by simply clicking to the left of the line number. A red dot should appear to mark the breakpoint as registered by the environment: -:::note -This guide assumes you are using `mxpy v9.7.1` or newer. -::: +![img](/developers/sc-debugging/breakpoint_setup.png) +Once you've done that, you can debug your test function by pressing the `Debug` button above the test function name: -## The easy way to start a localnet +![img](/developers/sc-debugging/start_test.png) -You can simply setup and start a localnet in a workspace (folder) of your choice by following the steps below. +If it doesn't appear, you might have to wait a bit for rust-analyser to load, or you might've forgotten the `#[test]` annotation. -Create a new folder (workspace) and navigate to it: +Once you've started the test, it should stop at the breakpoint and highlight the current line for you: -```bash -mkdir -p ~/my-first-localnet && cd ~/my-first-localnet -``` +![img](/developers/sc-debugging/first_step_debugging.png) -Create, build and configure a localnet (in one go): +Then, you can use VSCode's step by step debugging (usually F10 to step over, F11 to step into, or shift + F11 to step out). -```bash -mxpy localnet setup -``` -Then, start the localnet: +## Inspecting variables -```bash -mxpy localnet start -``` +For base Rust types, like u64 and such, you will be able to simply hover over them and see the value. -:::tip -Above, the command `mxpy localnet setup` performs the following sub-commands under the hood, in one go (so you don't have to run them individually): +You might however, try to hover over the `target` variable for instance, and will be immediately disappointed, since all you'll see is something like this: -```bash -mxpy localnet new -mxpy localnet prerequisites -mxpy localnet build -mxpy localnet config +```rust +handle:0 +_phantom:{...} ``` -::: -If everything goes well, in the terminal you should see logs coming from the nodes and proxy: +This is not very helpful. Unfortunately, for managed types you don't have the actual data in the type itself, you only have a handle (i.e. an index) in a stack somewhere. -``` -INFO:cli.localnet:Starting localnet... -... -INFO:localnet:Starting process ['./seednode', ... -... -INFO:localnet:Starting process ['./node', ... -... -INFO:localnet:Starting process ['./proxy', ... -[PID=...] DEBUG[...] [process/block] started committing block ... -``` +For that reason, we have the `sc_print!` macro: -:::tip -The logs from all processes of the localnet can also be found in `~/my-first-localnet/localnet`. Simply look for `*.log` files. -::: +```rust +sc_print!("{}", target); +``` -:::important -Note that the proxy starts with a delay of about 30 seconds. -::: +Adding this line to the beginning of the `#[init]` function will print `2000` in the console. -## Halting and resuming the localnet +## Printing formatted messages -In order to **halt the localnet**, press `Ctrl+C` in the terminal. This will stop all the processes (nodes, proxy etc.). The localnet can be **resumed at any time** by running again the command `mxpy localnet start` (from within your workspace). +If you want to print other data types, maybe even with a message, you can use the `sc_print!` macro all the same. +For example, if you were to add this to the start of the `#[init]` function: +```rust +sc_print!( + "I accept {}, a number of {}, and only until {}", + token_identifier, + target, + deadline +); +``` -## Removing the localnet +This macro would print the following: -In order to remove the localnet, run the command (from within your workspace): +`"I accept CROWD-123456, a number of 2000, and only until 604800"` -```bash -mxpy localnet clean -``` +Note: For ASCII or decimal representation, use `{}`, and for hex, use `{:x}`. -This will delete the `~/my-first-localnet/localnet` folder. Note that the configuration file (e.g. `localnet.toml`) will not be deleted automatically. +--- +### Smart Contract Deploy Events -## Gaining more control over the localnet +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; -### Perform setup steps individually +Contract deploy events are generated when a transaction involves either the deployment of a +smart contract or an upgrade to an existing contract. -If you want to have more control over the localnet, you can run the setup sub-commands individually, as described below. +### Contract deploy event -First, let's create a separate workspace (for the sake of this guide): +The contract deploy event is generated upon the successful execution of a transaction that includes +the deployment of a smart contract, without encountering any errors. -```bash -mkdir -p ~/my-second-localnet && cd ~/my-second-localnet -``` -Then, create a new configuration file for the localnet, as follows: + + -```bash -mxpy localnet new -``` +| Field | Value | +|------------|------------------------------------------------------------------------------------------------------------------------| +| identifier | SCDeploy | +| address | the address of the deployed contract | +| topics | `topics[0]` - the address bytes of the deployed contract base64 encoded
`topics[1]` - the address bytes of the deployer of the smart contract base64 encoded
`topics[2]` - the code hash bytes of the deployer smart contract base64 encoded | +| data | empty | -Upon running this command, a new file called `localnet.toml` will be added in the current directory. This file contains the default configuration of the localnet. **Make sure to open the file and inspect its content**. You should see something like this: +
+ +```json +{ + { + "address": "erd1qqqqqqqqqqqqqpgqnnl9nn0kuuckhg24g02hq2745n4jk2hp327qcay4nm", + "identifier": "SCDeploy", + "topics": [ + "AAAAAAAAAAAFAJz+Wc325zFroVVD1XAr1aTrKyrhirw=", + "NRl7AwoM3hEPC0t9RTDy7gdJUSJvKC5dpJwLYaHLirw=", + "bJtNdzjeaYecInf/NpHzSjHJEZ2l6hR/uJh0NkLIe+k=" + ], + "data": null + } +} ``` -[general] -... -rounds_per_epoch = 100 -round_duration_milliseconds = 6000 -[metashard] -... -num_validators = 1 + +
-[shards] -num_shards = 2 -... -[networking] -... -port_proxy = 7950 -port_first_validator_rest_api = 10200 +### Contract upgrade event -[software.mx_chain_go] -resolution = "remote" -archive_url = "https://github.com/multiversx/mx-chain-go/archive/refs/heads/master.zip" -... +The contract upgrade event is generated when a transaction, involving an upgrade, is successfully executed without any errors. -[software.mx_chain_proxy_go] -resolution = "remote" -archive_url = "https://github.com/multiversx/mx-chain-proxy-go/archive/refs/heads/master.zip" -... -``` -:::tip -Generally speaking, it's a good idea to only alter the `localnet.toml` **before first starting a localnet**. Once the localnet is started, the configuration file should not be modified anymore (e.g. when halting and resuming the localnet). -::: + + -Now, the following command will fetch the software prerequisites - **mx-chain-go**, **mx-chain-proxy-go** etc. - into `~/multiversx-sdk`: +| Field | Value | +|------------|------------------------------------------------------------------------------------------------------------------------| +| identifier | SCUpgrade | +| address | the address of the deployed contract | +| topics | `topics[0]` - the address bytes of the upgraded contract base64 encoded
`topics[1]` - the address bytes of the upgrader of the smart contract base64 encoded
`topics[2]` - the code hash bytes of the upgraded smart contract base64 encoded | +| data | empty | -```bash -mxpy localnet prerequisites +
+ + +```json +{ + "address": "erd1qqqqqqqqqqqqqpgqnnl9nn0kuuckhg24g02hq2745n4jk2hp327qcay4nm", + "identifier": "SCUpgrade", + "topics": [ + "AAAAAAAAAAAFAJz+Wc325zFroVVD1XAr1aTrKyrhirw=", + "NRl7AwoM3hEPC0t9RTDy7gdJUSJvKC5dpJwLYaHLirw=", + "kUVJtdwvHG2sCTi9l2uneSONUVonWfgHCK69gdB+52o=" + ], +} ``` -:::tip -The `prerequisites` step is only necessary when the localnet depends on remote source code archives - i.e. at least one software prerequisite has `resolution = remote` in `localnet.toml`. **This is actually the default, and it's the easiest way to get started with the localnet.** Later on, we'll see how to create the localnet from local source code (for advanced use-cases). -::: + +
-Once the software prerequisites (source code) are fetched, we can build them: -```bash -mxpy localnet build -``` +### Change owner event -:::tip -The actual build takes place within the download folders of the software prerequisites which, by default, are children of `~/multiversx-sdk/localnet_software_remote`. -::: +The `ChangeOwnerAddress` event is generated upon the successful execution of a transaction that specifically involves +a `ChangeOwnerAddress` built-in function call, and this execution must occur without encountering any errors. -Now let's configure (prepare) the localnet: -```bash -mxpy localnet config -``` + + -It is only upon running this command that the localnet subfolders are created. Make sure to inspect them: +| Field | Value | +|------------|------------------------------------------------------------------------------------------------------------------------| +| identifier | ChangeOwnerAddress | +| address | the address of the contract | +| topics | `topics[0]` - the address bytes of the new contract owner base64 encoded | +| data | empty | -```bash -tree -L 3 ~/my-second-localnet + + + +```json +{ + "address": "erd1qqqqqqqqqqqqqpgqnnl9nn0kuuckhg24g02hq2745n4jk2hp327qcay4nm", + "identifier": "ChangeOwnerAddress", + "topics": [ + "UKAg0hORMjk0oT6RalZp1w0Xulvvj0Wa/SSYstBepao=" + ], + "data": null +} ``` -Example output: + + -``` -├── localnet -│   ├── proxy -│   │   ├── config -│   │   └── proxy -│   ├── seednode -│   │   ├── config -│   │   ├── libwasmer_linux_amd64.so -│   | ├── ... -│   │   └── seednode -│   ├── validator00 -│   │   ├── config -│   │   ├── libwasmer_linux_amd64.so -│   | ├── ... -│   │   └── node -│   ├── validator01 -│   │   ├── config -│   │   ├── libwasmer_linux_amd64.so -│   | ├── ... -│   │   └── node -│   └── validator02 -│   ├── config -│   ├── libwasmer_linux_amd64.so -│   ├── ... -│   └── node -└── localnet.toml -``` +--- -We can then start, halt and resume the localnet as previously described. +### Smart contract interactions +Let's dive deeper into smart contract interactions and what you need to know to interact with a contract. If you followed the [previous `mxpy`](/docs/sdk-and-tools/mxpy/mxpy-cli.md) related documentation, you should be able to set up your prerequisites like proxy URL, the chain ID and the PEM file. -### Altering chronology parameters +For this, we need a file inside the contract's folder, with a suggestive name. For example: `devnet.snippets.sh`. -Let's create a new localnet workspace: +:::important +In order to be able to call methods from the file, we need to assign the shell file as a source file in the terminal. We can do this by running the next command: -```bash -mkdir -p ~/my-localnet-with-altered-chronology && cd ~/my-localnet-with-altered-chronology -mxpy localnet new +```shell +source devnet.snippets.sh ``` -**Before first starting a localnet**, you can alter the chronology parameters in `localnet.toml`. For example, let's have shorter epochs and shorter rounds: +After each change to the interactions file, we need to repeat the source command. +::: -``` -[general] -rounds_per_epoch = 50 -round_duration_milliseconds = 4000 -``` +Let's take the following example: -Then, setup and start the localnet as previously described. +1. We want to **deploy** a new smart contract on the Devnet. +2. We then need to **upgrade** the contract, to make it payable. +3. We **call** an endpoint without transferring any assets. +4. We **transfer** ESDT, in order to call a payable endpoint. +5. We call a **view** function. -```bash -mxpy localnet setup -mxpy localnet start -``` +## Prerequisites -### Altering sharding configuration +Before starting this tutorial, make sure you have the following: -Let's create a new localnet workspace: +- [`mxpy`](/sdk-and-tools/mxpy/mxpy-cli). Follow the [installation guide](/sdk-and-tools/mxpy/installing-mxpy) - make sure to use the latest version available. +- `stable` **Rust** version `≥ 1.83.0`. Follow the [installation guide](/docs/developers/toolchain-setup.md#installing-rust-and-sc-meta). +- `sc-meta`. Follow the [installation guide](/docs/developers/toolchain-setup.md#installing-rust-and-sc-meta). -```bash -mkdir -p ~/my-localnet-with-altered-sharding && cd ~/my-localnet-with-altered-sharding -mxpy localnet new -``` -**Before first starting a localnet**, you can alter the sharding configuration in `localnet.toml`. For example, let's have 3 shards, each with 2 validators and 1 observer (thus, 9 nodes, without the metachain ones): +## Deploy -``` -[shards] -num_shards = 3 -consensus_size = 2 -num_observers_per_shard = 1 -num_validators_per_shard = 2 +First things first. In order to deploy a new contract, we need to use `sc-meta` to build it, in the contract root, by invoking the next command: + +```shell +sc-meta all build ``` -Then, setup and start the localnet as previously described. +This will output the WASM bytecode, to be used within the interactions file: -```bash -mxpy localnet setup -mxpy localnet start +```shell +WASM_PATH="~/my-contract/output/my-contract.wasm" ``` +Now, in order to deploy the contract, we use the special **deploy** function of `mxpy`, that deploys the contract on the appointed chain, and runs the **init** function of the contract. -### Building from local source code - -Let's create a new localnet workspace: +```shell +WALLET_PEM="~/my-wallet/my-wallet.pem" +PROXY="https://devnet-gateway.multiversx.com" -```bash -mkdir -p ~/my-localnet-from-local-src && cd ~/my-localnet-from-local-src -mxpy localnet new +deploySC() { + mxpy --verbose contract deploy \ + --bytecode=${WASM_PATH} \ + --pem=${WALLET_PEM} \ + --proxy=${PROXY} \ + --arguments $1 $2 \ + --send || return +} ``` -In order to build the **node** or the **proxy** from local source code (instead of fetching the source code from a remote archive), you can set `resolution = local` in `localnet.toml`. For example, let's build both the node and the proxy from local source code: +Run in terminal the following command to deploy the smart contract on Devnet. Replace `arg1` and `arg2` with your desired deployment values. +```shell +source devnet.snippets.sh +deploySC arg1 arg2 ``` -[software.mx_chain_go] -resolution = "local" -local_path = "~/Desktop/workspace/mx-chain-go" -[software.mx_chain_proxy_go] -resolution = "local" -local_path = "~/Desktop/workspace/mx-chain-proxy-go" -``` +Now let's look at the structure of the interaction. It receives the path of the **wasm file**, where we previously built the contract. It also receives the path of the **wallet** (the PEM file) and the **proxy URL** where the contract will be deployed. -Then, setup and start the localnet as previously described. +Other than this, we also have the **arguments** keyword, that allows us to pass in the required parameters. As we previously said, deploying a smart contract means that we run the **init** function, which may or may not request some parameters. In our case, the **init** function has two different arguments, and we pass them when calling the **deploy** function. We'll come back later in this section at how we can pass parameters in function calls. -```bash -mxpy localnet setup -mxpy localnet start -``` +After the transaction is sent, `mxpy` will output information like the **transaction hash**, **data** and any other important information, based on the type of transaction. In case of a contract deployment, it will also output the **newly deployed contract address**. -## Test (development) wallets +## Upgrade -The development wallets **are minted at the genesis of the localnet** and their keys (both PEM files and Wallet JSON files) can be found in `~/multiversx-sdk/testwallets/latest/users`. +Let's now suppose we need to make the contract **payable**, in case it needs to receive funds. We could redeploy the contract but that will mean two different contracts, and not to mention that we will lose any existing storage. For that, we can use the **upgrade** command, that replaces the existing smart contract bytecode with the newly built contract version. :::caution -These wallets (Alice, Bob, Carol, ..., Mike) **are publicly known** - they should only be used for development and testing purpose. +It is import to handle data storage with caution when upgrading a smart contract. Data structure, especially for complex data types, must be preserved, otherwise the data may become corrupt. ::: +The upgrade function would look like this: -## Interacting with our localnet - - -### Sending transactions - -Let's send a simple transaction using **mxpy**: - -```bash -mxpy tx new --data="Hello, World" --gas-limit=70000 \ - --receiver=erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --chain=localnet --proxy=http://localhost:7950 \ - --send -``` - -You should see the prepared transaction and the **transaction hash** in the `stdout` (or in the `--outfile` of your choice). Using the transaction hash, you can query the status of the transaction against the Proxy: +```shell +CONTRACT_ADDRESS="erd1qqqqqqqqqqqqqpgqspymnxmfjve0vxhmep5vr3tf6sj8e80dd8ss2eyn3p" -```bash -curl http://localhost:7950/transaction/8b2cd8e61c12d6f02148537bdef40579c6cbff7ff0aba996f294d34a31992ba4 | jq +upgradeSC() { + mxpy --verbose contract upgrade ${CONTRACT_ADDRESS} --metadata-payable \ + --bytecode=${WASM_PATH} \ + --pem=${WALLET_PEM} \ + --proxy=${PROXY} \ + --arguments $1 $2 \ + --send || return +} ``` +`CONTRACT_ADDRESS` is a placeholder value which value needs to be replaced with the address previously generated in the deploy action. -### Deploying and interacting with Smart Contracts +Here we have 2 new different elements that we need to observe: -Let's deploy a Smart Contract using **mxpy**. +1. We changed the **deploy** function with the **upgrade** function. This new function requires the address of the previously deployed smart contract so the system can identify which contract to update. It is important to note that this function can only be called by the smart contract's owner. +2. The **metadata-payable** keyword, which represents a [code metadata](/docs/developers/data/code-metadata.md) flag that allows the smart contract to receive payments. -```bash -mxpy --verbose contract deploy --bytecode=./contract.wasm \ - --gas-limit=5000000 \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --outfile=contract.json \ - --chain=localnet --proxy=http://localhost:7950 \ - --send -``` -Upon deployment, you can check the status of the transaction and the existence of the Smart Contract: +## Non payable endpoint interaction -```bash -curl http://localhost:7950/transaction/4c5bd51ca0a051397bd6b0c89add2a5375106562b005c839f7e9bb113e2a8ce4 | jq -curl http://localhost:7950/address/erd1qqqqqqqqqqqqqpgqj5zftf3ef3gqm3gklcetpmxwg43rh8z2d8ss2e49aq | jq -``` +Let's suppose we want to call the following endpoint, that receives an address and three different `BigUint` arguments, in this specific order. -If everything is fine (transaction status is `executed` and the `code` property of the address is set), you can interact with the deployed contract: +```shell +###PARAMS +# $1 = FirstBigUintArgument +# $2 = SecondBigUintArgument +THIRD_BIGUINT_ARGUMENT=0x0f4240 +ADDRESS_ARGUMENT=erd14nw9pukqyqu75gj0shm8upsegjft8l0awjefp877phfx74775dsq49swp3 -```bash -mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqj5zftf3ef3gqm3gklcetpmxwg43rh8z2d8ss2e49aq \ - --gas-limit=1000000 --function=increment \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem --outfile=myCall.json \ - --chain=localnet --proxy=http://localhost:7950 \ - --send +myNonPayableEndpoint() { + address_argument="0x$(mxpy wallet bech32 --decode ${ADDRESS_ARGUMENT})" + mxpy --verbose contract call ${CONTRACT_ADDRESS} \ + --pem=${WALLET_PEM} \ + --proxy=${PROXY} \ + --function="myNonPayableEndpoint" \ + --arguments $address_argument $1 $2 ${THIRD_BIGUINT_ARGUMENT}\ + --send || return +} ``` -In order to perform queries against the contract using `mxpy`, do as follows: - -``` -mxpy --verbose contract query erd1qqqqqqqqqqqqqpgqj5zftf3ef3gqm3gklcetpmxwg43rh8z2d8ss2e49aq --function=get --proxy=http://localhost:7950 -``` +So, what happens in this interaction and how do we call it? +Besides the function and arguments parts, the snippet is more or less the same as when deploying or upgrading a contract. When calling a **non payable** function, we need to provide the **endpoint's name** as the function argument. As for the arguments, they have to be in the **same order** as in the endpoint's signature. Now, for the sake of example, we provided the arguments in multiple ways. -### Simulating transactions +It is up to each developer to choose the layout he prefers, but a few points need to be underlined: -At times, you can simulate transactions instead of broadcasting them, by replacing the flag `--send` with the flag `--simulate`. For example: +- Most of the supplied **arguments** need to be in the **hexadecimal format**: `0x...`. +- When converting a value to a hexadecimal format, we need to make sure it has an **even number** of characters. If not, we need to provide an extra `0` in order to make it even: + - Example: the number `911` -> in hexadecimal encoding, it is equal to: `38f` -> so we need to provide the argument `0x038f`. +- Arguments can be provided both as a fixed arguments (usually for unchangeable arguments like the contract's address or a fixed number) or can be provided as an input in the terminal, when interacting with the snippet (mostly used for arguments that change often like numbers). -```bash -# Simulate: Call Contract -mxpy contract call erd1qqqqqqqqqqqqqpgqj5zftf3ef3gqm3gklcetpmxwg43rh8z2d8ss2e49aq \ - --gas-limit=1000000 --function=increment \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --chain=localnet --proxy=http://localhost:7950 \ - --simulate +In our example we provide the address argument as a fixed argument. We then convert it to hexadecimal format (as it is in the bech32 format by default) and only after that we pass it as a parameter. As for the `BigUint` parameters, we provide the first two parameters directly in the terminal and the last one as a fixed argument, hexadecimal encoded. -# Simulate: Simple Transfer -mxpy tx new --data="Hello, World" --gas-limit=70000 \ - --receiver=erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --chain=localnet --proxy=http://localhost:7950 \ - --simulate -``` +:::tip +`mxpy` provides the following encoding conventions: ---- +- We can use `str:` for encoding strings. For example: `str:MYTOKEN-123456`. +- Blockchain addresses that start with `erd1` are automatically encoded, so there is no need to further hex encode them. +- The values **true** or **false** are automatically converted to **boolean** values. +- Values that are identified as **numbers** are hex encoded as `BigUint` values. +- Arguments like `0x...` are left unchanged, as they are interpreted as already encoded hex values. -### Set up a Localnet (raw) +::: -How to set up a local MultiversX Testnet on a workstation. +So, in case of our **myNonPayableEndpoint** interaction, we can write it like so: +```shell +###PARAMS +# $1 = FirstBigUintArgument +# $2 = SecondBigUintArgument +THIRD_BIGUINT_ARGUMENT=1000000 +ADDRESS_ARGUMENT=addr:erd14nw9pukqyqu75gj0shm8upsegjft8l0awjefp877phfx74775dsq49swp3 -## **Prerequisites** +myNonPayableEndpoint() { + mxpy --verbose contract call ${CONTRACT_ADDRESS} \ + --pem=${WALLET_PEM} \ + --proxy=${PROXY} \ + --function="myNonPayableEndpoint" \ + --arguments ${ADDRESS_ARGUMENT} $1 $2 ${THIRD_BIGUINT_ARGUMENT}\ + --send || return +} +``` -First, clone [mx-chain-go](https://github.com/multiversx/mx-chain-go) and [mx-chain-proxy-go](https://github.com/multiversx/mx-chain-proxy-go) in a directory of your choice. +A call example for this endpoint would look like: -```bash -$ mkdir mytestnet && cd mytestnet -$ git clone git@github.com:multiversx/mx-chain-go.git -$ git clone git@github.com:multiversx/mx-chain-proxy-go.git +```shell +source devnet.snippets.sh +myNonPayableEndpoint 10000 100000 ``` -Then, run the `prerequisites` command. +Using unencoded values (for easier reading) would translate into: -```bash -$ cd mx-chain-go/scripts/testnet -$ ./prerequisites.sh +```shell +myNonPayableEndpoint addr:erd14nw9pukqyqu75gj0shm8upsegjft8l0awjefp877phfx74775dsq49swp3 10000 100000 1000000 ``` -This will install some packages and also clone the [mx-chain-deploy-go](https://github.com/multiversx/mx-chain-deploy-go) repository, as a sibling of the previously cloned `mx-chain-go`. +:::caution +It is import to make sure all arguments have the correct encoding. Otherwise, the transaction will fail. +::: -Depending on your Linux distribution, you may need to run the following commands as well: -```bash -sudo apt install tmux -sudo apt install gnome-terminal -``` +## Payable endpoint interaction -## **Configure the Testnet** +### Fungible ESDT transfer -The variables that dictate the structure of the Testnet are located in the file `scripts/testnet/variables.sh`. For example: +Now let's take a look at the following example, where we want to call a payable endpoint. -```bash -export TESTNETDIR="$HOME/MultiversX/testnet" -export SHARDCOUNT=2 -... +```shell +myPayableEndpoint() { + token_identifier=$1 + token_amount=$2 + mxpy --verbose contract call ${CONTRACT_ADDRESS} \ + --pem=${WALLET_PEM} \ + --proxy=${PROXY} \ + --token-transfers $token_identifier $token_amount \ + --function="myPayableEndpoint" \ + --send || return +} ``` -You can override the default variables by creating a new file called `local.sh`, as a sibling of `variables.sh`. For example, in order to use a different directory than the default one: +To call a **payable endpoint**, we use the `--token-transfer` argument, which requires two values: -```bash -local.sh -export TESTNETDIR="$HOME/Desktop/mytestnet/sandbox" -export USETMUX=1 -export NODETERMUI=0 -``` +1. The token identifier. +2. The amount. -Once ready with overriding the desired parameters, run the `config` command. +In our case, we specify in the terminal the **token identifier** and **the amount of tokens** we want to transfer. -```bash -$ ./config.sh -``` +:::info +The format for the token identifier changes based on the type of asset you are sending: -After that, you can inspect the generated configuration files in the specified folder: +- **ESDTs Tokens**: Use the **standard** Token Identifier. +- **NFTs and SFTs**: Use the **extended** Token identifier format, which includes the token's nonce. The nonce must be hex-encoded. + - Example: `NFT-123456-0a` (where `0a` is the hex-encoded nonce). -``` -$HOME/Desktop/mytestnet/sandbox -├── filegen -│ ├── filegen -│ └── output -│ ├── delegationWalletKey.pem -│ ├── delegators.pem -│ ├── genesis.json -│ ├── genesisSmartContracts.json -│ ├── nodesSetup.json -│ ├── validatorKey.pem -│ └── walletKey.pem -├── node -│ └── config -│ ├── api.toml -│ ├── config_observer.toml -│ ├── config_validator.toml -│ ├── delegationWalletKey.pem -│ ├── delegators.pem -│ ├── economics.toml -│ ├── external.toml -│ ├── gasSchedule.toml -│ ├── genesisContracts -│ │ ├── delegation.wasm -│ │ └── dns.wasm -│ ├── genesis.json -│ ├── genesisSmartContracts.json -│ ├── nodesSetup.json -│ ├── p2p.toml -│ ├── prefs.toml -│ ├── ratings.toml -│ ├── systemSmartContractsConfig.toml -│ ├── validatorKey.pem -│ └── walletKey.pem -├── node_working_dirs -├── proxy -│ └── config -│ ├── config.toml -│ ├── economics.toml -│ ├── external.toml -│ └── walletKey.pem -└── seednode - └── config - ├── config.toml - └── p2p.toml -``` +::: +:::info +When specifying the amount of tokens to transfer, the value must include the token's decimal precision. -## **Starting and stopping the Testnet** +For example EGLD uses 18 decimals. This means that if you want to transfer 1.5 EGLD, the amount value will be $1.5 \times 10^{18}$. +::: -In order to start the Testnet, run the `start` command. -```bash -$ ./start.sh debug -``` +### Non-fungible ESDT transfer (NFT, SFT and META ESDT) -After waiting about 1 minute, you can inspect the logs of the running nodes in folder `mytestnet/sandbox/node_working_dirs`. +Now let's suppose we want to call an endpoint that accepts an NFT or an SFT as payment. -In order to stop the Testnet, run the `stop` command. +```shell +###PARAMS +# $1 = NFT/SFT Token Identifier, +# $2 = NFT/SFT Token Amount, +FIRST_BIGUINT_ARGUMENT=1000 +SECOND_BIGUINT_ARGUMENT=10000 -```bash -$ ./stop.sh +myESDTNFTPayableEndpoint() { + sft_token_identifier=$1 + sft_token_amount=$2 + mxpy --verbose contract call ${CONTRACT_ADDRESS} \ + --pem=${WALLET_PEM} \ + --proxy=${PROXY} \ + --token-transfers $sft_token_identifier $sft_token_amount \ + --function="myESDTNFTPayableEndpoint" \ + --arguments ${FIRST_BIGUINT_ARGUMENT} ${SECOND_BIGUINT_ARGUMENT} \ + --send || return +} ``` -If desired, you can also `pause` and `resume` the Testnet (without actually stopping the running nodes): -```bash -$ ./pause.sh -$ ./resume.sh -``` +### Multi-ESDT transfer +In case we need to call an endpoint that accepts multiple tokens (let's say for example 2 fungible tokens and an NFT). Let's take a look at the following example: -## **Recreating the Testnet** +```shell +###PARAMS +# $1 = First Token Identifier, +# $2 = First Token Amount, +# $3 = Second Token Identifier, +# $4 = Second Token Amount, +# $5 = Third Token Identifier, +# $6 = Third Token Amount, +FIRST_BIGUINT_ARGUMENT=1000 +SECOND_BIGUINT_ARGUMENT=10000 -In order to destroy the Testnet, run the `clean` command: +myMultiESDTNFTPayableEndpoint() { + first_token_identifier=$1 + first_token_amount=$2 + second_token_identifier=$3 + second_token_amount=$4 + third_token_identifier=$5 + third_token_amount=$6 -```bash -./stop.sh -./clean.sh + mxpy --verbose contract call ${CONTRACT_ADDRESS} \ + --pem=${WALLET_PEM} \ + --proxy=${PROXY} \ + --token-transfers $first_token_identifier $first_token_amount \ + $second_token_identifier $second_token_amount \ + $third_token_identifier $third_token_amount \ + --function="payable_nft_with_args" \ + --arguments ${FIRST_BIGUINT_ARGUMENT} ${SECOND_BIGUINT_ARGUMENT} \ + --send || return ``` -:::note Run config after clean -After running **clean,** you need to run **config** before **start**, in order to start the Testnet again. -::: +In this example, we call `myMultiESDTPayableEndpoint` endpoint, by transferring **3 different tokens**: the first two are fungible tokens and the last one is an NFT. -If you need to recreate a Testnet from scratch, use the `reset` command (which also executes `clean` under the hood): +:::tip +More information about ESDT Transfers [here](/tokens/fungible-tokens/#transfers). +::: -```bash -$ ./reset.sh -``` +## View interaction -## **Inspecting the Proxy** +In case we want to call a view function, we can use the **query** keyword. -By default, the local Testnet also includes a local MultiversX Proxy instance, listening on port **7950**. You can query in a browser or directly in the command line. Also see [REST API](/sdk-and-tools/rest-api/). +```shell +###PARAMS +# $1 = First argument +# $2 = Second argument -```bash -$ curl http://localhost:7950/network/config +myView() { + mxpy --verbose contract query ${CONTRACT_ADDRESS} \ + --proxy=${PROXY} \ + --function="myView" \ + --arguments $1 $2 +} ``` -Given the request above, extract and save the fields `erd_chain_id` and `erd_min_transaction_version` from the response. You will need them in order to send transactions against your local Testnet. - +When calling a **view** function, `mxpy` will output the standard information in the terminal, along with the results, formatted based on the requested data type. The arguments are specified in the same way as with endpoints. -## **Sending transactions** +--- -Let's send a simple transaction using **mxpy:** +### Smart contract modules -```bash -$ mxpy tx new --data="Hello, World" --gas-limit=70000 \ - --receiver=erd1... \ - --pem=./sandbox/node/config/walletKey.pem --pem-index=0 \ - --proxy=http://localhost:7950 \ - --send -``` +## Introduction -You should see the prepared transaction and the **transaction hash** in the `stdout` (or in the `--outfile` of your choice). Using the transaction hash, you can query the status of the transaction against the Proxy: +Smart contract modules are a handy way of dividing a contract into smaller components. Modules also reduce code duplication, since they can be reused across multiple contracts. -```bash -$ curl http://localhost:7950/transaction/1363... -``` +## Declaration -## **Deploying and interacting with Smart Contracts** +Modules can be defined both in the same crate as the main contract, or even in their own standalone crate. The latter is used when you want to use the same module in multiple contracts. -Let's deploy a Smart Contract using **mxpy**. +A module is trait declared with the `#[multiversx_sc::module]` macro. Inside the trait, you can write any code you would usually write in a smart contract, even endpoints, events, storage mappers, etc. -```bash -Deploy -mxpy --verbose contract deploy --bytecode=./mycontract/output/contract.wasm \ - --gas-limit=5000000 \ - --pem=./sandbox/node/config/walletKey.pem --pem-index=0 \ - --outfile=contract.json \ - --proxy=http://localhost:7950 \ - --send -``` +For example, let's say you want to have your storage mappers in a separate module. The implementation would look like this: -Upon deployment, you can check the status of the transaction and the existence of the Smart Contract: +```rust +#[multiversx_sc::module] +pub trait StorageModule { + #[view(getQuorum)] + #[storage_mapper("firstStorage")] + fn first_storage(&self) -> SingleValueMapper; -```bash -$ curl http://localhost:7950/transaction/daf2... -$ curl http://localhost:7950/address/erd1qqqqqqqqqqqqqpgql... + #[view] + #[storage_mapper("secondStorage")] + fn second_storage(&self) -> SingleValueMapper; +} ``` -If everything is fine (transaction status is `executed` and the `code` property of the address is set), you can interact with or perform queries against the deployed contract: -```bash -Call -mxpy --verbose contract call erd1qqqqqqqqqqqqqpgql... \ - --gas-limit=1000000 --function=increment \ - --pem=./sandbox/node/config/walletKey.pem --pem-index=0 --outfile=myCall.json \ - --proxy=http://localhost:7950 \ - --send - -``` +Then, in your main file (usually named `lib.rs`), you have to define the module. If the file for the above module is named `storage.rs`, then in the main file you'd declare it like this: -```bash -Query -mxpy --verbose contract query erd1qqqqqqqqqqqqqpgqlq... \ - --function=get \ - --proxy=http://localhost:7950 +```rust +pub mod storage; ``` ---- -### Setup & Basics +## Importing a module -Write, build and deploy a simple smart contract written in Rust. +A module can be imported both by other modules and contracts: -This tutorial will guide you through the process of writing, building and deploying a simple smart contract for the MultiversX Network, written in Rust. +```rust +pub trait SetupModule: + crate::storage::StorageModule + + crate::util::UtilModule { -:::important -The MultiversX Network supports smart contracts written in any programming language compiled into WebAssembly. -::: +} +``` +```rust +#[multiversx_sc::contract] +pub trait MainContract: + setup::SetupModule + + storage::StorageModule + + util::UtilModule { -## Scenario +} +``` -Let's say you need to raise EGLD for a cause you believe in. They will obviously be well spent, but you need to get the EGLD first. For this reason, you decided to run a crowdfunding campaign on the MultiversX Network, which naturally means that you will use a smart contract for the campaign. This tutorial will teach you how to do just that: **write a crowdfunding smart contract, deploy it, and use it**. +Keep in mind your main contract has to implement all modules that any sub-module might use. In this example, even if the `MainContract` does not use anything from the `UtilModule`, it still has to implement it if it wants to use `SetupModule`. -The idea is simple: the smart contract will accept transfers until a deadline is reached, tracking all contributors. -If the deadline is reached and the smart contract has gathered an amount of EGLD above the desired funds, then the smart contract will consider the crowdfunding a success, and it will consequently send all the EGLD to a predetermined account (yours!). +## Conclusion -However, if the donations fall short of the target, the contract will return all the all EGLD tokens to the donors. +We hope this module system will make it a lot easier to write maintainable smart contract code, and even reusable modules. +More modules and examples can be found here: https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/modules -## Design +--- -Here is how the smart contract methods are designed: +### Smart contract payments -- `init`: automatically triggered when the contract is deployed. It takes two inputs from you: - 1. The target amount of EGLD you want to raise; - 2. The crowdfunding deadline, which is expressed as a block nonce. -- `fund`: used by donors to contribute EGLD to the campaign. It will receive EGLD and save the necessary details so the contract can return funds if the campaign doesn't reach its goal; -- `claim`: if called before the deadline, it does nothing and returns an error. If called after the deadline: - - By you (the campaign creator), it sends all the raised EGLDs to your account if the target amount is met. Otherwise, it returns an error; - - By donor, it refunds their contribution if the target amount is not reached. If the target is met, it does nothing and returns an error; - - By anyone else, it does nothing and returns an error. -- `status`: Provides information about the campaign, such as whether it is still active or completed and how much EGLD has been raised so far. You will likely use this frequently to monitor progress. +## Some general points -In this part of the tutorial, we will start with the `init` method to familiarize you with the development process and tools. You will not only implement the init method but also **tests** to ensure it works as expected. +We want to offer an overview on how smart contracts process payments. This includes two complementary parts: receiving tokens and sending them. -:::important testing -Automated testing is exceptionally important for the development of smart contracts, due to the sensitive nature of the information they must handle. +:::important important +On MultiversX it is possible to send one or more tokens with any transaction. This includes EGLD, and it is also possible (though impractical) to send several payments of the same token at once. ::: -## Prerequisites - -:::important -Before starting this tutorial, make sure you have the following: - -- `stable` **Rust** version `≥ 1.85.0` (install via [rustup](/docs/developers/toolchain-setup.md#installing-rust-and-sc-meta)) -- `sc-meta` (install [multiversx-sc-meta](/docs/developers/meta/sc-meta-cli.md)) - +:::note note +Historically, it used to be impossible to send EGLD and ESDT at the same time, this is why some of the legacy APIs have this restriction. This restriction no longer applies since the [Spica release](https://multiversx.com/release/release-spica-patch-4-v1-8-12). ::: -For contract developers, we generally recommend [**VSCode**](https://code.visualstudio.com) with the following extensions: +--- -- [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) -- [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) +## Receiving payments -## Step 1: prepare the workspace +There are two ways in which a smart contract can receive payments: +1. Like any regular account, directly, without any contract code being called; +2. Via an endpoint. -The source code of each smart contract requires its own folder. We will start the development of the **crowdfunding** contract from the **empty** template. To get the development environment ready, simply run the following commands in your terminal: -```bash -sc-meta new --name crowdfunding --template empty -``` +### Receiving payments directly -You may choose any location you want for your smart contract. Either way, now that you are in the `crowdfunding` folder we can begin. +Sending EGLD and ESDT tokens directly to accounts works the same way for EOAs (externally owned accounts) as for smart contracts: the tokens are transferred from one account to the other without firing up the VM. -[sc-meta](/docs/developers/meta/sc-meta.md) created your project out of a template. These templates are contracts written and tested by MultiversX, which can be used by anybody as starting points. +However, not all smart contracts are allowed to receive tokens directly. There is a flag that controls this, called "payable". This flag is part of the [code metadata](/developers/data/code-metadata), and is specified in the transaction that deploys or upgrades the smart contract. -```toml title=Cargo.toml -[package] -name = "crowdfunding" -version = "0.0.0" -publish = false -edition = "2024" -authors = ["you"] +The rationale for this is as follows: the MultiversX blockchain doesn't offer any mechanism to allow contracts to react to direct calls. This is because we wanted to keep direct calls simple, consistent, and with a predictable gas cost, in all contexts. Most contracts, however, will likely want to keep track of all the funds that are fed into them, so they do not want to accept payments without an opportunity to also change their internal state. -[lib] -path = "src/crowdfunding.rs" -[dependencies.multiversx-sc] -version = "0.64.1" +### Receiving payments via endpoints -[dev-dependencies.multiversx-sc-scenario] -version = "0.64.1" +The most common way for contracts to accept payments is by having endpoints annotated with the `#[payable]` annotation (or `#[payable("*")]`). -[workspace] -members = [ - ".", - "meta", -] -``` +:::important important +The "payable" flag in the code metadata only refers to direct transfers. Transferring tokens via contract endpoint calls is not affected by it in any way. +::: -Let's inspect the file found at path `crowdfunding/Cargo.toml`: +To accept any kind of payment, annotate the endpoints with `#[payable]`: -- `[package]` represents the **project** which is unsurprisingly named `crowdfunding`, and has version `0.0.0`. You can set any version you like, just make sure it has 3 numbers separated by dots. It is a requirement. The `publish` is set to **false** to prevent the package from being published to Rust’s central package registry. It's useful for private or experimental projects; -- `[lib]` declares the source code of the smart contracts, which in our case is `src/crowdfunding.rs`. You can name this file anything you want. The default Rust naming is `lib.rs`, but it can be easier to organize your code when the main code files bear the names of the contracts; -- This project has `dependencies` and `dev-dependencies`. You'll need a few special and very helpful packages: - - `multiversx-sc`: developed by MultiversX, it is the interface that the smart contract sees and can use; - - `multiversx-sc-scenario`: developed by MultiversX, it is the interface that defines and runs blockchain scenarios involving smart contracts; - - `num-bigint`: for working with arbitrarily large integers. -- `[workspace]` is a group of related Rust projects that share common dependencies or build settings; -- The resulting binary will be the name of the project, which in our case is `crowdfunding` (actually, `crowdfunding.wasm`, but the compiler will add the `.wasm` part). +```rust +#[endpoint] +#[payable] +fn accept_any_payment(&self) { + // ... +} +``` +Usually on the first line there will be an instruction that processes, interprets, and validates the received payment ([see below](#call-value-methods)) -## Step 2: develop -With the structure in place, you can now write the code and build it. -Open `src/crowdfunding.rs`: +If an endpoint only accepts EGLD, it can be annotated with `#[payable("EGLD")]`, although this is slowly falling out of favor. ```rust -#![no_std] // [1] +#[endpoint] +#[payable("EGLD")] +fn accept_egld(&self) { + // ... +} +``` -use multiversx_sc::imports::*; // [2] -#[allow(unused_imports)] // [3] -/// An empty contract. To be used as a template when starting a new contract from scratch. -#[multiversx_sc::contract] // [4] -pub trait Crowdfunding { // [5] - #[init] // [6] - fn init(&self) {} // [7] +:::note Multi-transfer note +Note that it is currently possible to send two or more EGLD payments in the same transaction. The `#[payable("EGLD")]` annotation rejects that. +::: - #[upgrade] // [8] - fn upgrade(&self) {} // [9] +This snippet is equivalent to: + +```rust +#[endpoint] +#[payable] +fn accept_egld(&self) { + let payment_amount = self.call_value().egld(); + // ... } ``` -Let's take a look at the code: - -- **[1]**: means that the smart contract **has no access to standard libraries**. That will make the code lean and very light. -- **[2]**: brings imports module from the [multiversx_sc](https://crates.io/crates/multiversx-sc) crate into **Crowdfunding** contract. It effectively grants you access to the [MultiversX framework for Rust smart contracts](https://github.com/multiversx/mx-sdk-rs), which is designed to simplify the code **enormously**. -- **[3]**: since the contract is still in an early stage of development, clippy (Rust's linter) will flag some imports as unused. For now, we will ignore this kind of error. -- **[4]**: processes the **Crowdfunding** trait definition as a **smart contract** that can be deployed on the MultiversX blockchain. -- **[5]**: the contract [trait](https://doc.rust-lang.org/book/ch10-02-traits.html) where all the endpoints will be developed. -- **[6]**: marks the following method (`init`) as the constructor function for the contract. -- **[7]**: this is the constructor itself. It receives the contract's instance as a parameter (_&self_). The method is called once the contract is deployed on the MultiversX blockchain. You can name it any way you wish, but it must be annotated with `#[init]`. For the moment, no initialization logic is defined. -- **[8]**: marks the following method (`upgrade`) as the upgrade function for the contract. It is called when the contract is re-deployed to the same address. -- **[9]**: this is the upgrade method itself. Similar to [7], it takes a reference to the contract instance (_&self_) and performs no specific logic here. - -## Step 3: build -Now go back to the terminal, make sure the current folder is the one containing the Crowdfunding smart contract (`crowdfunding/`), then trigger the **build** command: +:::note Hard-coded token identifier +It is also possible to hard-code a token identifier in the `payable`, e.g. `#[payable("MYTOKEN-123456")]`. It is rarely, if ever, used, tokens should normally be configured in storage, or at runtime. +::: -```bash -sc-meta all build -``` -If this is the first time you build a Rust smart contract with the `sc-meta` command, it will take a little while before it's done. Subsequent builds will be much faster. +## Payment Types -When the command completes, a new folder will appear: `crowdfunding/output/`. This folder contains: +The framework provides a unified approach to handling payments using the `Payment` type that treats EGLD and ESDT tokens uniformly. EGLD is represented as `EGLD-000000` token identifier, making all payment handling consistent. -1. `crowdfunding.abi.json` -2. `crowdfunding.imports.json` -3. `crowdfunding.mxsc.json` -4. `crowdfunding.wasm` +**`Payment
`** - The primary payment type that combines: +- `token_identifier`: `TokenId` - unified token identifier (EGLD serialized as "EGLD-000000") +- `token_nonce`: `u64` - token nonce for NFTs/SFTs, which is zero for all fungible tokens (incl. EGLD) +- `amount`: `NonZeroBigUint` - guaranteed non-zero amount -We won't be doing anything with these files just yet - wait until we get to the deployment part. Along with `crowdfunding/output/`, there are a few other folders and files generated. You can safely ignore them for now, but do not delete the `/crowdfunding/wasm/` folder - it's what makes the build command faster after the initial run. +**`PaymentVec`** - A managed vector of `Payment` objects, representing multiple payments in a single transaction. -The following can be safely deleted, as they are not important for this contract: -- The `scenarios/` folder; -- The `crowdfunding/tests/crowdfunding_scenario_go_test.rs` file; -- The `crowdfunding/tests/crowdfunding_scenario_rs_test.rs` file. +## Call Value Methods -The structure of your folder should be like this (output printed using command `tree -L 2`): +Additional restrictions on the incoming tokens can be imposed in the body of the endpoint, by calling the call value API. Most of these functions retrieve data about the received payment, while also stopping execution if the payment is not of the expected type. -```bash -. -├── Cargo.lock -├── Cargo.toml -├── meta -│ ├── Cargo.toml -│ └── src -├── multiversx.json -├── output -│ ├── crowdfunding.abi.json -│ ├── crowdfunding.imports.json -│ ├── crowdfunding.mxsc.json -│ └── crowdfunding.wasm -├── src -│ └── crowdfunding.rs -├── target -│ ├── CACHEDIR.TAG -│ ├── debug -│ ├── release -│ ├── tmp -│ └── wasm32-unknown-unknown -├── tests -└── wasm - ├── Cargo.lock - ├── Cargo.toml - └── src -``` -It's time to add some functionality to the `init` function now. +### `all()` - Complete Payment Collection +`self.call_value().all()` retrieves all payments sent with the transaction as a `PaymentVec`. It handles all tokens uniformly, including EGLD (represented as "EGLD-000000"). Never stops execution. -## Step 4: persisting values +```rust +#[payable] +#[endpoint] +pub fn process_all_payments(&self) { + let payments = self.call_value().all(); + for payment in payments.iter() { + // Handle each payment uniformly + self.process_payment(&payment.token_identifier, payment.token_nonce, &payment.amount); + } +} +``` -In this step, you will use the `init` method to persist some values in the storage of the Crowdfunding smart contract. +### `single()` - Strict Single Payment -### Storage mappers +`self.call_value().single()` expects exactly one payment and returns it. Will halt execution if zero or multiple payments are received. Returns a `Payment` object. -Every smart contract can store key-value pairs in a persistent structure, created for the smart contract at its deployment on the MultiversX Network. +```rust +#[payable] +#[endpoint] +pub fn deposit(&self) { + let payment = self.call_value().single(); + // Guaranteed to be exactly one payment + let token_id = &payment.token_identifier; + let amount = payment.amount; + + self.deposits(&self.blockchain().get_caller()).set(&amount); +} +``` -The storage of a smart contract is, for all intents and purposes, **a generic hash map or dictionary**. When you want to store some arbitrary value, you store it under a specific key. To get the value back, you need to know the key you stored it under. -To help you keep the code clean, the framework enables you to write **setter** and **getter** methods for individual key-value pairs. There are several ways to interact with storage from a contract, but the simplest one is by using [**storage mappers**](/docs/developers/developer-reference/storage-mappers.md). +### `single_optional()` - Flexible Single Payment -Next, you will declare a [_SingleValueMapper_](/docs/developers/developer-reference/storage-mappers.md#singlevaluemapper) that has the purpose of storing a [_BigUint_](/docs/developers/best-practices/biguint-operations.md) number. This storage mapper is dedicated to storing/retrieving the value stored under the key `target`: +`self.call_value().single_optional()` accepts either zero or one payment. Returns `Option>` for graceful handling. Will halt execution if multiple payments are received. ```rust -#[storage_mapper("target")] -fn target(&self) -> SingleValueMapper; +#[payable] +#[endpoint] +pub fn execute_with_optional_fee(&self) { + match self.call_value().single_optional() { + Some(payment) => { + // Process the payment as fee + self.execute_premium_service(payment); + }, + None => { + // Handle no payment scenario + self.execute_basic_service(); + } + } +} ``` -:::important -`BigUint` **type** is a big unsigned number, handled by the VM. There is no need to import any library, big number arithmetic is provided for all contracts out of the box. -::: -Normally, smart contract developers are used to dealing with raw bytes when storing or loading values from storage. The MultiversX framework for Rust smart contracts makes it far easier to manage the storage because it can handle typed values automatically. +### `array()` - Fixed-Size Payment Array +`self.call_value().array()` expects exactly N payments and returns them as a fixed-size array. Will halt execution if the number of payments doesn't match exactly. -### Extend init +```rust +#[payable] +#[endpoint] +pub fn swap(&self) { + // Expect exactly 2 payments for the swap + let [input_payment, fee_payment] = self.call_value().array(); + + require!( + input_payment.token_identifier != fee_payment.token_identifier, + "Input and fee must be different tokens" + ); + + self.execute_swap(input_payment, fee_payment); +} +``` -You will now instruct the `init` method to store the amount of tokens that should be gathered upon deployment. -The owner of a smart contract is the account that deployed it (you). By design, your Crowdfunding smart contract will send all the donated EGLD to its owner (you), assuming the target amount was reached. Nobody else has this privilege, because there is only one single owner of any given smart contract. +## Legacy Call Value Methods -Here's how the `init` method looks, with the code that saves the target: +The following methods are available for backwards compatibility but may be deprecated in future versions: -```Rust -#[init] -fn init(&self, target: BigUint) { - self.target().set(&target); -} -``` +- `self.call_value().egld_value()` retrieves the EGLD value transferred, or zero. Never stops execution. +- `self.call_value().all_esdt_transfers()` retrieves all the ESDT transfers received, or an empty list. Never stops execution. +- `self.call_value().multi_esdt()` is ideal when we know exactly how many ESDT transfers we expect. It returns an array of `EsdtTokenPayment`. It knows exactly how many transfers to expect based on the return type (it is polymorphic in the length of the array). Will fail execution if the number of ESDT transfers does not match. +- `self.call_value().single_esdt()` expects a single ESDT transfer, fails otherwise. Will return the received `EsdtTokenPayment`. It is a special case of `multi_esdt`, where `N` is 1. +- `self.call_value().single_fungible_esdt()` further restricts `single_esdt` to only fungible tokens, so those with their nonce zero. Returns the token identifier and amount, as pair. +- `self.call_value().egld_or_single_esdt()` retrieves an object of type `EgldOrEsdtTokenPayment`. Will halt execution in case of ESDT multi-transfer. +- `self.call_value().egld_or_single_fungible_esdt()` further restricts `egld_or_single_esdt` to fungible ESDT tokens. It will return a pair of `EgldOrEsdtTokenIdentifier` and an amount. +- `self.call_value().any_payment()` is the most general payment retriever. Never stops execution. Returns an object of type `EgldOrMultiEsdtPayment`. *(Deprecated since 0.64.1 - use `all()` instead)* -We have added an argument to the constructor method. It is called `target` and will need to be supplied when we deploy the contract. The argument then promptly gets saved to storage. +--- -Now note the `self.target()` invocation. This gives us an object that acts like a proxy for a part of the storage. Calling the `.set()` method on it causes the value to be saved to the contract storage. -:::important -All of the stored values end up in the storage if the transaction completes successfully. Smart contracts cannot access the protocol directly, it is the VM that intermediates everything. -::: +## Sending payments -Whenever you want to make sure your code is in order, run the build command: +We have seen how contracts can accommodate receiving tokens. Sending them is, in principle, even more straightforward, as it only involves specializing the `Payment` generic of the transaction using specific methods, essentially attaching a payload to a regular transaction. Read more about payments [here](../transactions/tx-payment.md). -```bash -sc-meta all build -``` +--- -There's one more thing: by default, none of the `fn` statements declare smart contract methods that are _externally callable_. All the data in the contract is publicly available, but it can be cumbersome to search through the contract storage manually. That is why it is often nice to make getters public, so people can call them to get specific data out. +### Staking smart contract -Public methods are annotated with either `#[endpoint]` or `#[view]`. There is currently no difference in functionality between them (but there might be at some point in the future). Semantically, `#[view]` indicates readonly methods, while `#[endpoint]` suggests that the method also changes the contract state. +## Introduction -```rust - #[view] - #[storage_mapper("target")] - fn target(&self) -> SingleValueMapper; -``` +This tutorial aims to teach you how to write a simple staking contract, and to illustrate and correct the common pitfalls new smart contract developers might fall into. -You can also think of `#[init]` as a special type of endpoint. +:::tip +If you find anything not answered here, feel free to ask further questions on the MultiversX Developers Telegram channel: [https://t.me/MultiversXDevelopers](https://t.me/MultiversXDevelopers) +::: -## Step 5: testing +## Prerequisites -You must always make sure that the code you write functions as intended. That's what **automated testing** is for. +:::important +Before starting this tutorial, make sure you have the following: -For now, this is how your contract looks: +- `stable` **Rust** version `≥ 1.85.0` (install via [rustup](/docs/developers/toolchain-setup.md#installing-rust-and-sc-meta)) +- `sc-meta` (install [multiversx-sc-meta](/docs/developers/meta/sc-meta-cli.md)) -```rust -#![no_std] +::: -#[allow(unused_imports)] -use multiversx_sc::imports::*; +For contract developers, we generally recommend [**VSCode**](https://code.visualstudio.com) with the following extensions: -#[multiversx_sc::contract] -pub trait Crowdfunding { - #[init] - fn init(&self, target: BigUint) { - self.target().set(&target); - } +- [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) +- [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) - #[upgrade] - fn upgrade(&self) {} - #[view] - #[storage_mapper("target")] - fn target(&self) -> SingleValueMapper; -} -``` +## Creating the contract -There are several ways to write [smart contract tests in Rust](/docs/developers/testing/rust/sc-test-overview.md). Now, we will focus on developing a test using [black-box calls](/docs/developers/testing/rust/sc-blackbox-calls.md). +Run the following command in the folder in which you want your smart contract to be created: -:::important -Blackbox tests execution imitates the blockchain with no access to private contract functions. -::: +```bash +sc-meta new --name staking-contract --template empty +``` -Let's write a test against the `init` method to make sure that it definitely stores the address of the owner under the `target` key at deployment. +Open VSCode, select **File > Open Folder**, and open the newly created `staking-contract` folder. -### Set up +## Building the contract -In the folder of the Crowdfunding smart contract, there is a folder called `tests/`. Inside it, create a new Rust file called `crowdfunding_blackbox_test.rs`. +In the terminal, run the following command to build the contract: -Your folder should look like this (output from the command `tree -L 2`): +```bash +sc-meta all build +``` + +After the building has completed, our folder should look like this: ```bash -. ├── Cargo.lock ├── Cargo.toml ├── meta @@ -41050,12 +61082,14 @@ Your folder should look like this (output from the command `tree -L 2`): │ └── src ├── multiversx.json ├── output -│ ├── crowdfunding.abi.json -│ ├── crowdfunding.imports.json -│ ├── crowdfunding.mxsc.json -│ └── crowdfunding.wasm +│ ├── staking-contract.abi.json +│ ├── staking-contract.imports.json +│ ├── staking-contract.mxsc.json +│ └── staking-contract.wasm +├── scenarios +│ └── staking_contract.scen.json ├── src -│ └── crowdfunding.rs +│ └── staking_contract.rs ├── target │ ├── CACHEDIR.TAG │ ├── debug @@ -41063,19274 +61097,20162 @@ Your folder should look like this (output from the command `tree -L 2`): │ ├── tmp │ └── wasm32-unknown-unknown ├── tests -│ └── crowdfunding_blackbox_test.rs +│ ├── staking_contract_scenario_go_test.rs +│ └── staking_contract_scenario_rs_test.rs └── wasm ├── Cargo.lock ├── Cargo.toml └── src ``` -Before creating the first test, we need to [set up the environment](/docs/developers/testing/rust/sc-test-setup.md). We will: - -1. Generate the smart contract's proxy; -2. Register the contract; -3. Set up accounts. - - -### Generate Proxy - -A smart contract's [proxy](/docs/developers/transactions/tx-proxies.md) is an object that mimics the contract. We will use the proxy to call the endpoints of the Crowdfunding contracts. - -The proxy contains entirely autogenerated code. However, before running the command to generate the proxy, we need to set up a configuration file. - -In the root of the contract, at the path `crowdfunding/`, we will create the configuration file `sc-config.toml`, where we will specify the path to generate the proxy: - -```toml title=sc-config.toml -[settings] +A new folder, called `output` was created, which contains the compiled contract code. More on this is used later. -[[proxy]] -path = "src/crowdfunding_proxy.rs" -``` -In the terminal, in the root of the contract, we will run the next command that will generate the proxy for the Crowdfunding smart contract: +## First lines of Rust -```bash -sc-meta all proxy -``` +Currently, we just have an empty contract. Not very useful, is it? So let's add some simple code for it. Since this is a staking contract, we would expect to have a `stake` function, right? -Once the proxy is generated, our work is not over yet. The next thing to do is to import the module in the Crowdfunding smart contract's code: +First, remove all the code in the `staking-contract/src/staking_contract.rs` file and replace it with this: ```rust #![no_std] -#[allow(unused_imports)] use multiversx_sc::imports::*; -pub mod crowdfunding_proxy; - #[multiversx_sc::contract] -pub trait Crowdfunding { - // Here is the implementation of the crowdfunding contract -} -``` - -With each build of the contract executed by the developer, the proxy will be automatically updated with the changes made to the contract. - - -### Register - -The Rust backend does not run compiled contracts, instead, it hooks the actual Rust contract code to its engine. You can find more [here](/docs/developers/testing/rust/sc-test-setup.md#registering-contracts). - -In order to link the smart contract code to the test you are developing, you need to call `register_contract()` in the setup function of the blackbox test. - -```rust -use crowdfunding::crowdfunding_proxy; -use multiversx_sc_scenario::imports::*; - -const CODE_PATH: MxscPath = MxscPath::new("output/crowdfunding.mxsc.json"); - -fn world() -> ScenarioWorld { - let mut blockchain = ScenarioWorld::new(); - - blockchain.set_current_dir_from_workspace("crowdfunding"); - blockchain.register_contract(CODE_PATH, crowdfunding::ContractBuilder); - blockchain -} -``` - -### Account - -The environment you're working in is a mocked blockchain. This means you have to create and manage accounts, allowing you to test and verify the behavior of your functions without deploying to a real blockchain. - -Here's an example to get started in `crowdfunding_blackbox_test.rs`: - -```rust -const OWNER: TestAddress = TestAddress::new("owner"); +pub trait StakingContract { + #[init] + fn init(&self) {} -#[test] -fn crowdfunding_deploy_test() { - let mut world = world(); + #[upgrade] + fn upgrade(&self) {} - world.account(OWNER).nonce(0).balance(1000000); + #[payable("EGLD")] + #[endpoint] + fn stake(&self) {} } ``` -In the snippet above, we've added only one account to the fictional universe of Crowdfunding smart contract. It is an account with the address `owner`, which the testing environment will use to pretend it's you. Note that in this fictional universe, your account nonce is `0` (meaning you've never used this account yet) and your `balance` is `1,000,000`. +Since we want this function to be callable by users, we have to annotate it with `#[endpoint]`. Also, since we want to be able to [receive a payment](/docs/developers/developer-reference/sc-payments.md#receiving-payments), we mark it also as `#[payable("EGLD)]`. For now, we'll use EGLD as our staking token. :::important -No transaction can start if that account does not exist in the mocked blockchain. More explanations can be found [here](/docs/developers/testing/rust/sc-test-setup.md#setting-accounts). +The contract **does NOT** need to be payable for it to receive payments on endpoint calls. The payable flag at contract level is only for receiving payments without endpoint invocation. ::: -### Deploy - -The purpose of the account created previously is to act as the owner of the Crowdfunding smart contract. To make this happen, the **OWNER** constant will serve as the transaction **sender**. +We need to see how much a user paid, and save their staking information in storage. ```rust -const CROWDFUNDING_ADDRESS: TestSCAddress = TestSCAddress::new("crowdfunding"); - -#[test] -fn crowdfunding_deploy_test() { - /* - Set up account - */ - - let crowdfunding_address = world - .tx() - .from(OWNER) - .typed(crowdfunding_proxy::CrowdfundingProxy) - .init(500_000_000_000u64) - .code(CODE_PATH) - .new_address(CROWDFUNDING_ADDRESS) - .returns(ReturnsNewAddress) - .run(); -} -``` - -The transaction above is a deploy call that stores in `target` value `500,000,000,000`. It was fictionally submitted by "you", using your account with the address `owner`. - -`.new_address(CROWDFUNDING_ADDRESS)` marks that the address of the deployed contracts will be the value stored in the **CROWDFUNDING_ADDRESS** constant. - -`.code(CODE_PATH)` explicitly sets the deployment Crowdfunding's code source as bytes. - -:::note -Deploy calls are specified by the code source. You can find more details about what data needs a transaction [here](/docs/developers/transactions/tx-data.md). -::: - -:::important -Remember to run `sc-meta all build` before running the test, especially if you made recent changes to the smart contract source code! Code source will be read directly from the file you specify through the **MxscPath** constant, without rebuilding it automatically. -::: - -### Checks +#![no_std] -What's the purpose of testing if we do not validate the behavior of the entities interacting with the blockchain? Let's take the next step by enhancing the `crowdfunding_deploy_test()` function to include verification operations. +use multiversx_sc::imports::*; -Once the deployment is executed, we will verify if: +#[multiversx_sc::contract] +pub trait StakingContract { + #[init] + fn init(&self) {} -- The **contract address** is **CROWDFUNDING_ADDRESS**; -- The **owner** has no less EGLD than the value with which it was initialized: `1,000,000`; -- `target` contains the value set at deployment: `500,000,000,000`. + #[upgrade] + fn upgrade(&self) {} -```rust -#[test] -fn crowdfunding_deploy_test() { - /* - Set up account - Deploy - */ + #[payable("EGLD")] + #[endpoint] + fn stake(&self) { + let payment_amount = self.call_value().egld().clone(); + require!(payment_amount > 0, "Must pay more than 0"); - assert_eq!(crowdfunding_address, CROWDFUNDING_ADDRESS.to_address()); + let caller = self.blockchain().get_caller(); + self.staking_position(caller.clone()).set(&payment_amount); + self.staked_addresses().insert(caller.clone()); + } - world.check_account(OWNER).balance(1_000_000); + #[view(getStakedAddresses)] + #[storage_mapper("stakedAddresses")] + fn staked_addresses(&self) -> UnorderedSetMapper; - world - .query() - .to(CROWDFUNDING_ADDRESS) - .typed(crowdfunding_proxy::CrowdfundingProxy) - .target() - .returns(ExpectValue(500_000_000_000u64)) - .run(); + #[view(getStakingPosition)] + #[storage_mapper("stakingPosition")] + fn staking_position(&self, addr: ManagedAddress) -> SingleValueMapper; } ``` -Notice that there are two accounts now, not just one. There's evidently the account `owner` and the new account `crowdfunding`, as a result of the deployment transaction. - -:::important -Smart contracts _are_ accounts in the MultiversX Network, accounts with associated code, which can be executed when transactions are sent to them. -::: +[`require!`](/docs/developers/developer-reference/sc-messages.md#require) is a macro that is a shortcut for `if !condition { signal_error(msg) }`. Signalling an error will terminate the execution and revert any changes made to the internal state, including token transfers from and to the smart contract. In this case, there is no reason to continue if the user did not pay anything. -The **owner's balance** remains unchanged - the deployment transaction did not cost anything, because the gas price is set to `0` in the **testing environment**. +We've also added [`#[view]`](/docs/developers/developer-reference/sc-annotations.md#endpoint-and-view) annotation for the storage mappers, allowing us to later perform queries on those storage entries. You can read more about annotations [here](/developers/developer-reference/sc-annotations/). -:::note -The `.check_account(OWNER)` method verifies whether an account exists at the specified address and checks its ownership details. Details available [here](/docs/developers/testing/rust/sc-blackbox-example.md#check-accounts). -::: +If you're confused about some of the functions used or the storage mappers, you can read more here: -:::note -The `.query()` method is used to interact with the smart contract's view functions via the proxy, retrieving information without modifying the blockchain state. +- [Smart Contract API Functions](/developers/developer-reference/sc-api-functions) +- [Storage Mappers](/developers/developer-reference/storage-mappers) -There is no caller, no payment, and gas price/gas limit. On the real blockchain, a smart contract query does not create a transaction on the blockchain, so no account is needed. Details available [here](/docs/developers/testing/rust/sc-blackbox-calls.md#query). -::: +Now, there is intentionally written some bad code here. Can you spot any improvements we could make? -## Run test +1. The last `clone()` from `stake()` function is unnecessary. If you're cloning variables all the time,s then you need to take some time to read the [ownership](https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html) chapter of the Rust book and also about the [implications of cloning](/developers/best-practices/biguint-operations) types from the Rust framework. -Do you want to try it out first? Go ahead and issue this command on your terminal at path `/crowdfunding`: +2. The `staking_position` does not need an owned value of the `addr` argument. We can pass a reference instead. -```bash -cargo test -``` +3. There is a logic error: what happens if an user stakes twice? Their position will be overwritten with the new value. Instead, we should add the new stake amount to their existing amount, using the [`update`](/docs/developers/developer-reference/storage-mappers.md#update) method. -If everything went well, you should see the following being printed: +After fixing these issues, we end up with the following code: ```rust -running 1 test -test crowdfunding_deploy_test ... ok - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s -``` - -You need to understand the contents of this blackbox test - again, the importance of testing your smart contracts cannot be overstated. - - -## Next up - -The tutorial will continue with defining of the `fund`, `claim` and `status` function, and will guide you through writing [blackbox tests](/docs/developers//testing/rust/sc-blackbox-calls.md) for them. - ---- - -### Signing programmatically - -In order to sign a transaction (or a message) using one of the SDKs, follow: - -- [Signing objects using **sdk-js**](/sdk-and-tools/sdk-js/sdk-js-cookbook#signing-objects) -- [Signing objects using **sdk-py**](/sdk-and-tools/sdk-py#signing-objects) - ---- - -### Signing Providers for dApps - -This page will guide you through the process of integrating the **sdk-js signing providers** in a dApp which isn't based on `sdk-dapp`. - -:::important -Note that for most purposes, **we recommend using [sdk-dapp](https://github.com/multiversx/mx-sdk-dapp)** instead of integrating the signing providers on your own. -::: +#![no_std] +use multiversx_sc::imports::*; -## Anatomy of a signing provider +#[multiversx_sc::contract] +pub trait StakingContract { + #[init] + fn init(&self) {} -Generally speaking, a signing provider is a component that supports the following use-cases: + #[upgrade] + fn upgrade(&self) {} -- **Log in (trivial flow, not recommended)**: the user of a dApp is asked her MultiversX identity. The user reaches the wallet, unlocks it, and confirms the login. The flow continues back to the dApp, which is now informed about the user's blockchain address. Note, though, that this piece of information is not authenticated: the dApp receives a _hint_ about the user's address, not a _guarantee_ (proof). Sometimes (though rarely), this is enough. If in doubt, always have your users login using the **native authentication** flow (see below). -- **Log in using native authentication (recommended)**: once the user decides to log in, the dApp crafts a special piece of data called _the native authentication initial part_ - a shortly-lived artifact that contains, among others, a marker of the originating dApp and a marker of the target Network. The user is given this piece of data and she is asked to sign it, to prove her MultiversX identity. The user then reaches the wallet, which unwraps and (partly) displays the payload of _the native authentication initial part_. The user unlocks the wallet and confirms the login - under the hood, the _part_ is signed with the user's secret key. The flow continues back to the dApp, which now receives the user's blockchain address, along with a proof (signature). Then, the dApp (e.g. maybe a server-side component) can verify the signature to make sure that the user is indeed the owner of the address. -- **Log out**: once the user decides to log out from the dApp, the latter should ask the wallet to do so. Once the user is signed out, the flow continues back to the dApp. -- **Sign transactions**: while interacting with the dApp, the user might be asked to sign one or more transactions. The user reaches the wallet, unlocks it again if necessary, and confirms the signing. The flow continues back to the dApp, which receives the signed transactions, ready to be broadcasted to the Network. -- **Sign messages**: while interacting with the dApp, the user might be asked to sign an arbitrary message. The user reaches the wallet, unlocks it again if necessary, and confirms the signing. The flow continues back to the dApp, which receives the signed message. + #[payable("EGLD")] + #[endpoint] + fn stake(&self) { + let payment_amount = self.call_value().egld().clone(); + require!(payment_amount > 0, "Must pay more than 0"); + let caller = self.blockchain().get_caller(); + self.staking_position(&caller) + .update(|current_amount| *current_amount += payment_amount); + self.staked_addresses().insert(caller); + } -## Implementations (available providers) + #[view(getStakedAddresses)] + #[storage_mapper("stakedAddresses")] + fn staked_addresses(&self) -> UnorderedSetMapper; -For MultiversX dApps, the following signing providers are available: + #[view(getStakingPosition)] + #[storage_mapper("stakingPosition")] + fn staking_position(&self, addr: &ManagedAddress) -> SingleValueMapper; +} +``` -- [Web wallet provider](https://github.com/multiversx/mx-sdk-js-web-wallet-provider), compatible with the [Web Wallet](/wallet/web-wallet) and [xAlias](/wallet/xalias) -- [Browser extension provider](https://github.com/multiversx/mx-sdk-js-extension-provider), compatible with the [MultiversX DeFi Wallet](/wallet/wallet-extension) -- [WalletConnect provider](https://github.com/multiversx/mx-sdk-js-wallet-connect-provider), compatible with [xPortal App](/wallet/xportal) -- [Hardware wallet provider](https://github.com/multiversx/mx-sdk-js-hw-provider), compatible with [Ledger](/wallet/ledger) -:::important -The code samples depicted on this page are written in JavaScript, and can also be found [on GitHub](https://github.com/multiversx/mx-sdk-js-examples/tree/main/signing-providers). There, the signing providers are integrated into a basic web page (for example purposes). -::: +### What's with the empty init and upgrade function? +Every smart contract must include a function annotated with [`#[init]`](/docs/developers/developer-reference/sc-annotations.md#init) and another with `#[upgrade]`. -## The Web Wallet Provider +The `init()` function is called when the contract is first deployed, while `upgrade()` is triggered during an upgrade. For now, we need no logic inside it, but we still need to have those functions. -:::note -Make sure you have a look over the [webhooks](/wallet/webhooks), in advance. -::: -[`@multiversx/sdk-web-wallet-provider`](https://github.com/multiversx/mx-sdk-js-web-wallet-provider) allows the users of a dApp to log in and sign data using the [Web Wallet](/wallet/web-wallet) or [xAlias](/wallet/xalias). +## Creating a wallet -:::important -Remember that [xAlias](/wallet/xalias) exposes **the same [URL hooks and callbacks](/wallet/webhooks)** as the [Web Wallet](/wallet/web-wallet). Therefore, integrating xAlias is **identical to integrating the Web Wallet** - with one trivial exception: the configuration of the URL base (see below). +:::note +You can skip this section if you already have a Devnet wallet setup. ::: -In order to create an instance of the provider that talks to the **Web Wallet**, do as follows: - -```js -import { WalletProvider, WALLET_PROVIDER_DEVNET } from "@multiversx/sdk-web-wallet-provider"; - -const provider = new WalletProvider(WALLET_PROVIDER_DEVNET); -``` - -Or, for **xAlias**: - -```js -import { WalletProvider, XALIAS_PROVIDER_DEVNET } from "@multiversx/sdk-web-wallet-provider"; +Open the terminal and run the following commands: -const provider = new WalletProvider(XALIAS_PROVIDER_DEVNET); +```sh +mkdir -p ~/MyTestWallets +sc-meta wallet new --format pem --outfile ~/MyTestWallets/tutorialKey.pem ``` -The following provider URLs [are defined](https://github.com/multiversx/mx-sdk-js-web-wallet-provider/blob/main/src/constants.ts) by the package: - -- `WALLET_PROVIDER_TESTNET`, `WALLET_PROVIDER_DEVNET`, `WALLET_PROVIDER_MAINNET` -- `XALIAS_PROVIDER_MAINNET`, `XALIAS_PROVIDER_DEVNET`, `XALIAS_PROVIDER_TESTNET` +## Deploy the contract -### Login and logout {#wallet-login-and-logout} +Now that we've created a wallet, it's time to deploy our contract. -Then, ask the user to log in: +:::important +Make sure you build the contract before deploying it. Open the terminal and run the following command from the contract root directory: -```js -const callbackUrl = encodeURIComponent("http://my-dapp"); -await provider.login({ callbackUrl }); +```bash +sc-meta all build ``` -Once the user opens her wallet, the web wallet issues a redirected back to `callbackUrl`, along with the **address** of the user. You can get the address as follows: +::: -```js -import qs from "qs"; +Once the contract is built, generate the interactor: -const queryString = window.location.search.slice(1); -const queryStringParams = qs.parse(queryString); -console.log(queryStringParams.address); +```bash +sc-meta all snippets ``` -In order to log out, do as follows: +Add the interactor to your project. In `staking-contract/Cargo.toml`, add `interactor` as a member of the workspace: -```js -const callbackUrl = window.location.href.split("?")[0]; -await provider.logout({ callbackUrl: callbackUrl }); +```toml +[workspace] +members = [ + ".", + "meta", + "interactor" # <- new member added +] ``` -Though, most often, the dApps (or their server-side components) want to **reliably assign an off-chain user identity to a MultiversX address**. For this, the signing providers support an extra parameter to the `login()` method: **the initial part of the native authentication token**. That piece of data, generally crafted with the aid of [`sdk-native-auth-client`](https://www.npmjs.com/package/@multiversx/sdk-native-auth-client), is signed with the user's wallet at login-time, and the signature is made available to the dApp, which, in turn, packs it into the actual **native authentication token**. - -:::important -We always recommend using the **native authentication** flow, instead of the trivial one. That is, always pass the `token` parameter to the `login()` method. This is applicable to all signing providers. -::: - -```js -import { NativeAuthClient } from "@multiversx/sdk-native-auth-client"; - -const nativeAuthClient = new NativeAuthClient({ ... }); -const nativeAuthInitialPart = await nativeAuthClient.initialize(); +Next, update the wallet path for sending transactions. In `staking-contract/interactor/src/interact.rs` locate the function `new(config: Config)` and **modify** the `wallet_address` variable with the [absolute path](https://www.redhat.com/en/blog/linux-path-absolute-relative) to your wallet: -const callbackUrl = encodeURIComponent("https://my-dapp/on-wallet-login"); -await provider.login({ callbackUrl, token: nativeAuthInitialPart }); +```rust +let wallet_address = interactor + .register_wallet( + Wallet::from_pem_file("/MyTestWallets/tutorialKey.pem").expect( + "Unable to load wallet. Please ensure the file exists.", + ), + ) + .await; ``` -Once the flow returns to the dApp, the `address` and `signature` parameters are available. The actual **native authentication token** is obtained upon an additional processing step - a re-packing, by means of `NativeAuthClient.getToken()`: +Finally, deploy the contract to Devnet: -```js -const address = queryStringParams.address; -const signature = queryStringParams.signature; -const nativeAuthToken = nativeAuthClient.getToken(address, nativeAuthInitialPart, signature); +```bash +cd interactor/ +cargo run deploy ``` +:::note +To use Testnet instead, update `gateway_uri` in `staking-contract/interactor/config.toml` to: `https://testnet-gateway.multiversx.com`. -### Signing transactions {#wallet-signing-transactions} - -Transactions can be signed as follows: +For mainnet, use: `https://gateway.multiversx.com`. -```js -import { Transaction } from "@multiversx/sdk-core"; +More details can be found [here](/developers/constants/). +::: -const firstTransaction = new Transaction({ ... }); -const secondTransaction = new Transaction({ ... }); +You're going to see an error like the following: -await provider.signTransactions( - [firstTransaction, secondTransaction], - { callbackUrl: callbackUrl } -); +```bash +error sending tx (possible API failure): transaction generation failed: insufficient funds for address erd1... +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` -Upon signing the transactions, the user is redirected back to `callbackUrl`, while the _query string_ contains information about the transactions, including their signatures. The information can be used to reconstruct `Transaction` objects using `getTransactionsFromWalletUrl()`: - -```js -const plainSignedTransactions = provider.getTransactionsFromWalletUrl(); -``` +This is because your account has no EGLD in it. -:::important -The following workaround is subject to change. -::: -As of January 2024, this signing provider returns the data field as a plain string. However, sdk-js' `Transaction.fromPlainObject()` expects it to be base64-encoded. Therefore, we need to apply a workaround (an additional conversion) on the results of `getTransactionsFromWalletUrl()`. +### Getting EGLD on Devnet -```js -for (const plainTransaction of plainSignedTransactions) { - const plainTransactionClone = structuredClone(plainTransaction); - plainTransactionClone.data = Buffer.from(plainTransactionClone.data).toString("base64"); - const transaction = Transaction.fromPlainObject(plainTransactionClone); +There are many ways of getting EGLD on Devnet: - // "transaction" can now be broadcasted. -} -``` +- Through the Devnet wallet; +- Using an external faucet; +- From the [MultiversX Builders Discord Server faucet](https://discord.gg/multiversxbuilders); +- By asking a team member on [Telegram](https://t.me/MultiversXDevelopers). -### Signing messages {#wallet-signing-messages} +#### Getting EGLD through devnet wallet -Messages can be signed as follows: +Go to [Devnet Wallet](https://devnet-wallet.multiversx.com) and login to your account using your PEM file. From the left side menu, select *Faucet*: -```js -import { SignableMessage } from "@multiversx/sdk-core"; +![img](/developers/staking-contract-tutorial-img/wallet_faucet.png) -const message = new SignableMessage({ message: "hello" }); -await provider.signMessage(message, { callbackUrl }); -``` +Request the tokens. After a few seconds you should have **5 xEGLD** in your wallet. -Upon signing the transactions, the user is redirected back to `callbackUrl`, while the _query string_ includes the signature of the message. The signature can be retrieved as follows: -```js -const signature = provider.getMessageSignatureFromWalletUrl(); -``` +#### Getting EGLD through external faucet +Go to [https://r3d4.fr/faucet](https://r3d4.fr/faucet) and submit a request: +![img](/developers/staking-contract-tutorial-img/external_faucet.png) -## The browser extension provider +Make sure you selected `Devnet` and input **your** address! -:::note -Make sure you have a look over [this page](/wallet/wallet-extension), in advance. -::: +It might take a little while, depending on how "busy" the faucet is. -[`@multiversx/sdk-js-extension-provider`](https://github.com/multiversx/mx-sdk-sdk-js-extension-provider) allows the users of a dApp to log in and sign transactions using the [MultiversX DeFi Wallet](/wallet/wallet-extension). -In order to acquire the instance (singleton) of the provider, do as follows: +### Deploying the contract, second try -```js -import { ExtensionProvider } from "@multiversx/sdk-extension-provider"; +Run the `deploy` command again and let's see the results: -const provider = ExtensionProvider.getInstance(); +```bash +sender's recalled nonce: 0 +-- tx nonce: 0 +sc deploy tx hash: 8a007... +deploy address: erd1qqqqqqqqqqqqq... +new address: erd1qqqqqqqqqqqqq... ``` -Before performing any operation, make sure to initialize the provider: +Alternatively, you can check the address in the logs tab on [Devnet Explorer](https://devnet-explorer.multiversx.com/transactions), namely the `SCDeploy` method. -```js -await provider.init(); -``` +#### Too much gas error? -### Login and logout {#extension-login-and-logout} +Everything should work just fine, but you'll see this message: +![img](/developers/staking-contract-tutorial-img/too_much_gas.png) -Then, ask the user to log in: +This is **not** an error. This simply means you provided way more gas than needed, so all the gas was consumed instead of the leftover being returned to you. -```js -const address = await provider.login(); +This is done to protect the network against certain attacks. For instance, one could always provide the maximum gas limit and only use very little, decreasing the network's throughput significantly. -console.log(address); -console.log(provider.account); -``` -In order to log out, do as follows: +## The first stake -```js -await provider.logout(); +Let's update the `stake` function from `staking-contract/interactor/src/interact.rs` to do the first stake. + +Initialize the `egld_amount` variable with `1` instead of `0`: + +```rust +let egld_amount = BigUint::::from(1u128); ``` -The `login()` method supports the `token` parameter, for **the native authentication flow** (always recommended, see above): +This variable represents the amount of EGLD to be sent with the transaction. -```js -const nativeAuthInitialPart = await nativeAuthClient.initialize(); -await provider.login({ token: nativeAuthInitialPart }); +Let's stake! At path `staking-contract/interactor` run the following command: -const address = provider.account.address; -const signature = provider.account.signature; -const nativeAuthToken = nativeAuthClient.getToken(address, nativeAuthInitialPart, signature); +```bash +cargo run stake ``` +We've now successfully staked 1 EGLD... or have we? -### Signing transactions {#extension-signing-transactions} +If we look at the transaction, that's not quite the case: -Transactions can be signed as follows: +![img](/developers/staking-contract-tutorial-img/first_stake.png) -```js -import { Transaction } from "@multiversx/sdk-core"; -const firstTransaction = new Transaction({ ... }); -const secondTransaction = new Transaction({ ... }); +### Why was a smaller amount of EGLD sent? -await provider.signTransactions([firstTransaction, secondTransaction]); +This happens because EGLD uses **18 decimals**. So, to send 1 EGLD, you actually need to send the value `1000000000000000000` (i.e. 1018). -// "firstTransaction" and "secondTransaction" can now be broadcasted. -``` +The blockchain works only with unsigned integers. Floating point numbers are not allowed. The only reason the explorer displays the balances with a floating point is because it's much more user-friendly to tell someone they have 1 EGLD instead of 1000000000000000000 EGLD, but internally, only the integer value is used. -### Signing messages {#extension-signing-messages} +### But how do I send 0.5 EGLD? -Arbitrary messages can be signed as follows: +Since we know EGLD has 18 decimals, we have to simply multiply 0.5 by 1018, which yields 500000000000000000. -```js -import { SignableMessage } from "@multiversx/sdk-core"; -const message = new SignableMessage({ - message: Buffer.from("hello") -}); +### Actually staking 1 EGLD -await provider.signMessage(message); +To stake 1 EGLD, simply update the `egld_amount` in the `stake` function from `staking-contract/interactor/src/interact.rs` with the following: -console.log(message.toJSON()); +```rust +let egld_amount = BigUint::::from(1000000000000000000u128); ``` +Now let's try staking again: + +![img](/developers/staking-contract-tutorial-img/second_stake.png) -## The WalletConnect provider -[`@multiversx/sdk-js-wallet-connect-provider`](https://github.com/multiversx/mx-sdk-js-wallet-connect-provider) allows the users of a dApp to log in and sign transactions using [xPortal](https://xportal.com/) (the mobile application). +## Performing contract queries -For this example we will use the WalletConnect 2.0 provider since 1.0 is no longer maintained and it is [deprecated](https://medium.com/walletconnect/weve-reset-the-clock-on-the-walletconnect-v1-0-shutdown-now-scheduled-for-june-28-2023-ead2d953b595) +To perform smart contract queries for the `getStakingPosition` view, update the `addr` variable in the `staking_position` function from `staking-contract/interactor/src/interact.rs` with the address of the wallet you used for the staking transaction: -First, let's see a (simple) way to build a QR dialog using [`qrcode`](https://www.npmjs.com/package/qrcode) (and bootstrap): +```rust +let addr = bech32::decode("erd1vx..."); +``` -```js -import QRCode from "qrcode"; +Query the staking position by running the following command in the terminal, inside the `staking-contract/interactor` directory: -async function openModal(connectorUri) { - const svg = await QRCode.toString(connectorUri, { type: "svg" }); +```bash +cargo run getStakingPosition +``` - // The referenced elements must be added to your page, in advance - $("#MyWalletConnectQRContainer").html(svg); - $("#MyWalletConnectModal").modal("show"); -} +This will show the staking amount according to the smart contract's internal state: -function closeModal() { - $("#MyWalletConnectModal").modal("hide"); -} +```bash +Result: 1000000000000000001 ``` -In order to create an instance of the provider, do as follows: +The result includes 1 EGLD plus the initial 10-18 EGLD that was sent. -```js -import { WalletConnectV2Provider } from "@multiversx/sdk-wallet-connect-provider"; +Next, query the **stakers list**. Replace the next line from `staked_addresses` **function** in `staking-contract/interactor/src/interact.rs`: -// Generate your own WalletConnect 2 ProjectId here: -// https://cloud.walletconnect.com/app -const projectId = "9b1a9564f91cb659ffe21b73d5c4e2d8"; -// The default WalletConnect V2 Cloud Relay -const relayUrl = "wss://relay.walletconnect.com"; -// T for Testnet, D for Devnet and 1 for Mainnet -const chainId = "T" +```rust +println!("Result: {result_value:?}"); +``` -const callbacks = { - onClientLogin: async function () { - // closeModal() is defined above - closeModal(); - const address = await provider.getAddress(); - console.log("Address:", address); - }, - onClientLogout: async function () { - console.log("onClientLogout()"); - }, - onClientEvent: async function (event) { - console.log("onClientEvent()", event); - } -}; +with the following: -const provider = new WalletConnectProvider(callbacks, chainId, relayUrl, projectId); +```rust +for result in result_value.iter() { + println!("Result: {}", Bech32Address::from(result).to_bech32_string()); +} ``` -:::note -You can customize the Core WalletConnect functionality by passing `WalletConnectProvider` an optional 5th parameter: `options` -For example `metadata` and `storage` for [React Native](https://docs.walletconnect.com/2.0/javascript/guides/react-native) or `{ logger: 'debug' }` for a detailed under the hood logging -::: +It is necessary to iterate through `result_value` because it is a [`MultiValueVec`](/docs/developers/data/multi-values.md#standard-multi-values) of `Address`. Each address is converted to `Bech32Address` to ensure it’s printed in Bech32 format, not as raw ASCII. -Before performing any operation, make sure to initialize the provider: +Run in terminal at path `staking-contract/interactor`: -```js -await provider.init(); +```bash +cargo run getStakedAddresses ``` +Running this function should yield a result like this: -### Login and logout {#walletconnect-login-and-logout} +```bash +Result: erd1vx... +``` -Then, ask the user to log in using xPortal on her phone: -```js -const { uri, approval } = await provider.connect(); -// connect will provide the uri required for the qr code display -// and an approval Promise that will return the connection session -// once the user confirms the login +## Adding unstake functionality -// openModal() is defined above -openModal(uri); +Currently, users can only stake, but they cannot actually get their EGLD back... at all. Let's add the unstake functionality in our smart contract: -// pass the approval Promise -await provider.login({ approval }); -``` +```rust +#[endpoint] +fn unstake(&self) { + let caller = self.blockchain().get_caller(); + let stake_mapper = self.staking_position(&caller); -The `login()` method supports the `token` parameter, for **the native authentication flow** (always recommended, see above): + let caller_stake = stake_mapper.get(); + if caller_stake == 0 { + return; + } -```js -const nativeAuthInitialPart = await nativeAuthClient.initialize(); -await provider.login({ approval, token: nativeAuthInitialPart }); + self.staked_addresses().swap_remove(&caller); + stake_mapper.clear(); -const address = await provider.getAddress(); -const signature = await provider.getSignature(); -const nativeAuthToken = nativeAuthClient.getToken(address, nativeAuthInitialPart, signature); + self.tx().to(caller).egld(caller_stake).transfer(); +} ``` -:::important -The pairing proposal between a wallet and a dapp is made using an [URI](https://docs.walletconnect.com/2.0/specs/clients/core/pairing/pairing-uri). In WalletConnect v2.0 the session and pairing are decoupled from each other. This means that a URI is shared to construct a pairing proposal, and only after settling the pairing the dapp can propose a session using that pairing. In simpler words, the dapp generates an URI that can be used by the wallet for pairing. -::: - -Once the user confirms the login, the `onClientLogin()` callback (declared above) is executed. - -In order to log out, do as follows: +You might notice the `stake_mapper` variable. Just to remind you, the mapper's definition looks like this: -```js -await provider.logout(); +```rust +#[storage_mapper("stakingPosition")] +fn staking_position(&self, addr: &ManagedAddress) -> SingleValueMapper; ``` +In Rust terms, this is a method of our contract trait, with one argument, that returns a [`SingleValueMapper`](/docs/developers/developer-reference/storage-mappers.md#singlevaluemapper). All [mappers](/docs/developers/developer-reference/storage-mappers.md) are nothing more than structure types that provide an interface to the storage API. -### Signing transactions {#walletconnect-signing-transactions} +So then, why save the mapper in a variable? -Transactions can be signed as follows: -```js -import { Transaction } from "@multiversx/sdk-core"; +### Better usage of storage mapper types -const firstTransaction = new Transaction({ ... }); -const secondTransaction = new Transaction({ ... }); +Each time you access `self.staking_position(&addr)`, the storage key has to be constructed again, by concatenating the static string `stakingPosition` with the given `addr` argument. The mapper saves its key internally, so if we reuse the same mapper, the key is only constructed once. -await provider.signTransactions([firstTransaction, secondTransaction]); +This saves us the following operations: -// "firstTransaction" and "secondTransaction" can now be broadcasted. +```rust +let mut key = ManagedBuffer::new_from_bytes(b"stakingPosition"); +key.append(addr.as_managed_buffer()); ``` -Alternatively, one can sign a single transaction using the method `signTransaction()`. - +Instead, we just reuse the key we built previously. This can be a great performance enhancement, especially for mappers with multiple arguments. For mappers with no arguments, the improvement is minimal, but might still be worth thinking about. -### Signing messages {#walletconnect-signing-messages} -Arbitrary messages can be signed as follows: +### Partial unstake -```js -import { SignableMessage } from "@multiversx/sdk-core"; +Some users might only want to unstake a part of their tokens, so we could simply add an `unstake_amount` argument: -const message = new SignableMessage({ - message: Buffer.from("hello") -}); +```rust +#[endpoint] +fn unstake(&self, unstake_amount: BigUint) { + let caller = self.blockchain().get_caller(); + let remaining_stake = self.staking_position(&caller).update(|staked_amount| { + require!( + unstake_amount > 0 && unstake_amount <= *staked_amount, + "Invalid unstake amount" + ); + *staked_amount -= &unstake_amount; -await provider.signMessage(message); + staked_amount.clone() + }); + if remaining_stake == 0 { + self.staked_addresses().swap_remove(&caller); + } -console.log(message.toJSON()); + self.tx().to(caller).egld(unstake_amount).transfer(); +} ``` +As you might notice, the code changed quite a bit: -## The hardware wallet provider +1. To handle invalid user input, we used the `require!` statement; +2. Previously, we used `clear` to reset the staking position. However, now that we need to modify the stored value, we use the `update` method, which allows us to change the currently stored value through a mutable reference. -:::note -Make sure you have a look over [this page](/wallet/ledger), in advance. -::: +[`update`](/docs/developers/developer-reference/storage-mappers.md#update) is the same as doing [`get`](/docs/developers/developer-reference/storage-mappers.md#get), followed by computation, and then [`set`](/docs/developers/developer-reference/storage-mappers.md#set), but it's just a lot more compact. Additionally, it also allows us to return anything we want from the given closure, so we use that to detect if this was a full unstake. -[`@multiversx/sdk-hw-provider`](https://github.com/multiversx/mx-sdk-js-hw-provider) allows the users of a dApp to log in and sign transactions using a [Ledger device](/wallet/ledger). -In order to create an instance of the provider, do as follows: +### Optional arguments -```js -import { HWProvider } from "@multiversx/sdk-hw-provider"; +For a bit of performance enhancement, we could have the `unstake_amount` as an optional argument, with the default being full unstake. -const provider = new HWProvider(); -``` +```rust +#[endpoint] +fn unstake(&self, opt_unstake_amount: OptionalValue) { + let caller = self.blockchain().get_caller(); + let stake_mapper = self.staking_position(&caller); + let unstake_amount = match opt_unstake_amount { + OptionalValue::Some(amt) => amt, + OptionalValue::None => stake_mapper.get(), + }; -Before performing any operation, make sure to initialize the provider (also, the MultiversX application has to be open on the device): + let remaining_stake = stake_mapper.update(|staked_amount| { + require!( + unstake_amount > 0 && unstake_amount <= *staked_amount, + "Invalid unstake amount" + ); + *staked_amount -= &unstake_amount; -```js -await provider.init(); -``` + staked_amount.clone() + }); + if remaining_stake == 0 { + self.staked_addresses().swap_remove(&caller); + } + self.tx().to(caller).egld(unstake_amount).transfer(); +} +``` -### Login +This makes it so if someone wants to perform a full unstake, they can simply not give the argument at all. -Before asking the user to log in using the Ledger, you may want to get all the available addresses on the device, display them, and let the user choose one of them: -```js -const addresses = await provider.getAccounts(); -console.log(addresses); -``` +### Unstaking our Devnet tokens -The login looks like this: +Now that the unstake function has been added, let's test it out on Devnet. Build the smart contract again. In the contract root (`staking-contract/`), run the next command: -```js -const chosenAddressIndex = 3; -await provider.login({ addressIndex: chosenAddressIndex }); -alert(`Logged in. Address: ${await provider.getAddress()}`); +```bash +sc-meta all build ``` -Alternatively, in order to select a specific address on the device after login, call `setAddressIndex()`: +After building the contract, regenerate the interactor by running the following command in the terminal, also in the contract root: -```js -const addressIndex = 3; -await provider.setAddressIndex(addressIndex); -console.log(`Address has been set: ${await provider.getAddress()}.`); +```bash +sc-meta all snippets ``` -The Ledger provider does not support a _logout_ operation per se (not applicable in this context). +:::warning +Make sure `wallet_address` stores the wallet that has to execute the transactions. +::: -The **the native authentication flow** (always recommended, see above) is supported using the `tokenLogin()` method: +Let's unstake some EGLD! Replace variable `opt_unstake_amount` from `unstake` function in `staking-contract/interactor/src/interact.rs` with: -```js -const nativeAuthInitialPart = await nativeAuthClient.initialize(); -const nativeAuthInitialPartAsBuffer = Buffer.from(nativeAuthInitialPart); -const { address, signature } = await provider.tokenLogin({ addressIndex: 0, token: nativeAuthInitialPartAsBuffer }); +```rust +let opt_unstake_amount = OptionalValue::Some(BigUint::::from(500000000000000000u128)); +``` -const nativeAuthToken = nativeAuthClient.getToken(address, nativeAuthInitialPart, signature.toString("hex")); +Then, run in terminal at path `staking-contract/interactor`: + +```bash +cargo run unstake ``` +Now run this function, and you'll get this result: -### Signing transactions {#hw-signing-transactions} +![img](/developers/staking-contract-tutorial-img/first_unstake.png) -Transactions can be signed as follows: +...but why? We just added the function! -```js -import { Transaction } from "@multiversx/sdk-core"; +Well, we might've added it to our code, but the contract on the Devnet still has our old code. So, how do we upload our new code? -const firstTransaction = new Transaction({ ... }); -const secondTransaction = new Transaction({ ... }); -await provider.signTransactions([firstTransaction, secondTransaction]); +## Upgrading smart contracts -// "firstTransaction" and "secondTransaction" can now be broadcasted. -``` +Since we've added some new functionality, we also want to update the currently deployed implementation. **Build the contract** and then run the following command at path `staking-contract/interactor`: -Alternatively, one can sign a single transaction using the method `signTransaction()`. +```bash +cargo run upgrade +``` +:::note Attention required +All the storage is kept on upgrade, so make sure any storage changes you make to storage mapping are **backwards compatible**! +::: -### Signing messages {#hw-signing-messages} -Arbitrary messages can be signed as follows: +## Try unstaking again -```js -import { SignableMessage } from "@multiversx/sdk-core"; +Run the `unstake` snippet again. This time, it should work just fine. Afterwards, let's query our staked amount through `getStakingPosition`, to see if it updated our amount properly. -const message = new SignableMessage({ - message: Buffer.from("hello") -}); +:::warning +Make sure that function `staking_position` has the changes previously made. +::: -await provider.signMessage(message); +```bash +cargo run getStakingPosition +``` -console.log(message.toJSON()); +```bash +Result: 500000000000000001 ``` +We had 1 EGLD, and we unstaked 0.5 EGLD. Now, we have 0.5 EGLD staked. (with the extra 1 fraction of EGLD we've staked initially). -## The Web Wallet Cross Window Provider -[`@multiversx/sdk-web-wallet-cross-window-provider`](https://github.com/multiversx/mx-sdk-js-web-wallet-cross-window-provider) allows the users of a dApp to log in and sign transactions using the MultiversX Web Wallet. +### Unstake with no arguments -In order to acquire the instance (singleton) of the provider, do as follows: +Now let’s test how the contract behaves when no amount is explicitly provided. This will confirm that the `OptionalValue::None` path works as expected. -```js -import { CrossWindowProvider } from "@multiversx/sdk-web-wallet-cross-window-provider"; +In `staking-contract/interactor/src/interact.rs`, update the `unstake` function. Replace the `opt_unstake_amount` variable with: -const provider = CrossWindowProvider.getInstance(); +```rust +let opt_unstake_amount: OptionalValue> = OptionalValue::None; ``` -Before performing any operation, make sure to initialize the provider and web wallet address: +And then unstake: -```js -await provider.init(); -provider.setWalletUrl("https://wallet.multiversx.com"); +```bash +cargo run unstake ``` +After unstaking, query `stakingPosition` and `stakedAddresses` to see if the state was cleaned up properly: -### Login and logout {#cross-window-login-and-logout} +These should show that: -Then, ask the user to log in: +- Your staking position is now empty: 0 EGLD; +- Your address has been removed from the stakers list. -```js -const address = await provider.login(); -console.log(address); -console.log(provider.account); -``` +## Writing tests -In order to log out, do as follows: +As you might've noticed, it can be quite a chore to keep upgrading the contract after every little change, especially if all we want to do is test a new feature. So, let's recap what we've done until now: -```js -await provider.logout(); -``` +- deploy our contract +- stake +- partial unstake +- full unstake -The `login()` method supports the `token` parameter, for **the native authentication flow** (always recommended, see above): +:::tip +A more detailed explanation of Rust tests can be found [here](https://docs.multiversx.com/developers/testing/rust/sc-test-overview/). +::: + +Before developing the tests, you will have to generate the contract's [proxy](/docs/developers/transactions/tx-proxies.md). + +You will add to `staking-contract/sc-config.toml`: + +```toml +[[proxy]] +path = "tests/staking_contract_proxy.rs" +``` -```js -const nativeAuthInitialPart = await nativeAuthClient.initialize(); -await provider.login({ token: nativeAuthInitialPart }); +Then run in terminal at path `staking-contract/`: -const address = provider.account.address; -const signature = provider.account.signature; -const nativeAuthToken = nativeAuthClient.getToken(address, nativeAuthInitialPart, signature); +```bash +sc-meta all proxy ``` +You’ll find the contract’s proxy in `staking-contract/tests`, inside the file `staking_contract_proxy.rs`. This proxy will be used to help us write and run tests for the smart contract. -### Signing transactions {#cross-window-signing-transactions} - -Transactions can be signed as follows: +To test the previously described scenario, we're going to need a user address, and a new test function. -```js -import { Transaction } from "@multiversx/sdk-core"; +**Create** file `staking_contract_blackbox_test.rs` in `staking-contract/tests` with the following: -const firstTransaction = new Transaction({ ... }); -const secondTransaction = new Transaction({ ... }); +```rust +mod staking_contract_proxy; +use multiversx_sc::{ + imports::OptionalValue, + types::{TestAddress, TestSCAddress}, +}; +use multiversx_sc_scenario::imports::*; -await provider.signTransactions([firstTransaction, secondTransaction]); +const OWNER_ADDRESS: TestAddress = TestAddress::new("owner"); +const STAKING_CONTRACT_ADDRESS: TestSCAddress = TestSCAddress::new("staking-contract"); +const USER_ADDRESS: TestAddress = TestAddress::new("user"); +const WASM_PATH: MxscPath = MxscPath::new("output/staking-contract.mxsc.json"); +const USER_BALANCE: u64 = 1_000_000_000_000_000_000; -// "firstTransaction" and "secondTransaction" can now be broadcasted. -``` +struct ContractSetup { + pub world: ScenarioWorld, +} +impl ContractSetup { + pub fn new() -> Self { + let mut world = ScenarioWorld::new(); + world.set_current_dir_from_workspace("staking-contract"); + world.register_contract(WASM_PATH, staking_contract::ContractBuilder); -### Signing messages {#cross-window-signing-messages} + world.account(OWNER_ADDRESS).nonce(1).balance(0); + world.account(USER_ADDRESS).nonce(1).balance(USER_BALANCE); -Arbitrary messages can be signed as follows: + // simulate deploy + world + .tx() + .from(OWNER_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .init() + .code(WASM_PATH) + .new_address(STAKING_CONTRACT_ADDRESS) + .run(); -```js -import { SignableMessage } from "@multiversx/sdk-core"; + ContractSetup { world } + } +} -const message = new SignableMessage({ - message: Buffer.from("hello") -}); +#[test] +fn stake_unstake_test() { + let mut setup = ContractSetup::new(); -await provider.signMessage(message); + setup + .world + .check_account(USER_ADDRESS) + .balance(USER_BALANCE); + setup.world.check_account(OWNER_ADDRESS).balance(0); -console.log(message.toJSON()); -``` + // stake full + setup + .world + .tx() + .from(USER_ADDRESS) + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .stake() + .egld(USER_BALANCE) + .run(); + setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staking_position(USER_ADDRESS) + .returns(ExpectValue(USER_BALANCE)) + .run(); -## The Metamask Proxy Provider + setup.world.check_account(USER_ADDRESS).balance(0); + setup + .world + .check_account(STAKING_CONTRACT_ADDRESS) + .balance(USER_BALANCE); -[`@multiversx/sdk-metamask-proxy-provider`](https://github.com/multiversx/mx-sdk-js-metamask-proxy-provider) allows the users of a dApp to log in and sign transactions using the Metamask wallet by using MultiversX Web Wallet as a proxy widget in iframe. + // unstake partial + setup + .world + .tx() + .from(USER_ADDRESS) + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .unstake(OptionalValue::Some(USER_BALANCE / 2)) + .run(); -In order to acquire the instance (singleton) of the provider, do as follows: + setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staking_position(USER_ADDRESS) + .returns(ExpectValue(USER_BALANCE / 2)) + .run(); -```js -import { MetamaskProxyProvider } from "@multiversx/sdk-metamask-proxy-provider"; + setup + .world + .check_account(USER_ADDRESS) + .balance(USER_BALANCE / 2); + setup + .world + .check_account(STAKING_CONTRACT_ADDRESS) + .balance(USER_BALANCE / 2); -const provider = MetamaskProxyProvider.getInstance(); -``` + // unstake full + setup + .world + .tx() + .from(USER_ADDRESS) + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .unstake(OptionalValue::None::) + .run(); -Before performing any operation, make sure to initialize the provider and metamask snap web wallet address: + setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staking_position(USER_ADDRESS) + .returns(ExpectValue(0u8)) + .run(); -```js -await provider.init(); -provider.setWalletUrl("https://snap-wallet.multiversx.com"); + setup + .world + .check_account(USER_ADDRESS) + .balance(USER_BALANCE); + setup + .world + .check_account(STAKING_CONTRACT_ADDRESS) + .balance(0); +} ``` +We've added a `USER_ADDRESS` constant, which is initialized with `USER_BALANCE` EGLD in their account. -### Login and logout {#metamask-proxy-login-and-logout} +:::note +For the test we're going to use small numbers for balances, since there is no reason to work with big numbers. For this test, we're using 1 EGLD for user balance. +::: -Then, ask the user to log in: +Then, we've staked the user's entire balance, unstaked half, then unstaked fully. After each transaction, we've checked the smart contract's internal staking storage, and also the balance of the user and the smart contract respectively. -```js -const address = await provider.login(); -console.log(address); -console.log(provider.account); -``` +### Running the test -In order to log out, do as follows: +To run a test, you can use click on the `Run Test` button from under the test name. -```js -await provider.logout(); -``` +![img](/developers/staking-contract-tutorial-img/running_rust_test.png) -The `login()` method supports the `token` parameter, for **the native authentication flow** (always recommended, see above): +There is also a `Debug` button, which can be used to debug smart contracts. More details on that [here](/developers/testing/sc-debugging/). -```js -const nativeAuthInitialPart = await nativeAuthClient.initialize(); -await provider.login({ token: nativeAuthInitialPart }); +Alternatively, you can run all the tests in the file by running the following command in the terminal, in the `staking-contract/` folder: -const address = provider.account.address; -const signature = provider.account.signature; -const nativeAuthToken = nativeAuthClient.getToken(address, nativeAuthInitialPart, signature); +```bash +sc-meta test ``` -### Signing transactions {#metamask-proxy-signing-transactions} +## Staking Rewards -Transactions can be signed as follows: +Right now, there is no incentive to stake EGLD in this smart contract. Let's say we want to give every staker 10% APY (Annual Percentage Yield). For example, if someone staked 100 EGLD, they will receive a total of 10 EGLD per year. -```js -import { Transaction } from "@multiversx/sdk-core"; +For this, we're also going to need to save the time at which each user staked. Also, we can't simply make each user wait one year to get their rewards. We need a more fine-tuned solution, so we're going to calculate rewards per block instead of per year. -const firstTransaction = new Transaction({ ... }); -const secondTransaction = new Transaction({ ... }); +:::tip +You can also use rounds, timestamp, epochs etc. for time keeping in smart contracts, but number of blocks is the recommended approach. +::: -await provider.signTransactions([firstTransaction, secondTransaction]); -// "firstTransaction" and "secondTransaction" can now be broadcasted. -``` +### User-defined struct types +A single `BigUint` for each user is not enough anymore. As stated before, we need to also store the stake block, and we need to update this block number on every action. -### Signing messages {#metamask-proxy-signing-messages} +So, we're going to use a struct: -Arbitrary messages can be signed as follows: +```rust +pub struct StakingPosition { + pub stake_amount: BigUint, + pub last_action_block: u64, +} +``` -```js -import { SignableMessage } from "@multiversx/sdk-core"; +:::note +Every managed type from the SpaceCraft needs a `ManagedTypeApi` implementation, which allows it to access the VM functions for performing operations. For example, adding two `BigUint` numbers, concatenating two `ManagedBuffers`, etc. Inside smart contract code, the `ManagedTypeApi` associated type is automatically added, but outside of it, we have to manually specify it. +::: -const message = new SignableMessage({ - message: Buffer.from("hello") -}); -await provider.signMessage(message); +Additionally, since we need to store this in storage, we need to tell the Rust framework how to encode and decode this type. This can be done automatically by deriving (i.e. auto-implementing) these traits, via the `#[derive]` annotation: -console.log(message.toJSON()); +```rust +use multiversx_sc::derive_imports::*; + +#[type_abi] +#[derive(TopEncode, TopDecode, PartialEq, Debug)] +pub struct StakingPosition { + pub stake_amount: BigUint, + pub last_action_block: u64, +} ``` +We've also added `#[type_abi]`, since this is required for ABI generation. ABIs are used by decentralized applications and such to decode custom smart contract types, but this is out of scope of this tutorial. -## Verifying the signature of a login token +Additionally, we've added `PartialEq` and `Debug` derives, for easier use within tests. This will not affect performance in any way, as the code for these is only used during testing/debugging. `PartialEq` allows us to use `==` for comparing instances, while `Debug` will pretty-print the struct, field by field, in case of errors. -:::note -It's recommended to use the libraries [`sdk-native-auth-client`](https://www.npmjs.com/package/@multiversx/sdk-native-auth-client) and [`sdk-native-auth-server`](https://www.npmjs.com/package/@multiversx/sdk-native-auth-server) to handle the **native authentication** flow. -::: +If you want to learn more about how such a structure is encoded, and the difference between top and nested encoding/decoding, you can read more [here](/developers/data/serialization-overview). -Please follow [this](https://github.com/multiversx/mx-sdk-js-native-auth-server) for more details about verifying the signature of a login token (i.e. within a server-side component of the dApp). ---- +### Rewards formula -### Signing Transactions +A block is produced about every 6 seconds, so total blocks in a year would be seconds in year, divided by 6: -By reading this page you will find out how to serialize and sign the Transaction payload. +```rust +pub const BLOCKS_IN_YEAR: u64 = 60 * 60 * 24 * 365 / 6; +``` -Transactions must be **signed** with the Sender's Private Key before submitting them to the MultiversX Network. Signing is performed with the [Ed25519](https://ed25519.cr.yp.to/) algorithm. +More specifically: *60 seconds per minute* x *60 minutes per hour* x *24 hours per day* x *365 days*, divided by the 6-second block duration. +:::note +This is calculated and replaced with the exact value at compile time, so there is no performance penalty of having a constant with mathematical operations in its value definition. +::: -## **General structure** -An _unsigned transaction_ has the following fields: +Having defined this constant, rewards formula should look like this: -| Field | Type | Required | Description | -| ---------- | ------ | ------------------ | ------------------------------------------------------------------------------ | -| `nonce` | number | Yes | The account sequence number | -| `value` | string | Yes (can be `"0"`) | The value to transfer, represented in atomic units:`EGLD` times `denomination` | -| `receiver` | string | Yes | The address of the receiver (bech32 format) | -| `sender` | string | Yes | The address of the sender (bech32 format) | -| `gasPrice` | number | Yes | The gas price to be used in the scope of the transaction | -| `gasLimit` | number | Yes | The maximum number of gas units allocated for the transaction | -| `data` | string | No | Arbitrary information about the transaction, **base64-encoded**. | -| `chainID` | string | Yes | The chain identifier. | -| `version` | number | Yes | The version of the transaction (e.g. `1`). | +```rust +let reward_amt = apy / 100 * user_stake * blocks_since_last_claim / BLOCKS_IN_YEAR; +``` -A signed transaction has the additional **`signature`** field: +Using 10% as the APY, and assuming exactly one year has passed since last claim. -| Field | Type | Description | -| --------- | ------ | ---------------------------------------------------------------------------------------------- | -| signature | string | The digital signature consisting of 128 hex-characters (thus 64 bytes in a raw representation) | +In this case, the reward should be calculated as: `10/100 * user_stake`, which is exactly 10% APY. +However, there is something wrong with the current formula. We will always get `reward_amt` = 0. -## **Serialization for signing** -Before signing a transaction, one has to **serialize** it, that is, to obtain its raw binary representation - as a sequence of bytes. This is achieved through the following steps: +### BigUint division -1. order the fields of the transaction with respect to their appearance order in the table above (`nonce` is first, `version` is last). -2. discard the `data` field if it's empty. -3. convert the `data` payload to its **base64** representation. -4. obtain a JSON representation (UTF-8 string) of the transaction, maintaining the order of the fields. This JSON representation must contain **no indentation** and **no separating spaces**. -5. encode the resulted JSON (UTF-8) string as a sequence of bytes. +BigUint division works the same as unsigned integer division. If you divide `x` by `y`, where `x < y`, you will always get `0` as result. So in our previous example, 10/100 is **NOT** `0.1`, but `0`. -For example, given the transaction: +To fix this, we need to take care of our operation order: -```js -nonce = 7 -value = "10000000000000000000" # 10 EGLD -receiver = "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r" -sender = "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz" -gasPrice = 1000000000 -gasLimit = 70000 -data = "for the book" -chainID = "1" -version = 1 +```rust +let reward_amt = user_stake * apy / 100 * blocks_since_last_claim / BLOCKS_IN_YEAR; ``` -By applying steps 1-3 (step 4 is omitted in this example), one obtains: -```js -{"nonce":7,"value":"10000000000000000000","receiver":"erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r","sender":"erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz","gasPrice":1000000000,"gasLimit":70000,"data":"Zm9yIHRoZSBib29r","chainID":"1","version":1} -``` +### How to express percentages like 50.45%? -If the transaction has an empty **no data field**: +In this case, we need to extend our precision by using fixed point precision. Instead of having `100` as the maximum percentage, we will extend it to `100_00`, and give `50.45%` as `50_45`. Updating our above formula results in this: -```js -nonce = 8 -value = "10000000000000000000" # 10 ERD -receiver = "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r" -sender = "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz" -gasPrice = 1000000000 -gasLimit = 50000 -data = "" -chainID = "1" -version = 1 +```rust +pub const MAX_PERCENTAGE: u64 = 100_00; + +let reward_amt = user_stake * apy / MAX_PERCENTAGE * blocks_since_last_claim / BLOCKS_IN_YEAR; ``` -Then it's serialized form (step 5 is omitted in this example) is as follows: +For example, let's assume the user stake is 100, and 1 year has passed. Using `50_45` as APY value, the formula would become: -```js -{"nonce":8,"value":"10000000000000000000","receiver":"erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r","sender":"erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz","gasPrice":1000000000,"gasLimit":50000,"chainID":"1","version":1} +```rust +reward_amt = 100 * 50_45 / 100_00 = 5045_00 / 100_00 = 50 ``` +:::note +Since we're still using the BigUint division, we don't get `50.45`, but `50`. This precision can be increased by using more zeroes for the `MAX_PERCENTAGE` and the respective APY, but this is also "inherently fixed" on the blockchain because we work with very big numbers for `user_stake`. +::: -## **Ed25519 signature** -MultiversX uses the [Ed25519](https://ed25519.cr.yp.to/) algorithm to sign transactions. In order to obtain the signature, one can use generic software libraries such as [PyNaCl](https://pynacl.readthedocs.io/en/stable/signing/), [tweetnacl-js](https://github.com/dchest/tweetnacl-js#signatures) or components of MultiversX SDK such as [mx-sdk-js-core](https://github.com/multiversx/mx-sdk-js-core), [mx-sdk-py](https://github.com/multiversx/mx-sdk-py), [sdk-go](https://github.com/multiversx/mx-sdk-go), [mxjava](https://github.com/multiversx/mx-sdk-java) etc. +## Rewards functionality -The raw signature consisting of 64 bytes has to be **hex-encoded** afterwards and placed in the transaction object. +### Implementation -## **Ready to broadcast** +Now let's see how this would look in our Rust smart contract code. The smart contract looks like this after doing all the specified changes: -Once the `signature` field is set as well, the transaction is ready to be broadcasted. Following the examples above, their ready-to-broadcast form is as follows: +```rust +#![no_std] -```js -# With data field -nonce = 7 -value = "10000000000000000000" # 10 EGLD -receiver = "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r" -sender = "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz" -gasPrice = 1000000000 -gasLimit = 70000 -data = "Zm9yIHRoZSBib29r" -chainID = "1" -version = 1 -signature = "1702bb7696f992525fb77597956dd74059b5b01e88c813066ad1f6053c6afca97d6eaf7039b2a21cccc7d73b3e5959be4f4c16f862438c7d61a30c91e3d16c01" -``` +use multiversx_sc::derive_imports::*; +use multiversx_sc::imports::*; -```js -# Without data field -nonce = 8 -value = "10000000000000000000" # 10 EGLD -receiver = "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r" -sender = "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz" -gasPrice = 1000000000 -gasLimit = 50000 -data = "" -chainID = "1" -version = 1 -signature = "4a6d8186eae110894e7417af82c9bf9592696c0600faf110972e0e5310d8485efc656b867a2336acec2b4c1e5f76c9cc70ba1803c6a46455ed7f1e2989a90105" -``` +pub const BLOCKS_IN_YEAR: u64 = 60 * 60 * 24 * 365 / 6; +pub const MAX_PERCENTAGE: u64 = 10_000; ---- +#[type_abi] +#[derive(TopEncode, TopDecode, PartialEq, Debug)] +pub struct StakingPosition { + pub stake_amount: BigUint, + pub last_action_block: u64, +} -### Simple Values +#[multiversx_sc::contract] +pub trait StakingContract { + #[init] + fn init(&self, apy: u64) { + self.apy().set(apy); + } -We will start by going through the basic types used in smart contracts: + #[upgrade] + fn upgrade(&self) {} -- Fixed-width numbers -- Arbitrary width (big) numbers -- Boolean values + #[payable("EGLD")] + #[endpoint] + fn stake(&self) { + let payment_amount = self.call_value().egld().clone(); + require!(payment_amount > 0, "Must pay more than 0"); + let caller = self.blockchain().get_caller(); + self.staking_position(&caller).update(|staking_pos| { + self.claim_rewards_for_user(&caller, staking_pos); -### Fixed-width numbers + staking_pos.stake_amount += payment_amount + }); + self.staked_addresses().insert(caller); + } -Small numbers can be stored in variables of up to 64 bits. We use big endian encoding for all numbers in our projects. + #[endpoint] + fn unstake(&self, opt_unstake_amount: OptionalValue) { + let caller = self.blockchain().get_caller(); + let stake_mapper = self.staking_position(&caller); + let mut staking_pos = stake_mapper.get(); -**Rust types**: `u8`, `u16`, `u32`, `usize`, `u64`, `i8`, `i16`, `i32`, `isize`, `i64`. + let unstake_amount = match opt_unstake_amount { + OptionalValue::Some(amt) => amt, + OptionalValue::None => staking_pos.stake_amount.clone(), + }; + require!( + unstake_amount > 0 && unstake_amount <= staking_pos.stake_amount, + "Invalid unstake amount" + ); -**Top-encoding**: The same as for all numerical types, the minimum number of bytes that -can fit their 2's complement, big endian representation. + self.claim_rewards_for_user(&caller, &mut staking_pos); + staking_pos.stake_amount -= &unstake_amount; -**Nested encoding**: Fixed width big endian encoding of the type, using 2's complement. + if staking_pos.stake_amount > 0 { + stake_mapper.set(&staking_pos); + } else { + stake_mapper.clear(); + self.staked_addresses().swap_remove(&caller); + } -:::important -A note about the types `usize` and `isize`: these Rust-specific types have the width of the underlying architecture, -i.e. 32 on 32-bit systems and 64 on 64-bit systems. However, smart contracts always run on a wasm32 architecture, so -these types will always be identical to `u32` and `i32` respectively. -Even when simulating smart contract execution on 64-bit systems, they must still be serialized on 32 bits. -::: + self.tx().to(caller).egld(unstake_amount).transfer(); + } -**Examples** + #[endpoint(claimRewards)] + fn claim_rewards(&self) { + let caller = self.blockchain().get_caller(); + let stake_mapper = self.staking_position(&caller); -| Type | Number | Top-level encoding | Nested encoding | -| -| ------- | --------------------- | -------------------- | -------------------- | -| `u8` | `0` | `0x` | `0x00` | -| `u8` | `1` | `0x01` | `0x01` | -| `u8` | `0x11` | `0x11` | `0x11` | -| `u8` | `255` | `0xFF` | `0xFF` | -| `u16` | `0` | `0x` | `0x0000` | -| `u16` | `0x11` | `0x11` | `0x0011` | -| `u16` | `0x1122` | `0x1122` | `0x1122` | -| `u32` | `0` | `0x` | `0x00000000` | -| `u32` | `0x11` | `0x11` | `0x00000011` | -| `u32` | `0x1122` | `0x1122` | `0x00001122` | -| `u32` | `0x112233` | `0x112233` | `0x00112233` | -| `u32` | `0x11223344` | `0x11223344` | `0x11223344` | -| `u64` | `0` | `0x` | `0x0000000000000000` | -| `u64` | `0x11` | `0x11` | `0x0000000000000011` | -| `u64` | `0x1122` | `0x1122` | `0x0000000000001122` | -| `u64` | `0x112233` | `0x112233` | `0x0000000000112233` | -| `u64` | `0x11223344` | `0x11223344` | `0x0000000011223344` | -| `u64` | `0x1122334455` | `0x1122334455` | `0x0000001122334455` | -| `u64` | `0x112233445566` | `0x112233445566` | `0x0000112233445566` | -| `u64` | `0x11223344556677` | `0x11223344556677` | `0x0011223344556677` | -| `u64` | `0x1122334455667788` | `0x1122334455667788` | `0x1122334455667788` | -| `usize` | `0` | `0x` | `0x00000000` | -| `usize` | `0x11` | `0x11` | `0x00000011` | -| `usize` | `0x1122` | `0x1122` | `0x00001122` | -| `usize` | `0x112233` | `0x112233` | `0x00112233` | -| `usize` | `0x11223344` | `0x11223344` | `0x11223344` | -| `i8` | `0` | `0x` | `0x00` | -| `i8` | `1` | `0x01` | `0x01` | -| `i8` | `-1` | `0xFF` | `0xFF` | -| `i8` | `127` | `0x7F` | `0x7F` | -| `i8` | `-0x11` | `0xEF` | `0xEF` | -| `i8` | `-128` | `0x80` | `0x80` | -| `i16` | `-1` | `0xFF` | `0xFFFF` | -| `i16` | `-0x11` | `0xEF` | `0xFFEF` | -| `i16` | `-0x1122` | `0xEEDE` | `0xEEDE` | -| `i32` | `-1` | `0xFF` | `0xFFFFFFFF` | -| `i32` | `-0x11` | `0xEF` | `0xFFFFFFEF` | -| `i32` | `-0x1122` | `0xEEDE` | `0xFFFFEEDE` | -| `i32` | `-0x112233` | `0xEEDDCD` | `0xFFEEDDCD` | -| `i32` | `-0x11223344` | `0xEEDDCCBC` | `0xEEDDCCBC` | -| `i64` | `-1` | `0xFF` | `0xFFFFFFFFFFFFFFFF` | -| `i64` | `-0x11` | `0xEF` | `0xFFFFFFFFFFFFFFEF` | -| `i64` | `-0x1122` | `0xEEDE` | `0xFFFFFFFFFFFFEEDE` | -| `i64` | `-0x112233` | `0xEEDDCD` | `0xFFFFFFFFFFEEDDCD` | -| `i64` | `-0x11223344` | `0xEEDDCCBC` | `0xFFFFFFFFEEDDCCBC` | -| `i64` | `-0x1122334455` | `0xEEDDCCBBAB` | `0xFFFFFFEEDDCCBBAB` | -| `i64` | `-0x112233445566` | `0xEEDDCCBBAA9A` | `0xFFFFEEDDCCBBAA9A` | -| `i64` | `-0x11223344556677` | `0xEEDDCCBBAA9989` | `0xFFEEDDCCBBAA9989` | -| `i64` | `-0x1122334455667788` | `0xEEDDCCBBAA998878` | `0xEEDDCCBBAA998878` | -| `isize` | `0` | `0x` | `0x00000000` | -| `isize` | `-1` | `0xFF` | `0xFFFFFFFF` | -| `isize` | `-0x11` | `0xEF` | `0xFFFFFFEF` | -| `isize` | `-0x1122` | `0xEEDE` | `0xFFFFEEDE` | -| `isize` | `-0x112233` | `0xEEDDCD` | `0xFFEEDDCD` | -| `isize` | `-0x11223344` | `0xEEDDCCBC` | `0xEEDDCCBC` | + let mut staking_pos = stake_mapper.get(); + self.claim_rewards_for_user(&caller, &mut staking_pos); + stake_mapper.set(&staking_pos); + } ---- + fn claim_rewards_for_user( + &self, + user: &ManagedAddress, + staking_pos: &mut StakingPosition, + ) { + let reward_amount = self.calculate_rewards(staking_pos); + let current_block = self.blockchain().get_block_nonce(); + staking_pos.last_action_block = current_block; + if reward_amount > 0 { + self.tx().to(user).egld(&reward_amount).transfer(); + } + } -### Arbitrary width (big) numbers + fn calculate_rewards(&self, staking_position: &StakingPosition) -> BigUint { + let current_block = self.blockchain().get_block_nonce(); + if current_block <= staking_position.last_action_block { + return BigUint::zero(); + } -For most smart contracts applications, number larger than the maximum uint64 value are needed. -EGLD balances for instance are represented as fixed-point decimal numbers with 18 decimals. -This means that to represent even just 100 EGLD we use the number 100*1018, which already exceeds the capacity of a regular 64-bit integer. + let apy = self.apy().get(); + let block_diff = current_block - staking_position.last_action_block; -**Rust types**: `BigUint`, `BigInt`, + &staking_position.stake_amount * apy / MAX_PERCENTAGE * block_diff / BLOCKS_IN_YEAR + } -:::important -These types are managed by MultiversX VM, in many cases the contract never sees the data, only a handle. -This is to reduce the burden on the smart contract. -::: + #[view(calculateRewardsForUser)] + fn calculate_rewards_for_user(&self, addr: ManagedAddress) -> BigUint { + let staking_pos = self.staking_position(&addr).get(); + self.calculate_rewards(&staking_pos) + } -**Top-encoding**: The same as for all numerical types, the minimum number of bytes that -can fit their 2's complement, big endian representation. + #[view(getStakedAddresses)] + #[storage_mapper("stakedAddresses")] + fn staked_addresses(&self) -> UnorderedSetMapper; -**Nested encoding**: Since these types are variable length, we need to encode their length, so that the decodes knows when to stop decoding. -The length of the encoded number always comes first, on 4 bytes (`usize`/`u32`). -Next we encode: + #[view(getStakingPosition)] + #[storage_mapper("stakingPosition")] + fn staking_position( + &self, + addr: &ManagedAddress, + ) -> SingleValueMapper>; -- For `BigUint` the big endian bytes -- For `BigInt` the shortest 2's complement number that can unambiguously represent the number. Positive numbers must always have the most significant bit `0`, while the negative ones `1`. See examples below. + #[view(getApy)] + #[storage_mapper("apy")] + fn apy(&self) -> SingleValueMapper; +} +``` -**Examples** +Now, rebuild the contract and regenerate the proxy: -| Type | Number | Top-level encoding | Nested encoding | Explanation | -| --------- | ------ | ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------ | -| `BigUint` | `0` | `0x` | `0x00000000` | The length of `0` is considered `0`. | -| `BigUint` | `1` | `0x01` | `0x0000000101` | `1` can be represented on 1 byte, so the length is 1. | -| `BigUint` | `256` | `0x0100` | `0x000000020100` | `256` is the smallest number that takes 2 bytes. | -| `BigInt` | `0` | `0x` | `0x00000000` | Signed `0` is also represented as zero-length bytes. | -| `BigInt` | `1` | `0x01` | `0x0000000101` | Signed `1` is also represented as 1 byte. | -| `BigInt` | `-1` | `0xFF` | `0x00000001FF` | The shortest 2's complement representation of `-1` is `FF`. The most significant bit is 1. | -| `BigUint` | `127` | `0x7F` | `0x000000017F` | | -| `BigInt` | `127` | `0x7F` | `0x000000017F` | | -| `BigUint` | `128` | `0x80` | `0x0000000180` | | -| `BigInt` | `128` | `0x0080` | `0x000000020080` | The most significant bit of this number is 1, so to avoid ambiguity an extra `0` byte needs to be prepended. | -| `BigInt` | `255` | `0x00FF` | `0x0000000200FF` | Same as above. | -| `BigInt` | `256` | `0x0100` | `0x000000020100` | `256` requires 2 bytes to represent, of which the MSB is 0, no more need to prepend a `0` byte. | +```bash +sc-meta all build +sc-meta all proxy +``` ---- +Let's update our test, to use our new `StakingPosition` structure, and also provide the `APY` as an argument for the `init` function. +```rust +mod staking_contract_proxy; +use multiversx_sc::{ + imports::OptionalValue, + types::{BigUint, TestAddress, TestSCAddress}, +}; +use multiversx_sc_scenario::imports::*; +use staking_contract_proxy::StakingPosition; -### Boolean values +const OWNER_ADDRESS: TestAddress = TestAddress::new("owner"); +const USER_ADDRESS: TestAddress = TestAddress::new("user"); +const STAKING_CONTRACT_ADDRESS: TestSCAddress = TestSCAddress::new("staking-contract"); +const WASM_PATH: MxscPath = MxscPath::new("output/staking-contract.mxsc.json"); +const USER_BALANCE: u64 = 1_000_000_000_000_000_000; +const APY: u64 = 10_00; // 10% -Booleans are serialized the same as a byte (`u8`) that can take values `1` or `0`. +struct ContractSetup { + pub world: ScenarioWorld, +} -**Rust type**: `bool` +impl ContractSetup { + pub fn new() -> Self { + let mut world = ScenarioWorld::new(); + world.set_current_dir_from_workspace("staking-contract"); + world.register_contract(WASM_PATH, staking_contract::ContractBuilder); -**Values** + world.account(OWNER_ADDRESS).nonce(1).balance(0); + world.account(USER_ADDRESS).nonce(1).balance(USER_BALANCE); -| Type | Value | Top-level encoding | Nested encoding | -| ------ | ------- | ------------------ | --------------- | -| `bool` | `true` | `0x01` | `0x01` | -| `bool` | `false` | `0x` | `0x00` | + // simulate deploy + world + .tx() + .from(OWNER_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .init(APY) + .code(WASM_PATH) + .new_address(STAKING_CONTRACT_ADDRESS) + .run(); ---- + ContractSetup { world } + } +} +#[test] +fn stake_unstake_test() { + let mut setup = ContractSetup::new(); -### Byte slices and ASCII strings + setup + .world + .check_account(USER_ADDRESS) + .balance(USER_BALANCE); + setup.world.check_account(OWNER_ADDRESS).balance(0); -Byte slices are technically a special case of the [list types](/developers/data/composite-values#lists-of-items), but they are usually thought of as basic types. Their encoding is, in any case, consistent with the rules for lists of "byte items". + // stake full + setup + .world + .tx() + .from(USER_ADDRESS) + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .stake() + .egld(USER_BALANCE) + .run(); -:::important -Strings are treated from the point of view of serialization as series of bytes. Using Unicode strings, while often a good practice in programming, tends to add unnecessary overhead to smart contracts. The difference is that Unicode strings get validated on input and concatenation. + let expected_result_1 = StakingPosition { + stake_amount: BigUint::from(USER_BALANCE), + last_action_block: 0, + }; -We consider best practice to use Unicode on the frontend, but keep all messages and error messages in ASCII format on smart contract level. -::: + setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staking_position(USER_ADDRESS) + .returns(ExpectValue(expected_result_1)) + .run(); -**Rust types**: `ManagedBuffer`, `BoxedBytes`, `&[u8]`, `Vec`, `String`, `&str`. + setup.world.check_account(USER_ADDRESS).balance(0); + setup + .world + .check_account(STAKING_CONTRACT_ADDRESS) + .balance(USER_BALANCE); -**Top-encoding**: The byte slice, as-is. + // unstake partial + setup + .world + .tx() + .from(USER_ADDRESS) + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .unstake(OptionalValue::Some(USER_BALANCE / 2)) + .run(); -**Nested encoding**: The length of the byte slice on 4 bytes, followed by the byte slice as-is. + let expected_result_2 = StakingPosition { + stake_amount: BigUint::from(USER_BALANCE / 2), + last_action_block: 0, + }; + setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staking_position(USER_ADDRESS) + .returns(ExpectValue(expected_result_2)) + .run(); -**Examples** + let staked_addresses_1 = setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staked_addresses() + .returns(ReturnsResultUnmanaged) + .run(); -| Type | Value | Top-level encoding | Nested encoding | Explanation | -| --------------- | --------------------------- | ------------------ | ------------------ | ----------------------------------------------------------------- | -| `&'static [u8]` | `b"abc"` | `0x616263` | `0x00000003616263` | ASCII strings are regular byte slices of buffers. | -| `ManagedBuffer` | `ManagedBuffer::from("abc")`| `0x616263` | `0x00000003616263` | Use `Vec` for a buffer that can grow. | -| `BoxedBytes` | `BoxedBytes::from( b"abc")` | `0x616263` | `0x00000003616263` | BoxedBytes are just optimized owned byte slices that cannot grow. | -| `Vec` | `b"abc".to_vec()` | `0x616263` | `0x00000003616263` | Use `Vec` for a buffer that can grow. | -| `&'static str` | `"abc"` | `0x616263` | `0x00000003616263` | Unicode string (slice). | -| `String` | `"abc".to_string()` | `0x616263` | `0x00000003616263` | Unicode string (owned). | + assert!(staked_addresses_1 + .into_vec() + .contains(&USER_ADDRESS.to_address())); -:::info Note -Inside contracts, `ManagedBuffer` is [the only recommended type for generic bytes](/developers/best-practices/the-dynamic-allocation-problem). -::: + setup + .world + .check_account(USER_ADDRESS) + .balance(USER_BALANCE / 2); + setup + .world + .check_account(STAKING_CONTRACT_ADDRESS) + .balance(USER_BALANCE / 2); ---- + // unstake full + setup + .world + .tx() + .from(USER_ADDRESS) + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .unstake(OptionalValue::None::) + .run(); + let staked_addresses_2 = setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staked_addresses() + .returns(ReturnsResultUnmanaged) + .run(); + assert!(staked_addresses_2.is_empty()); -### Address + setup + .world + .check_account(USER_ADDRESS) + .balance(USER_BALANCE); + setup + .world + .check_account(STAKING_CONTRACT_ADDRESS) + .balance(0); +} +``` -MultiversX addresses are 32 byte arrays, so they get serialized as such, both in top and nested encodings. +Now let's run the test... **it didn't work**. You should see the following error: ---- +```bash +Error: result code mismatch. +Tx id: '' +Want: "0" +Have: 4 +Message: storage decode error (key: stakingPositionuser____________________________): input too short +``` +But why? Everything worked fine before. -### Token identifiers +This is because instead of using a simple `BigUint` for staking positions, we now use the `StakingPosition` structure. If you follow the error trace, you will see exactly where it failed: -MultiversX ESDT token identifiers are of the form `XXXXXX-123456`, where the first part is the token ticker, 3 to 20 characters in length, and the last is a random generated number. +```bash +28: staking_contract::StakingContract::stake + at ./src/staking_contract.rs:33:9 +``` -They are top-encoded as is, the exact bytes and nothing else. +Which leads to the following line: -Because of their variable length, they need to be serialized like variable length byte slices when nested, so the length is explicitly encoded at the start. +```rust +self.staking_position(&caller).update(|staking_pos| { + self.claim_rewards_for_user(&caller, staking_pos); -| Type | Value | Top-level encoding | Nested encoding | -| --------------- | --------------------------- | ------------------ | ------------------ | -| `TokenIdentifier` | `ABC-123456` | `0x4142432d313233343536` | `0x0000000A4142432d313233343536` | + staking_pos.stake_amount += payment_amount +}); +``` ---- +Because we're trying to add a new user, which has no staking entry yet, the decoding fails. ---- +For a simple `BigUint`, decoding from an empty storage yields the `0` value, which is exactly what we want, but for a struct type, it cannot give us any default value. -### Smart contract annotations +For this reason, we have to add some additional checks. The endpoint implementations will have to be changed to the following (the rest of the code remains the same): -## Introduction +```rust +#[payable("EGLD")] +#[endpoint] +fn stake(&self) { + let payment_amount = self.call_value().egld().clone(); + require!(payment_amount > 0, "Must pay more than 0"); -Annotations (also known as Rust "attributes") are the bread and butter of the `multiversx-sc` smart contract development framework. While contracts can in principle be written without any annotations or code generation macros in place, it is infinitely more difficult to do so. + let caller = self.blockchain().get_caller(); + let stake_mapper = self.staking_position(&caller); -One of the main purposes of the framework is to make the code as readable and concise as possible, and annotations are the path to get there. + let new_user = self.staked_addresses().insert(caller.clone()); + let mut staking_pos = if !new_user { + stake_mapper.get() + } else { + let current_block = self.blockchain().get_block_epoch(); + StakingPosition { + stake_amount: BigUint::zero(), + last_action_block: current_block, + } + }; -For an introduction, check out [the Crowdfunding tutorial](/developers/tutorials/crowdfunding-p1). This page is supposed to be a complete index of all annotations that can be encountered in smart contracts. + self.claim_rewards_for_user(&caller, &mut staking_pos); + staking_pos.stake_amount += payment_amount; + stake_mapper.set(&staking_pos); +} -## Trait annotations +#[endpoint] +fn unstake(&self, opt_unstake_amount: OptionalValue) { + let caller = self.blockchain().get_caller(); + self.require_user_staked(&caller); + let stake_mapper = self.staking_position(&caller); + let mut staking_pos = stake_mapper.get(); -### `#[multiversx_sc::contract]` + let unstake_amount = match opt_unstake_amount { + OptionalValue::Some(amt) => amt, + OptionalValue::None => staking_pos.stake_amount.clone(), + }; + require!( + unstake_amount > 0 && unstake_amount <= staking_pos.stake_amount, + "Invalid unstake amount" + ); -The `contract` annotation must always be placed on a trait and will automatically make that trait the main container for the smart contract endpoints and logic. There should be only one such trait defined per crate. + self.claim_rewards_for_user(&caller, &mut staking_pos); + staking_pos.stake_amount -= &unstake_amount; -Note that the annotation takes no additional arguments. + if staking_pos.stake_amount > 0 { + stake_mapper.set(&staking_pos); + } else { + stake_mapper.clear(); + self.staked_addresses().swap_remove(&caller); + } ---- + self.tx().to(caller).egld(unstake_amount).transfer(); +} +#[endpoint(claimRewards)] +fn claim_rewards(&self) { + let caller = self.blockchain().get_caller(); + self.require_user_staked(&caller); -### `#[multiversx_sc::module]` + let stake_mapper = self.staking_position(&caller); -The `module` annotation must always be placed on a trait and will automatically make that trait a smart contract module. + let mut staking_pos = stake_mapper.get(); + self.claim_rewards_for_user(&caller, &mut staking_pos); -Note that the annotation takes no additional arguments. + stake_mapper.set(&staking_pos); +} -:::caution -Only one contract, module or proxy annotation is allowed per Rust module. If they are in separate files there is no problem, but if several share a file, explicit `mod module_name { ... }` must enclose the module. -::: +fn require_user_staked(&self, user: &ManagedAddress) { + require!(self.staked_addresses().contains(user), "Must stake first"); +} +``` ---- +For the `stake` endpoint, if the user was not previously staked, we provide a default entry. The `insert` method of `UnorderedSetMapper` returns `true` if the entry is new and `false` if the user already exists in the list. This means we can use that result directly, instead of checking for `stake_mapper.is_empty()`. +For the `unstake` and `claimRewards` endpoints, we have to check if the user was already staked and return an error otherwise (as they'd have nothing to unstake/claim anyway). -### `#[multiversx_sc::proxy]` +Once you've applied all the suggested changes, **rebuilt the contract** and **regenerate the proxy**, running the test should work just fine now: -The `proxy` annotation must always be placed on a trait and will automatically make that trait a smart contract call proxy. More about smart contract proxies in [the contract calls reference](/developers/transactions/tx-legacy-calls). +```bash +running 1 test +test stake_unstake_test ... ok +``` -In short, contracts always get an auto-generated proxy. However, if such an auto-generated proxy of another contract is not available, it is possible to define such a "contract interface" by hand, using the `proxy` attribute. +In order to apply these changes on devnet, you should build the contract, regenerate the interactor and then upgrade it. -Note that the annotation takes no additional arguments. +:::warning +Whenever you regenerate the interactor, make sure that wallet_address points to the wallet you intend to use for executing transactions. -:::caution -Only one contract, module or proxy annotation is allowed per Rust module. If they are in separate files there is no problem, but if several share a file, explicit `mod proxy_name { ... }` must enclose the module. +By default, the interactor registers Alice's wallet, so you’ll need to update wallet_address manually if you’re using a different one. ::: +```bash +sc-meta all build +sc-meta all snippets +cd interactor/ +``` -## Method annotations - - -### `#[init]` - -Every smart contract needs one constructor that only gets called once when the contract is deployed. The method annotated with init is the constructor. +Set the `apy` variable inside the `upgrade` function from `staking-contract/interactor/src/interact.rs` to `100`, then run: -```rust -#[multiversx_sc::contract] -pub trait Example { - #[init] - fn this_is_the_constructor( - constructor_arg_1: u32, - constructor_arg_2: BigUint) { - // ... - } -} +```bash +cargo run upgrade ``` -:::note -When upgrading a smart contract, the constructor in the new code is called. It is also called only once, and it can also never be called again. -::: +To verify the change, query the `apy` storage mapper by running: +```bash +cargo run getApy +``` -### `#[endpoint]` and `#[view]` +You should see the following output: -Endpoints are the public methods of contracts, which can be called in transactions. A contract can define any number of methods, but only those annotated with `#[endpoint]` or `#[view]` are visible to the outside world. +```bash +Result: 100 +``` -`#[view]` is meant to indicate readonly methods, but this is currently not enforced in any way. Functionally, `#[view]` and `#[endpoint]` are currently perfectly synonymous. However, there are plans for the future to enforce views to be verified at compile time to be readonly. When that happens, smart contracts that will already have been correctly annotated will be easier to migrate. Until then, there is still value in having 2 annotations, since they indicate intent. -If no arguments are provided to the attribute, the name of the Rust method will be the name of the endpoint. Alternatively, an explicit endpoint name can be provided in brackets. +### Testing -Example: +Now that we've implemented rewards logic, let's add the following test to `staking-contract/tests/staking_contract_blackbox_test.rs`: ```rust -#[multiversx_sc::contract] -pub trait Example { - #[endpoint] - fn example(&self) { - } - - #[endpoint(camelCaseEndpointName)] - fn snake_case_method_name(&self, value: BigUint) { - } - - fn private_method(&self, value: &BigUint) { - } - - #[view(getData)] - fn get_data(&self) -> u32 { - 0 - } -} -``` +#[test] +fn rewards_test() { + let mut setup = ContractSetup::new(); -In this example, 3 methods are public endpoints. They are named `example`, `camelCaseEndpointName` and `getData`. All other names are internal and do not show up in the resulting contract. + // stake full + setup + .world + .tx() + .from(USER_ADDRESS) + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .stake() + .egld(USER_BALANCE) + .run(); -:::note -All endpoint arguments and results must be either serializable or special endpoint argument types such as `MultiValueEncoded`. They must also all implement the `TypeAbi` trait. There is no such restriction for private methods. -::: + let expected_result_1 = StakingPosition { + stake_amount: BigUint::from(USER_BALANCE), + last_action_block: 0, + }; + setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staking_position(USER_ADDRESS) + .returns(ExpectValue(&expected_result_1)) + .run(); -### Callbacks + setup.world.current_block().block_nonce(BLOCKS_IN_YEAR); -There are 2 annotations for callbacks: `#[callback]` and `#[callback_raw]`. The second is only used in extreme cases. + // query rewards + setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .calculate_rewards_for_user(USER_ADDRESS) + .returns(ExpectValue( + BigUint::from(USER_BALANCE) * APY / MAX_PERCENTAGE, + )) + .run(); -Callbacks are special methods that get called automatically when the response comes after an asynchronous contract call. They give the contract the possibility to react to the result of a cross-shard call, but for consistency they get called the same way if the asynchronous call happens in the same shard. + // claim rewards + setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staking_position(USER_ADDRESS) + .returns(ExpectValue(&expected_result_1)) + .run(); -They also act as closures, since they can retain some of the context of the transaction that performed the asynchronous call in the first place. + setup + .world + .tx() + .from(USER_ADDRESS) + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .claim_rewards() + .run(); -A more detailed explanation on how they work in [the contract calls reference](/developers/transactions/tx-legacy-calls). + let expected_result_2 = StakingPosition { + stake_amount: BigUint::from(USER_BALANCE), + last_action_block: BLOCKS_IN_YEAR, + }; + setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staking_position(USER_ADDRESS) + .returns(ExpectValue(expected_result_2)) + .run(); + setup + .world + .check_account(USER_ADDRESS) + .balance(BigUint::from(USER_BALANCE) * APY / MAX_PERCENTAGE); -### Storage + // query rewards after claim + setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .calculate_rewards_for_user(USER_ADDRESS) + .returns(ExpectValue(0u32)) + .run(); +} +``` -It is possible for a developer to access storage manually in a contract, but this is error-prone and involves a lot of boilerplate code. For this reason, `multiversx-sc` offers storage annotations that manage and serialize the keys and values behind the scenes. +In the test, we perform the following steps: -Each contract has a storage where arbitrary data can be stored on-chain. This storage is organized as a map of arbitrary length keys and values. The blockchain has no concept of storage key or value types, they are all stored as raw bytes. It is the job of the contract to interpret these values. +- Stake 1 EGLD; +- Set block nonce after 1 year (i.e. simulating 1 year worth of blocks passing); +- Querying rewards, which should give use 10% of 1 EGLD = 0.1 EGLD; +- Claiming said rewards and checking the internal state and user balance; +- Querying again after claim, to check that double-claim is not possible. -All trait methods annotated for storage handling must have no implementation. +This test should work without any errors. -#### `#[storage_get("key")]` +## Conclusion -This is the simplest way to retrieve data from the storage. Let's start with an example of usage: +Currently, there is no way to deposit rewards into the smart contract, unless the owner makes it payable, which is generally bad practice, and not recommended. -```rust -#[multiversx_sc::contract] -pub trait Adder { - #[view(getSum)] - #[storage_get("sum")] - fn get_sum(&self) -> BigUint; +As this is a fairly simple task compared to what we've done already, we'll leave this as an exercise to the reader. You'll have to add a `payable("EGLD")` endpoint, and additionally, a storage mapper that keeps track of the remaining rewards. - #[storage_get("example_map")] - fn get_value(&self, key_1: u32, key_2: u32) -> SerializableType; -} -``` +Good luck! -First off, please note that a storage method can also be annotated with `#[view]` or `#[endpoint]`. The endpoint annotations refer to the role of the method in the contract, while the storage annotation refers to its implementation, so there is no overlap. +--- -Then, also note that there are 2 ways to use this annotation. In the first example, we simply specify the key in the annotation and from here on the method will always read from the same storage key, `"sum"` in this case. +### Storage Mappers -In the second example the get method also takes some arguments. Any number of arguments is allowed. These get concatenated to the base key to form a composite key, effectively turning a section of the contract storage into a dictionary or map. +The Rust framework provides various storage mappers you can use. Deciding which one to use for every situation is critical for performance. There will be a comparison section after each mapper is described. -For instance calling `self.get_value(1, 2)` will retrieve from the storage key `"example_map\x00\x00\x00\x01\x00\x00\x00\x02"` or `0x6578616d706c655f6d61700000000100000002`. `self.get_value(1, 3)` will read from a different place in storage, and so on. +Note: All the storage mappers support additional key arguments. -This is the easiest way to get the equivalent of a HashMap in a smart contract. -Lastly, storage getters must always return a deserializable type. The framework will automatically deserialize the object from whatever bytes it finds in the storage value. +# General purpose mappers -#### `#[storage_set("key")]` +## SingleValueMapper -This is the simplest way to write data to storage. Example: +Stores a single value. Examples: ```rust -#[multiversx_sc::contract] -pub trait Adder { - #[storage_set("sum")] - fn set_sum(&self, sum: &BigUint); - - #[storage_set("example_map")] - fn set_value(&self, key_1: u32, key_2: u32, value: &SerializableType); -} +fn single_value(&self) -> SingleValueMapper; +fn single_value_with_single_key_arg(&self, key_arg: Type1) -> SingleValueMapper; +fn single_value_with_multi_key_arg(&self, key_arg1: Type1, key_arg2: Type2) -> SingleValueMapper; ``` -It works very similarly to `storage_get`, with the notable difference that instead of returning a value, the value must be provided as an argument. The value to store is always the last argument. +Keep in mind there is no way of iterating over all `key_arg`s, so if you need to do that, consider using another mapper. -Again, just like for the getter, an arbitrary number of additional map keys can be specified, as for `set_value` in the example. This is how we can write values to a section of our storage that behaves like a map. +Available methods: -:::caution -There is no mechanism in place to ensure that there is no overlap between storage keys. Nothing prevents a developer from writing: +### get ```rust -#[storage_set("sum")] -fn set_sum(&self, sum: &BigUint); - -#[storage_set("sum")] -fn set_another_sum(&self, another_sum: &BigUint); - -#[storage_set("s")] -fn set_value(&self, key: u16, value: &SerializableType); +fn get() -> Type ``` -The first problem is easy to spot: we have 2 setters with the same key. - -The second is harder to notice. Calling `self.set_value(0x756d, value)` or `self.set_value(30061, value)` will also overwrite `"sum"`. This is because `"um"` = `"\x75\6d"`, which gets concatenated to the `"s"`, forming `"sum"`. - -To avoid this vulnerability, **never have a key that is the prefix of another key!** - -::: - +Reads the value from storage and deserializes it to the given `Type`. For numerical types and vector types, this will return the default value for empty storage, i.e. 0 and empty vec respectively. For custom structs, this will signal an error if trying to read from empty storage. -#### `#[storage_mapper("key")]` -Storage mappers are objects that can manage multiple storage keys at once. They are in charge with both reading and writing values. Some of them read and write values to multiple storage keys at once. +### set +```rust +fn set(value: &Type) +``` -There are many storage mappers in the framework and more can be custom-defined. +Sets the stored value to the provided `value` argument. For base Rust numerical types, the reference is not needed. -Example: +### is_empty ```rust -#[storage_mapper("user_status")] -fn user_status(&self) -> SingleValueMapper; - -#[storage_mapper("list_mapper")] -fn list_mapper(&self, sub_key: usize) -> LinkedListMapper; +fn is_empty() -> bool ``` -The `SingleValueMapper` is the simplest of them all, since it only manages one storage key. Even though it only works with one storage entry, its syntax is more compact than `storage_get`/`storage_set` so it is used quite a lot. - -In the `LinkedListMapper` we are dealing with a list of items, each with its own key. +Returns `true` is the storage entry is empty. Usually used when storing struct types to prevent crashes on `get()`. -Also note that additional sub-keys are also allowed for storage mappers, the same as for `storage_get` and `storage_set`. +### set_if_empty +```rust +fn set_if_empty(value: &Type) +``` -#### `#[storage_is_empty("key")]` +Sets the value only if the storage for that value is currently empty. Usually used in #init functions to not overwrite values on contract upgrade. -This is very similar to `storage_get`, but instead of retrieving the value, it returns a boolean indicating whether the serialized value is empty or not. It does not attempt to deserialize the value, so it can be faster and more resilient than `storage_get`, depending on type. +### clear ```rust -#[storage_is_empty("opt_addr")] -fn is_empty_opt_addr(&self) -> bool; +fn clear() ``` -Nowadays, it is more common to use storage mappers. The `SingleValueMapper` has an `is_empty()` method that does the same. +Clears the entry. -#### `#[storage_clear("key")]` +### update +```rust +fn update R>(f: F) -> R +``` -This is very similar to `storage_set`, but instead of serializing and writing the storage value, it simply clears the raw bytes. -It does not do any serializing, so it can be faster than `storage_set`, depending on type. +Takes a closure as argument, applies that closure to the currently stored value, saves the new value, and returns any value the given closure might return. Examples: +Incrementing a value: ```rust -#[storage_clear("field_to_clear")] -fn clear_storage_value(&self); +fn my_value(&self) -> SingleValueMapper; + +self.my_value().update(|val| *val += 1); ``` -Nowadays, it is more common to use storage mappers. The `SingleValueMapper` has an `clear()` method that does the same. +Modifying a struct's field: +```rust +pub struct MyStruct { + pub field_1: u64, + pub field_2: u32 +} +fn my_value(&self) -> SingleValueMapper; -### Events +self.my_value().update(|val| val.field1 = 5); +``` -Events are a way of returning data from smart contract, by leaving a trace of what happened during the execution. Event logs are not saved on the blockchain, but a hash of them is. This means that we can always check whether certain events were emitted by a transactions or not. +Returning a value from the closure: +```rust +fn my_value(&self) -> SingleValueMapper; -Because they are not saved on the chain in full, they are also a lot cheaper than storage. +let new_val = self.my_value().update(|val| { + *val += 1; + *val +}); +``` -In smart contracts we define them as trait methods with no implementation, as follows: +### raw_byte_length ```rust -#[event("transfer")] -fn transfer_event( - &self, - #[indexed] from: &ManagedAddress, - #[indexed] to: &ManagedAddress, - #[indexed] token_id: u32, - data: ManagedBuffer, -); +fn raw_byte_length() -> usize ``` -The annotation always requires the name of the event to be specified explicitly in brackets. +Returns the raw byte length of the stored value. This should be rarely used. -Events have 2 types of arguments: -- "Topics" are annotated with `#[indexed]`. When saving event logs to a database, indexes will be created for all these fields, so they can be searched for efficiently. -- The "data" argument has no annotation. There can be only one data field in an event, and it cannot be indexed later. +## VecMapper -Event arguments (fields) can be of any serializable type. There is no return value for events. +Stores elements of the same type, each under their own storage key. Allows access by index for said items. Keep in mind indexes start at 1 for VecMapper. Examples: +```rust +fn my_vec(&self) -> VecMapper; +fn my_vec_with_args(&self, arg: Type1) -> VecMapper; +``` -### Events (legacy) +Available methods: -There is a legacy annotation, `#[legacy_event]` still used by some older contracts. It is deprecated and should no longer be used. +### push +```rust +fn push(elem: &T) +``` -### `#[proxy]` +Stores the element at index `len` and increments `len` afterwards. -This is a simple getter, which provides a convenient instance of a contract proxy. It is used when wanting to call another contract. +### get ```rust -#[multiversx_sc::module] -pub trait ForwarderAsyncCallModule { - #[proxy] - fn vault_proxy(&self, to: Address) -> vault::Proxy; - - // ... -} +fn get(index: usize) -> Type ``` -There is no need for arguments, the annotation will figure out the contract to call by the provided return type. +Gets the element at the specific index. Valid indexes are 1 to `len`, both ends included. Attempting to read from an invalid index will signal an error. -:::important -Proxy types need to be specified with an explicit module. In the example `vault::` is compulsory. -::: +### set +```rust +fn set(index: usize, value: &Type) +``` -### `#[output_names]` +Sets the element at the given index. Index must be in inclusive range 1 to `len`. -This one is used for ABI result names. In Rust, it is impossible to write Rust Docs for method returns, so we are using this annotation to optionally name the outputs of an endpoint. ---- +### clear_entry +```rust +fn clear_entry(index: usize) +``` -### Smart Contract API Functions +Clears the entry at the given index. This does not decrease the length. -## Introduction -The Rust framework provides a wrapper over the MultiversX VM API functions and over account-level built-in functions. They are split into multiple modules, grouped by category: +### is_empty +```rust +fn is_empty() -> bool +``` -- BlockchainApi: Provides general blockchain information, which ranges from account balances, NFT metadata/roles to information about the current and previous block (nonce, epoch, etc.) -- CallValueApi: Used in payable endpoints, providing information about the tokens received as payment (token type, nonce, amount) -- CryptoApi: Provides support for cryptographic functions like hashing and signature checking -- SendApi: Handles all types of transfers to accounts and smart contract calls/deploys/upgrades, as well as support for ESDT local built-in functions +Returns `true` if the mapper has no elements stored. -The base trait for the APi is: https://docs.rs/multiversx-sc/0.39.0/multiversx_sc/api/trait.VMApi.html -The source code for the APIs can be found here: https://github.com/multiversx/mx-sdk-rs/tree/master/framework/base/src/api +### len +```rust +fn len() -> usize +``` +Returns the number of items stored in the mapper. -## Blockchain API -This API is accessible through `self.blockchain()`. Available functions: +### extend_from_slice +```rust +fn extend_from_slice(slice: &[Type]) +``` +Pushes all elements from the given slice at the end of the mapper. More efficient than manual `for` of `push`, as the internal length is only read and updated once. -### get_sc_address +### swap_remove ```rust -get_sc_address() -> ManagedAddress +fn swap_remove(index: usize) ``` -Returns the smart contract's own address. - +Removes the element at `index`, moves the last element to `index` and decreases the `len` by 1. There is no way of removing an element and preserving the order. -### get_owner_address +### clear ```rust -get_owner_address() -> ManagedAddress +fn clear() ``` -Returns the owner's address. - +Clears all the elements from the mapper. This function can run out of gas for big collections. -### check_caller_is_owner +### iter ```rust -check_caller_is_owner() +fn iter() -> Iter ``` -Terminates the execution and signals an error if the caller is not the owner. +Provides an iterator over all the elements. -Use `#[only_owner]` endpoint annotation instead of directly calling this function. +## SetMapper -### get_shard_of_address +Stores a set of values, with no duplicates being allowed. It also provides methods for checking if a value already exists in the set. Values order is given by their order of insertion. + +Unless you need to maintain the order of the elements, consider using `UnorderedSetMapper` or `WhitelistMapper` instead, as they're more efficient. +Examples: ```rust -get_shard_of_address(address: &ManagedAddress) -> u32 +fn my_set(&self) -> SetMapper; ``` -Returns the shard of the address passed as argument. - +Available methods: -### is_smart_contract +### insert ```rust -is_smart_contract(address: &ManagedAddress) -> bool +fn insert(value: Type) -> bool ``` -Returns `true` if the address passed as parameter is a Smart Contract address, `false` for simple accounts. - +Insers the value into the set. Returns `false` if the item was already present. -### get_caller +### remove ```rust -get_caller() -> ManagedAddress +fn remove(value: &Type) ``` -Returns the current caller. - -Keep in mind that for SC Queries, this function will return the SC's own address, so having a view function that uses this API function will not have the expected behaviour. - +Removes the value from the set. Returns `false` if the set did not contain the value. -### get_balance +### contains ```rust -get_balance(address: &ManagedAddress) -> BigUint +fn contains(value: &Type) -> bool ``` -Returns the EGLD balance of the given address. - -This only works for addresses that are in the same shard as the smart contract. - +Returns `true` if the mapper contains the given value. -### get_sc_balance +### is_empty ```rust -get_sc_balance(token: &EgldOrEsdtTokenIdentifier, nonce: u64) -> BigUint +fn is_empty() -> bool ``` -Returns the EGLD/ESDT/NFT balance of the smart contract. - -For fungible ESDT, nonce should be 0. To get the EGLD balance, you can simply pass `EgldOrEsdtTokenIdentifier::egld()` as parameter. - +Returns `true` if the mapper has no elements stored. -### get_tx_hash +### len ```rust -get_tx_hash() -> ManagedByteArray +fn len() -> usize ``` -Returns the current tx hash. - -In case of asynchronous calls, the tx hash is the same both in the original call and in the associated callback. - +Returns the number of items stored in the mapper. -### get_gas_left +### clear ```rust -get_gas_left() - > u64 +fn clear() ``` -Returns the remaining gas, at the time of the call. - -This is useful for expensive operations, like iterating over an array of users in storage and sending rewards. - -A smart contract call that runs out of gas will revert all operations, so this function can be used to return _before_ running out of gas, saving a checkpoint, and continuing on a second call. - +Clears all the elements from the mapper. This function can run out of gas for big collections. -### get_block_timestamp_seconds +### iter ```rust -get_block_timestamp_millis() -> TimestampSeconds +fn iter() -> Iter ``` -Returns the timestamp of the current block, in seconds (UNIX timestamp). +Returns an iterator over all the stored elements. +## UnorderedSetMapper -### get_block_timestamp_millis +Same as SetMapper, but does not guarantee the order of the items. More efficient than `SetMapper`, and should be used instead unless you need to maintain the order. Internally, `UnorderedSetMapper` uses a `VecMapper` to store the elements, and additionally, it stores each element's index to provide O(1) `contains`. +Examples: ```rust -get_block_timestamp_millis() -> TimestampMillis +fn my_set(&self) -> UnorderedSetMapper; ``` -Returns the timestamp of the current block, in milliseconds (UNIX timestamp). - +Available methods: +`UnorderedSetMapper` contains the same methods as `SetMapper`, the only difference being item removal. Instead of `remove`, we only have `swap_remove` available. -### get_block_nonce +### swap_remove ```rust -get_block_nonce() -> u64 +fn swap_remove(value: &Type) -> bool ``` -Returns the unique nonce of the block that includes the current transaction. +Uses the internal `VecMapper`'s swap_remove method to remove the element. Additionally, it overwrites the last element's stored index with the removed value's index. Returns `false` if the element was not present in the set. -### get_block_round +## WhitelistMapper + +Stores a whitelist of items. Does not provide any means of iterating over the elements, so if you need to iterate over the elements, use `UnorderedSetMapper` instead. Internally, this mapper simply stores a flag in storage for each item if they're whitelisted. +Examples: ```rust -get_block_round() -> u64 +fn my_whitelist(&self) -> WhitelistMapper ``` -Returns the round number of the current block. Each epoch consists of a fixed number of rounds. The round number resets to 1 at the start of every new epoch. - +Available methods: -### get_block_epoch +### add ```rust -get_block_epoch() -> u64 +fn add(value: &Type) ``` -Returns the epoch of the current block. - -These functions are mostly used for setting up deadlines, so they've been grouped together. - +Adds the value to the whitelist. -### get_block_random_seed +### remove ```rust -get_block_random_seed() -> ManagedByteArray +fn remove(value: &Type) ``` -Returns the block random seed, which can be used for generating random numbers. - -This will be the same for all the calls in the current block, so it can be predicted by someone calling this at the start of the round and only then calling your contract. - +Removes the value from the whitelist. -### get_prev_block_timestamp_seconds +### contains ```rust -get_prev_block_timestamp_seconds() -> TimestampSeconds +fn contains(value: &Type) -> bool ``` -Returns the timestamp of the previous block, in seconds (UNIX timestamp). - - +Returns `true` if the mapper contains the given value. -### get_prev_block_timestamp_millis +### require_whitelisted ```rust -get_prev_block_timestamp_millis() -> TimestampMillis +fn require_whitelisted(value: &Type) ``` -Returns the timestamp of the previous block, in milliseconds (UNIX timestamp). +Will signal an error if the item is not whitelisted. Does nothing otherwise. +## LinkedListMapper -### get_prev_block_nonce +Stores a linked list, which allows fast insertion/removal of elements, as well as possibility to iterate over the whole list. +Examples: ```rust -get_prev_block_nonce() -> u64 +fn my_linked_list(&self) -> LinkedListMapper ``` +Available methods: -### get_prev_block_round +### is_empty ```rust -get_prev_block_round() -> u64 +fn is_empty() -> bool ``` +Returns `true` if the mapper has no elements stored. -### get_prev_block_epoch +### len ```rust -get_prev_block_epoch() -> u64 +fn len() -> usize ``` +Returns the number of items stored in the mapper. -### get_prev_block_random_seed +### clear ```rust -get_prev_block_random_seed() -> ManagedByteArray +fn clear() ``` +Clears all the elements from the mapper. This function can run out of gas for big collections. -### get_block_round_time_millis +### iter ```rust -get_block_round_time_millis(&self) -> DurationMillis +fn iter() -> Iter ``` -The block round time, in milliseconds, i.e the time between consecutive blocks. - - +Returns an iterator over all the stored elements. -### get_current_esdt_nft_nonce +### iter_from_node_id ```rust -get_current_esdt_nft_nonce(address: &ManagedAddress, token_id: &TokenIdentifier) -> u64 +fn iter_from_node_id(node_id: u32) -> Iter ``` -Gets the last nonce for an SFT/NFT. Nonces are incremented after every ESDTNFTCreate operation. - -This only works for accounts that have the ESDTNFTCreateRole set and only for accounts in the same shard as the smart contract. - -This function is usually used with `self.blockchain().get_sc_address()` for smart contracts that create SFT/NFTs themselves. - +Returns an iterator starting from the given `node_id`. Useful when splitting iteration over multiple SC calls. -### get_esdt_balance +### front ```rust -get_esdt_balance(address: &ManagedAddress, token_id: &TokenIdentifier, nonce: u64) -> BigUint +fn front() -> Option> +fn back() -> Option> ``` -Gets the ESDT/SFT/NFT balance for the specified address. - -This only works for addresses that are in the same shard as the smart contract. - -For fungible ESDT, nonce should be 0. For EGLD balance, use the `get_balance` instead. +Returns the first/last element if the list is not empty, `None` otherwise. A `LinkedListNode` has the following format: +```rust +pub struct LinkedListNode { + value: Type, + node_id: u32, + next_id: u32, + prev_id: u32, +} +impl LinkedListNode { + pub fn get_value_cloned(&self) -> Type { + self.value.clone() + } -### get_esdt_token_data + pub fn get_value_as_ref(&self) -> &Type { + &self.value + } -```rust -get_esdt_token_data(address: &ManagedAddress, token_id: &TokenIdentifier, nonce: u64) -> EsdtTokenData -``` + pub fn into_value(self) -> Type { + self.value + } -Gets the ESDT token properties for the specific token type, owned by the specified address. + pub fn get_node_id(&self) -> u32 { + self.node_id + } -`EsdtTokenData` has the following format: + pub fn get_next_node_id(&self) -> u32 { + self.next_id + } -```rust -pub struct EsdtTokenData { - pub token_type: EsdtTokenType, - pub amount: BigUint, - pub frozen: bool, - pub hash: ManagedBuffer, - pub name: ManagedBuffer, - pub attributes: ManagedBuffer, - pub creator: ManagedAddress, - pub royalties: BigUint, - pub uris: ManagedVec>, + pub fn get_prev_node_id(&self) -> u32 { + self.prev_id + } } ``` -`token_type` is an enum, which can have one of the following values: +### pop_front/pop_back ```rust -pub enum EsdtTokenType { - Fungible, - NonFungible, - SemiFungible, - Meta, - Invalid, -} +fn pop_front(&mut self) -> Option> +fn pop_back(&mut self) -> Option> ``` -You will only receive basic distinctions for the token type, i.e. only `Fungible` and `NonFungible` (The smart contract has no way of telling the difference between non-fungible, semi-fungible and meta tokens). - -`amount` is the current owned balance of the account. - -`frozen` is a boolean indicating if the account is frozen or not. - -`hash` is the hash of the NFT. Generally, this will be the hash of the `attributes`, but this is not enforced in any way. Also, the hash length is not fixed either. - -`name` is the name of the NFT, often used as display name in front-end applications. - -`attributes` can contain any user-defined data. If you know the format, you can use the `EsdtTokenData::decode_attributes` method to deserialize them. +Removes and returns the first/last element from the list. -`creator` is the creator's address. -`royalties` a number between 0 and 10,000, meaning a percentage of any selling price the creator receives. This is used in the ESDT NFT marketplace, but is not enforced in any other way. (The way these percentages work is 5,444 would be 54.44%, which you would calculate: price \* 5,444 / 10,000. This convention is used to grant some additional precision) +### push_after/push_before +```rust +pub fn push_after(node: &mut LinkedListNode, element: Type) -> Option> +pub fn push_before(node: &mut LinkedListNode, element: Type) -> Option> +``` -`uris` list of URIs to an image/audio/video, which represents the given NFT. +Inserts the given `element` into the list after/before the given `node`. Returns the newly inserted node if the insertion was successful, `None` otherwise. -This only works for addresses that are in the same shard as the smart contract. -Most of the time, this function is used with `self.blockchain().get_sc_address()` as address to get the properties of a token that is owned by the smart contract, or was transferred to the smart contract in the current executing call. +### push_after_node_id/push_before_node_id +```rust +pub fn push_after_node_id(node_id: usize, element: Type) -> Option> +pub fn push_before_node_id(node_id: usize, element: Type) -> Option> +``` +Same as the methods above, but uses node_id instead of a full node struct. -### get_esdt_local_roles +### push_front/push_back ```rust -get_esdt_local_roles(token_id: &TokenIdentifier) -> EsdtLocalRoleFlags +fn push_front(element: Type) +fn push_back(element: Type) ``` -Gets the ESDTLocalRoles set for the smart contract, as a bitflag. The returned type contains methods of checking if a role exists and iterating over all the roles. +Pushes the given `element` at the front/back of the list. Can be seen as specialized versions of `push_before_node_id` and `push_after_node_id`. -This is done by simply reading protected storage, but this is a convenient function to use. +### set_node_value +```rust +fn set_node_value(mut node: LinkedListNode, new_value: Type) +``` -## Call Value API +Sets a node's value, if the node exists in the list. -This API is accessible through `self.call_value()`. The alternative is to use the `#[payment]` annotations, but we no longer recommend them. They have a history of creating confusion, especially for new users. -Available functions: +### set_node_value_by_id +```rust +fn set_node_value_by_id(node_id: usize, new_value: Type) +``` +Same as the method above, but uses node_id instead of a full node struct. -### egld_value +### remove_node ```rust -egld_value() -> BigUint +fn remove_node(node: &LinkedListNode) ``` -Returns the amount of EGLD transferred in the current transaction. Will return 0 for ESDT transfers. - +Removes the node from the list, if it exists. -### all_esdt_transfers +### remove_node_by_id ```rust -all_esdt_transfers() -> ManagedVec +fn remove_node(node_id: usize) ``` -Returns all ESDT transfers. Useful when you're expecting a variable number of transfers. +Same as the method above, but uses node_id instead of a full node struct. -Returns the payments into a `ManagedVec` of structs, that contain the token type, token ID, token nonce and the amount being transferred: +### iter ```rust -pub struct EsdtTokenPayment { - pub token_identifier: TokenIdentifier, - pub token_nonce: u64, - pub amount: BigUint, -} +fn iter() -> Iter ``` +Returns an iterator over all the stored elements. -### multi_esdt +### iter_from_node_id ```rust -multi_esdt() -> [EsdtTokenPayment; N] +fn iter_from_node_id(node_id: u32) -> Iter ``` -Returns a fixed number of ESDT transfers as an array. Will signal an error if the number of ESDT transfers differs from `N`. +Returns an iterator starting from the given `node_id`. Useful when splitting iteration over multiple SC calls. -For example, if you always expect exactly 3 payments in your endpoint, you can use this function like so: -`let [payment_a, payment_b, payment_c] = self.call_value().multi_esdt();` +## MapMapper -### single_esdt +Stores (key, value) pairs, while also allowing iteration over keys. This is the most expensive mapper to use, so make sure you really need to use it. Keys order is given by their order of insertion (same as `SetMapper`). +Examples: ```rust -single_esdt() -> EsdtTokenPayment +fn my_map(&self) -> MapMapper ``` -Returns the received ESDT token payment if exactly one was received. Will signal an error in case of multi-transfer or no transfer. - +Available methods: -### single_fungible_esdt +### is_empty ```rust -single_fungible_esdt(&self) -> (TokenIdentifier, BigUint) +fn is_empty() -> bool ``` -Similar to the function above, but also enforces the payment to be a fungible ESDT. - +Returns `true` if the mapper has no elements stored. -### egld_or_single_fungible_esdt +### len ```rust -egld_or_single_fungible_esdt(&self) -> (EgldOrEsdtTokenIdentifier, BigUint) +fn len() -> usize ``` -Same as the function above, but also allows EGLD to be received. - +Returns the number of items stored in the mapper. -### egld_or_single_esdt +### contains_key ```rust -egld_or_single_esdt() -> EgldOrEsdtTokenPayment +fn contains_key(k: &KeyType) -> bool ``` -Allows EGLD or any single ESDT token to be received. - - -## Crypto API +Returns `true` if the mapper contains the given key. -This API is accessible through `self.crypto()`. It provides hashing functions and signature verification. Since those functions are widely known and have their own pages of documentation, we will not go into too much detail in this section. -Hashing functions: +### get +```rust +fn get(k: &KeyType) -> Option +``` +Returns `Some(value)` if the key exists. Returns `None` if the key does not exist in the map. -### sha256 +### insert ```rust -sha256(data: &ManagedBuffer) -> ManagedByteArray +fn insert(k: KeyType, v: ValueType) -> Option ``` +Inserts the given key, value pair into the map, and returns `Some(old_value)` if the key was already present. -### keccak256 +### remove ```rust -keccak256(data: &ManagedBuffer) -> ManagedByteArray +fn remove(k: &KeyType) -> Option ``` +Removes the key and the corresponding value from the map, and returns the value. If the key was not present in the map, `None` is returned. -### ripemd160 +### keys/values/iter ```rust -ripemd160(data: &ManagedBuffer) -> ManagedByteArray +fn keys() -> Keys +fn values() -> Values +fn iter() -> Iter ``` -Signature verification functions: +Provides an iterator over all keys, values, and (key, value) pairs respectively. -### verify_ed25519_legacy_managed +# Specialized mappers -```rust -verify_ed25519_legacy_managed(key: &ManagedByteArray, message: &ManagedBuffer, signature: &ManagedByteArray) -> bool -``` +## FungibleTokenMapper -### verify_bls +Stores a token identifier (like a `SingleValueMapper`) and provides methods for using this token ID directly with the most common API functions. Note that most method calls will fail if the token was not issued previously. +Examples: ```rust -verify_bls(key: &[u8], message: &[u8], signature: &[u8]) -> bool +fn my_token_id(&self) -> FungibleTokenMapper ``` +Available methods: -### verify_secp256k1 +### issue/issue_and_set_all_roles ```rust -verify_secp256k1(key: &[u8], message: &[u8], signature: &[u8]) -> bool +fn issue(issue_cost: BigUint, token_display_name: ManagedBuffer, token_ticker: ManagedBuffer,initial_supply: BigUint, num_decimals: usize, opt_callback: Option>) -> ! + +fn issue_and_set_all_roles(issue_cost: BigUint, token_display_name: ManagedBuffer, token_ticker: ManagedBuffer,initial_supply: BigUint, num_decimals: usize, opt_callback: Option>) -> ! ``` +Issues a new fungible token. `issue_cost` is 0.05 EGLD (5000000000000000) at the time of writing this, but since this changed in the past, we've let it as an argument it case it changes again in the future. -### verify_custom_secp256k1 +This mapper allows only one issue, so trying to issue multiple types will signal an error. -```rust -verify_custom_secp256k1(key: &[u8], message: &[u8], signature: &[u8], hash_type: MessageHashType) -> bool +`opt_callback` is an optional custom callback you can use for your issue call. We recommend using the default callback. To do so, you need to import multiversx-sc-modules in your Cargo.toml: +```toml +[dependencies.multiversx-sc-modules] +version = "0.39.0" ``` -`MessageHashType` is an enum, representing the hashing algorithm that was used to create the `message` argument. Use `ECDSAPlainMsg` if the message is in "plain text". +Note: current released multiversx-sc version at the time of writing this was 0.39.0, upgrade if necessary. +Then you should import the `DefaultCallbacksModule` in your contract: ```rust -pub enum MessageHashType { - ECDSAPlainMsg, - ECDSASha256, - ECDSADoubleSha256, - ECDSAKeccak256, - ECDSARipemd160, +#[multiversx_sc::contract] +pub trait MyContract: multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule { + /* ... */ } ``` -To be able to use the hashing functions without dynamic allocations, we use a concept in Rust known as `const generics`. This allows the function to have a constant value as a generic instead of the usual trait types you'd see in generics. The value is used to allocate a static buffer in which the data is copied temporarily, to then be passed to the legacy API. - -To call such a function, the call would look like this: - -```rust -let hash = self.crypto().sha256_legacy_managed::<200>(&data); -``` +Additionally, pass `None` for `opt_callback`. -Where `200` is the max expected byte length of `data`. +Note the "never" type `-> !` as return type for this function. This means this function will terminate the execution and launch the issue async call, so any code after this call will not be executed. +Alternatively, if you want to issue and also have all roles set for the SC, you can use the `issue_and_set_all_roles` method instead. -### encode_secp256k1_der_signature +### mint ```rust -encode_secp256k1_der_signature(r: &[u8], s: &[u8]) -> BoxedBytes +fn mint(amount: BigUint) -> EsdtTokenPayment ``` -Creates a signature from the corresponding elliptic curve parameters provided. - +Mints `amount` tokens for the stored token ID, using the `ESDTLocalMint` built-in function. Returns a payment struct, containing the token ID and the given amount. -## Send API -This API is accessible through `self.send()`. It provides functionalities like sending tokens, performing smart contract calls, calling built-in functions and much more. +### mint_and_send +```rust +fn mint_and_send(to: &ManagedAddress, amount: BigUint) -> EsdtTokenPayment +``` -We will not describe every single function in the API, as that would create confusion. We will only describe those that are recommended to be used (as they're mostly wrappers around more complicated low-level functions). +Same as the method above, but also sends the minted tokens to the given address. -For Smart Contract to Smart Contract calls, use the Proxies, as described in the [contract calls](/developers/transactions/tx-legacy-calls) section. -Without further ado, let's take a look at the available functions: +### burn +```rust +fn burn(amount: &BigUint) +``` +Burns `amount` tokens, using the `ESDTLocalBurn` built-in function. -### direct +### get_balance ```rust -direct(to: &ManagedAddress, token: &EgldOrEsdtTokenIdentifier, nonce: u64, amount: &BigUint) +fn get_balance() -> BigUint ``` -Performs a simple EGLD/ESDT/NFT transfer to the target address, with some optional additional data. If you want to send EGLD, simply pass `EgldOrEsdtTokenIdentifier::egld()`. For both EGLD and fungible ESDT, `nonce` should be 0. - -This will fail if the destination is a non-payable smart contract, but the current executing transaction will only fail if the destination SC is in the same shard, and as such, any changes done to the storage will persist. The tokens will not be lost though, as they will be automatically returned. - -Even though an invalid destination will not revert, an illegal transfer will return an error and revert. An illegal transfer is any transfer that would leave the SC with a negative balance for the specific token. +Gets the current balance the SC has for the token. -If you're unsure about the destination's account type, you can use the `is_smart_contract` function from `Blockchain API`. -If you need a bit more control, use the `direct_with_gas_limit` function instead. +## NonFungibleTokenMapper +Similar to the `FungibleTokenMapper`, but is used for NFT, SFT and META-ESDT tokens. -### direct_egld +### issue/issue_and_set_all_roles ```rust -direct_egld(to: &ManagedAddress, amount: &BigUint) -``` +fn issue(token_type: EsdtTokenType, issue_cost: BigUint, token_display_name: ManagedBuffer, token_ticker: ManagedBuffer,initial_supply: BigUint, num_decimals: usize, opt_callback: Option) -> ! -The EGLD-transfer version for the `direct` function. +fn issue_and_set_all_roles(token_type: EsdtTokenType, issue_cost: BigUint, token_display_name: ManagedBuffer, token_ticker: ManagedBuffer,initial_supply: BigUint, num_decimals: usize, opt_callback: Option) -> ! +``` +Same as the previous issue function, but also takes an `EsdtTokenType` enum as argument, to decide which type of token to issue. Accepted values are `EsdtTokenType::NonFungible`, `EsdtTokenType::SemiFungible` and `EsdtTokenType::Meta`. -### direct_esdt +### nft_create/nft_create_named ```rust -direct_esdt(to: &ManagedAddress, token_id: &TokenIdentifier, token_nonce: u64, amount: &BigUint) -``` +fn nft_create(amount: BigUint, attributes: &T) -> EsdtTokenPayment -The ESDT-only version for the `direct` function. Used so you don't have to wrap `TokenIdentifier` into an `EgldOrEsdtTokenIdentifier`. +fn nft_create_named(amount: BigUint, name: &ManagedBuffer, attributes: &T) -> EsdtTokenPayment +``` +Creates an NFT (optionally with a display `name`) and returns the token ID, the created token's nonce, and the given amount in a payment struct. -### direct_multi +### nft_create_and_send/nft_create_and_send_named ```rust -direct_multi(to: &ManagedAddress, payments: &ManagedVec) -``` +fn nft_create_and_send(to: &ManagedAddress, amount: BigUint, attributes: &T,) -> EsdtTokenPayment -The multi-transfer version for the `direct_esdt` function. Keep in mind you cannot transfer EGLD with this function, only ESDTs. +fn nft_create_and_send_named(to: &ManagedAddress, amount: BigUint, name: &ManagedBuffer, attributes: &T,) -> EsdtTokenPayment +``` +Same as the methods above, but also sends the created token to the provided address. -### change_owner_address +### nft_add_quantity ```rust -change_owner_address(child_sc_address: &ManagedAddress, new_owner: &ManagedAddress) +fn nft_add_quantity(token_nonce: u64, amount: BigUint) -> EsdtTokenPayment ``` -Changes the ownership of target child contract to another address. This will fail if the current contract is not the owner of the `child_sc_address` contract. - -This also has the implication that the current contract will not be able to call `#[only_owner]` functions of the child contract, upgrade, or change owner again. - +Adds quantity for the given token nonce. This can only be used if one of the `nft_create` functions was used before AND the SC holds at least 1 token for the given nonce. -### esdt_local_mint +### nft_add_quantity_and_send ```rust -esdt_local_mint(token: &TokenIdentifier, nonce: u64, amount: &BigUint) +fn nft_add_quantity_and_send(to: &ManagedAddress, token_nonce: u64, amount: BigUint) -> EsdtTokenPayment ``` -Allows synchronous minting of ESDT/SFT (depending on nonce). Execution is resumed afterwards. Note that the SC must have the `ESDTLocalMint` or `ESDTNftAddQuantity` roles set, or this will fail with "action is not allowed". +Same as the method above, but also sends the tokens to the provided address. -For SFTs, you must use `esdt_nft_create` before adding additional quantity. -This function cannot be used for NFTs. +### nft_burn +```rust +fn nft_burn(token_nonce: u64, amount: &BigUint) +``` +Burns `amount` tokens for the given nonce. -### esdt_local_burn +### get_all_token_data ```rust -esdt_local_burn(token: &TokenIdentifier, nonce: u64, amount: &BigUint) +fn get_all_token_data(token_nonce: u64) -> EsdtTokenData ``` -The inverse operation of `esdt_local_mint`, which permanently removes the tokens. Note that the SC must have the `ESDTLocalBurn` or `ESDTNftBurn` roles set, or this will fail with "action is not allowed". - -Unlike the mint function, this can be used for NFTs. +Gets all the token data for the given nonce. The SC must own the given nonce for this function to work. +`EsdtTokenData` contains the following fields: +```rust +pub struct EsdtTokenData { + pub token_type: EsdtTokenType, + pub amount: BigUint, + pub frozen: bool, + pub hash: ManagedBuffer, + pub name: ManagedBuffer, + pub attributes: ManagedBuffer, + pub creator: ManagedAddress, + pub royalties: BigUint, + pub uris: ManagedVec>, +} +``` -### esdt_nft_create +### get_balance ```rust -esdt_nft_create(token: &TokenIdentifier, amount: &BigUint, name: &ManagedBuffer, royalties: &BigUint, hash: &ManagedBuffer, attributes: &T, uris: &ManagedVec< ManagedBuffer>) -> u64 +fn get_balance(token_nonce: u64) -> BigUint ``` -Creates a new SFT/NFT, and returns its nonce. +Gets the SC's balance for the given token nonce. -Must have `ESDTNftCreate` role set, or this will fail with "action is not allowed". -`token` is identifier of the SFT/NFT brand. +### get_token_attributes +```rust +fn get_token_attributes(token_nonce: u64) -> T +``` -`amount` is the amount of tokens to be minted. For NFTs, this should be "1". +Gets the attributes for the given token nonce. The SC must own the given nonce for this function to work. -`name` is the display name of the token, which will be used in explorers, marketplaces, etc. -`royalties` is a number between 0 and 10,000, which represents the percentage of any selling amount the creator receives. This representation is used to be able to have more precision. For example, a percentage like `55.66%` is stored as `5566`. These royalties are not enforced, and will mostly be used in "official" NFT marketplaces. +## Common functions for FungibleTokenMapper and NonFungibleTokenMapper -`hash` is a user-defined hash for the token. Recommended value is sha256(attributes), but it can be anything. +Both mappers work similarly, so some functions have the same implementation for both. -`attributes` can be any serializable user-defined struct, more specifically, any type that implements the `TopEncode` trait. There is no real standard for attributes format at the point of writing this document, but that might change in the future. -`uris` is a list of links to the NFTs visual/audio representation, most of the time, these will be links to images, videos or songs. If empty, the framework will automatically add an "empty" URI. +### is_empty +```rust +fn is_empty() -> bool +``` +Returns `true` if the token ID is not set yet. -### esdt_nft_create_compact +### get_token_id ```rust -esdt_nft_create_compact(token: &TokenIdentifier, amount: &BigUint, attributes: &T) -> u64 +fn get_token_id() -> TokenIdentifier ``` -Same as `esdt_nft_create`, but fills most arguments with default values. Mostly used in contracts that use NFTs as a means of information rather than for display purposes. - +Gets the stored token ID. -### sell_nft +### set_token_id ```rust -sell_nft(nft_id: &TokenIdentifier, nft_nonce: u64, nft_amount: &BigUint, buyer: &ManagedAddress, payment_token: &EgldOrEsdtTokenIdentifier, payment_nonce: u64, payment_amount: &BigUint) -> BigUint +fn set_token_id(token_id: &TokenIdentifier) ``` -Sends the SFTs/NFTs to target address, while also automatically calculating and sending NFT royalties to the creator. Returns the amount left after deducting royalties. - -`(nft_id, nft_nonce, nft_amount)` are the SFTs/NFTs that are going to be sent to the `buyer` address. +Manually sets the token ID for this mapper. This can only be used once, and can not be overwritten afterwards. This will fail if the token was issue previously, as the token ID was automatically set. -`(payment_token, payment_nonce, payment_amount)` are the tokens that are used to pay the creator royalties. -This function's purpose is mostly to be used in marketplace-like smart contracts, where the contract sells NFTs to users. +### require_same_token/require_all_same_token +```rust +fn require_same_token(expected_token_id: &TokenIdentifier) +fn require_all_same_token(payments: &ManagedVec) +``` +Will signal an error if the provided token ID argument(s) differs from the stored token. Useful in `#[payable]` methods when you only want to this token as payment. -### nft_add_uri +### set_local_roles ```rust -nft_add_uri(token_id: &TokenIdentifier, nft_nonce: u64, new_uri: ManagedBuffer) +fn set_local_roles(roles: &[EsdtLocalRole], opt_callback: Option) -> ! ``` -Adds an URI to the selected NFT. The SC must own the NFT and have the `ESDTRoleNFTAddURI` to be able to use this function. +Sets the provided local roles for the token. By default, no callback is used for this call, but you may provide a custom callback if you want to. -If you need to add multiple URIs at once, you can use `nft_add_multiple_uri` function, which takes a `ManagedVec` as argument instead. +You don't need to call this function if you use `issue_and_set_all_roles` for issuing. +Same as the issue function, this will terminate execution when called. -### nft_update_attributes +### set_local_roles_for_address ```rust -nft_update_attributes(token_id: &TokenIdentifier, nft_nonce: u64, new_attributes: &T) +fn set_local_roles_for_address(address: &ManagedAddress, roles: &[EsdtLocalRole], opt_callback: Option) -> ! ``` -Updates the attributes of the selected NFT to the provided value. The SC must own the NFT and have the `ESDTRoleNFTUpdateAttributes` to be able to update the attributes. - - -## Conclusion - -While there are still other various APIs in multiversx-sc, they are mostly hidden from the user. These are the ones you're going to be using in your day-to-day smart contract development. +Similar to the previous function, but sets the roles for a specific address instead of the SC address. ---- -### Smart Contract Call Events +## UniqueIdMapper +A special mapper that holds the values from 1 to N, with the following property: if `mapper[i] == i`, then nothing is actually stored. ---- +This makes it so the mapper initialization is O(1) instead of O(N). Very useful when you want to have a list of available IDs, as its name suggests. -### Smart Contract Calls Data Format +Both the IDs and the indexes are `usize`. -This page provides an in-depth examination of the Smart Contract Calls Data Format. +Note: If you want an in-memory version of this, you can use the `SparseArray` type provided by the framework. +Examples: +```rust +fn my_id_mapper(&self) -> UniqueIdMapper +``` -## Introduction +Available methods: -Besides regular move-balance transactions (address A sends the amount X to address B, while optionally including a note in the `data` field), -MultiversX transactions can trigger a Smart Contract call, or a [built-in function call](/developers/built-in-functions). -This can happen in the following situations: +### set_initial_len +```rust +fn set_initial_len(&mut self, len: usize) +``` -- the receiver of the transaction is a Smart Contract Address and the data field begins with a valid function of the contract. -- the data field of the transaction begins with a valid built-in function name. +Sets the initial mapper length, i.e. the `N`. The length may only be set once. -Calls to Smart Contracts functions (or built-in functions) on MultiversX have the following format: +### is_empty ```rust -ScCallTransaction { - Sender: - Receiver: # can be a SC, or other address in case of built in functions - Value: X # to be determined for each case - GasLimit: Y # to be determined for each case - Data: "functionName" + - "@" + + - "@" + + - ... -} +fn is_empty() -> bool ``` -The number of arguments is specific to each function. +Returns `true` if the mapper has no elements stored. -_Example_. We have a smart contract A with the address `erd1qqqqqqqqqqqqqpgqrchxzx5uu8sv3ceg8nx8cxc0gesezure5awqn46gtd`. The contract -has a function `add(numberToAdd numeric)` which adds the `numberToAdd` to an internally managed sum. If we want to call the -function and add `15` to the internal sum, the transaction would look like: +### len ```rust -ExampleScCallTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqpgqrchxzx5uu8sv3ceg8nx8cxc0gesezure5awqn46gtd - Value: 0 # no value needed for this call - GasLimit: 1_000_000 # let's suppose we need this much gas for calling the function - Data: "add@0f" # call the function add with the argument 15, hex-encoded -} +fn len() -> usize ``` +Returns the number of items stored in the mapper. -### Constraints -Focusing only on the data field of a Smart Contract call / Built-In Function Call, there are some limitation for the function name and the arguments: +### get +```rust +fn get(index: usize) -> usize +``` -- `function name` has to be the plain text name of the function to be called. -- `arguments` must be hexadecimal encoded with an **even number of characters** (eq: `7` - invalid, `07` - valid; `f6f` - invalid, `6f6b` - valid). -- the `function name` and the `arguments` must be separated by the `@` character. +Gets the value for the given index. If the entry is empty, then `index` is returned, as per the mapper's property. -The next section of this page will focus on how different data types have to be encoded in order to be compliant with the desired format. +### set +```rust +fn set(&mut self, index: usize, id: usize) +``` -## How to convert arguments for Smart Contract calls +Sets the value at the given index. The mapper's internal property of `mapper[i] == i` if empty entry is maintained. -There are multiple ways of converting arguments from their original format to the hexadecimal encoding. -For manually created transactions, arguments can be encoded by using tools that can be found online. For example, `hex to string`, `hex to decimal` and so on. +### swap_remove +```rust +fn swap_remove(index: usize) -> usize +``` -For programmatically created transactions, arguments can be encoded by using one of our SDKs (`sdk-js`, `mxpy`, `sdk-go`, `sdk-java`, and so on) or by using built-in components or other libraries -of the language the transaction is created in. +Removes the ID at the given `index` and returns it. Also, the value at `index` is now set the value of the last entry in the map. Length is decreased by 1. -There are multiple ways of formatting the data field: -- manually convert each argument, and then join the function name, alongside the argument via the `@` character. -- use a pre-defined arguments serializer, such as [the one found in sdk-js](https://github.com/multiversx/mx-sdk-js-core/blob/main/src/smartcontracts/argSerializer.ts). -- use sdk-js's [contract calls](/sdk-and-tools/sdk-js/sdk-js-cookbook/#smart-contracts). -- use sdk-cpp's [contract calls](https://github.com/multiversx/mx-sdk-cpp/blob/main/src/smartcontracts/contract_call.cpp). -- and so on +### iter +```rust +fn iter() -> Iter +``` +Provides an iterator over all the IDs. -## Converting bech32 addresses (erd1) -MultiversX uses `bech32` addresses with the HRP `erd`. Therefore, an address would look like: +## Comparisons between the different mappers -`erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th` -:::caution -Converting a bech32 address into hexadecimal encoding _is not_ a simple `string to hex` operation, but requires specialized -tools or helpers. -::: +### SingleValueMapper vs old storage_set/storage_get pairs -There are many smart contract calls (or built-in function calls) that receive an address as one of their arguments. Obviously, -they have to be hexadecimal encoded. +There is no difference between `SingleValueMapper` and the old-school setters/getters. In fact, `SingleValueMapper` is basically a combination between `storage_set`, `storage_get`, `storage_is_empty` and `storage_clear`. Use of `SingleValueMapper` is encouraged, as it's a lot more compact, and has no performance penalty (if, for example, you never use `is_empty()`, that code will be removed by the compiler). -### Examples +### SingleValueMapper vs VecMapper -bech32 --> hex +Storing a `ManagedVec` can be done in two ways: -``` -erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th ---> -0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1 +```rust +#[storage_mapper("my_vec_single")] +fn my_vec_single(&self) -> SingleValueMapper> -erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r ---> -c70cf50b238372fffaf7b7c5723b06b57859d424a2da621bcc1b2f317543aa36 +#[storage_mapper("my_vec_mapper")] +fn my_vec_mapper(&self) -> VecMapper; ``` +Both of those approaches have their merits. The `SingleValueMapper` concatenates all elements and stores them under a single key, while the `VecMapper` stores each element under a different key. This also means that `SingleValueMapper` uses nested-encoding for each element, while `VecMapper` uses top-encoding. -### Converting addresses using online tools - -There are multiple tools that one can use in order to convert an address into hexadecimal encoding: - -- [https://utils.multiversx.com/converters#addresses-bech32-to-hexadecimal](https://utils.multiversx.com/converters#addresses-bech32-to-hexadecimal) - -- [https://slowli.github.io/bech32-buffer](https://slowli.github.io/bech32-buffer) (go to `Data`, select `erd` as Tag and `Bech32` as Encoding) - -- [http://207.244.241.38/elrond-converters/#bech32-to-hex](http://207.244.241.38/elrond-converters/#bech32-to-hex) - +Use `SingleValueMapper` when: +- you need to read the whole array on every use +- the array is expected to be of small length -### Converting addresses using mxpy +Use `VecMapper` when: +- you only require reading a part of the array +- `T`'s top-encoding is vastly more efficient than `T`'s nested-encoding (for example: `u64`) -Make sure you have `mxpy` [installed](/sdk-and-tools/mxpy/installing-mxpy). -```bash -mxpy wallet bech32 --decode erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th -``` +### VecMapper vs SetMapper -will output `0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1`. +The primary use for `SetMapper` is storing a whitelist of addresses, token ids, etc. A token ID whitelist can be stored in these two ways: -Additionally, hex addresses can be converted to bech32 as follows: +```rust +#[storage_mapper("my_vec_whitelist")] +fn my_vec_whitelist(&self) -> VecMapper -```bash -mxpy wallet bech32 --encode 0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1 +#[storage_mapper("my_set_mapper")] +fn my_set_mapper(&self) -> SetMapper; ``` -will output `erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th`. - -The encoding algorithm that handles these conversions can be found [here](https://github.com/multiversx/mx-sdk-py-core/blob/main/multiversx_sdk_core/bech32.py). - - -### Converting addresses using sdk-js - -Find more about `sdk-js` [here](/sdk-and-tools/sdk-js/). +This might look very similar, but the implications of using `VecMapper` for this are very damaging to the potential gas costs. Checking for an item's existence in `VecMapper` is done in O(n), with each iteration requiring a new storage read! Worst case scenario is the Token ID is not in the whitelist and the whole Vec is read. -```js -import { Address } from "@multiversx/sdk-core"; -... +`SetMapper` is vastly more efficient than this, as it provides checking for a value in O(1). However, this does not come without a cost. This is how the storage looks for a `SetMapper` with two elements (this snippet is taken from a scenario test): -const address = Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); -console.log(address.hex()); +```rust +"str:tokenWhitelist.info": "u32:2|u32:1|u32:2|u32:2", +"str:tokenWhitelist.node_idEGLD-123456": "2", +"str:tokenWhitelist.node_idETH-123456": "1", +"str:tokenWhitelist.node_links|u32:1": "u32:0|u32:2", +"str:tokenWhitelist.node_links|u32:2": "u32:1|u32:0", +"str:tokenWhitelist.value|u32:2": "str:EGLD-123456", +"str:tokenWhitelist.value|u32:1": "str:ETH-123456" ``` -will output `0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1`. - -Additionally, hex addresses can be converted to bech32 as follows: +A `SetMapper` uses 3 * N + 1 storage entries, where N is the number of elements. Checking for an element is very easy, as the only thing the mapper has to do is check the `node_id` entry for the provided token ID. -```js -import { Address } from "@multiversx/sdk-core"; -... +Even so, for this particular case, `SetMapper` is way better than `VecMapper`. -const address = Address.fromHex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1"); -console.log(address.bech32()); -``` -will output `erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th`. +### VecMapper vs LinkedListMapper -The encoding algorithm that handles these conversions can be found [here](https://github.com/multiversx/mx-sdk-js-core/blob/main/src/address.ts). +`LinkedListMapper` can be seen as a specialization for the `VecMapper`. It allows insertion/removal only at either end of the list, known as pushing/popping. It's also storage-efficient, as it only requires 2 * N + 1 storage entries. The storage for such a mapper looks like this: +```rust +"str:list_mapper.node_links|u32:1": "u32:0|u32:2", +"str:list_mapper.node_links|u32:2": "u32:1|u32:0", +"str:list_mapper.value|u32:1": "123", +"str:list_mapper.value|u32:2": "111", +"str:list_mapper.info": "u32:2|u32:1|u32:2|u32:2" +``` -### Converting addresses using sdk-go +This is one of the lesser used mappers, as its purpose is very specific, but it's very useful if you ever need to store a queue. -Find more about `sdk-go` [here](/sdk-and-tools/sdk-go/). -```js -import ( - ... - "github.com/multiversx/mx-sdk-go/data" - ... -) +### SingleValueMapper vs MapMapper -addressObj, err := data.NewAddressFromBech32String("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") -if err != nil { - return err -} +Believe it or not, most of the time, `MapMapper` is not even needed, and can simply be replaced by a `SingleValueMapper`. For example, let's say you want to store an ID for every Address. It might be tempting to use `MapMapper`, which would look like this: -fmt.Println(hex.EncodeToString(addressObj.AddressBytes())) +```rust +#[storage_mapper("address_id_mapper")] +fn address_id_mapper(&self) -> MapMapper; ``` -will output `0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1`. +This can be replaced with the following `SingleValueMapper`: +```rust +#[storage_mapper("address_id_mapper")] +fn address_id_mapper(&self, address: &ManagedAddress) -> SingleValueMapper; +``` -Additionally, hex addresses can be converted to bech32 as follows: +Both of them provide (almost) the same functionality. The difference is that the `SingleValueMapper` does not provide a way to iterate over all the keys, i.e. Addresses in this case, but it's also 4-5 times more efficient. -```js -import ( - ... - "ggithub.com/multiversx/mx-sdk-go/data" - ... -) +Unless you need to iterate over all the entries, `MapMapper` should be avoided, as this is the most expensive mapper. It uses 4 * N + 1 storage entries. The storage for a `MapMapper` looks like this: -addressBytes, err := hex.DecodeString("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1") -if err != nil { - return err -} -addressObj := data.NewAddressFromBytes(addressBytes) +```rust +"str:map_mapper.node_links|u32:1": "u32:0|u32:2", +"str:map_mapper.node_links|u32:2": "u32:1|u32:0", +"str:map_mapper.value|u32:1": "123", +"str:map_mapper.value|u32:2": "111", +"str:map_mapper.node_id|u32:123": "1", +"str:map_mapper.node_id|u32:111": "2", +"str:map_mapper.mapped|u32:123": "456", +"str:map_mapper.mapped|u32:111": "222", +"str:map_mapper.info": "u32:2|u32:1|u32:2|u32:2" +``` -fmt.Println(addressObj.AddressAsBech32String()) +Keep in mind that all the mappers can have as many additional arguments for the main key. For example, you can have a `VecMapper` for every user pair, like this: +```rust +#[storage_mapper("list_per_user_pair")] +fn list_per_user_pair(&self, first_addr: &ManagedAddress, second_addr: &ManagedAddress) -> VecMapper; ``` -will output `erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th`. +Using the correct mapper for your situation can greatly decrease gas costs and complexity, so always remember to carefully evaluate your use-case. -The encoding algorithm that handles these conversions can be found [here](https://github.com/multiversx/mx-chain-core-go/blob/main/core/pubkeyConverter/bech32PubkeyConverter.go). +## Accessing a value at an address -### Converting addresses using sdk-java +Because of the way the storage mappers are structured, it is very easy to access a "remote" value, meaning a value stored under a key at a different address than the current one. -Find more about `sdk-java` [here](/sdk-and-tools/mxjava). +```rust +#[storage_mapper("key_example")] +fn content(&self) -> MapMapper; -```java -System.out.println(Address.fromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th").hex()); +#[storage_mapper_from_address("key_example")] +fn content_from_address(&self, address: ManagedAddress, ...) -> SetMapper; ``` -will output `0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1`. - -Additionally, hex addresses can be converted to bech32 as follows: +The `content_from_address` function is used to create a **new map** for accessing the storage of another contract, identified by its address. -```java -System.out.println(Address.fromHex("0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1").bech32()); -``` +The function can have any name, but it is necessary to be tagged with `#[storage_mapper_from_address("key_example")]`, where **"key_example"** is the **exact key** used by the storage they wish to access. -will output `erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th`. -The encoding algorithm that handles these conversions can be found [here](https://github.com/multiversx/mx-sdk-java/blob/main/src/main/java/multiversx/Address.java). +### Parameters +- `&self`: reference to the current instance of the contract. +- `address: ManagedAddress`: required parameter; it specifies the address of the contract whose storage mapper you want to access. +- **optional** extra keys of any type. -## Converting string values -For situations when a string argument is desired for a smart contract call, it can be simply obtained by using -built-in libraries to convert them into hexadecimal format. +### Return type -:::important -Make sure that the result has an even number of characters. -::: +The function will return the desired mapper that will store the data. In addition, `ManagedAddress` will always be added to the end of the list of generics in the storage mapper. -Below you can find some examples: +:::important important +This feature only works `intra-shard`. -:::note -By no means, these code snippets provide a coding guideline; they are more of simple examples on how to perform the necessary actions. +Also note that a remote value found under a key at an address can only be `read`, not modified. ::: -### Examples +### Example -string --> hex +If a developer wanted, for example, to iterate over another contract's `SetMapper`, instead of retrieving the values through a call to an endpoint and then iterating, one could simply create a new `SetMapper` with a specific `address` parameter. Afterwards, the `iter` function can be called easily to accomplish the task. -``` -ok --> 6f6b -MEX-455c57 --> 4d45582d343535633537 +```rust title=contract_to_be_called/lib.rs +#[storage_mapper("my_remote_mapper")] +fn my_set_mapper(&self) -> SetMapper ``` +This is a simple `SetMapper` registered under the address of `contract_to_be_called`, and the value stored will be registered under the key provided, `my_remote_mapper`. If we wanted to iterate over the values of `my_set_mapper` intra-shard, we could write: -### Converting string values in javascript +```rust title=caller_contract/lib.rs +// the address of the contract containing the storage (contract_to_be_called) +#[storage_mapper("contract_address")] +fn contract_address(&self) -> SingleValueMapper; -```js -console.log(Buffer.from("ok").toString("hex")); // 6f6b -``` +// by creating the mapper with the address of the sc and exact storage key +// we get access to the value stored under that key +#[storage_mapper_from_address("contract_address")] +fn contract_from_address(&self, address: ManagedAddress) -> SetMapper; -for converting hex-encoded string to regular string: +#[endpoint] +fn my_endpoint(&self) -> u32 { + let mut sum = 0u32; + let address = self.contract_address().get(); -```js -console.log(Buffer.from("6f6b", "hex").toString()); // ok + for number in self.contract_from_address(address).iter() { + sum += number + } + + sum +} ``` +Calling `my_endpoint` will return the sum of the values stored in `contract_to_be_called`'s SetMapper. -### Converting string values in java +By specifying the expected type, storage key and address, the value can be read and used inside our logic. -```java -String inputHex = Hex.encodeHexString("ok".getBytes(StandardCharsets.UTF_8)); -if (inputHex.length() % 2 != 0) { - inputHex = "0" + inputex; -} +--- -System.out.println(inputHex); // 6f6b -``` +### Storage mappers: which to pick, when -for converting hex-encoded string to regular string: +A working, tested contract exercising six storage mappers side by side, plus a +decision table built from the real `multiversx-sc` crate source doc comments. Two +easy-to-get-wrong entries (a mapper's `contains()` complexity and its storage +cost) are called out below in "Two clarifications from the crate source". -```java -byte[] bytes = Hex.decodeHex("6f6b".toCharArray()); +## Prerequisites -String result = new String(bytes, StandardCharsets.UTF_8); // ok -``` +- Rust via `rustup`, with the `wasm32v1-none` target installed. +- `sc-meta` (`cargo install multiversx-sc-meta`). +- Familiarity with + [New contract from sc-meta new --template empty](/sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/new-contract-from-template), + this recipe assumes that scaffolding workflow. +## The contract -### Converting string values in go +```rust title="storage-mappers/src/storage_mappers.rs" +#![no_std] -```go -fmt.Println(hex.EncodeToString([]byte("ok"))) // 6f6b -``` +use multiversx_sc::imports::*; -for converting hex-encoded string to regular string: +pub mod storage_mappers_proxy; -```go -decodedInput, err := hex.DecodeString("6f6b") -if err != nil { - return err -} +/// One endpoint pair per mapper type this recipe covers with working, tested +/// code. Method names and complexity claims below are taken directly from the +/// real `multiversx-sc` crate source doc comments +/// (`~/.cargo/registry/.../multiversx-sc-0.64.2/src/storage/mappers/*.rs`). +#[multiversx_sc::contract] +pub trait StorageMappers { + #[init] + fn init(&self) {} -fmt.Println(string(decodedInput)) // ok -``` + #[upgrade] + fn upgrade(&self) {} + // ---- SingleValueMapper: one value, no parameters. Simplest mapper, + // baseline for storage cost (1 entry). ---- + #[endpoint(setCounter)] + fn set_counter(&self, value: BigUint) { + self.counter().set(value); + } -## Converting numeric values + #[view(getCounter)] + #[storage_mapper("counter")] + fn counter(&self) -> SingleValueMapper; -For situations when a numeric argument is desired for a smart contract call, it can be simply obtained by using -built-in libraries to convert them into hexadecimal format. + // ---- VecMapper: ordered, 1-indexed, allows duplicates, random + // access by index. ---- + #[endpoint(pushItem)] + fn push_item(&self, item: ManagedBuffer) -> usize { + self.items().push(&item) + } -:::important -Make sure that the result has an even number of characters. -::: + #[view(getItem)] + fn get_item(&self, index: usize) -> ManagedBuffer { + self.items().get(index) + } -Below you can find some examples. They use big integer / number libraries to ensure the code works for large values as well: + #[view(itemCount)] + fn item_count(&self) -> usize { + self.items().len() + } -:::note -By no means, these code snippets provide a coding guideline; they are more of simple examples on how to perform the necessary actions. -::: + #[storage_mapper("items")] + fn items(&self) -> VecMapper; + // ---- SetMapper: ordered (insertion order) set. The crate source confirms + // O(1) contains via an internal value->node_id lookup. ---- + #[endpoint(addToOrderedSet)] + fn add_to_ordered_set(&self, value: u64) -> bool { + self.ordered_set().insert(value) + } -### Examples + #[view(orderedSetContains)] + fn ordered_set_contains(&self, value: u64) -> bool { + self.ordered_set().contains(&value) + } -numeric --> hex + #[view(orderedSetLen)] + fn ordered_set_len(&self) -> usize { + self.ordered_set().len() + } -``` -7 --> 07 -10 --> 0a -35 --> 23 -``` + #[storage_mapper("ordered_set")] + fn ordered_set(&self) -> SetMapper; + // ---- UnorderedSetMapper: no ordering guarantee, O(1) contains via + // VecMapper + a reverse index lookup (2N+1 entries total). ---- + #[endpoint(addToUnorderedSet)] + fn add_to_unordered_set(&self, value: u64) -> bool { + self.unordered_set().insert(value) + } -### Converting numeric values in javascript + #[view(unorderedSetContains)] + fn unordered_set_contains(&self, value: u64) -> bool { + self.unordered_set().contains(&value) + } -```js -const intValue = 37; -const bn = new BigNumber(intValue, 10); -let bnStr = bn.toString(16); -if (bnStr.length % 2 != 0) { - bnStr = "0" + bnStr; -} -console.log(bnStr); // 25 -``` + #[view(unorderedSetLen)] + fn unordered_set_len(&self) -> usize { + self.unordered_set().len() + } -for converting hex-encoded string to regular number: + #[storage_mapper("unordered_set")] + fn unordered_set(&self) -> UnorderedSetMapper; -```js -const hexValue = "25"; -let bn = new BigNumber(hexValue, 16); -console.log(bn.toString()); // 37 -``` + // ---- WhitelistMapper: membership-only, no iteration, most + // space-efficient of the set-shaped mappers. ---- + #[endpoint(addToWhitelist)] + fn add_to_whitelist(&self, address: ManagedAddress) { + self.whitelist().add(&address); + } -Also, `sdk-js` includes some [utility functions](https://github.com/multiversx/mx-sdk-js-core/blob/main/src/utils.codec.ts) for padding the results. + #[view(isWhitelisted)] + fn is_whitelisted(&self, address: ManagedAddress) -> bool { + self.whitelist().contains(&address) + } + #[storage_mapper("whitelist")] + fn whitelist(&self) -> WhitelistMapper; -### Converting numeric values in go + // ---- MapMapper: key-value with iteration, HashMap-like API. Uses a + // SetMapper internally for key tracking plus separate value + // storage — the crate source confirms the 4N+1 entries this costs. ---- + #[endpoint(setBalance)] + fn set_balance(&self, address: ManagedAddress, amount: BigUint) { + self.balances().insert(address, amount); + } -```go -inputNum := int64(37) + #[view(getBalance)] + fn get_balance(&self, address: ManagedAddress) -> BigUint { + self.balances().get(&address).unwrap_or_default() + } -bi := big.NewInt(inputNum) -fmt.Println(hex.EncodeToString(bi.Bytes())) // 25 + #[view(hasBalanceEntry)] + fn has_balance_entry(&self, address: ManagedAddress) -> bool { + self.balances().contains_key(&address) + } + + #[storage_mapper("balances")] + fn balances(&self) -> MapMapper; +} ``` -for converting hex-encoded number to regular number: +## The decision table -```go -hexString := "25" +| Mapper | Ordering | Membership check | Storage entries for N items | Iterable? | Pick it when | +| --- | --- | --- | --- | --- | --- | +| `SingleValueMapper` | n/a (one value) | n/a | 1 | n/a | You need exactly one value: a counter, a config flag, a total. | +| `VecMapper` | Insertion order | Linear scan only | N + 1 | Yes, 1 to `len()` | Ordered, indexable, append-friendly storage without fast membership checks. **Indexes start at 1, not 0.** | +| `SetMapper` | Insertion order (doubly-linked internally) | **O(1)**, confirmed from the crate source | ~3N + 1 | Yes, in insertion order, plus `next()`/`previous()` | You need both ordered iteration AND fast membership checks. | +| `UnorderedSetMapper` | None | O(1) | 2N + 1 (the reverse-lookup keys are easy to undercount as N+1) | Yes, arbitrary order | Fast membership checks, order does not matter: deduping, a processed-IDs set. | +| `WhitelistMapper` | n/a | O(1), most storage-efficient | N | **No**, cannot enumerate at all | "Is X allowed?" only, never "list everyone allowed." | +| `MapMapper` | Insertion order of keys | O(1) via `contains_key()` | ~4N + 1 | Yes: `.iter()`, `.keys()`, `.values()` | A real key-value store with iteration: balances, per-user settings. | +| `LinkedListMapper` | Insertion order, efficient front/back ops | Not built in | ~2N + 1 | Yes | Efficient push/pop from both ends. Not exercised with working code here, see "What this recipe did not test." | -decodedHex, err := hex.DecodeString(hexString) -if err != nil { - return err -} -bi := big.NewInt(0).SetBytes(decodedHex) -fmt.Println(bi.String()) // 37 -``` +The storage-cost column is expressed in unique storage keys, not bytes; the point +is relative ordering between choices for the same logical data, not an absolute +gas number. ---- +## Two clarifications from the crate source -### Smart Contract Debugging +Read directly from `multiversx-sc-0.64.2`'s source doc comments: -## Introduction +:::danger[SetMapper's contains() is O(1), not O(n)] +It is easy to assume `SetMapper.contains()` is O(n) because the mapper keeps +insertion order, but the crate source's doc comment states plainly: +`"Contains: contains(value) - Checks membership. O(1) with one storage read."`, +listing `"O(1) insert, remove, and contains"` as a Pro. `SetMapper` maintains a +separate value→node-ID lookup specifically to make `contains()` O(1); that is why +its storage layout is more complex than a plain ordered list. +::: -Debugging smart contracts is possible with the integrated debugger in Visual Studio Code. You will be able to debug your contract just like you would debug a regular program. +:::danger[UnorderedSetMapper costs 2N+1 entries, not N+1] +It is easy to undercount this as N+1. The real storage layout has value storage +(`.len` + `.item{index}`, the N+1) AND a separate `.index{encoded_value}` +reverse-lookup key per element, which is what actually delivers O(1) +`contains()`. You cannot get O(1) membership testing from N+1 keys with no reverse +index. +::: +Everything else this recipe independently checked against the crate source held +up, including `MapMapper`'s "4N+1 entries (expensive!)", confirmed by reading how +it is built on top of `SetMapper` internally plus its own value storage. -## Prerequisites +## Tests -For this tutorial, you will need: -- Visual Studio Code -- the [rust-analyser](https://marketplace.visualstudio.com/items?itemName=matklad.rust-analyzer) extension. -- the [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) extension. -- A [Rust test](rust/sc-blackbox-example) +```rust title="storage-mappers/tests/storage_mappers_blackbox_test.rs" +// tests/storage_mappers_blackbox_test.rs — one test per mapper this recipe +// covers, each proving the specific behavioral claim its decision-table entry +// makes (not just "it compiles"). -If you want to follow along, you can clone the [mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs) repository and use the [crowdfunding](https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples/crowdfunding) example. +use multiversx_sc_scenario::imports::*; +use storage_mappers::storage_mappers_proxy; -## Step by step debugging +const OWNER: TestAddress = TestAddress::new("owner"); +const CONTRACT: TestSCAddress = TestSCAddress::new("storage-mappers-contract"); +const CODE_PATH: MxscPath = MxscPath::new("output/storage-mappers.mxsc.json"); -In VSCode, you can put breakpoints anywhere in your code, by simply clicking to the left of the line number. A red dot should appear to mark the breakpoint as registered by the environment: +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + blockchain.register_contract(CODE_PATH, storage_mappers::ContractBuilder); + blockchain +} -![img](/developers/sc-debugging/breakpoint_setup.png) +fn deploy(world: &mut ScenarioWorld) { + world.account(OWNER).nonce(1); + world + .tx() + .from(OWNER) + .typed(storage_mappers_proxy::StorageMappersProxy) + .init() + .code(CODE_PATH) + .new_address(CONTRACT) + .run(); +} -Once you've done that, you can debug your test function by pressing the `Debug` button above the test function name: +#[test] +fn single_value_mapper_set_and_get() { + let mut world = world(); + deploy(&mut world); -![img](/developers/sc-debugging/start_test.png) + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .set_counter(BigUint::::from(42u64)) + .run(); -If it doesn't appear, you might have to wait a bit for rust-analyser to load, or you might've forgotten the `#[test]` annotation. + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .counter() + .returns(ExpectValue(BigUint::::from(42u64))) + .run(); +} -Once you've started the test, it should stop at the breakpoint and highlight the current line for you: +#[test] +fn vec_mapper_is_one_indexed() { + let mut world = world(); + deploy(&mut world); -![img](/developers/sc-debugging/first_step_debugging.png) + // Push three items; VecMapper's own doc comment says indexes start + // at 1 — confirm index 1 is the FIRST push, not the second. + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .push_item(ManagedBuffer::::from(b"first")) + .run(); + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .push_item(ManagedBuffer::::from(b"second")) + .run(); -Then, you can use VSCode's step by step debugging (usually F10 to step over, F11 to step into, or shift + F11 to step out). + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .item_count() + .returns(ExpectValue(2usize)) + .run(); + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .get_item(1usize) + .returns(ExpectValue(ManagedBuffer::::from(b"first"))) + .run(); -## Inspecting variables + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .get_item(2usize) + .returns(ExpectValue(ManagedBuffer::::from(b"second"))) + .run(); +} -For base Rust types, like u64 and such, you will be able to simply hover over them and see the value. +#[test] +fn set_mapper_contains_and_ordering() { + let mut world = world(); + deploy(&mut world); -You might however, try to hover over the `target` variable for instance, and will be immediately disappointed, since all you'll see is something like this: + for value in [30u64, 10u64, 20u64] { + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .add_to_ordered_set(value) + .run(); + } -```rust -handle:0 -_phantom:{...} -``` + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .ordered_set_contains(10u64) + .returns(ExpectValue(true)) + .run(); -This is not very helpful. Unfortunately, for managed types you don't have the actual data in the type itself, you only have a handle (i.e. an index) in a stack somewhere. + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .ordered_set_contains(99u64) + .returns(ExpectValue(false)) + .run(); -For that reason, we have the `sc_print!` macro: + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .ordered_set_len() + .returns(ExpectValue(3usize)) + .run(); +} -```rust -sc_print!("{}", target); -``` +#[test] +fn unordered_set_mapper_contains_after_insert_and_absent_value() { + let mut world = world(); + deploy(&mut world); -Adding this line to the beginning of the `#[init]` function will print `2000` in the console. + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .add_to_unordered_set(7u64) + .run(); + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .unordered_set_contains(7u64) + .returns(ExpectValue(true)) + .run(); -## Printing formatted messages + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .unordered_set_contains(8u64) + .returns(ExpectValue(false)) + .run(); -If you want to print other data types, maybe even with a message, you can use the `sc_print!` macro all the same. + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .unordered_set_len() + .returns(ExpectValue(1usize)) + .run(); +} -For example, if you were to add this to the start of the `#[init]` function: -```rust -sc_print!( - "I accept {}, a number of {}, and only until {}", - token_identifier, - target, - deadline -); -``` +#[test] +fn whitelist_mapper_membership_only() { + let mut world = world(); + deploy(&mut world); + let allowed: TestAddress = TestAddress::new("allowed-user"); + let stranger: TestAddress = TestAddress::new("stranger"); + world.account(allowed).nonce(1); + world.account(stranger).nonce(1); -This macro would print the following: + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .add_to_whitelist(allowed.to_address()) + .run(); -`"I accept CROWD-123456, a number of 2000, and only until 604800"` + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .is_whitelisted(allowed.to_address()) + .returns(ExpectValue(true)) + .run(); -Note: For ASCII or decimal representation, use `{}`, and for hex, use `{:x}`. + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .is_whitelisted(stranger.to_address()) + .returns(ExpectValue(false)) + .run(); +} ---- +#[test] +fn map_mapper_insert_get_and_contains_key() { + let mut world = world(); + deploy(&mut world); + let holder: TestAddress = TestAddress::new("balance-holder"); + let nobody: TestAddress = TestAddress::new("no-balance"); + world.account(holder).nonce(1); + world.account(nobody).nonce(1); -### Smart Contract Deploy Events + world + .tx() + .from(OWNER) + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .set_balance(holder.to_address(), BigUint::::from(500u64)) + .run(); -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .get_balance(holder.to_address()) + .returns(ExpectValue(BigUint::::from(500u64))) + .run(); + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .has_balance_entry(holder.to_address()) + .returns(ExpectValue(true)) + .run(); -Contract deploy events are generated when a transaction involves either the deployment of a -smart contract or an upgrade to an existing contract. + // A key that was never inserted: contains_key is false, and the + // convenience getter's unwrap_or_default() reads as zero rather than + // erroring — a real design choice worth testing explicitly, since it + // means "balance of zero" and "never had an entry" are + // indistinguishable through get_balance alone. + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .has_balance_entry(nobody.to_address()) + .returns(ExpectValue(false)) + .run(); -### Contract deploy event + world + .query() + .to(CONTRACT) + .typed(storage_mappers_proxy::StorageMappersProxy) + .get_balance(nobody.to_address()) + .returns(ExpectValue(BigUint::::from(0u64))) + .run(); +} +``` -The contract deploy event is generated upon the successful execution of a transaction that includes -the deployment of a smart contract, without encountering any errors. +```bash +cargo test +``` +8/8 passing: the 6 tests above (one per mapper, each proving the specific claim in +the table: `VecMapper`'s first push lands at index 1, +`SetMapper` / `UnorderedSetMapper.contains()` returns correctly for present and +absent values, `WhitelistMapper` checked via `contains()` only, `MapMapper`'s +zero-default trap), plus the 2 scaffold-provided scenario tests. - - +Every method name used (`.insert()`, `.contains()`, `.contains_key()`, `.add()`, +`.push()`) was copied from the real crate source's own doc-comment examples, worth +calling out since, for instance, `SetMapper` / `UnorderedSetMapper` both use +`.contains()` while `MapMapper` uses `.contains_key()` instead, an easy name to +get wrong by assuming symmetry. -| Field | Value | -|------------|------------------------------------------------------------------------------------------------------------------------| -| identifier | SCDeploy | -| address | the address of the deployed contract | -| topics | `topics[0]` - the address bytes of the deployed contract base64 encoded
`topics[1]` - the address bytes of the deployer of the smart contract base64 encoded
`topics[2]` - the code hash bytes of the deployer smart contract base64 encoded | -| data | empty | +## Pitfalls -
- +:::danger[Pitfall 1: VecMapper is 1-indexed] +Index 0 is invalid and panics. Confirmed directly: this recipe's test pushes two +items and reads back index 1 as the first one pushed. +::: -```json -{ - { - "address": "erd1qqqqqqqqqqqqqpgqnnl9nn0kuuckhg24g02hq2745n4jk2hp327qcay4nm", - "identifier": "SCDeploy", - "topics": [ - "AAAAAAAAAAAFAJz+Wc325zFroVVD1XAr1aTrKyrhirw=", - "NRl7AwoM3hEPC0t9RTDy7gdJUSJvKC5dpJwLYaHLirw=", - "bJtNdzjeaYecInf/NpHzSjHJEZ2l6hR/uJh0NkLIe+k=" - ], - "data": null - } -} -``` +:::warning[Pitfall 2: MapMapper.get() returning a default value looks identical to a real stored zero] +If "never set" and "set to zero" need to be distinguishable, check +`contains_key()` explicitly rather than trusting a default-valued read. +::: - -
+:::note[Pitfall 3: SetMapper and UnorderedSetMapper both offer O(1) contains()] +The deciding factor between them is ordering and storage cost, not lookup speed. +Pick `UnorderedSetMapper` unless you specifically need insertion-order iteration +or `next()`/`previous()` navigation. +::: +:::danger[Pitfall 4: WhitelistMapper cannot list its members at all] +Not even inefficiently. If you might ever need to enumerate, use `SetMapper` or +`UnorderedSetMapper` from the start; there is no way to add enumeration later +without migrating storage. +::: -### Contract upgrade event +:::note[Pitfall 5: method names differ across the mapper family] +`SetMapper` / `UnorderedSetMapper.contains()` vs `MapMapper.contains_key()`, check +the exact mapper's own method names rather than assuming consistency. +::: -The contract upgrade event is generated when a transaction, involving an upgrade, is successfully executed without any errors. +## What this recipe did not test +`LinkedListMapper`, `QueueMapper`, `UserMapper`, `UniqueIdMapper`, and `BiDiMapper` +are real, exported mapper types, but this recipe does not ship working code +exercising them: six mappers with real, tested endpoints was already substantial +scope. `FungibleTokenMapper` / `NonFungibleTokenMapper` / `TokenAttributesMapper` +need real ESDT system contract interaction to demonstrate meaningfully, which +belongs in a token-issuance-from-a-contract recipe, not a storage-mapper +comparison. - - +## See also -| Field | Value | -|------------|------------------------------------------------------------------------------------------------------------------------| -| identifier | SCUpgrade | -| address | the address of the deployed contract | -| topics | `topics[0]` - the address bytes of the upgraded contract base64 encoded
`topics[1]` - the address bytes of the upgrader of the smart contract base64 encoded
`topics[2]` - the code hash bytes of the upgraded smart contract base64 encoded | -| data | empty | +- [New contract from sc-meta new --template empty](/sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/new-contract-from-template) + is the scaffolding this recipe's contract builds on. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is the dApp side that would eventually call an endpoint reading from one of + these mappers. -
- +--- -```json -{ - "address": "erd1qqqqqqqqqqqqqpgqnnl9nn0kuuckhg24g02hq2745n4jk2hp327qcay4nm", - "identifier": "SCUpgrade", - "topics": [ - "AAAAAAAAAAAFAJz+Wc325zFroVVD1XAr1aTrKyrhirw=", - "NRl7AwoM3hEPC0t9RTDy7gdJUSJvKC5dpJwLYaHLirw=", - "kUVJtdwvHG2sCTi9l2uneSONUVonWfgHCK69gdB+52o=" - ], -} -``` +### System Delegation Events + + +--- + +### System Smart Contracts - -
+For transactions which call System Smart Contracts, the **actual gas cost** of processing contains the two previously mentioned cost components - and they are easily computable. +For more details, please follow: -### Change owner event + - [The Staking Smart Contract](/validators/staking/staking-smart-contract) + - [The Delegation Manager](/validators/delegation-manager) + - [ESDT tokens](/tokens/fungible-tokens) + - [NFT tokens](/tokens/nft-tokens) -The `ChangeOwnerAddress` event is generated upon the successful execution of a transaction that specifically involves -a `ChangeOwnerAddress` built-in function call, and this execution must occur without encountering any errors. +--- +### tags - - +This page describes the structure of the `tags` index (Elasticsearch), and also depicts a few examples of how to query it. -| Field | Value | -|------------|------------------------------------------------------------------------------------------------------------------------| -| identifier | ChangeOwnerAddress | -| address | the address of the contract | -| topics | `topics[0]` - the address bytes of the new contract owner base64 encoded | -| data | empty | - - +## _id -```json -{ - "address": "erd1qqqqqqqqqqqqqpgqnnl9nn0kuuckhg24g02hq2745n4jk2hp327qcay4nm", - "identifier": "ChangeOwnerAddress", - "topics": [ - "UKAg0hORMjk0oT6RalZp1w0Xulvvj0Wa/SSYstBepao=" - ], - "data": null -} -``` +The `_id` field of this index is represented by the tag name in a base64 encoding. - - ---- +## Fields -### Smart contract interactions -Let's dive deeper into smart contract interactions and what you need to know to interact with a contract. If you followed the [previous `mxpy`](/docs/sdk-and-tools/mxpy/mxpy-cli.md) related documentation, you should be able to set up your prerequisites like proxy URL, the chain ID and the PEM file. +| Field | Description | +|-------|---------------------------------------------------------------------| +| count | The count field represents the number of NFTs with the current tag. | +| tag | This field represents the tag in an alphanumeric format. | -For this, we need a file inside the contract's folder, with a suggestive name. For example: `devnet.snippets.sh`. -:::important -In order to be able to call methods from the file, we need to assign the shell file as a source file in the terminal. We can do this by running the next command: +## Query examples -```shell -source devnet.snippets.sh + +### Fetch NFTs count with a given tag + +``` +curl --request GET \ + --url ${ES_URL}/tags/_search \ + --header 'Content-Type: application/json' \ + --data '{ + "query": { + "match": { + "tag":"sport" + } + } +}' ``` -After each change to the interactions file, we need to repeat the source command. -::: +--- -Let's take the following example: +### Test setup -1. We want to **deploy** a new smart contract on the Devnet. -2. We then need to **upgrade** the contract, to make it payable. -3. We **call** an endpoint without transferring any assets. -4. We **transfer** ESDT, in order to call a payable endpoint. -5. We call a **view** function. +[comment]: # "mx-abstract" +## Overview -## Prerequisites -Before starting this tutorial, make sure you have the following: +### Registering contracts -- [`mxpy`](/sdk-and-tools/mxpy/mxpy-cli). Follow the [installation guide](/sdk-and-tools/mxpy/installing-mxpy) - make sure to use the latest version available. -- `stable` **Rust** version `≥ 1.83.0`. Follow the [installation guide](/docs/developers/toolchain-setup.md#installing-rust-and-sc-meta). -- `sc-meta`. Follow the [installation guide](/docs/developers/toolchain-setup.md#installing-rust-and-sc-meta). +Since we don't have native execution in the Rust backend yet, the only way to run contracts is to register the contract implementation for the given contract code identifier. In simpler words, we tell the environment "whenever you encounter this contract code, run this code that I've written instead". +Since this operation is specific to only the Rust debugger, it doesn't go through the mandos pipeline. -## Deploy -First things first. In order to deploy a new contract, we need to use `sc-meta` to build it, in the contract root, by invoking the next command: -```shell -sc-meta all build -``` +### Setting accounts -This will output the WASM bytecode, to be used within the interactions file: +Setting accounts in blackbox tests can be easily done by using a `SetStateBuilder`. In order to create an instance of the builder, we have to call the `.account(...)` method from `ScenarioWorld`. -```shell -WASM_PATH="~/my-contract/output/my-contract.wasm" +```rust title=blackbox_test.rs +world // ScenarioWorld struct + .account(USER_ADDRESS) // SetStateBuilder with USER_ADDRESS account + .nonce(1) // custom nonce + .balance(50) // egld balance + .esdt_balance(TRANSFER_TOKEN, 1000) // esdt balance + .esdt_nft_balance(NFT_TOKEN_ID, 1u64, 1u64, ManagedBuffer::new()); // nft balance ``` -Now, in order to deploy the contract, we use the special **deploy** function of `mxpy`, that deploys the contract on the appointed chain, and runs the **init** function of the contract. +There are no mandatory fields, so we can only add the fields that we actually need. For example, if we only need to create the account and we don't care about other fields, `world.account(ADDRESS)` will compile. -```shell -WALLET_PEM="~/my-wallet/my-wallet.pem" -PROXY="https://devnet-gateway.multiversx.com" +However, there is no possibility to upgrade and existing account, so there can only be one set block per account. -deploySC() { - mxpy --verbose contract deploy \ - --bytecode=${WASM_PATH} \ - --pem=${WALLET_PEM} \ - --proxy=${PROXY} \ - --arguments $1 $2 \ - --send || return -} +We can also chain the set state declarations (if useful) as such: + +```rust title=blackbox_test.rs + world + .account(first) // SetStateBuilder for account `first` + .nonce(1) + .balance(100) + .account(second) // SetStateBuilder for `second`, ends set state for `first` + .nonce(2) + .balance(300) + .esdt_balance(TOKEN_ID, 500); // end set state for `second` ``` -Run in terminal the following command to deploy the smart contract on Devnet. Replace `arg1` and `arg2` with your desired deployment values. -```shell -source devnet.snippets.sh -deploySC arg1 arg2 -``` +### Checking accounts -Now let's look at the structure of the interaction. It receives the path of the **wasm file**, where we previously built the contract. It also receives the path of the **wallet** (the PEM file) and the **proxy URL** where the contract will be deployed. +Similar to setting accounts, the framework provides a `CheckStateBuilder` that we can use to check state values. The check builder is instantiated using `.check_account(...)` from `ScenarioWorld`. -Other than this, we also have the **arguments** keyword, that allows us to pass in the required parameters. As we previously said, deploying a smart contract means that we run the **init** function, which may or may not request some parameters. In our case, the **init** function has two different arguments, and we pass them when calling the **deploy** function. We'll come back later in this section at how we can pass parameters in function calls. +```rust title=blackbox_test.rs + world + .check_account(first) // CheckStateBuilder for `first` + .nonce(3) + .balance(100); +``` -After the transaction is sent, `mxpy` will output information like the **transaction hash**, **data** and any other important information, based on the type of transaction. In case of a contract deployment, it will also output the **newly deployed contract address**. +The same rules apply when chaining multiple account checks as for chaining accounts set. +```rust title=blackbox_test.rs + world + .check_account(first) // CheckStateBuilder for `first` + .nonce(3) + .balance(100); + .check_account(second) // CheckStateBuilder for `second`, ends check state for `first` + .check_storage("str:sum", "6"); +``` -## Upgrade -Let's now suppose we need to make the contract **payable**, in case it needs to receive funds. We could redeploy the contract but that will mean two different contracts, and not to mention that we will lose any existing storage. For that, we can use the **upgrade** command, that replaces the existing smart contract bytecode with the newly built contract version. +### Mandos trace -:::caution -It is import to handle data storage with caution when upgrading a smart contract. Data structure, especially for complex data types, must be preserved, otherwise the data may become corrupt. -::: +A mandos [trace](../rust/mandos-trace) can quickly be generated by wrapping the integration test logic into the trace generation as such: -The upgrade function would look like this: +```rust title=blackbox_test.rs + world.start_trace(); -```shell -CONTRACT_ADDRESS="erd1qqqqqqqqqqqqqpgqspymnxmfjve0vxhmep5vr3tf6sj8e80dd8ss2eyn3p" + // integration test logic -upgradeSC() { - mxpy --verbose contract upgrade ${CONTRACT_ADDRESS} --metadata-payable \ - --bytecode=${WASM_PATH} \ - --pem=${WALLET_PEM} \ - --proxy=${PROXY} \ - --arguments $1 $2 \ - --send || return -} + world.write_scenario_trace("trace1.scen.json"); ``` -`CONTRACT_ADDRESS` is a placeholder value which value needs to be replaced with the address previously generated in the deploy action. +--- -Here we have 2 new different elements that we need to observe: +### Testing in Go -1. We changed the **deploy** function with the **upgrade** function. This new function requires the address of the previously deployed smart contract so the system can identify which contract to update. It is important to note that this function can only be called by the smart contract's owner. -2. The **metadata-payable** keyword, which represents a [code metadata](/docs/developers/data/code-metadata.md) flag that allows the smart contract to receive payments. +At some point in the past we built some testing solutions in Go. They were never very popular, but are worth mentioning. +This page is currently a stub. If there is interest in this type of testing, we will expand it further. -## Non payable endpoint interaction -Let's suppose we want to call the following endpoint, that receives an address and three different `BigUint` arguments, in this specific order. +## **Embedding in Go** -```shell -###PARAMS -# $1 = FirstBigUintArgument -# $2 = SecondBigUintArgument -THIRD_BIGUINT_ARGUMENT=0x0f4240 -ADDRESS_ARGUMENT=erd14nw9pukqyqu75gj0shm8upsegjft8l0awjefp877phfx74775dsq49swp3 +Scenario steps can be embedded in Go, in order to program for more flexible behavior. One can even save dynamically generated scenarios. For a comprehensive example on how to do that, check out the [delegation contract fuzzer in MultiversX VM](https://github.com/multiversx/mx-chain-vm-go/tree/master/fuzz/delegation) or the [DNS contract deployment scenario test generator](https://github.com/multiversx/mx-chain-vm-go/tree/master/cmd/testgen/dns). Just a snippet from the fuzzer: -myNonPayableEndpoint() { - address_argument="0x$(mxpy wallet bech32 --decode ${ADDRESS_ARGUMENT})" - mxpy --verbose contract call ${CONTRACT_ADDRESS} \ - --pem=${WALLET_PEM} \ - --proxy=${PROXY} \ - --function="myNonPayableEndpoint" \ - --arguments $address_argument $1 $2 ${THIRD_BIGUINT_ARGUMENT}\ - --send || return -} +```rust +_, + (err = pfe.executeTxStep( + fmt.Sprintf( + ` + { + "step": "scDeploy", + "txId": "-deploy-", + "tx": { + "from": "''%s", + "value": "0", + "contractCode": "file:delegation.wasm", + "arguments": [ + "''%s", + "%d", + "%d", + "%d" + ], + "gasLimit": "100,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "", + "logs": [], + "gas": "*", + "refund": "*" + } + }`, + string(pfe.ownerAddress), + string(pfe.auctionMockAddress), + args.serviceFee, + args.numBlocksBeforeForceUnstake, + args.numBlocksBeforeUnbond + ) + )); ``` -So, what happens in this interaction and how do we call it? - -Besides the function and arguments parts, the snippet is more or less the same as when deploying or upgrading a contract. When calling a **non payable** function, we need to provide the **endpoint's name** as the function argument. As for the arguments, they have to be in the **same order** as in the endpoint's signature. Now, for the sake of example, we provided the arguments in multiple ways. +--- -It is up to each developer to choose the layout he prefers, but a few points need to be underlined: +### Testing Overview -- Most of the supplied **arguments** need to be in the **hexadecimal format**: `0x...`. -- When converting a value to a hexadecimal format, we need to make sure it has an **even number** of characters. If not, we need to provide an extra `0` in order to make it even: - - Example: the number `911` -> in hexadecimal encoding, it is equal to: `38f` -> so we need to provide the argument `0x038f`. -- Arguments can be provided both as a fixed arguments (usually for unchangeable arguments like the contract's address or a fixed number) or can be provided as an input in the terminal, when interacting with the snippet (mostly used for arguments that change often like numbers). +What does it mean to test a smart contract? -In our example we provide the address argument as a fixed argument. We then convert it to hexadecimal format (as it is in the bech32 format by default) and only after that we pass it as a parameter. As for the `BigUint` parameters, we provide the first two parameters directly in the terminal and the last one as a fixed argument, hexadecimal encoded. +Well, smart contracts are little programs that run on the blockchain, so the first step is to find an environment for them to run. -:::tip -`mxpy` provides the following encoding conventions: -- We can use `str:` for encoding strings. For example: `str:MYTOKEN-123456`. -- Blockchain addresses that start with `erd1` are automatically encoded, so there is no need to further hex encode them. -- The values **true** or **false** are automatically converted to **boolean** values. -- Values that are identified as **numbers** are hex encoded as `BigUint` values. -- Arguments like `0x...` are left unchanged, as they are interpreted as already encoded hex values. +## Types of tests -::: +The simplest answer would be to test them directly on a blockchain. We advise against testing them on mainnet, but we have the public devnet and testnet especially made for this purpose. It is even possible to run a blockchain on your local machine, this way there is no interference with anyone. -So, in case of our **myNonPayableEndpoint** interaction, we can write it like so: +But testing on a blockchain can be cumbersome. It's a great way to test a final product, but until our product reaches maturity, we want something else. Our testing solution should be: +- fast, +- local, +- something that can we can automate (continuous integration is important), +- something that we might also debug. -```shell -###PARAMS -# $1 = FirstBigUintArgument -# $2 = SecondBigUintArgument -THIRD_BIGUINT_ARGUMENT=1000000 -ADDRESS_ARGUMENT=addr:erd14nw9pukqyqu75gj0shm8upsegjft8l0awjefp877phfx74775dsq49swp3 +This is what integration tests are for. Conveniently, the MultiversX framework offers the possibility to run and debug smart contracts in a sandboxed environment that imitates a real blockchain. -myNonPayableEndpoint() { - mxpy --verbose contract call ${CONTRACT_ADDRESS} \ - --pem=${WALLET_PEM} \ - --proxy=${PROXY} \ - --function="myNonPayableEndpoint" \ - --arguments ${ADDRESS_ARGUMENT} $1 $2 ${THIRD_BIGUINT_ARGUMENT}\ - --send || return -} -``` +There are two flavors of integration tests: +- Black-box tests: execution imitates the blockchain, no access to private contract functions; +- White-box tests: the test has access to the inner workings of the contract. -A call example for this endpoint would look like: +They both have their uses and we will see more about them further on. -```shell -source devnet.snippets.sh -myNonPayableEndpoint 10000 100000 -``` +There is a third type of test: the unit test. This is the most underrated type of test. It is ideal for testing a small function, or a small component of a smart contract. They are quick to write and quick to run. A healthy project should contain plenty of unit tests. -Using unencoded values (for easier reading) would translate into: +So, to recap, the ways to test a smart contract are as follows: +- On a blockchain, +- Integration tests: + - Black-box tests, + - White-box tests; +- Unit tests. -```shell -myNonPayableEndpoint addr:erd14nw9pukqyqu75gj0shm8upsegjft8l0awjefp877phfx74775dsq49swp3 10000 100000 1000000 -``` -:::caution -It is import to make sure all arguments have the correct encoding. Otherwise, the transaction will fail. -::: +## What language should I use for my tests? +Since smart contracts are written in **Rust**, it is most convenient to have the tests also written in Rust. The Rust framework currently supports all types of testing mentioned above. -## Payable endpoint interaction +Let's, however, quickly go through all options available on MultiversX: +- On a blockchain: + - Rust interactor framework; + - [Any other SDK that can interact with the MultiversX blockchains](/sdk-and-tools/overview); + - Launching transactions from the wallet directly. +- Integration tests: + - Black-box tests: + - Rust, + - JSON, via the [JSON scenarios](/developers/testing/scenario/structure-json), + - Go, by building [around the Go VM tool](/developers/testing/testing-in-go); + - White-box tests: + - Rust only (since direct access to the contract code is needed); +- Unit tests: + - Rust only (since direct access to the contract code is needed). -### Fungible ESDT transfer +## Testing backends -Now let's take a look at the following example, where we want to call a payable endpoint. +A smart contract needs an environment to run. Any smart contract test needs to set up this environment. All these environments are modelled after the same blockchain, so their behavior is necessarily similar. This also means that in general, with some proper engineering, the same test should be able to run on any compatible infrastructure. -```shell -myPayableEndpoint() { - token_identifier=$1 - token_amount=$2 - mxpy --verbose contract call ${CONTRACT_ADDRESS} \ - --pem=${WALLET_PEM} \ - --proxy=${PROXY} \ - --token-transfers $token_identifier $token_amount \ - --function="myPayableEndpoint" \ - --send || return -} +```mermaid +graph TD + sc[Smart Contract] --> interact + interact[Interactor] --> blockchain["⚙️ Blockchain"] + sc --> bb[BlackBox Test] --> vm-go["⚙️ Go VM"] + bb -.-> vm-k["⚙️ K framework VM"] + sc --> wb[WhiteBox Test] --> vm-rust["⚙️ Rust VM (Debugger)"] + bb --> vm-rust + style vm-k stroke-dasharray: 5 5 ``` -To call a **payable endpoint**, we use the `--token-transfer` argument, which requires two values: +The backends are as follows: +- A blockchain + - Can be thought of as the backend for blockchain interactions; + - Currently the only place to test cross-shard transactions and contract calls. +- The Go VM: + - The is the official VM, and currently the only VM implementation on a public MultiverX blockchain. + - This is currently the most complete implementation. If there is any difference in VM execution, the Go VM is considered the correct implementation. + - The only VM that can currently model gas. +- The Rust VM. + - Alternative implementation of the VM, written specifically for testing and debugging contracts. + - Is able to run contract code directly, without the need to compile contracts to WebAssembly. + - Supports step-by-step debugging of contract code, for all types of tests. + - Can provide code coverage. +- The K Framework VM specification. + - Currently in the works. Only test prototypes are currently available. + - It is a formal specification of WebAssembly and the MultiversX VM. + - It is an _executable_ specification, can therefore be used as backend for tests. + - Supports symbolic execution. -1. The token identifier. -2. The amount. +White-box and unit tests can only run on the Rust VM backend. For the black-box tests, however, all backends are available. -In our case, we specify in the terminal the **token identifier** and **the amount of tokens** we want to transfer. +Interactors and black-box tests are very similar, so our goal is to at some point create a common test API that is compatible with all backends, including the real blockchain. We are half-way there. -:::info -The format for the token identifier changes based on the type of asset you are sending: +--- -- **ESDTs Tokens**: Use the **standard** Token Identifier. -- **NFTs and SFTs**: Use the **extended** Token identifier format, which includes the token's nonce. The nonce must be hex-encoded. - - Example: `NFT-123456-0a` (where `0a` is the hex-encoded nonce). +### The dynamic allocation problem + +### Avoiding memory allocation +:::caution +**Smart contracts must avoid dynamic allocation**. Due to the performance penalty incurred by dynamic allocation, the MultiversX Virtual Machine is configured with hard limits and will stop a contract that attempts too much allocation. ::: -:::info -When specifying the amount of tokens to transfer, the value must include the token's decimal precision. +Here are a few simple guidelines you can use to ensure your contract performs efficiently. By following them, you might notice a considerable reduction of gas consumption when your contract is called. It is also likely that the WASM binary resulting from compilation may become smaller in size, thus faster and cheaper to call overall. -For example EGLD uses 18 decimals. This means that if you want to transfer 1.5 EGLD, the amount value will be $1.5 \times 10^{18}$. -::: +### It's all about the types -### Non-fungible ESDT transfer (NFT, SFT and META ESDT) +Many basic Rust types (like `String` and `Vec`) are dynamically allocated on the heap. In simple terms, it means the program (in this case, the smart contract) keeps asking for more and more memory from the runtime environment (the VM). For small collections, this doesn't matter much, but for bigger collection, this can become slow and the VM might even stop the contract and mark the execution as failed. -Now let's suppose we want to call an endpoint that accepts an NFT or an SFT as payment. +The main issue is that basic Rust types are quite eager with dynamic memory allocation: they ask for more memory than they actually need. For ordinary programs, this is great for performance, but for smart contracts, where every instruction costs gas, can be quite impactful, on both cost and even runtime failures. -```shell -###PARAMS -# $1 = NFT/SFT Token Identifier, -# $2 = NFT/SFT Token Amount, -FIRST_BIGUINT_ARGUMENT=1000 -SECOND_BIGUINT_ARGUMENT=10000 +The alternative is to use **managed types** instead of the usual Rust types. All managed types, like `BigUint`, `ManagedBuffer` etc. store all their contents inside the VM's memory, as opposed to the contract memory, so they have a great performance advantage. But you don't need to be concerned with "where" the contents are, because managed types automatically keep track of the contents with help from the VM. -myESDTNFTPayableEndpoint() { - sft_token_identifier=$1 - sft_token_amount=$2 - mxpy --verbose contract call ${CONTRACT_ADDRESS} \ - --pem=${WALLET_PEM} \ - --proxy=${PROXY} \ - --token-transfers $sft_token_identifier $sft_token_amount \ - --function="myESDTNFTPayableEndpoint" \ - --arguments ${FIRST_BIGUINT_ARGUMENT} ${SECOND_BIGUINT_ARGUMENT} \ - --send || return -} -``` +The managed types work by only storing a `handle` within the contract memory, which is a `u32` index, while the actual payload resides in reserved VM memory. So whenever you have to add two `BigUint`s for example, the `+` operation in your code will only pass the three handles: the result, the first operand, and the second operand. This way, there is very little data being passed around, which in turn makes everything cheaper. And since these types only store a handle, their memory allocation is fixed in size, so it can be allocated on the stack instead of having to be allocated on the heap. +:::caution +If you need to update older code to take advantage of managed types, please take the time to understand the changes you need to make. Such an update is important and cannot be done automatically. +::: -### Multi-ESDT transfer -In case we need to call an endpoint that accepts multiple tokens (let's say for example 2 fungible tokens and an NFT). Let's take a look at the following example: +### Base Rust types vs managed types -```shell -###PARAMS -# $1 = First Token Identifier, -# $2 = First Token Amount, -# $3 = Second Token Identifier, -# $4 = Second Token Amount, -# $5 = Third Token Identifier, -# $6 = Third Token Amount, -FIRST_BIGUINT_ARGUMENT=1000 -SECOND_BIGUINT_ARGUMENT=10000 +Below is a table of unmanaged types (basic Rust types) and their managed counterparts, provided by the MultiversX framework: -myMultiESDTNFTPayableEndpoint() { - first_token_identifier=$1 - first_token_amount=$2 - second_token_identifier=$3 - second_token_amount=$4 - third_token_identifier=$5 - third_token_amount=$6 +| Unmanaged (safe to use) | Unmanaged (allocates on the heap) | Managed | +| :---------------------: | :-------------------------------: | :------------------------------------------: | +| - | - | `BigUint` | +| `&[u8]` | - | `&ManagedBuffer` | +| - | `BoxedBytes` | `ManagedBuffer` | +| `ArrayVec`[^1] | `Vec` | `ManagedBuffer` | +| - | `String` | `ManagedBuffer` | +| - | - | `TokenIdentifier` | +| - | `MultiValueVec` | `MultiValueEncoded` / `MultiValueManagedVec` | +| `ArrayVec`[^1] | `Vec` | `ManagedVec` | +| `[T; N]`[^2] | `Box<[T; N]>` | `ManagedByteArray` | +| - | `Address` | `ManagedAddress` | +| - | `H256` | `ManagedByteArray<32>` | +| - | - | `EsdtTokenData` | +| - | - | `EsdtTokenPayment` | - mxpy --verbose contract call ${CONTRACT_ADDRESS} \ - --pem=${WALLET_PEM} \ - --proxy=${PROXY} \ - --token-transfers $first_token_identifier $first_token_amount \ - $second_token_identifier $second_token_amount \ - $third_token_identifier $third_token_amount \ - --function="payable_nft_with_args" \ - --arguments ${FIRST_BIGUINT_ARGUMENT} ${SECOND_BIGUINT_ARGUMENT} \ - --send || return -``` +[^1]: `ArrayVec` allocates on the stack, and so it has a fixed capacity - it cannot grow indefinitely. You can make it as large as you please, but be warned that adding beyond this capacity results in a panic. Use `try_push` instead of `push` for more graceful error handling. +[^2]: Be careful when passing arrays around, since they get copied when returned from functions. This can add a lot of expensive memory copies in your contract. -In this example, we call `myMultiESDTPayableEndpoint` endpoint, by transferring **3 different tokens**: the first two are fungible tokens and the last one is an NFT. +In most cases, the managed types can be used as drop-in replacements for the basic Rust types. For a simple example, see [BigUint Operations](/developers/best-practices/biguint-operations). + +We also recommend _allocating Rust arrays directly on the stack_ (as local variables) whenever a contiguous area of useful memory is needed. Moreover, avoid allocating mutable global buffers for this purpose, which require `unsafe` code to work with. + +Also, consider using `ArrayVec`, which provides the functionality of a `Vec`, but without allocation on the heap. Instead, it requires allocation of a block of memory directly on the stack, like a basic Rust local array, but retains the flexibility of `Vec`. + +:::caution +Make sure you migrate to the managed types **incrementally** and **thoroughly test your code** before even considering deploying to the mainnet. +::: :::tip -More information about ESDT Transfers [here](/tokens/fungible-tokens/#transfers). +You can use the `sc-meta report` command to verify whether your contract still requires dynamic allocation or not. ::: +--- -## View interaction +### The MultiversX Serialization Format -In case we want to call a view function, we can use the **query** keyword. +In MultiversX, there is a specific serialization format for all data that interacts with a smart contract. The serialization format is central to any project because all values entering and exiting a contract are represented as byte arrays that need to be interpreted according to a consistent specification. -```shell -###PARAMS -# $1 = First argument -# $2 = Second argument +In Rust, the **multiversx-sc-codec** crate ([crate](https://crates.io/crates/multiversx-sc-codec), [docs](https://docs.rs/multiversx-sc-codec/latest/multiversx_sc_codec/)) exclusively deals with this format. Both Go and Rust implementations of scenarios have a component that serializes to this format. DApp developers need to be aware of this format when interacting with the smart contract on the backend. -myView() { - mxpy --verbose contract query ${CONTRACT_ADDRESS} \ - --proxy=${PROXY} \ - --function="myView" \ - --arguments $1 $2 -} -``` -When calling a **view** function, `mxpy` will output the standard information in the terminal, along with the results, formatted based on the requested data type. The arguments are specified in the same way as with endpoints. +## Rationale + +We want the format to be somewhat readable and to interact with the rest of the blockchain ecosystem as easily as possible. This is why we have chosen **big endian representation for all numeric types.** + +More importantly, the format needs to be as compact as possible, since each additional byte costs additional fees. + + +## The concept of top-level vs. nested objects + +There is a perk that is central to the formatter: we know the size of the byte arrays entering the contract. All arguments have a known size in bytes, and we normally learn the length of storage values before loading the value itself into the contract. This gives us some additional data straight away that allows us to encode less. + +Imagine that we have an argument of type int32. During a smart contract call we want to transmit the value "5" to it. A standard deserializer might expect us to send the full 4 bytes `0x00000005`, but there is clearly no need for the leading zeroes. It's a single argument, and we know where to stop, there is no risk of reading too much. So sending `0x05` is enough. We saved 3 bytes. Here we say that the integer is represented in its **top-level form**, it exists on its own and can be represented more compactly. + +But now imagine that an argument that deserializes as a vector of int32. The numbers are serialized one after the other. We no longer have the possibility of having variable length integers because we won't know where one number begins and one ends. Should we interpret `0x0101` as`[1, 1]` or `[257]`? So the solution is to always represent each integer in its full 4-byte form. `[1, 1]` is thus represented as `0x0000000100000001` and`[257]`as `0x00000101`, there is no more ambiguity. The integers here are said to be in their **nested form**. This means that because they are part of a larger structure, the length of their representation must be apparent from the encoding. ---- +But what about the vector itself? Its representation must always be a multiple of 4 bytes in length, so from the representation we can always deduce the length of the vector by dividing the number of bytes by 4. If the encoded byte length is not divisible by 4, this is a deserialization error. Because the vector is top-level we don't have to worry about encoding its length, but if the vector itself gets embedded into an even larger structure, this can be a problem. If, for instance, the argument is a vector of vectors of int32, each nested vector also needs to have its length encoded before its data. -### Smart contract modules -## Introduction +## A note about the value zero -Smart contract modules are a handy way of dividing a contract into smaller components. Modules also reduce code duplication, since they can be reused across multiple contracts. +We are used to writing the number zero as "0" or "0x00", but if we think about it, we don't need 1 byte for representing it, 0 bytes or an "empty byte array" represent the number 0 just as well. In fact, just like in `0x0005`, the leading 0 byte is superfluous, so is the byte `0x00` just like an unnecessary leading 0. +With this being said, the format always encodes zeroes of any type as empty byte arrays. -## Declaration -Modules can be defined both in the same crate as the main contract, or even in their own standalone crate. The latter is used when you want to use the same module in multiple contracts. +## How each type gets serialized -A module is trait declared with the `#[multiversx_sc::module]` macro. Inside the trait, you can write any code you would usually write in a smart contract, even endpoints, events, storage mappers, etc. +This guide is split into several sections: +- [Simple values, such as numbers, strings, etc.](/developers/data/simple-values) +- [Lists, tuples, Option](/developers/data/composite-values) +- [Custom types defined in the contracts.](/developers/data/custom-types) +- [Var-args and other multi-values](/developers/data/multi-values) +- [The code metadata flag](/developers/data/code-metadata) -For example, let's say you want to have your storage mappers in a separate module. The implementation would look like this: +There is a special section about [uninitialized data and how defaults relate to serialization](/developers/data/defaults). -```rust -#[multiversx_sc::module] -pub trait StorageModule { - #[view(getQuorum)] - #[storage_mapper("firstStorage")] - fn first_storage(&self) -> SingleValueMapper; +--- - #[view] - #[storage_mapper("secondStorage")] - fn second_storage(&self) -> SingleValueMapper; -} -``` +### Time-related Types -Then, in your main file (usually named `lib.rs`), you have to define the module. If the file for the above module is named `storage.rs`, then in the main file you'd declare it like this: +The Supernova release introduces increased block frequency and encourages transitioning to millisecond timestamps, instead of seconds. To support this, the SpaceCraft SDK (starting with `v0.63.0`) provides strong type wrappers for time values to prevent common bugs. -```rust -pub mod storage; -``` -## Importing a module +## Overview -A module can be imported both by other modules and contracts: +Traditionally, smart contracts used plain `u64` values to represent timestamps and durations. As the ecosystem transitions to millisecond precision, mixing seconds and milliseconds becomes error-prone. Runtime errors are difficult to detect, so the compiler now assists by enforcing type correctness. -```rust -pub trait SetupModule: - crate::storage::StorageModule - + crate::util::UtilModule { -} -``` -```rust -#[multiversx_sc::contract] -pub trait MainContract: - setup::SetupModule - + storage::StorageModule - + util::UtilModule { +## The Four Time-Related Types -} -``` +The framework introduces four new types: -Keep in mind your main contract has to implement all modules that any sub-module might use. In this example, even if the `MainContract` does not use anything from the `UtilModule`, it still has to implement it if it wants to use `SetupModule`. +* **`TimestampSeconds`** — moment in time measured in seconds +* **`TimestampMillis`** — moment in time measured in milliseconds +* **`DurationSeconds`** — duration measured in seconds +* **`DurationMillis`** — duration measured in milliseconds +Each is a **newtype wrapper around `u64`**, with an identical underlying representation but distinct meaning. -## Conclusion -We hope this module system will make it a lot easier to write maintainable smart contract code, and even reusable modules. -More modules and examples can be found here: https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/modules +## Why These Types Exist ---- +Using plain `u64` makes it easy to: -### Smart contract payments +* accidentally mix seconds and milliseconds +* add timestamps together (nonsensical) +* subtract mismatched units +* perform invalid arithmetic without realizing it -## Some general points +The new types make such mistakes **fail at compile time**. -We want to offer an overview on how smart contracts process payments. This includes two complementary parts: receiving tokens and sending them. -:::important important -On MultiversX it is possible to send one or more tokens with any transaction. This includes EGLD, and it is also possible (though impractical) to send several payments of the same token at once. -::: +## Supported Operations -:::note note -Historically, it used to be impossible to send EGLD and ESDT at the same time, this is why some of the legacy APIs have this restriction. This restriction no longer applies since the [Spica release](https://multiversx.com/release/release-spica-patch-4-v1-8-12). -::: +The framework implements common operators only where they make sense. ---- +Examples: +``` +TimestampMillis - TimestampMillis → DurationMillis +TimestampSeconds + DurationSeconds → TimestampSeconds +DurationSeconds + DurationSeconds → DurationSeconds +``` -## Receiving payments +Examples of invalid operations (will not compile): -There are two ways in which a smart contract can receive payments: -1. Like any regular account, directly, without any contract code being called; -2. Via an endpoint. +``` +TimestampMillis - TimestampSeconds +TimestampMillis + TimestampMillis +``` -### Receiving payments directly -Sending EGLD and ESDT tokens directly to accounts works the same way for EOAs (externally owned accounts) as for smart contracts: the tokens are transferred from one account to the other without firing up the VM. +## Codec and ABI Support -However, not all smart contracts are allowed to receive tokens directly. There is a flag that controls this, called "payable". This flag is part of the [code metadata](/developers/data/code-metadata), and is specified in the transaction that deploys or upgrades the smart contract. +All four types: -The rationale for this is as follows: the MultiversX blockchain doesn't offer any mechanism to allow contracts to react to direct calls. This is because we wanted to keep direct calls simple, consistent, and with a predictable gas cost, in all contexts. Most contracts, however, will likely want to keep track of all the funds that are fed into them, so they do not want to accept payments without an opportunity to also change their internal state. +* support codec and ABI traits +* can be used in storage +* can be used in events and arguments +* behave like `u64` for serialization +This makes them safe to adopt even in existing contracts. -### Receiving payments via endpoints -The most common way for contracts to accept payments is by having endpoints annotated with the `#[payable]` annotation (or `#[payable("*")]`). -:::important important -The "payable" flag in the code metadata only refers to direct transfers. Transferring tokens via contract endpoint calls is not affected by it in any way. -::: +## When to Use Seconds vs. Milliseconds -To accept any kind of payment, annotate the endpoints with `#[payable]`: +* **Seconds**: acceptable when your contract already stores timestamps in seconds or relies on existing storage/metadata +* **Milliseconds**: recommended for all new contracts and for future-proof logic -```rust -#[endpoint] -#[payable] -fn accept_any_payment(&self) { - // ... -} -``` +The key rule: **Never mix seconds and milliseconds without explicit conversion.** -Usually on the first line there will be an instruction that processes, interprets, and validates the received payment ([see below](#call-value-methods)) +## Testing -If an endpoint only accepts EGLD, it can be annotated with `#[payable("EGLD")]`, although this is slowly falling out of favor. +Time-related types also work in testing frameworks. + +In blackbox tests, one can write: ```rust -#[endpoint] -#[payable("EGLD")] -fn accept_egld(&self) { - // ... -} + let block_timestamp_ms = TimestampMillis::new(123_000_000); + + world + .epoch_start_block() + .block_timestamp_ms(block_timestamp_ms) + .block_nonce(15_000) + .block_round(17_000); ``` -:::note Multi-transfer note -Note that it is currently possible to send two or more EGLD payments in the same transaction. The `#[payable("EGLD")]` annotation rejects that. -::: +Mandos supports both `blockTimestamp` and the newer `blockTimestampMs`. Set both if they are used together for backward compatibility. -This snippet is equivalent to: -```rust -#[endpoint] -#[payable] -fn accept_egld(&self) { - let payment_amount = self.call_value().egld(); - // ... -} -``` +## Summary +* Time-related newtypes enforce correctness at compile time +* They eliminate a class of subtle bugs related to timestamp unit mismatches +* Both seconds and milliseconds will continue to work, but milliseconds are recommended for new code -:::note Hard-coded token identifier -It is also possible to hard-code a token identifier in the `payable`, e.g. `#[payable("MYTOKEN-123456")]`. It is rarely, if ever, used, tokens should normally be configured in storage, or at runtime. -::: +Use these types to ensure your contract remains safe and consistent across all future protocol updates. +--- -## Payment Types +### Token lifecycle — freeze, unfreeze, pause, unpause, wipe -The framework provides a unified approach to handling payments using the `Payment` type that treats EGLD and ESDT tokens uniformly. EGLD is represented as `EGLD-000000` token identifier, making all payment handling consistent. +A token manager can control a token after issuance through the ESDT system +contract: -**`Payment
`** - The primary payment type that combines: -- `token_identifier`: `TokenId` - unified token identifier (EGLD serialized as "EGLD-000000") -- `token_nonce`: `u64` - token nonce for NFTs/SFTs, which is zero for all fungible tokens (incl. EGLD) -- `amount`: `NonZeroBigUint` - guaranteed non-zero amount +- **pause / unpause**, globally halt or resume *all* transfers of the token. +- **freeze / unfreeze**, freeze or unfreeze *one* account's holding. +- **wipe**, remove a frozen account's holding entirely. -**`PaymentVec`** - A managed vector of `Payment` objects, representing multiple payments in a single transaction. +These are privileged manager calls, distinct from the builtin `ESDTLocalMint` / +`ESDTNFTCreate` self-calls in +[Local mint and burn](/sdk-and-tools/sdk-js/cookbook/tokens/local-mint-burn-supply). +And in sdk-core v15.4.1 they ship with a confirmed bug that this recipe both +demonstrates and works around. +## Prerequisites -## Call Value Methods +- Node.js >= 20.13.1. +- A devnet wallet, see + [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld)'s + Prerequisites. **No devnet EGLD required**, see "How it works". -Additional restrictions on the incoming tokens can be imposed in the body of the endpoint, by calling the call value API. Most of these functions retrieve data about the received payment, while also stopping execution if the payment is not of the expected type. +## Install +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/token-lifecycle-operations +npm install +npm run build +npm start -- ./wallet.pem COOK-123456 +``` + +## The code + +```ts title="src/lifecycle.ts" +// src/lifecycle.ts — the subject of this recipe: the manager-only lifecycle +// operations a token owner can perform through the ESDT system contract. +// +// - pause / unpause — globally halt / resume all transfers of a token +// - freeze / unfreeze — freeze / unfreeze ONE account's holding of a token +// - wipe — remove a frozen account's holding entirely +// +// These are distinct from ESDTLocalMint/Burn and ESDTNFTCreate (builtin +// functions on the caller's own account — see local-mint-burn-supply). A +// pause/freeze/wipe is a privileged call the token MANAGER makes TO the ESDT +// system contract, which enforces it. +// +// ============================================================================ +// CONFIRMED sdk-core v15.4.1 BUG — wrong receiver. +// ---------------------------------------------------------------------------- +// createTransactionForPausing / Unpausing / Freezing / Unfreezing / Wiping +// all build the transaction with `receiver: sender` (the caller's own +// address). Every real on-chain pause/freeze/wipe is instead addressed to the +// ESDT system contract erd1qqqq...zllls8a5w6u (verified via the mainnet API), +// which is the SAME receiver the SDK correctly uses for issue and +// setSpecialRole. A transaction sent to the caller's own address will NOT +// perform the operation. This recipe builds via the factory and overrides the +// receiver before signing — see withCorrectReceiver below. +// +// Confirmed in both the installed build and the v15.4.1 source tag. Contrast: +// issue/setSpecialRole correctly target the ESDT contract; ESDTLocalMint/Burn +// and ESDTNFTCreate correctly target the sender. Only these five are wrong. +// ============================================================================ + +import { Address } from '@multiversx/sdk-core'; +import type { Account, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +/** + * The ESDT system contract — the correct receiver for pause/freeze/wipe. + * The documented ESDT system contract address; verified on-chain as the + * receiver of every real freeze/pause transaction. + */ +export const ESDT_SYSTEM_CONTRACT = Address.newFromBech32( + 'erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u', +); -### `all()` - Complete Payment Collection +/** Build a pause (or unpause) transaction. Factory path, unsigned. */ +export async function buildPause( + entrypoint: DevnetEntrypoint, + sender: Account, + tokenIdentifier: string, + unpause: boolean, +): Promise { + const factory = entrypoint.createTokenManagementTransactionsFactory(); + return unpause + ? factory.createTransactionForUnpausing(sender.address, { tokenIdentifier }) + : factory.createTransactionForPausing(sender.address, { tokenIdentifier }); +} + +/** Build a freeze (or unfreeze) transaction for one user's holding. */ +export async function buildFreeze( + entrypoint: DevnetEntrypoint, + sender: Account, + tokenIdentifier: string, + user: Address, + unfreeze: boolean, +): Promise { + const factory = entrypoint.createTokenManagementTransactionsFactory(); + // Factory: createTransactionForUnfreezing (lowercase "f"). The controller + // spells the same method createTransactionForUnFreezing (capital "F"). + return unfreeze + ? factory.createTransactionForUnfreezing(sender.address, { user, tokenIdentifier }) + : factory.createTransactionForFreezing(sender.address, { user, tokenIdentifier }); +} + +/** Build a wipe transaction — removes a frozen account's holding. */ +export async function buildWipe( + entrypoint: DevnetEntrypoint, + sender: Account, + tokenIdentifier: string, + user: Address, +): Promise { + const factory = entrypoint.createTokenManagementTransactionsFactory(); + return factory.createTransactionForWiping(sender.address, { user, tokenIdentifier }); +} + +/** + * The v15.4.1 workaround: if the SDK addressed this lifecycle transaction to + * the caller instead of the ESDT system contract, fix it. Returns true if a + * correction was applied. Call this BEFORE setting the nonce and signing, so + * the signature covers the corrected receiver. + */ +export function withCorrectReceiver(transaction: Transaction): boolean { + if (transaction.receiver.toBech32() === ESDT_SYSTEM_CONTRACT.toBech32()) { + return false; + } + transaction.receiver = ESDT_SYSTEM_CONTRACT; + return true; +} + +/** Decode a lifecycle transaction's `data` field. */ +export function describeLifecyclePayload(transaction: Transaction): { + function: string; + receiver: string; + args: string[]; +} { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + function: parts[0] ?? '', + receiver: transaction.receiver.toBech32(), + args: parts.slice(1), + }; +} +``` -`self.call_value().all()` retrieves all payments sent with the transaction as a `PaymentVec`. It handles all tokens uniformly, including EGLD (represented as "EGLD-000000"). Never stops execution. +## Run it -```rust -#[payable] -#[endpoint] -pub fn process_all_payments(&self) { - let payments = self.call_value().all(); - for payment in payments.iter() { - // Handle each payment uniformly - self.process_payment(&payment.token_identifier, payment.token_nonce, &payment.amount); - } -} +```bash +npm start -- [--user ] ``` +A real captured run (unfunded wallet): -### `single()` - Strict Single Payment +```text +As the SDK builds them in v15.4.1 (note the receiver): +PAUSE: function: pause receiver: erd18l2c...z3jscaqd4dl3k <- SENDER (bug) +FREEZE: function: freeze receiver: erd18l2c...z3jscaqd4dl3k <- SENDER (bug) +WIPE: function: wipe receiver: erd18l2c...z3jscaqd4dl3k <- SENDER (bug) + +Corrected freeze receiver -> ESDT system contract (patched: true): +FREEZE (corrected): + function: freeze + receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u <- ESDT contract + +Broadcasting the corrected freeze... +Rejected (expected for an unfunded wallet): ... insufficient funds for address erd18l2c... +``` + +## How it works + +**The v15.4.1 bug: wrong receiver.** `createTransactionForPausing`, `Unpausing`, +`Freezing`, `Unfreezing`, and `Wiping` all build the transaction with +`receiver: sender`, the caller's own address. But every real on-chain +pause/freeze/wipe is addressed to the **ESDT system contract** +(`erd1qqqq…zllls8a5w6u`), the same receiver the SDK correctly uses for `issue` and +`setSpecialRole`. This was verified two ways: the decoded run above shows +`receiver = SENDER`, and a lookup of real mainnet `pause` and `freeze` +transactions shows `receiver = the ESDT contract`. A lifecycle transaction sent to +the caller's own address will not perform the operation. Confirmed in both the +installed build and the tagged v15.4.1 source. + +**The fix.** `withCorrectReceiver()` overrides `transaction.receiver` to the ESDT +system contract. Because this recipe builds via the **factory** (which does not +sign), the override happens *before* the nonce is set and the transaction is +signed, so the signature covers the corrected receiver. The corrected `freeze` +above is a genuine, well-formed transaction: it is rejected only for insufficient +funds, keyed to the sender. + +**Argument shapes.** `pause`/`unPause` take only ``. +`freeze`/`unFreeze`/`wipe` take `@` (the account +whose holding is affected). On the wire the unfreeze function is spelled +`UnFreeze` (capital "F"). + +## Pitfalls + +:::danger[Pitfall 1: v15.4.1 addresses pause/freeze/wipe to the sender, not the ESDT contract] +As shipped, these transactions will not take effect. Override +`transaction.receiver` to +`erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u` before signing +(build via the factory so the signature covers the fix), exactly as +`withCorrectReceiver()` does. +::: -`self.call_value().single()` expects exactly one payment and returns it. Will halt execution if zero or multiple payments are received. Returns a `Payment` object. +:::warning[Pitfall 2: createTransactionForUnFreezing (Controller) vs ...Unfreezing (Factory)] +The controller capitalizes the "F" (`UnFreezing`); the factory does not +(`Unfreezing`). Another instance of this SDK's Controller/Factory casing drift. +::: -```rust -#[payable] -#[endpoint] -pub fn deposit(&self) { - let payment = self.call_value().single(); - // Guaranteed to be exactly one payment - let token_id = &payment.token_identifier; - let amount = payment.amount; - - self.deposits(&self.blockchain().get_caller()).set(&amount); -} -``` +:::note[Pitfall 3: wipe only works on a frozen account] +You must `freeze` an account's holding before you can `wipe` it. Freeze/wipe also +require the token to have been issued with `canFreeze` / `canWipe`, see +[Issue a fungible ESDT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token). +::: +:::note[Pitfall 4: pause is global, freeze is per-account] +`pause` halts every transfer of the token for everyone; `freeze` targets one +address. Do not reach for `pause` when you mean to restrict a single account. +::: -### `single_optional()` - Flexible Single Payment +## See also -`self.call_value().single_optional()` accepts either zero or one payment. Returns `Option>` for graceful handling. Will halt execution if multiple payments are received. +- [Set and unset special roles](/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles) + is the other manager-side surface; grants operational roles rather than + restricting holders. +- [Issue a fungible ESDT](/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token) + sets `canFreeze` / `canWipe` / `canPause` at issuance so these operations are + permitted. +- [Local mint and burn](/sdk-and-tools/sdk-js/cookbook/tokens/local-mint-burn-supply) + covers the builtin self-calls whose `receiver: sender` is correct, for contrast + with this recipe's bug. -```rust -#[payable] -#[endpoint] -pub fn execute_with_optional_fee(&self) { - match self.call_value().single_optional() { - Some(payment) => { - // Process the payment as fee - self.execute_premium_service(payment); - }, - None => { - // Handle no payment scenario - self.execute_basic_service(); - } - } -} -``` +--- +### tokens -### `array()` - Fixed-Size Payment Array +This page describes the structure of the `tokens` index (Elasticsearch), and also depicts a few examples of how to query it. -`self.call_value().array()` expects exactly N payments and returns them as a fixed-size array. Will halt execution if the number of payments doesn't match exactly. -```rust -#[payable] -#[endpoint] -pub fn swap(&self) { - // Expect exactly 2 payments for the swap - let [input_payment, fee_payment] = self.call_value().array(); - - require!( - input_payment.token_identifier != fee_payment.token_identifier, - "Input and fee must be different tokens" - ); - - self.execute_swap(input_payment, fee_payment); -} -``` +## _id +The `_id` field of this index is represented by token identifier of an ESDT token. -## Legacy Call Value Methods -The following methods are available for backwards compatibility but may be deprecated in future versions: +## Fields -- `self.call_value().egld_value()` retrieves the EGLD value transferred, or zero. Never stops execution. -- `self.call_value().all_esdt_transfers()` retrieves all the ESDT transfers received, or an empty list. Never stops execution. -- `self.call_value().multi_esdt()` is ideal when we know exactly how many ESDT transfers we expect. It returns an array of `EsdtTokenPayment`. It knows exactly how many transfers to expect based on the return type (it is polymorphic in the length of the array). Will fail execution if the number of ESDT transfers does not match. -- `self.call_value().single_esdt()` expects a single ESDT transfer, fails otherwise. Will return the received `EsdtTokenPayment`. It is a special case of `multi_esdt`, where `N` is 1. -- `self.call_value().single_fungible_esdt()` further restricts `single_esdt` to only fungible tokens, so those with their nonce zero. Returns the token identifier and amount, as pair. -- `self.call_value().egld_or_single_esdt()` retrieves an object of type `EgldOrEsdtTokenPayment`. Will halt execution in case of ESDT multi-transfer. -- `self.call_value().egld_or_single_fungible_esdt()` further restricts `egld_or_single_esdt` to fungible ESDT tokens. It will return a pair of `EgldOrEsdtTokenIdentifier` and an amount. -- `self.call_value().any_payment()` is the most general payment retriever. Never stops execution. Returns an object of type `EgldOrMultiEsdtPayment`. *(Deprecated since 0.64.1 - use `all()` instead)* ---- +| Field | Description | +|---------------|-----------------------------------------------------------------------------------------------------------------------------------| +| name | The name field holds the name of the token. It contains alphanumeric characters only. | +| ticker | The ticker field represents the token's ticker (uppercase alphanumeric characters). | +| token | The token field is composed of the `ticker` field and a random sequence generated when the token is created(e.g. `ABCD-012345`). | +| issuer | The issuer field holds the bech32 encoded address of the token's issuer. | +| currentOwner | The currentOwner field holds the address in a bech32 format of the current owner of the token. | +| type | The type field holds the type of the token. It can be `FungibleESDT`, `NonFungibleESDT`, `SemiFungibleESDT`, or `MetaESDT`. | +| timestamp | The timestamp field represents the timestamp of the block in which the token was created. | +| ownersHistory | The ownersHistory field holds a list of all the owners of a token. | +| paused | The paused field is true if the token is paused. | -## Sending payments +## Query examples -We have seen how contracts can accommodate receiving tokens. Sending them is, in principle, even more straightforward, as it only involves specializing the `Payment` generic of the transaction using specific methods, essentially attaching a payload to a regular transaction. Read more about payments [here](../transactions/tx-payment.md). ---- +### Fetch details of a token -### Staking smart contract +``` +curl --request GET \ + --url ${ES_URL}/tokens/_search \ + --header 'Content-Type: application/json' \ + --data '{ + "query": { + "match": { + "_id":"ABCD-012345" + } + } +}' +``` -## Introduction +--- -This tutorial aims to teach you how to write a simple staking contract, and to illustrate and correct the common pitfalls new smart contract developers might fall into. +### Toolchain Setup -:::tip -If you find anything not answered here, feel free to ask further questions on the MultiversX Developers Telegram channel: [https://t.me/MultiversXDevelopers](https://t.me/MultiversXDevelopers) -::: +## Installing Rust and sc-meta +:::note +`sc-meta` is universal smart contract management tool. Please follow [this](/developers/meta/sc-meta) for more information. +::: -## Prerequisites -:::important -Before starting this tutorial, make sure you have the following: +For systems running Ubuntu or Windows with WSL, you should first ensure the following system-level dependencies required by Rust and sc-meta are in place: -- `stable` **Rust** version `≥ 1.85.0` (install via [rustup](/docs/developers/toolchain-setup.md#installing-rust-and-sc-meta)) -- `sc-meta` (install [multiversx-sc-meta](/docs/developers/meta/sc-meta-cli.md)) +```bash +sudo apt-get install build-essential pkg-config libssl-dev +``` -::: +Install Rust as recommended on [rust-lang.org](https://www.rust-lang.org/tools/install): -For contract developers, we generally recommend [**VSCode**](https://code.visualstudio.com) with the following extensions: +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` -- [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) -- [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) +Then, choose **Proceed with installation (default)**. +:::tip +Generally speaking, you should install Rust `v1.85.0` (stable channel) or later. +::: -## Creating the contract +```bash +rustup update +rustup default stable +``` -Run the following command in the folder in which you want your smart contract to be created: +Afterwards, open a new terminal (shell) and install `sc-meta`: ```bash -sc-meta new --name staking-contract --template empty +cargo install multiversx-sc-meta --locked ``` -Open VSCode, select **File > Open Folder**, and open the newly created `staking-contract` folder. +Once `sc-meta` is ready, install the `wasm32` target (for the Rust compiler), `wasm-opt`, and others dependencies as follows: +```bash +# Installs `wasm32`, `wasm-opt`, and others in one go: +sc-meta install all -## Building the contract +cargo install twiggy +``` -In the terminal, run the following command to build the contract: + +### Within Continuous Integration / Continuous Delivery + +For automated environments like CI/CD pipelines, start by installing the necessary foundational libraries. On platforms such as Ubuntu (or WSL), this means installing: ```bash -sc-meta all build +sudo apt-get install build-essential pkg-config libssl-dev ``` -After the building has completed, our folder should look like this: +For CI / CD, install Rust as follows: ```bash -├── Cargo.lock -├── Cargo.toml -├── meta -│ ├── Cargo.toml -│ └── src -├── multiversx.json -├── output -│ ├── staking-contract.abi.json -│ ├── staking-contract.imports.json -│ ├── staking-contract.mxsc.json -│ └── staking-contract.wasm -├── scenarios -│ └── staking_contract.scen.json -├── src -│ └── staking_contract.rs -├── target -│ ├── CACHEDIR.TAG -│ ├── debug -│ ├── release -│ ├── tmp -│ └── wasm32-unknown-unknown -├── tests -│ ├── staking_contract_scenario_go_test.rs -│ └── staking_contract_scenario_rs_test.rs -└── wasm - ├── Cargo.lock - ├── Cargo.toml - └── src +wget -O rustup.sh https://sh.rustup.rs && \ + chmod +x rustup.sh && \ + ./rustup.sh --verbose --default-toolchain stable -y + +cargo install multiversx-sc-meta --locked + +sc-meta install wasm32 ``` -A new folder, called `output` was created, which contains the compiled contract code. More on this is used later. +## Check your Rust installation -## First lines of Rust +You can check your Rust installation by invoking `rustup show`: -Currently, we just have an empty contract. Not very useful, is it? So let's add some simple code for it. Since this is a staking contract, we would expect to have a `stake` function, right? +```sh +$ rustup show -First, remove all the code in the `staking-contract/src/staking_contract.rs` file and replace it with this: +Default host: x86_64-unknown-linux-gnu +rustup home: /home/ubuntu/.rustup -```rust -#![no_std] +installed toolchains +-------------------- +stable-x86_64-unknown-linux-gnu (default) +[...] -use multiversx_sc::imports::*; +active toolchain +---------------- +name: stable-x86_64-unknown-linux-gnu +installed targets: + wasm32-unknown-unknown + wasm32v1-none + x86_64-unknown-linux-gnu -#[multiversx_sc::contract] -pub trait StakingContract { - #[init] - fn init(&self) {} +--- - #[upgrade] - fn upgrade(&self) {} +### Tooling Overview - #[payable("EGLD")] - #[endpoint] - fn stake(&self) {} -} +## Introduction + +We have developed a universal smart contract management tool, called `multiversx-sc-meta` (`sc-meta` in short). + +It is called that, because it provides a layer of meta-programming over the regular smart contract development. It can read and interact with some of the code written by developers. + +You can find it on [crates.io](https://crates.io/crates/multiversx-sc-meta) [![crates.io](https://img.shields.io/crates/v/multiversx-sc-meta.svg?style=flat)](https://crates.io/crates/multiversx-sc-meta) + +To install it, simply call + +``` +cargo install multiversx-sc-meta --locked ``` -Since we want this function to be callable by users, we have to annotate it with `#[endpoint]`. Also, since we want to be able to [receive a payment](/docs/developers/developer-reference/sc-payments.md#receiving-payments), we mark it also as `#[payable("EGLD)]`. For now, we'll use EGLD as our staking token. +After that, try calling `sc-meta help` or `sc-meta -h` to see the CLI docs. -:::important -The contract **does NOT** need to be payable for it to receive payments on endpoint calls. The payable flag at contract level is only for receiving payments without endpoint invocation. +:::note endure dependencies +Ubuntu users have to ensure the existence of the `build_essential` package installed in their system. ::: -We need to see how much a user paid, and save their staking information in storage. + +## Standalone tool vs. contract tool + +The unusual thing about this tool is that it comes in two flavors. One of them is the standalone tool, installed as above. The other is a tool that gets provided specifically for every contract, and which helps with building. + +The contract tool lies in the `meta` folder under each contract. It just contains these 3 lines of code: ```rust -#![no_std] +fn main() { + multiversx_sc_meta::cli_main::(); +} +``` -use multiversx_sc::imports::*; +... but they are important, because they link the contract tool to the contract code, via the [ABI](/developers/data/abi). -#[multiversx_sc::contract] -pub trait StakingContract { - #[init] - fn init(&self) {} +The contract tool is required in order to build contracts, because it is the only tool that we have that calls the ABI generator, manages the wasm crate and the multi-contract config, and has the data on how to build the contract. - #[upgrade] - fn upgrade(&self) {} +Therefore, all the functionality that needs the ABI goes into the contract tool, whereas the rest in the standalone tool. - #[payable("EGLD")] - #[endpoint] - fn stake(&self) { - let payment_amount = self.call_value().egld().clone(); - require!(payment_amount > 0, "Must pay more than 0"); +To see the contract meta CLI docs, `cd` into the `/meta` crate and call `cargo run help` or `cargo run -- -h`. - let caller = self.blockchain().get_caller(); - self.staking_position(caller.clone()).set(&payment_amount); - self.staked_addresses().insert(caller.clone()); - } - #[view(getStakedAddresses)] - #[storage_mapper("stakedAddresses")] - fn staked_addresses(&self) -> UnorderedSetMapper; +## Contract functionality - #[view(getStakingPosition)] - #[storage_mapper("stakingPosition")] - fn staking_position(&self, addr: ManagedAddress) -> SingleValueMapper; -} -``` +Currently the contract functionality is: + - `abi` Generates the contract ABI and nothing else. + - `build` Builds contract(s) for deploy on the blockchain. + - `build-dbg` Builds contract(s) with symbols and WAT. + - `twiggy` Builds contract(s) and generate twiggy reports. + - `clean` Clean the Rust project and the output folder. + - `update` Update the Cargo.lock files in all wasm crates. + - `snippets` Generates a snippets project, based on the contract ABI. -[`require!`](/docs/developers/developer-reference/sc-messages.md#require) is a macro that is a shortcut for `if !condition { signal_error(msg) }`. Signalling an error will terminate the execution and revert any changes made to the internal state, including token transfers from and to the smart contract. In this case, there is no reason to continue if the user did not pay anything. +To learn more about the smart contract ABI and ABI-based individual contract tools, see [the CLI reference](/developers/meta/sc-meta-cli). -We've also added [`#[view]`](/docs/developers/developer-reference/sc-annotations.md#endpoint-and-view) annotation for the storage mappers, allowing us to later perform queries on those storage entries. You can read more about annotations [here](/developers/developer-reference/sc-annotations/). -If you're confused about some of the functions used or the storage mappers, you can read more here: +## Standalone functionality + +The standalone functionality is: + - `info` General info about the contract an libraries residing in the targeted directory. + - `all` Calls the meta crates for all contracts under given path with the given arguments. + - `new` Creates a new smart contract from a template. + - `templates` Lists the available templates. + - `upgrade` Upgrades a contract to the latest version. Multiple contract crates are allowed. + - `local-deps` Generates a report on the local dependencies of contract crates. Will explore indirect dependencies too. -- [Smart Contract API Functions](/developers/developer-reference/sc-api-functions) -- [Storage Mappers](/developers/developer-reference/storage-mappers) +All the standalone tools take an optional `--path` argument. if not provided, it will be the current directory. -Now, there is intentionally written some bad code here. Can you spot any improvements we could make? +--- -1. The last `clone()` from `stake()` function is unnecessary. If you're cloning variables all the time,s then you need to take some time to read the [ownership](https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html) chapter of the Rust book and also about the [implications of cloning](/developers/best-practices/biguint-operations) types from the Rust framework. +### Tools for signing -2. The `staking_position` does not need an owned value of the `addr` argument. We can pass a reference instead. +In order to sign a transaction without actually dispatching it, several tools can be used. One of the most popular ones is [mxpy](/sdk-and-tools/sdk-py). -3. There is a logic error: what happens if an user stakes twice? Their position will be overwritten with the new value. Instead, we should add the new stake amount to their existing amount, using the [`update`](/docs/developers/developer-reference/storage-mappers.md#update) method. -After fixing these issues, we end up with the following code: +## **Sign using [mxpy](/sdk-and-tools/sdk-py/) (Command Line Interface)** -```rust -#![no_std] +Using a **pem** file: -use multiversx_sc::imports::*; +``` +$ mxpy tx new --nonce=41 --data="Hello, World" --gas-limit=70000 \ + --receiver=erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz \ + --pem=aliceKey.pem --pem-index=0 --outfile=myTransaction.json -#[multiversx_sc::contract] -pub trait StakingContract { - #[init] - fn init(&self) {} +``` - #[upgrade] - fn upgrade(&self) {} +Using a JSON wallet key (and its password): - #[payable("EGLD")] - #[endpoint] - fn stake(&self) { - let payment_amount = self.call_value().egld().clone(); - require!(payment_amount > 0, "Must pay more than 0"); +``` +mxpy tx new --nonce=41 --data="Hello, World" --gas-limit=70000 \ + --receiver=erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz \ + --keyfile=walletKeyOfAlice.json --passfile=passwordOfAlice.txt \ + --outfile=myTransaction.json - let caller = self.blockchain().get_caller(); - self.staking_position(&caller) - .update(|current_amount| *current_amount += payment_amount); - self.staked_addresses().insert(caller); - } +``` - #[view(getStakedAddresses)] - #[storage_mapper("stakedAddresses")] - fn staked_addresses(&self) -> UnorderedSetMapper; +In either case, the output file looks like this: - #[view(getStakingPosition)] - #[storage_mapper("stakingPosition")] - fn staking_position(&self, addr: &ManagedAddress) -> SingleValueMapper; +``` +{ + "tx": { + "nonce": 41, + "value": "0", + "receiver": "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz", + "sender": "erd1aedmqfsflx4rhwvs7v9z52e7eylkevz4w342jzuaa9ezy5unsc5qqy963v", + "gasPrice": 1000000000, + "gasLimit": 70000, + "data": "SGVsbG8sIFdvcmxk", + "chainID": "1596807148", + "version": 123, + "signature": "f432442ebfee6edf4518c10d006ab571d8ecbd6f2601995554c75d3402b424364908235d45449ba5dd28575e4a8129271020e4718cf8a4c6f44e22c0885ac40a" + }, + "hash": "", + "data": "Hello, World" } ``` -### What's with the empty init and upgrade function? - -Every smart contract must include a function annotated with [`#[init]`](/docs/developers/developer-reference/sc-annotations.md#init) and another with `#[upgrade]`. - -The `init()` function is called when the contract is first deployed, while `upgrade()` is triggered during an upgrade. For now, we need no logic inside it, but we still need to have those functions. +## **Other signing tools** +Each SDK includes functions for signing and broadcasting transactions. Please refer to [SDKs & Tools](/sdk-and-tools/overview) for the full list. -## Creating a wallet +--- -:::note -You can skip this section if you already have a Devnet wallet setup. -::: +### Track a transaction (WebSocket + polling fallback) -Open the terminal and run the following commands: +Track a transaction's live status after sending it. sdk-dapp's +`trackTransactions()` monitors status via WebSocket with a polling fallback, +configured during `initApp` via `transactionTracking` callbacks. This recipe +shows both halves: the `transactionTracking` config in `initApp`, and the flat +React hooks (`useGetPendingTransactions`, `useGetSuccessfulTransactions`, +`useGetFailedTransactions`) that re-render automatically as status changes, no +polling code of your own anywhere in this recipe. -```sh -mkdir -p ~/MyTestWallets -sc-meta wallet new --format pem --outfile ~/MyTestWallets/tutorialKey.pem -``` +## Prerequisites +- Node.js >= 20.13.1. +- A MultiversX wallet with devnet access, to click through the demo. **Not + required to verify this recipe.** -## Deploy the contract +## Install -Now that we've created a wallet, it's time to deploy our contract. +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/track-transaction-status +npm install +npm run dev +``` + +Open the HTTPS URL Vite prints, accept the local dev certificate warning, connect +a wallet, then send the demo transaction. + +## Configuring transactionTracking + +```ts title="src/lib/multiversx.ts" +// src/lib/multiversx.ts — environment configuration for sdk-dapp's initApp, +// PLUS the `transactionTracking` config block this recipe is actually +// about. +// +// A confirmed, real discrepancy some docs get wrong: a snippet showing +// `onFail: (sessionId, error) => { /* callback */ }` implies two +// parameters. The real type, read directly from +// node_modules/@multiversx/sdk-dapp/out/methods/initApp/initApp.types.d.ts: +// +// export type TransactionTrackingConfigType = { +// successfulToastLifetime?: number; +// onSuccess?: (sessionId: string) => Promise; +// onFail?: (sessionId: string) => Promise; +// }; +// +// takes only `sessionId` — there is no `error` parameter. Confirmed again +// at the actual call site +// (out/methods/trackTransactions/helpers/checkTransactionStatus/helpers/checkBatch/helpers/runSessionCallbacks.cjs): +// `onSuccess?.(sessionId)` / `onFail?.(sessionId)`, always exactly one +// argument. Declaring `onFail: (sessionId, error) => {...}` as a two +// -parameter function literal here is a real `tsc --strict` error (the +// declared function requires 2 arguments; the type only ever supplies 1) +// — not a style nit. This file uses the confirmed-correct one-parameter +// signature. -:::important -Make sure you build the contract before deploying it. Open the terminal and run the following command from the contract root directory: +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; -```bash -sc-meta all build -``` +const WALLET_CONNECT_PROJECT_ID = + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'multiversx-cookbook-demo'; -::: +// A tiny, page-scoped log the demo UI reads from — see TransactionTracker.tsx. +// Not part of the sdk-dapp API surface; just how this recipe surfaces the +// two callbacks' firing on screen instead of only in the console. +export const trackingEvents: string[] = []; -Once the contract is built, generate the interactor: +export const dappConfig = { + storage: { + getStorageCallback: (): Storage => sessionStorage, + }, + dAppConfig: { + environment: EnvironmentsEnum.devnet, + nativeAuth: true, + theme: ThemesEnum.dark, + providers: { + walletConnect: { + walletConnectV2ProjectId: WALLET_CONNECT_PROJECT_ID, + }, + }, + // The actual subject of this recipe's config side. `onSuccess`/`onFail` + // fire once per tracked SESSION (a group of one or more transactions + // sent together via the same `.track()` call), not once per + // transaction — confirmed from runSessionCallbacks.cjs, which also + // shows `onFail` fires for FOUR terminal states, not just a generic + // "fail": TransactionBatchStatusesEnum.fail, .cancelled, .timedOut, + // and .invalid all route to the same onFail callback. + transactionTracking: { + successfulToastLifetime: 5000, + onSuccess: async (sessionId: string): Promise => { + trackingEvents.push(`onSuccess(sessionId=${sessionId}) at ${new Date().toLocaleTimeString()}`); + }, + onFail: async (sessionId: string): Promise => { + trackingEvents.push(`onFail(sessionId=${sessionId}) at ${new Date().toLocaleTimeString()}`); + }, + }, + }, +}; -```bash -sc-meta all snippets +export { EnvironmentsEnum }; ``` -Add the interactor to your project. In `staking-contract/Cargo.toml`, add `interactor` as a member of the workspace: +## Sending and handing off to the tracker -```toml -[workspace] -members = [ - ".", - "meta", - "interactor" # <- new member added -] -``` +```ts title="src/transactions.ts" +// src/transactions.ts — the canonical sign / send / track flow (same +// verified pattern as the sign-and-send recipe's lib/transactions.ts), +// sending a trivial self-transfer of 0 EGLD purely as a vehicle to observe +// tracked status change — this recipe is about what happens AFTER +// `.track()` is called, not about the transaction itself. -Next, update the wallet path for sending transactions. In `staking-contract/interactor/src/interact.rs` locate the function `new(config: Config)` and **modify** the `wallet_address` variable with the [absolute path](https://www.redhat.com/en/blog/linux-path-absolute-relative) to your wallet: +import { Address, Transaction } from '@multiversx/sdk-core'; +import { GAS_PRICE, GAS_LIMIT } from '@multiversx/sdk-dapp/out/constants/mvx.constants'; +import { TransactionManager } from '@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager'; +import { getAccount } from '@multiversx/sdk-dapp/out/methods/account/getAccount'; +import { getNetworkConfig } from '@multiversx/sdk-dapp/out/methods/network/getNetworkConfig'; +import { getAccountProvider } from '@multiversx/sdk-dapp/out/providers/helpers/accountProvider'; +import type { SignedTransactionType } from '@multiversx/sdk-dapp/out/types/transactions.types'; + +export interface SendTrackedTransactionOutput { + sessionId: string; + transactionHash: string; +} + +/** + * Builds a zero-value self-transfer (sender === receiver), signs it via + * the connected provider, sends it, and starts tracking it — + * `TransactionManager.track()` is the call that hands the sent + * transaction over to sdk-dapp's WebSocket-with-polling-fallback tracker. + * Returns the sessionId the pending/successful/failed hooks key off of. + */ +export async function sendTrackedTransaction(): Promise { + const account = getAccount(); + const { network } = getNetworkConfig(); + + if (!account.address) { + throw new Error('No account connected. Call after a successful login.'); + } -```rust -let wallet_address = interactor - .register_wallet( - Wallet::from_pem_file("/MyTestWallets/tutorialKey.pem").expect( - "Unable to load wallet. Please ensure the file exists.", - ), - ) - .await; -``` + const selfAddress = Address.newFromBech32(account.address); + + const tx = new Transaction({ + sender: selfAddress, + receiver: selfAddress, + value: 0n, + gasLimit: BigInt(GAS_LIMIT), + gasPrice: BigInt(GAS_PRICE), + chainID: network.chainId, + nonce: BigInt(account.nonce), + version: 1, + }); -Finally, deploy the contract to Devnet: + const provider = getAccountProvider(); + const [signed] = await provider.signTransactions([tx]); + if (!signed) { + throw new Error('User cancelled signing.'); + } -```bash -cd interactor/ -cargo run deploy -``` + const txManager = TransactionManager.getInstance(); + const sentTransactions = await txManager.send([signed]); -:::note -To use Testnet instead, update `gateway_uri` in `staking-contract/interactor/config.toml` to: `https://testnet-gateway.multiversx.com`. + if (!isFlatSentTransactions(sentTransactions)) { + throw new Error('Unexpected response shape from TransactionManager.send().'); + } + const [sent] = sentTransactions; + if (!sent) { + throw new Error('Send failed: TransactionManager returned no transactions.'); + } -For mainnet, use: `https://gateway.multiversx.com`. + // THE actual subject of this recipe: handing the sent transaction to the + // tracker. Everything that happens after this line — the WebSocket + // subscription, the polling-interval fallback, the pending/successful/ + // failed hook updates, and the onSuccess/onFail callbacks configured in + // src/lib/multiversx.ts — runs on its own, with no further code needed + // here. + const sessionId = await txManager.track(sentTransactions, { + transactionsDisplayInfo: { + processingMessage: 'Tracking self-transfer…', + successMessage: 'Tracked transaction confirmed.', + errorMessage: 'Tracked transaction failed.', + }, + }); -More details can be found [here](/developers/constants/). -::: + return { sessionId, transactionHash: sent.hash }; +} + +function isFlatSentTransactions( + value: SignedTransactionType[] | SignedTransactionType[][], +): value is SignedTransactionType[] { + return value.length === 0 || !Array.isArray(value[0]); +} +``` + +## Reading live status + +```tsx title="src/TransactionTracker.tsx" +// src/TransactionTracker.tsx — the actual subject of this recipe: reading +// LIVE tracked-transaction status via sdk-dapp's flat React hooks, while a +// transaction moves from "pending" to "successful" or "failed" — updated +// by the WebSocket-with-polling-fallback tracker `sendTrackedTransaction()` +// (src/transactions.ts) hands the transaction to, with no manual refresh +// needed anywhere in this component. +// +// `useGetPendingTransactions()` / `useGetSuccessfulTransactions()` / +// `useGetFailedTransactions()` ALL return a flat `SignedTransactionType[]` +// — confirmed from their .d.ts files +// (node_modules/@multiversx/sdk-dapp/out/react/transactions/*.d.ts), the +// same shape the sign-and-send recipe already confirmed for the pending +// case alone. This recipe confirms the successful/failed hooks share that +// exact shape too. Each array reflects the WHOLE store's current state, not +// scoped to one sessionId — fine here since this demo only ever tracks one +// session at a time; for multiple concurrent sessions, the *Sessions +// variants (useGetPendingTransactionsSessions(), returning +// Record) are the ones keyed by +// sessionId — see this recipe's Pitfall 3. + +import { useState } from 'react'; +import { useGetPendingTransactions } from '@multiversx/sdk-dapp/out/react/transactions/useGetPendingTransactions'; +import { useGetSuccessfulTransactions } from '@multiversx/sdk-dapp/out/react/transactions/useGetSuccessfulTransactions'; +import { useGetFailedTransactions } from '@multiversx/sdk-dapp/out/react/transactions/useGetFailedTransactions'; +import { sendTrackedTransaction } from './transactions'; +import { trackingEvents } from './lib/multiversx'; + +export function TransactionTracker(): JSX.Element { + const [status, setStatus] = useState<'idle' | 'signing' | 'error'>('idle'); + const [error, setError] = useState(null); + const [lastSessionId, setLastSessionId] = useState(null); + + // Flat arrays, re-rendered automatically as the store updates — no + // polling or WebSocket code anywhere in THIS component. That machinery + // lives entirely inside sdk-dapp, started the moment + // sendTrackedTransaction() calls TransactionManager.track(). + const pending = useGetPendingTransactions(); + const successful = useGetSuccessfulTransactions(); + const failed = useGetFailedTransactions(); + + const handleSend = (): void => { + setStatus('signing'); + setError(null); + sendTrackedTransaction() + .then(({ sessionId }) => { + setLastSessionId(sessionId); + setStatus('idle'); + }) + .catch((err: unknown) => { + setError(err instanceof Error ? err.message : String(err)); + setStatus('error'); + }); + }; -You're going to see an error like the following: + return ( +
+

Track a transaction

+ + + {lastSessionId && ( +

+ Last sessionId: {lastSessionId} +

+ )} + {error &&

Error: {error}

} + +
+
+

useGetPendingTransactions()

+

{pending.length} pending

+
    + {pending.map((tx) => ( +
  • + {tx.hash.slice(0, 12)}… — {tx.status ?? 'unknown'} +
  • + ))} +
+
+
+

useGetSuccessfulTransactions()

+

{successful.length} successful

+
    + {successful.map((tx) => ( +
  • + {tx.hash.slice(0, 12)}… — {tx.status ?? 'unknown'} +
  • + ))} +
+
+
+

useGetFailedTransactions()

+

{failed.length} failed

+
    + {failed.map((tx) => ( +
  • + {tx.hash.slice(0, 12)}… — {tx.status ?? 'unknown'} +
  • + ))} +
+
+
+ +

transactionTracking.onSuccess / onFail events (src/lib/multiversx.ts)

+ {trackingEvents.length === 0 ? ( +

(none fired yet)

+ ) : ( +
    + {trackingEvents.map((event) => ( +
  • + {event} +
  • + ))} +
+ )} +
+ ); +} -```bash -error sending tx (possible API failure): transaction generation failed: insufficient funds for address erd1... -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace -``` +const cardStyle: React.CSSProperties = { + border: '1px solid #444', + borderRadius: '8px', + padding: '1.25rem', + marginTop: '1rem', +}; -This is because your account has no EGLD in it. +const buttonStyle: React.CSSProperties = { + padding: '0.75rem 1.25rem', + fontSize: '1rem', + cursor: 'pointer', +}; +const columnsStyle: React.CSSProperties = { + display: 'grid', + gridTemplateColumns: '1fr 1fr 1fr', + gap: '1rem', + marginTop: '1rem', +}; -### Getting EGLD on Devnet +const errorStyle: React.CSSProperties = { + color: '#e05d44', +}; -There are many ways of getting EGLD on Devnet: +const rawStyle: React.CSSProperties = { + color: '#888', + fontSize: '0.85em', +}; +``` -- Through the Devnet wallet; -- Using an external faucet; -- From the [MultiversX Builders Discord Server faucet](https://discord.gg/multiversxbuilders); -- By asking a team member on [Telegram](https://t.me/MultiversXDevelopers). +## How it works + +**All three status hooks return a flat array, not a sessionId-keyed object.** +Confirmed from the installed `.d.ts` files: `useGetPendingTransactions()`, +`useGetSuccessfulTransactions()`, and `useGetFailedTransactions()` all return +`SignedTransactionType[]`. This extends what +[Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) +already confirmed for the pending case alone to the successful/failed hooks too. +Each reflects the whole store's current tracked state, not scoped to one +sessionId, the `*Sessions` variants are what you would use for multiple concurrent +sessions (see Pitfall 3). + +**The actual WebSocket-vs-polling mechanism, read directly from the compiled +source** (`trackTransactions.cjs`), not guessed from a one-line description: + +- `trackTransactions()` subscribes to the store's `websocketStatus` field. +- While the WebSocket is `PENDING` or not yet initialized, it runs + `setInterval(checkTransactionStatus, pollingInterval)`, the polling fallback. +- Once status flips to `COMPLETED`, it clears that interval and switches to + checking status only when a NEW `websocketEvent` arrives in the store, no + interval running while the socket is live. +- `pollingInterval` is `max(1000, roundDuration / 2)` when the network's block + round duration is known, falling back to a fixed `90000` (90 seconds, confirmed + from `constants/transactions.constants.cjs`) only when it is not. This is a + different constant, and a more adaptive mechanism, than sdk-core's own + `TransactionWatcher` default (every 6000ms, timeout after 90000ms), that class + is a separate, backend-oriented poller used by + `entrypoint.awaitCompletedTransaction()`, not what powers these browser hooks. + +**A confirmed, real bug some docs get wrong in their `transactionTracking` +snippet.** A snippet showing `onFail: (sessionId, error) => { }` implies two +parameters. The real type: +```ts +export type TransactionTrackingConfigType = { + successfulToastLifetime?: number; + onSuccess?: (sessionId: string) => Promise; + onFail?: (sessionId: string) => Promise; +}; +``` -#### Getting EGLD through devnet wallet +Only `sessionId`, confirmed again at the actual call site +(`onSuccess?.(sessionId)` / `onFail?.(sessionId)`, always one argument). Tested +directly: declaring a two-parameter `onFail` fails `tsc --strict` with +`Target signature provides too few arguments. Expected 2 or more, but got 1.` -Go to [Devnet Wallet](https://devnet-wallet.multiversx.com) and login to your account using your PEM file. From the left side menu, select *Faucet*: +**`onSuccess`/`onFail` fire once per tracked SESSION, and `onFail` covers four +terminal states**, `fail`, `cancelled`, `timedOut`, and `invalid` all route to the +same callback, confirmed from source. -![img](/developers/staking-contract-tutorial-img/wallet_faucet.png) +## Pitfalls -Request the tokens. After a few seconds you should have **5 xEGLD** in your wallet. +:::danger[Pitfall 1: a two-parameter onFail fails tsc --strict] +Use a single-parameter `(sessionId: string) => Promise`, see "How it works" +above for the exact error message this produces. +::: +:::warning[Pitfall 2: do not assume a fixed polling interval] +It adapts to the network's actual round duration when known; the 90-second +constant is a fallback, not the steady-state behavior on a healthy connection to a +live network. +::: -#### Getting EGLD through external faucet +:::note[Pitfall 3: the flat hooks do not scope by sessionId] +If your app tracks multiple independent sessions concurrently and needs each one's +status separately, use the `*Sessions` variants +(`useGetPendingTransactionsSessions()` and friends), keyed by sessionId, instead +of filtering the flat arrays yourself. +::: -Go to [https://r3d4.fr/faucet](https://r3d4.fr/faucet) and submit a request: -![img](/developers/staking-contract-tutorial-img/external_faucet.png) +## See also -Make sure you selected `Devnet` and input **your** address! +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + covers the build, sign, send steps this recipe's `transactions.ts` shares, + without the tracking focus. +- [Read the connected account with useGetAccount](/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account) + applies the same "flat hooks reflect live store state, no manual refresh" + pattern to account data. +- [Send EGLD to an address](/sdk-and-tools/sdk-js/cookbook/transactions/send-egld) + is the sdk-core-only equivalent for backend/script contexts, where a different + poller (`TransactionWatcher`) plays the analogous role. -It might take a little while, depending on how "busy" the faucet is. +--- +### Transaction Overview -### Deploying the contract, second try +A big part of the life of a blockchain developer is to create and launch blockchain transactions. -Run the `deploy` command again and let's see the results: +Whether it's an off-chain tool, smart contract code, or a testing scenario, it is important that we have a powerful syntax to express any conceivable transaction. -```bash -sender's recalled nonce: 0 --- tx nonce: 0 -sc deploy tx hash: 8a007... -deploy address: erd1qqqqqqqqqqqqq... -new address: erd1qqqqqqqqqqqqq... -``` +During the process of creating the development framework, we realized that the following are equivalent to a large extent and could be expressed using the same syntax: +- transactions launched from smart contracts, +- blackbox integration tests, +- off-chain calls. -Alternatively, you can check the address in the logs tab on [Devnet Explorer](https://devnet-explorer.multiversx.com/transactions), namely the `SCDeploy` method. +We called this "unified transaction syntax" or "unified syntax", and the first version of it was released in [multiversx-sc version 0.49.0](https://github.com/multiversx/mx-sdk-rs/releases/tag/v0.49.0). +In this documentation, you will get a complete explanation of all the features of this syntax, organized around the various components of a blockchain transaction. -#### Too much gas error? -Everything should work just fine, but you'll see this message: -![img](/developers/staking-contract-tutorial-img/too_much_gas.png) -This is **not** an error. This simply means you provided way more gas than needed, so all the gas was consumed instead of the leftover being returned to you. +## Motivation and design -This is done to protect the network against certain attacks. For instance, one could always provide the maximum gas limit and only use very little, decreasing the network's throughput significantly. +Transactions can be fairly complex, and in order to get them to use the same syntax in very different environments, we had a few challenges: +1. Transactions have many fields, which can be configured in many ways. It is important to be able to configure most of them independently, otherwise the framework becomes too large, or unreliable. +2. Since this syntax is also used in contracts, it was essential to choose a design that adds almost no runtime and code size overhead. So the syntax must make heavy use of generics, to resolve at compile-time all type checks, conversions, and restrictions. +3. Also, because syntax is used in contracts, it had to rely on managed types. +4. We wanted as much type safety as possible, so we had to find a way to rely on the ABI to produce type checks both for contract inputs and outputs. -## The first stake -Let's update the `stake` function from `staking-contract/interactor/src/interact.rs` to do the first stake. -Initialize the `egld_amount` variable with `1` instead of `0`: +## The `Tx` object -```rust -let egld_amount = BigUint::::from(1u128); +We decided to model all transactions using a single object, but with exactly 7 generic arguments, one for each of the transaction fields. + +```mermaid +graph LR + subgraph Tx + Env + From + To + Payment + Gas + Data + rh[Result Handler] + end ``` -This variable represents the amount of EGLD to be sent with the transaction. +
-Let's stake! At path `staking-contract/interactor` run the following command: +Each one of these 7 fields has a trait that governs what types are allowed to occupy the respective position. -```bash -cargo run stake -``` +All of these positions (except the environment `Env`) can be empty, uninitialized. This is signaled at compile time by the unit type, `()`. In fact, a transaction always starts out with all fields empty, except for the environment. -We've now successfully staked 1 EGLD... or have we? +For instance, if we are in a contract and write `self.tx()`, the universal start of a transaction, the resulting type will be `Tx, (), (), (), (), (), ()>`, where `TxScEnv` is simply the smart contract call environment. Of course, the transaction at this stage is unusable, it is up to the developer to add the required fields and send it. -If we look at the transaction, that's not quite the case: -![img](/developers/staking-contract-tutorial-img/first_stake.png) -### Why was a smaller amount of EGLD sent? +## The `Tx` fields -This happens because EGLD uses **18 decimals**. So, to send 1 EGLD, you actually need to send the value `1000000000000000000` (i.e. 1018). +We have dedicated a page to each of these 7 fields: -The blockchain works only with unsigned integers. Floating point numbers are not allowed. The only reason the explorer displays the balances with a floating point is because it's much more user-friendly to tell someone they have 1 EGLD instead of 1000000000000000000 EGLD, but internally, only the integer value is used. +| Field | Description | +| ------------------------------------- | --------------------------------------------------------------------------------------------------------- | +| [Environment](tx-env) | Some representation of the environment where the transaction runs. | +| [From](tx-from) | The transaction sender. Implicit for SC calls (the contract is the sender), but mandatory anywhere else. | +| [To](tx-to) | The receiver. Needs to be specified for any transaction expect for deploys. | +| [Payment](tx-payment) | Optional, can be EGLD, single or multi-ESDT. We also have some payment types that get decided at runtime. | +| [Gas](tx-gas) | Some transactions need explicit gas, others don't. | +| [Data](tx-data) | [Proxies](tx-proxies) (ideally) or raw | The main part of the payload. Can be inhabited by a function call, deploy data, upgrade data, or nothing, in the case of direct transfers. | +| [Result Handlers](tx-result-handlers) | Anything that deals with results, from callbacks to decoding logic. | +We could also group them in three broad categories: +- The [environment](tx-env) is its own category, pertaining to both **inputs and outputs**. +- 5 **inputs**: [From](tx-from), [To](tx-to), [Payment](tx-payment), [Gas](tx-gas), [Data](tx-data). +- 1 field dealing with the **output**: [the result handlers](tx-result-handlers). -### But how do I send 0.5 EGLD? -Since we know EGLD has 18 decimals, we have to simply multiply 0.5 by 1018, which yields 500000000000000000. +## Transaction builder -### Actually staking 1 EGLD +Now that we've seen what the contents of a transaction are, let's see how we can specify them in code. -To stake 1 EGLD, simply update the `egld_amount` in the `stake` function from `staking-contract/interactor/src/interact.rs` with the following: +Of course, the developer shouldn't access the fields of the transaction directly. There is sort of a builder pattern when constructing a transaction. + +In its most basic form, a transaction might be constructed as follows: ```rust -let egld_amount = BigUint::::from(1000000000000000000u128); +self.tx() + .from(from) + .to(to) + .payment(payment) + .gas(gas) + .raw_call("function") + .with_result(result_handler) ``` -Now let's try staking again: +:::caution Important +While this may look like a traditional OOP builder pattern, there is one important aspect to point out: -![img](/developers/staking-contract-tutorial-img/second_stake.png) +*Each of these setters outputs a type that is different from its input.* +We are not merely setting new data into an existing field, we are also specifying the type of each field, at compile-time. At each step we are not only specifying the data, but also the types. +::: -## Performing contract queries +Even though these look like simple setters, we are first of all constructing a type using this syntax. -To perform smart contract queries for the `getStakingPosition` view, update the `addr` variable in the `staking_position` function from `staking-contract/interactor/src/interact.rs` with the address of the wallet you used for the staking transaction: +Also note that the way these methods are set up, it is impossible to call most of them twice. They only work when the respective field is not yet set (of unit type `()`), so writing something like `self.tx().from(a).from(b)` causes a compiler error. -```rust -let addr = bech32::decode("erd1vx..."); -``` +Here we have some of the most common methods used to construct a transaction: -Query the staking position by running the following command in the terminal, inside the `staking-contract/interactor` directory: +| Field | Filed name | Initialize with | +| ------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------- | +| [Environment](tx-env) | `env` | `.tx()` | +| [From](tx-from) | `from` | `.from(...)` | +| [To](tx-to) | `to` | `.to(...)` | +| [Payment](tx-payment) | `payment` | `.payment(...)` | +| [Gas](tx-gas) | `gas` | `.gas(...)` | +| [Data](tx-data) | `data` | `.typed(...)` (ideally)
`.raw_call(...)`
`.raw_deploy()`
`.raw_upgrade()` | +| [Result Handlers](tx-result-handlers) | `result_handler` | `.callback(...)`
`.returns(...)`
`.with_result(...)` | -```bash -cargo run getStakingPosition -``` +The list is by no means exhaustive, it is just an initial overview. Please find the full documentation on each of the linked pages. -This will show the staking amount according to the smart contract's internal state: -```bash -Result: 1000000000000000001 -``` +## Execution -The result includes 1 EGLD plus the initial 10-18 EGLD that was sent. +Ultimately, the purpose of a transaction is to be executed. Simply constructing a transaction has no effect in itself. So we must finalize each transaction construct with a call that sends it to the blockchain. -Next, query the **stakers list**. Replace the next line from `staked_addresses` **function** in `staking-contract/interactor/src/interact.rs`: +The allowed execution methods depend on the environment. More specifically: +- In a contract, the options are `.transfer()`, `.transfer_execute()`, `.async_call_and_exit()`, `.sync_call()`, etc. +- In tests just `.run()` is universal. +- In interactors, we call `.run().await`. It's the same method name, but this time it's asynchronous Rust, so it can only be called in an `async` function, and `.await` is necessary. -```rust -println!("Result: {result_value:?}"); -``` +More on this in the [launch page](tx-run). -with the following: -```rust -for result in result_value.iter() { - println!("Result: {}", Bech32Address::from(result).to_bech32_string()); -} -``` +## Map of the fields types -It is necessary to iterate through `result_value` because it is a [`MultiValueVec`](/docs/developers/data/multi-values.md#standard-multi-values) of `Address`. Each address is converted to `Bech32Address` to ensure it’s printed in Bech32 format, not as raw ASCII. +This is a graph of what the common types are that fit in each of the transaction fields. -Run in terminal at path `staking-contract/interactor`: -```bash -cargo run getStakedAddresses +```mermaid +graph LR + subgraph Tx + Env + From + To + Payment + Gas + Data + rh[Result Handler] + end + Env --> TxScEnv + Env --> ScenarioEnvExec + Env --> ScenarioEnvQuery + Env --> InteractorEnvExec + Env --> InteractorEnvQuery + From --> from-unit["()"] + From --> from-man-address[ManagedAddress] + From --> from-address[Address] + From --> from-bech32[Bech32Address] + From --> from-test-addr[TestAddress] + To --> to-unit["()"] + To --> to-man-address[ManagedAddress] + To --> to-address[Address] + To --> to-bech32[Bech32Address] + To --> to-test-addr[TestAddress] + To --> to-test-sc[TestSCAddress] + To --> to-caller[ToCaller] + To --> to-self[ToSelf] + Payment --> payment-unit["()"] + Payment --> egld["Egld(EgldValue)"] + egld --> egld-biguint["Egld(BigUint)"] + egld --> egld-u64["Egld(u64)"] + Payment --> EsdtTokenPayment + Payment --> EsdtTokenPaymentRefs + Payment --> MultiEsdtPayment + Payment --> EgldOrEsdtTokenPaymentRefs + Payment --> EgldOrMultiEsdtPayment + Gas --> gas-unit["()"] + Gas --> gas-explicit["ExplicitGas(u64)"] + Gas --> GasLeft + Data --> data-unit["()"] + Data --> deploy["DeployCall<()>"] + deploy --> deploy-from-source["DeployCall<FromSource<ManagedAddress>>"] + deploy --> deploy-code["DeployCall<Code<ManagedBuffer>>"] + Data --> upgrade["UpgradeCall<()>"] + upgrade --> upgrade-from-source["UpgradeCall<CodeSource<ManagedAddress>>"] + upgrade --> upgrade-code["UpgradeCall<Code<ManagedBuffer>>"] + Data --> fc[FunctionCall] + rh --> rh-unit("()") + rh --> rh-ot("OriginalTypeMarker") + rh --> CallbackClosure --> CallbackClosureWithGas + rh --> Decoder ``` -Running this function should yield a result like this: -```bash -Result: erd1vx... -``` +## Map of the setters +Constructing a transaction is similar to exploring a map, or running a finite state machine. You start with an initial position, and then get to choose in which direction to go. -## Adding unstake functionality +Choosing a path at one point closes off many other options. The compiler is always guiding us and preventing us from ending up with an invalid transaction. -Currently, users can only stake, but they cannot actually get their EGLD back... at all. Let's add the unstake functionality in our smart contract: +Here is a map of all the paths you can take when configuring a transaction. The fields are mostly independent, so the map is split into 7 sections. See more details on each of their respective pages. -```rust -#[endpoint] -fn unstake(&self) { - let caller = self.blockchain().get_caller(); - let stake_mapper = self.staking_position(&caller); - let caller_stake = stake_mapper.get(); - if caller_stake == 0 { - return; - } +```mermaid +graph LR + subgraph Environment + sc-code["SC Code"] -->|"self.tx()"| sc-env[TxScEnv] + test-code["Test Code"] -->|"world.tx()"| ScenarioEnvExec + test-code["Test Code"] -->|"world.query()"| ScenarioEnvQuery + intr-code["Interactor Code"] -->|"interactor.tx()"| InteractorEnvExec + intr-code["Interactor Code"] -->|"interactor.query()"| InteractorEnvQuery + end +``` - self.staked_addresses().swap_remove(&caller); - stake_mapper.clear(); +```mermaid +graph LR + subgraph From + from-unit["()"] + from-unit -->|from| from-man-address[ManagedAddress] + from-unit -->|from| from-address[Address] + from-unit -->|from| from-bech32[Bech32Address] + end +``` - self.tx().to(caller).egld(caller_stake).transfer(); -} +```mermaid +graph LR + subgraph To + to-unit["()"] + to-unit -->|to| to-man-address[ManagedAddress] + to-unit -->|to| to-address[Address] + to-unit -->|to| to-bech32[Bech32Address] + to-unit -->|to| to-esdt-system-sc[ESDTSystemSCAddress] + to-unit -->|to| to-caller[ToCaller] + to-unit -->|to| to-self[ToSelf] + end ``` -You might notice the `stake_mapper` variable. Just to remind you, the mapper's definition looks like this: +```mermaid +graph LR + subgraph Payment + payment-unit["()"] + payment-unit -->|egld| egld-biguint["Egld(BigUint)"] + payment-unit -->|egld| egld-u64["Egld(u64)"] + payment-unit -->|esdt| EsdtTokenPayment + EsdtTokenPayment -->|esdt| MultiEsdtPayment + MultiEsdtPayment -->|esdt| MultiEsdtPayment + payment-unit -->|payment| MultiEsdtPayment + payment-unit -->|multi_esdt| EsdtTokenPayment + payment-unit -->|payment| EgldOrEsdtTokenPaymentRefs + payment-unit -->|egld_or_single_esdt| EgldOrEsdtTokenPaymentRefs + payment-unit -->|payment| EgldOrMultiEsdtPayment + payment-unit -->|egld_or_multi_esdt| EgldOrMultiEsdtPayment + end +``` -```rust -#[storage_mapper("stakingPosition")] -fn staking_position(&self, addr: &ManagedAddress) -> SingleValueMapper; +```mermaid +graph LR + subgraph Gas + gas-unit["()"] + gas-unit -->|gas| gas-explicit["ExplicitGas(u64)"] + gas-unit -->|gas| GasLeft + end ``` -In Rust terms, this is a method of our contract trait, with one argument, that returns a [`SingleValueMapper`](/docs/developers/developer-reference/storage-mappers.md#singlevaluemapper). All [mappers](/docs/developers/developer-reference/storage-mappers.md) are nothing more than structure types that provide an interface to the storage API. +```mermaid +graph LR + subgraph Data + data-unit["()"] + data-unit ----->|raw_deploy| deploy["DeployCall<()>"] + deploy -->|from_source| deploy-from-source["DeployCall<FromSource<_>>"] + deploy -->|code| deploy-code["DeployCall<Code<_>>"] + deploy -->|code_metadata| deploy + data-unit ----->|raw_upgrade| upgrade["UpgradeCall<()>"] + upgrade -->|from_source| upgrade-from-source["UpgradeCall<CodeSource<_>>"] + upgrade -->|code| upgrade-code["UpgradeCall<Code<_>>"] + upgrade -->|code_metadata| upgrade + data-unit ---->|raw_call| fc[FunctionCall] + data-unit -->|typed| Proxy + Proxy -->|init| deploy + Proxy -->|upgrade| upgrade + Proxy -->|endpoint| fc[Function Call] + end +``` + +```mermaid +graph LR + subgraph Result Handlers + rh-unit("()") + rh-unit -->|original_type| rh-ot("OriginalTypeMarker") + rh-ot -->|callback| CallbackClosure -->|gas_for_callback| CallbackClosureWithGas + dh[Decode Handler] + rh-unit -->|"returns
with_result"| dh + rh-ot -->|"returns
with_result"| dh + dh -->|"returns
with_result"| dh + end +``` + +--- + +### transactions -So then, why save the mapper in a variable? +This page describes the structure of the `transactions` index (Elasticsearch), and also depicts a few examples of how to query it. -### Better usage of storage mapper types +## _id -Each time you access `self.staking_position(&addr)`, the storage key has to be constructed again, by concatenating the static string `stakingPosition` with the given `addr` argument. The mapper saves its key internally, so if we reuse the same mapper, the key is only constructed once. +:::warning Important -This saves us the following operations: +**The `transactions` index will be deprecated and removed in the near future.** +We recommend using the [operations](/sdk-and-tools/indices/es-index-operations) index, which contains all the transaction data. +The only change required in your queries is to include the `type` field with the value `normal` to fetch all transactions. -```rust -let mut key = ManagedBuffer::new_from_bytes(b"stakingPosition"); -key.append(addr.as_managed_buffer()); -``` +Please make the necessary updates to ensure a smooth transition. +If you need further assistance, feel free to reach out. +::: -Instead, we just reuse the key we built previously. This can be a great performance enhancement, especially for mappers with multiple arguments. For mappers with no arguments, the improvement is minimal, but might still be worth thinking about. +The `_id` field for this index is composed of hex-encoded transaction hash. +(example: `cad4692a092226d68fde24840586bdf36b30e02dc4bf2a73516730867545d53c`) -### Partial unstake +## Fields -Some users might only want to unstake a part of their tokens, so we could simply add an `unstake_amount` argument: -```rust -#[endpoint] -fn unstake(&self, unstake_amount: BigUint) { - let caller = self.blockchain().get_caller(); - let remaining_stake = self.staking_position(&caller).update(|staked_amount| { - require!( - unstake_amount > 0 && unstake_amount <= *staked_amount, - "Invalid unstake amount" - ); - *staked_amount -= &unstake_amount; +| Field | Description | +|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| miniBlockHash | The miniBlockHash field represents the hash of the miniblock in which the transaction was included. | +| nonce | The nonce field represents the transaction sequence number of the sender address. | +| round | The round field represents the round of the block when the transaction was executed. | +| value | The value field represents the amount of EGLD to be sent from the sender to the receiver. | +| receiver | The receiver field represents the destination address of the transaction. | +| sender | The sender field represents the address of the transaction sender. | +| receiverShard | The receiverShard field represents the shard ID of the receiver address. | +| senderShard | The senderShard field represents the shard ID of the sender address. | +| gasPrice | The gasPrice field represents the amount to be paid for each gas unit. | +| gasLimit | The gasLimit field represents the maximum gas units the sender is willing to pay for. | +| gasUsed | The gasUsed field represents the amount of gas used by the transaction. | +| fee | The fee field represents the amount of EGLD the sender paid for the transaction. | +| initialPaidFee | The initialPaidFee field represents the initial amount of EGLD the sender paid for the transaction, before the refund. | +| data | The data field holds additional information for a transaction. It can contain a simple message, a function call, an ESDT transfer payload, and so on. | +| signature | The signature of the transaction, hex-encoded. | +| timestamp | The timestamp field represents the timestamp of the block in which the transaction was executed. | +| status | The status field represents the status of the transaction. | +| senderUserName | The senderUserName field represents the username of the sender address. | +| receiverUserName | The receiverUserName field represents the username of the receiver address. | +| hasScResults | The hasScResults field is true if the transaction has smart contract results. | +| isScCall | The isScCall field is true if the transaction is a smart contract call. | +| hasOperations | The hasOperations field is true if the transaction has smart contract results. | +| tokens | The tokens field contains a list of ESDT tokens that are transferred based on the data field. The indices from the `tokens` list are linked with the indices from `esdtValues` list. | +| esdtValues | The esdtValues field contains a list of ESDT values that are transferred based on the data field. | +| receivers | The receivers field contains a list of receiver addresses in case of ESDTNFTTransfer or MultiESDTTransfer. | +| receiversShardIDs | The receiversShardIDs field contains a list of receiver addresses shard IDs. | +| type | The type field represents the type of the transaction based on the data field. | +| operation | The operation field represents the operation of the transaction based on the data field. | +| function | The function field holds the name of the function that is called in case of a smart contract call. | +| isRelayed | The isRelayed field is true if the transaction is a relayed transaction. | +| version | The version field represents the version of the transaction. | +| hasLogs | The hasLogs field is true if the transaction has logs. | - staked_amount.clone() - }); - if remaining_stake == 0 { - self.staked_addresses().swap_remove(&caller); - } - self.tx().to(caller).egld(unstake_amount).transfer(); -} -``` +## Query examples -As you might notice, the code changed quite a bit: -1. To handle invalid user input, we used the `require!` statement; -2. Previously, we used `clear` to reset the staking position. However, now that we need to modify the stored value, we use the `update` method, which allows us to change the currently stored value through a mutable reference. +### Fetch the latest transactions of an address -[`update`](/docs/developers/developer-reference/storage-mappers.md#update) is the same as doing [`get`](/docs/developers/developer-reference/storage-mappers.md#get), followed by computation, and then [`set`](/docs/developers/developer-reference/storage-mappers.md#set), but it's just a lot more compact. Additionally, it also allows us to return anything we want from the given closure, so we use that to detect if this was a full unstake. +``` +curl --request GET \ + --url ${ES_URL}/transactions/_search \ + --header 'Content-Type: application/json' \ + --data '{ + "query": { + "bool": { + "should": [ + { + "match": { + "sender": "erd..." + } + }, + { + "match": { + "receiver": "erd..." + } + }, + { + "match": { + "receivers": "erd..." + } + } + ] + } + }, + "sort": [ + { + "timestamp": { + "order": "desc" + } + } + ] +}' +``` +--- -### Optional arguments +### TypeScript strict-mode checklist for sdk-dapp consumers -For a bit of performance enhancement, we could have the `unstake_amount` as an optional argument, with the default being full unstake. +Every item below is a real bug this Cookbook's own build found while verifying its +recipes against actually-installed `@multiversx/sdk-dapp`, not a hypothetical list. +Each is the concrete, runnable version of a class of failure: docs examples that +do not survive `tsc --strict`. -```rust -#[endpoint] -fn unstake(&self, opt_unstake_amount: OptionalValue) { - let caller = self.blockchain().get_caller(); - let stake_mapper = self.staking_position(&caller); - let unstake_amount = match opt_unstake_amount { - OptionalValue::Some(amt) => amt, - OptionalValue::None => stake_mapper.get(), - }; +## Prerequisites - let remaining_stake = stake_mapper.update(|staked_amount| { - require!( - unstake_amount > 0 && unstake_amount <= *staked_amount, - "Invalid unstake amount" - ); - *staked_amount -= &unstake_amount; +- A TypeScript project consuming `@multiversx/sdk-dapp` v5. +- `strict: true` in `tsconfig.json`. - staked_amount.clone() - }); - if remaining_stake == 0 { - self.staked_addresses().swap_remove(&caller); - } +## The tsconfig - self.tx().to(caller).egld(unstake_amount).transfer(); -} -``` +| Flag | Why it matters here | +| --- | --- | +| `strict: true` | Umbrella flag; without it, most of the bugs below compile silently. | +| `strictNullChecks` | Catches `account.balance` used before confirming `account` is not `null`/`undefined`, common when reading store state before `initApp()` resolves. | +| `noUncheckedIndexedAccess` | Array/object index access returns `T \| undefined` instead of `T`. Catches assuming `sentTransactions[0]` exists after a `.send()` call, see Item 4. | +| `exactOptionalPropertyTypes` | Distinguishes "property omitted" from "property explicitly `undefined`" in config objects like `dAppConfig`. | +| `noImplicitAny` | Without it, an unresolved sdk-dapp import path silently degrades to `any`, and every bug on this page stops being a compile error. | +| `noUnusedLocals` / `noUnusedParameters` | Catches leftover v4 imports after a migration pass. | -This makes it so if someone wants to perform a full unstake, they can simply not give the argument at all. +## The checklist +### 1. theme is a ThemesEnum member, not a string literal -### Unstaking our Devnet tokens +```ts title="src/theme.ts" +// src/theme.ts — Checklist item 1: `theme` is an enum member, not a string. +// +// The naive port of sdk-dapp's own prose docs (which show `theme: 'dark'` +// without importing anything) fails strict-mode compile. Confirmed +// against the real, installed +// node_modules/@multiversx/sdk-dapp/out/methods/initApp/initApp.types.d.ts +// while verifying this Cookbook's start-here recipes (nextjs-minimal, +// sign-and-send, vite-react-minimal all had this bug). -Now that the unstake function has been added, let's test it out on Devnet. Build the smart contract again. In the contract root (`staking-contract/`), run the next command: +import { EnvironmentsEnum } from '@multiversx/sdk-dapp/out/types/enums.types'; +import { ThemesEnum } from '@multiversx/sdk-dapp/out/types/theme.types'; + +// WRONG — fails tsc --strict: +// +// const dappConfig = { +// dAppConfig: { +// environment: EnvironmentsEnum.devnet, +// theme: 'dark' as const, +// }, +// }; +// +// error TS2322: Type '"dark"' is not assignable to type +// '`${ThemesEnum}`'. + +// RIGHT: +export const dappConfig = { + dAppConfig: { + environment: EnvironmentsEnum.devnet, + theme: ThemesEnum.dark, + }, +}; +``` -```bash -sc-meta all build +Found in three of this Cookbook's own recipes during verification (`nextjs-minimal`, +`sign-and-send`, `vite-react-minimal`) before being fixed. + +### 2. UnlockPanelManager callbacks must return Promise<void> + +```ts title="src/unlockPanel.ts" +// src/unlockPanel.ts — Checklist item 2: UnlockPanelManager callbacks must +// return Promise, not void. +// +// A real bug this Cookbook hit. The docs' own example for +// UnlockPanelManager.init uses plain synchronous callbacks; that shape +// does not satisfy the real installed type. Confirmed from +// node_modules/@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager.types.d.ts. + +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; + +// WRONG — fails tsc --strict: +// +// UnlockPanelManager.init({ +// loginHandler: () => { +// console.log('logged in'); +// }, +// }); +// +// error TS2322: Type '() => void' is not assignable to type +// 'OnProviderLoginType'. +// Type 'void' is not assignable to type 'Promise'. + +// RIGHT — async, even if the body has nothing to await: +export function initUnlockPanel(): void { + UnlockPanelManager.init({ + loginHandler: async (): Promise => { + // eslint-disable-next-line no-console + console.log('logged in'); + }, + onClose: async (): Promise => { + // eslint-disable-next-line no-console + console.log('panel closed'); + }, + }); +} ``` -After building the contract, regenerate the interactor by running the following command in the terminal, also in the contract root: +This is the reason `OnCloseUnlockPanelType` shows up by name in this Cookbook's +migration recipes. + +### 3. @typescript-eslint/no-floating-promises must actually be wired, and actually run + +```ts title="src/floatingPromise.ts" +// src/floatingPromise.ts — Checklist item 3: the ESLint rule that catches +// what tsc alone does not, PLUS the meta-bug of the rule silently not +// running at all. +// +// UnlockPanelManager.getInstance().openUnlockPanel() returns Promise +// (confirmed from the real .d.ts). Calling it from a synchronous onClick +// handler without `void` or `await` is a floating promise: if it rejects +// (e.g. the user's environment has no injected wallet), the rejection is +// silently swallowed — no error boundary, no console warning by default. +// tsc --strict does NOT flag this on its own; a floating Promise is +// still assignable to a `void`-returning callback signature. This is +// exactly why @typescript-eslint/no-floating-promises exists as a +// SEPARATE gate from tsc. +// +// Validation performed while authoring this recipe: temporarily removed +// the `void` operator below and re-ran `npm run lint` against this +// exact .eslintrc.cjs. It failed with: +// error Promises must be awaited, end with a call to .catch, end with +// a call to .then with a rejection handler or be explicitly marked as +// ignored with the `void` operator @typescript-eslint/no-floating-promises +// — confirming this recipe's own lint config does NOT have the +// silent-no-op bug it warns about (see .eslintrc.cjs's header comment). +// Restored the `void` operator afterward; this file lints clean. + +import { UnlockPanelManager } from '@multiversx/sdk-dapp/out/managers/UnlockPanelManager/UnlockPanelManager'; + +export function handleConnectClick(): void { + // WRONG — a real, reproducible eslint error under this exact config: + // + // UnlockPanelManager.getInstance().openUnlockPanel(); + // + // RIGHT — explicitly discard the promise (there's nothing to await from + // a sync onClick handler here): + void UnlockPanelManager.getInstance().openUnlockPanel(); +} +``` + +Two distinct failure modes, both real: the rule silently does nothing if +`parserOptions.project` is not set (this Cookbook's own `nextjs-minimal` recipe +shipped exactly this bug once), and, once wired, it catches real bugs `tsc` alone +does not, since a floating `Promise` is still assignable everywhere a +`void`-returning callback is expected. + +### 4. Don't assume a .send() result is the class instance you signed + +```ts title="src/transactionHash.ts" +// src/transactionHash.ts — Checklist item 4: don't assume a helper class +// accepts the object shape a hand-signed value happens to look like, AND +// don't assume a union return type narrows just because your own input +// didn't. +// +// TransactionManager.getInstance().send() returns SignedTransactionType[] +// (a plain object shape: `{ ...Transaction fields, hash, status }`), +// NOT sdk-core Transaction class instances. Passing that return value +// into TransactionComputer.computeTransactionHash() — which expects a +// real Transaction instance with its class methods — is a real, +// reproducible tsc --strict failure, confirmed while authoring this +// Cookbook's sign-and-send recipe. The fix isn't a cast: SignedTransactionType already +// carries `hash: string` directly, computed server-side, so there's +// nothing to recompute client-side at all. +// +// A second, related trap this file's own first draft hit while being +// verified for this Cookbook: send()'s declared return type is +// `SignedTransactionType[] | SignedTransactionType[][]` — a union, +// UNCONDITIONALLY, even when the input you passed was a plain flat +// `Transaction[]` (see the +// send-transactions-to-transaction-manager recipe for why the return +// type is a union at all: send() also accepts a nested batch input). +// There is no overload that narrows the return based on which arm of the +// input union you passed, so `const [first] = sent; first.hash` fails — +// `first`'s type includes `SignedTransactionType[]` (the nested-batch +// member), which has no `.hash`. Narrow explicitly at runtime instead of +// casting blindly. + +import { TransactionManager } from '@multiversx/sdk-dapp/out/managers/TransactionManager/TransactionManager'; +import type { SignedTransactionType } from '@multiversx/sdk-dapp/out/types/transactions.types'; +import { TransactionComputer, type Transaction } from '@multiversx/sdk-core'; + +/** + * Narrows send()'s always-a-union return type down to the flat shape, + * using the same Array.isArray check the SDK's own internal + * isBatchTransaction() helper uses — not a blind `as` cast. + */ +function assertFlatResult( + sent: SignedTransactionType[] | SignedTransactionType[][], +): SignedTransactionType[] { + if (sent.length > 0 && Array.isArray(sent[0])) { + throw new Error( + 'Expected a flat transaction result; got a nested batch result instead.', + ); + } + return sent as SignedTransactionType[]; +} + +export async function sendAndGetHash(signed: Transaction[]): Promise { + const txManager = TransactionManager.getInstance(); + + // WRONG — fails tsc --strict, even though `signed` above is flat: + // + // const sent = await txManager.send(signed); + // const [first] = sent; + // return first.hash; + // + // error TS2339: Property 'hash' does not exist on type + // 'SignedTransactionType | SignedTransactionType[]'. + + // RIGHT — narrow explicitly, then read the hash the network already + // computed; no TransactionComputer call needed: + const flatSent = assertFlatResult(await txManager.send(signed)); + const [first] = flatSent; + if (!first) { + throw new Error('No transaction was sent.'); + } + return first.hash; +} -```bash -sc-meta all snippets +// TransactionComputer.computeTransactionHash IS the right tool — just for +// a different input: an sdk-core Transaction instance you haven't sent +// yet (e.g. to display a hash before broadcasting). Kept here to show +// the type it actually wants, not to suggest it's unused: +export function hashBeforeSending(tx: Transaction): string { + const computer = new TransactionComputer(); + return computer.computeTransactionHash(tx); +} ``` -:::warning -Make sure `wallet_address` stores the wallet that has to execute the transactions. -::: +Two stacked traps: `TransactionManager.getInstance().send()` returns +`SignedTransactionType[]` (plain objects with `hash` already on them), not +`Transaction` class instances, so `TransactionComputer.computeTransactionHash()` +rejects it. And `send()`'s declared return type is a union +(`SignedTransactionType[] | SignedTransactionType[][]`) **even when your input was +flat** (see +[Migration: grouped/batch sends](/sdk-and-tools/sdk-js/cookbook/migration/send-transactions-to-transaction-manager)), +so destructuring the first element needs an explicit `Array.isArray` narrow, not a +blind cast. -Let's unstake some EGLD! Replace variable `opt_unstake_amount` from `unstake` function in `staking-contract/interactor/src/interact.rs` with: +## How to use this checklist on an existing codebase -```rust -let opt_unstake_amount = OptionalValue::Some(BigUint::::from(500000000000000000u128)); -``` +1. Turn on every flag in the table above, one at a time if the codebase is large. +2. Fix in the order `tsc --noEmit --strict` reports them; resist the urge to + blanket-suppress with `// @ts-ignore`. +3. Add the two `@typescript-eslint` promise rules last, once the codebase compiles. +4. Confirm the ESLint rules are actually running (Item 3) before trusting a clean + `eslint` pass. -Then, run in terminal at path `staking-contract/interactor`: +## See also -```bash -cargo run unstake -``` +- [Minimal sdk-dapp v5 in Next.js](/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal) + and + [Minimal sdk-dapp v5 in Vite](/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal) + are where Items 1 to 3 were first found and fixed. +- [Sign and send a transaction](/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send) + is where Item 4 was first found and fixed. +- [Migrate v4 to v5: hook-by-hook diffs](/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration) + references Item 2 by name (`OnCloseUnlockPanelType`). -Now run this function, and you'll get this result: +--- -![img](/developers/staking-contract-tutorial-img/first_unstake.png) +### Undelegate and withdraw -...but why? We just added the function! +Exiting a delegation is a two-step flow with a mandatory wait in between. First +`unDelegate(amount)` asks the contract to unstake a given amount, but the EGLD is +not returned yet; it enters an unbonding period (10 epochs, about 10 days on +mainnet). Then, after that period elapses, `withdraw()` pulls all matured EGLD +back to your wallet. This recipe builds both, both ways (controller and factory), +and parses a completed `unDelegate`. -Well, we might've added it to our code, but the contract on the Devnet still has our old code. So, how do we upload our new code? +The default `npm start` parses a real, already-completed devnet `unDelegate`, so +you see the parse work without a funded wallet. +## Prerequisites -## Upgrading smart contracts +- Node.js >= 20.13.1. +- For the default `parse` and `payload` demos: devnet network access only. +- For an actual exit: a devnet PEM wallet with an active delegation, plus gas. -Since we've added some new functionality, we also want to update the currently deployed implementation. **Build the contract** and then run the following command at path `staking-contract/interactor`: +## Install ```bash -cargo run upgrade +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/undelegate-and-withdraw +npm install +npm run build ``` -:::note Attention required -All the storage is kept on upgrade, so make sure any storage changes you make to storage mapping are **backwards compatible**! -::: +## Undelegating and withdrawing + +```ts title="src/exit.ts" +// src/exit.ts - the subject of this recipe: exiting a delegation, which is a +// TWO-STEP flow with a mandatory unbonding wait in between: +// 1. unDelegate(amount) -> ask the contract to unstake `amount`. The EGLD is +// not returned yet; it enters an unbonding period (10 epochs, ~10 days on +// mainnet). Unlike `delegate`, `unDelegate` DOES carry an argument (the +// amount) on the wire: `unDelegate@`, with no value. +// 2. withdraw() -> after the unbonding period elapses, pull all matured +// unbonded EGLD back to your wallet. No arguments, no value; the contract +// pays out whatever has finished unbonding. +// Calling `withdraw` before anything has matured is a no-op (nothing to pay). + +import { Account, Address, DelegationTransactionsOutcomeParser } from '@multiversx/sdk-core'; +import type { DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +/** Undelegate (unstake) `amount` - controller path. */ +export async function undelegateViaController( + entrypoint: DevnetEntrypoint, + sender: Account, + delegationContract: string, + amount: bigint, +): Promise { + const controller = entrypoint.createDelegationController(); + return controller.createTransactionForUndelegating(sender, sender.getNonceThenIncrement(), { + delegationContract: Address.newFromBech32(delegationContract), + amount, + }); +} +/** Undelegate (unstake) `amount` - factory path. */ +export async function undelegateViaFactory( + entrypoint: DevnetEntrypoint, + sender: Account, + delegationContract: string, + amount: bigint, +): Promise { + const factory = entrypoint.createDelegationTransactionsFactory(); + const transaction = await factory.createTransactionForUndelegating(sender.address, { + delegationContract: Address.newFromBech32(delegationContract), + amount, + }); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; +} + +/** Withdraw all matured unbonded EGLD - controller path. */ +export async function withdrawViaController( + entrypoint: DevnetEntrypoint, + sender: Account, + delegationContract: string, +): Promise { + const controller = entrypoint.createDelegationController(); + return controller.createTransactionForWithdrawing(sender, sender.getNonceThenIncrement(), { + delegationContract: Address.newFromBech32(delegationContract), + }); +} -## Try unstaking again +/** Withdraw all matured unbonded EGLD - factory path. */ +export async function withdrawViaFactory( + entrypoint: DevnetEntrypoint, + sender: Account, + delegationContract: string, +): Promise { + const factory = entrypoint.createDelegationTransactionsFactory(); + const transaction = await factory.createTransactionForWithdrawing(sender.address, { + delegationContract: Address.newFromBech32(delegationContract), + }); + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + return transaction; +} + +export interface UndelegatePayload { + function: string; + /** The unstake amount, decoded from the wire argument. */ + amount: bigint; + receiver: string; + valueWei: bigint; + gasLimit: bigint; +} + +/** Decode an unDelegate transaction: `unDelegate@`. */ +export function describeUndelegatePayload(transaction: Transaction): UndelegatePayload { + const parts = Buffer.from(transaction.data).toString().split('@'); + const amountHex = parts[1] ?? ''; + return { + function: parts[0] ?? '', + amount: amountHex ? BigInt('0x' + amountHex) : 0n, + receiver: transaction.receiver.toBech32(), + valueWei: transaction.value, + gasLimit: transaction.gasLimit, + }; +} -Run the `unstake` snippet again. This time, it should work just fine. Afterwards, let's query our staked amount through `getStakingPosition`, to see if it updated our amount properly. +export interface WithdrawPayload { + function: string; + receiver: string; + valueWei: bigint; + gasLimit: bigint; +} -:::warning -Make sure that function `staking_position` has the changes previously made. -::: +/** Decode a withdraw transaction (no arguments). */ +export function describeWithdrawPayload(transaction: Transaction): WithdrawPayload { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + function: parts[0] ?? '', + receiver: transaction.receiver.toBech32(), + valueWei: transaction.value, + gasLimit: transaction.gasLimit, + }; +} -```bash -cargo run getStakingPosition +/** + * Parse a completed unDelegate transaction for the amount that entered + * unbonding. Reads the `unDelegate` log event. + */ +export async function parseUndelegatedAmount( + entrypoint: DevnetEntrypoint, + txHash: string, +): Promise { + const transactionOnNetwork = await entrypoint.getTransaction(txHash); + const parser = new DelegationTransactionsOutcomeParser(); + return parser.parseUndelegate(transactionOnNetwork)[0]?.amount ?? 0n; +} ``` +## Run it + ```bash -Result: 500000000000000001 +# Parse a real completed devnet unDelegate - no wallet, no funds: +npm start + +# Inspect both wire payloads offline: +npm start -- payload + +# Actually undelegate (needs a funded devnet PEM with active delegation): +npm start -- send ./wallet.pem +# ...or withdraw matured funds instead: +npm start -- send ./wallet.pem --withdraw ``` -We had 1 EGLD, and we unstaked 0.5 EGLD. Now, we have 0.5 EGLD staked. (with the extra 1 fraction of EGLD we've staked initially). +Output of the default `parse` mode, and of `payload`: +```text +Parsing completed unDelegate f1f50870...3c0392b3 ... + unbonding amount: 5000000000000000000 wei (5 EGLD) -### Unstake with no arguments +unDelegate: function=unDelegate amountArg=2000000000000000000 wei (2 EGLD) + value=0 receiver=erd1qqq...scktaww gasLimit=11090500 +withdraw: function=withdraw value=0 receiver=erd1qqq...scktaww gasLimit=11062000 +``` -Now let’s test how the contract behaves when no amount is explicitly provided. This will confirm that the `OptionalValue::None` path works as expected. +## How it works -In `staking-contract/interactor/src/interact.rs`, update the `unstake` function. Replace the `opt_unstake_amount` variable with: +**Controller vs factory.** `controller.createTransactionForUndelegating` / +`createTransactionForWithdrawing` build, set the nonce, and sign in one call; the +factory equivalents build only. `unDelegate` takes `{ delegationContract, amount }`; +`withdraw` takes just `{ delegationContract }`. -```rust -let opt_unstake_amount: OptionalValue> = OptionalValue::None; -``` +**unDelegate carries an argument; withdraw does not.** The `unDelegate` wire is +`unDelegate@` (the amount to unstake, with no `value`), while `withdraw` +is bare. This is the one delegation write besides create that puts a real argument +in the data. -And then unstake: +**Withdraw is settle-all, not per-request.** `withdraw()` pays out every unbonding +entry that has matured; it takes no amount. Calling it before anything has matured +is a successful no-op that moves 0 EGLD. -```bash -cargo run unstake -``` +## Pitfalls -After unstaking, query `stakingPosition` and `stakedAddresses` to see if the state was cleaned up properly: +:::warning[Pitfall 1: withdraw does nothing until the unbonding period elapses] +`unDelegate` starts a ~10-epoch unbonding timer per entry. A `withdraw` before any +entry matures completes successfully but returns 0 EGLD. Sequencing the two +back-to-back in one script will not return your funds. +::: -These should show that: +:::warning[Pitfall 2: unDelegate takes an amount; withdraw does not] +`unDelegate` needs the amount to unstake in its input (`amount`), encoded on the +wire as `unDelegate@`. `withdraw` takes no amount, it settles everything +matured. Do not expect a per-request withdraw. +::: -- Your staking position is now empty: 0 EGLD; -- Your address has been removed from the stakers list. +:::note[Pitfall 3: partial exits must respect the provider's minimum] +Undelegating part of your stake can leave a remainder below the provider's minimum +delegation, which some contracts reject. If in doubt, undelegate the full active +amount (read it first with the query recipe). +::: +## See also -## Writing tests +- [Delegate (stake) EGLD](/sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake) + is the inverse operation. +- [Claim and re-delegate rewards](/sdk-and-tools/sdk-js/cookbook/delegation/claim-and-redelegate-rewards) + handles rewards without exiting. +- [Read a delegation contract's state](/sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract) + reads your active stake to know how much to undelegate. -As you might've noticed, it can be quite a chore to keep upgrading the contract after every little change, especially if all we want to do is test a new feature. So, let's recap what we've done until now: +--- -- deploy our contract -- stake -- partial unstake -- full unstake +### Upgrade a smart contract -:::tip -A more detailed explanation of Rust tests can be found [here](https://docs.multiversx.com/developers/testing/rust/sc-test-overview/). -::: +Upgrade an already-deployed smart contract to new WASM bytecode, two ways +(controller and factory). An upgrade is almost identical to a deploy: it carries +new WASM plus constructor arguments. The differences are that the contract +address is already known and the transaction is addressed to the contract, with a +`data` field that starts with the `upgradeContract` builtin function. This recipe +targets the real **adder** fixtures. -Before developing the tests, you will have to generate the contract's [proxy](/docs/developers/transactions/tx-proxies.md). +The default `npm start` builds an upgrade and prints its wire payload without +sending anything, so you can see the exact shape offline. -You will add to `staking-contract/sc-config.toml`: +## Prerequisites -```toml -[[proxy]] -path = "tests/staking_contract_proxy.rs" -``` +- Node.js >= 20.13.1. +- For the default payload demo: nothing (it builds offline). +- For an actual upgrade: a devnet PEM wallet that **owns** the target contract + and has a little EGLD for gas. -Then run in terminal at path `staking-contract/`: +## Install ```bash -sc-meta all proxy +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/upgrade-contract +npm install +npm run build ``` -You’ll find the contract’s proxy in `staking-contract/tests`, inside the file `staking_contract_proxy.rs`. This proxy will be used to help us write and run tests for the smart contract. +## Upgrading + +```ts title="src/upgrade.ts" +// src/upgrade.ts - upgrading an already-deployed smart contract to new bytecode. +// An upgrade is almost identical to a deploy (it carries new WASM plus +// constructor arguments), with two differences: +// 1. The contract address is already known, so you pass it in `contract`. +// 2. The transaction is addressed to the CONTRACT (not the system deploy +// address), and its `data` starts with the `upgradeContract` builtin +// function: `upgradeContract@@@`. +// +// The contract must have been deployed as upgradeable (codeMetadata bit set) and +// the caller must be its owner, otherwise the network rejects the upgrade at +// execution time. The default deploy code metadata `0504` DOES set the +// Upgradeable bit (see the deploy recipe). +// +// Targets the real **adder** contract fixtures (adder.wasm + adder.abi.json). +// Adder declares a dedicated `upgradeConstructor` in its ABI, also taking +// `initial_value: BigUint`, which is what the upgrade arguments feed. + +import type { Abi, Account, Address, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +/** + * Upgrade path 1 - the controller. `createTransactionForUpgrade` builds, sets + * the nonce, and signs, taking the whole `Account`. Same shape as + * `createTransactionForDeploy` plus the `contract` field. + */ +export async function upgradeViaController( + entrypoint: DevnetEntrypoint, + abi: Abi, + sender: Account, + contract: Address, + bytecode: Uint8Array, + newInitialValue: number, +): Promise { + const controller = entrypoint.createSmartContractController(abi); + + const transaction = await controller.createTransactionForUpgrade(sender, sender.getNonceThenIncrement(), { + contract, + bytecode, + gasLimit: 6_000_000n, + arguments: [newInitialValue], // plain JS value - allowed because we passed the ABI + }); -To test the previously described scenario, we're going to need a user address, and a new test function. + return entrypoint.sendTransaction(transaction); +} + +/** + * Upgrade path 2 - the factory. Builds the unsigned transaction only; the caller + * sets the nonce and signs. Use when a wallet or hardware device signs. + */ +export async function upgradeViaFactory( + entrypoint: DevnetEntrypoint, + abi: Abi, + sender: Account, + contract: Address, + bytecode: Uint8Array, + newInitialValue: number, +): Promise { + const factory = entrypoint.createSmartContractTransactionsFactory(abi); + + const transaction = await factory.createTransactionForUpgrade(sender.address, { + contract, + bytecode, + gasLimit: 6_000_000n, + arguments: [newInitialValue], + }); -**Create** file `staking_contract_blackbox_test.rs` in `staking-contract/tests` with the following: + transaction.nonce = sender.getNonceThenIncrement(); + transaction.signature = await sender.signTransaction(transaction); + + return entrypoint.sendTransaction(transaction); +} + +/** Decode an upgrade transaction's `data` field into its wire parts. */ +export function describeUpgradePayload(transaction: Transaction): { + builtinFunction: string; + codeHex: string; + codeMetadata: string; + args: string[]; + receiver: string; +} { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + builtinFunction: parts[0] ?? '', + codeHex: parts[1] ?? '', + codeMetadata: parts[2] ?? '', + args: parts.slice(3), + receiver: transaction.receiver.toBech32(), + }; +} +``` -```rust -mod staking_contract_proxy; -use multiversx_sc::{ - imports::OptionalValue, - types::{TestAddress, TestSCAddress}, -}; -use multiversx_sc_scenario::imports::*; +## Run it -const OWNER_ADDRESS: TestAddress = TestAddress::new("owner"); -const STAKING_CONTRACT_ADDRESS: TestSCAddress = TestSCAddress::new("staking-contract"); -const USER_ADDRESS: TestAddress = TestAddress::new("user"); -const WASM_PATH: MxscPath = MxscPath::new("output/staking-contract.mxsc.json"); -const USER_BALANCE: u64 = 1_000_000_000_000_000_000; +```bash +# Build an upgrade and print its wire payload - no wallet, offline: +npm start -struct ContractSetup { - pub world: ScenarioWorld, -} +# Actually upgrade a contract you own; add --factory for the factory path: +npm start -- upgrade ./wallet.pem erd1qqqqqqqqqqqqqpgq...your-contract 42 +``` -impl ContractSetup { - pub fn new() -> Self { - let mut world = ScenarioWorld::new(); - world.set_current_dir_from_workspace("staking-contract"); - world.register_contract(WASM_PATH, staking_contract::ContractBuilder); +Expected output of the default payload demo: - world.account(OWNER_ADDRESS).nonce(1).balance(0); - world.account(USER_ADDRESS).nonce(1).balance(USER_BALANCE); +```text +Upgrade transaction wire payload (built, not sent): + receiver: erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug (the contract itself) + builtinFunction: upgradeContract + code === wasm: true + codeMetadata: 0504 (0504 = Upgradeable + Readable + PayableBySmartContract) + args: 2a (2a = 42) +``` + +## How it works + +**Upgrade is deploy with a known address.** +`controller.createTransactionForUpgrade(account, nonce, { contract, bytecode, gasLimit, arguments })` +and `factory.createTransactionForUpgrade(address, { contract, ... })` take the +same options as their deploy counterparts plus `contract`. The controller signs; +the factory only builds. + +**The wire shape differs from deploy.** A deploy is addressed to the system +deploy address with `data` = `@@@`. An +upgrade is addressed to the **contract** with `data` = +`upgradeContract@@@` (no vmType part; the +`upgradeContract` builtin replaces it). + +**Storage survives, code is replaced.** Adder declares a dedicated +`upgradeConstructor` in its ABI (also `initial_value: BigUint`), which the upgrade +arguments feed. Existing storage persists across the upgrade unless the new code +changes the layout. + +## Pitfalls + +:::warning[Pitfall 1: the contract must be upgradeable and you must own it] +If the Upgradeable code metadata bit was not set at deploy, or the caller is not +the owner, the network rejects the upgrade at execution time. The default deploy +metadata `0504` does set the Upgradeable bit. +::: - // simulate deploy - world - .tx() - .from(OWNER_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .init() - .code(WASM_PATH) - .new_address(STAKING_CONTRACT_ADDRESS) - .run(); +:::warning[Pitfall 2: without an ABI, arguments must be TypedValue objects] +Same trap as deploy: with the ABI, `arguments: [42]` works; without it you must +pass `arguments: [new BigUIntValue(42)]` or the SDK throws `Can't convert args to +TypedValues`. +::: - ContractSetup { world } - } -} +:::note[Pitfall 3: codeMetadata is rebuilt, not inherited] +Whatever metadata you pass (or the `0504` default) replaces the old metadata. +Upgrading with the defaults keeps the contract Upgradeable; passing +`isUpgradeable: false` locks it against future upgrades. +::: -#[test] -fn stake_unstake_test() { - let mut setup = ContractSetup::new(); +## See also - setup - .world - .check_account(USER_ADDRESS) - .balance(USER_BALANCE); - setup.world.check_account(OWNER_ADDRESS).balance(0); +- [Deploy a smart contract](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/deploy-contract) + is the deploy counterpart with the same bytecode-plus-args shape. +- [Compute a contract address before deploy](/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/compute-contract-address) + is how the address you upgrade was assigned. +- [Call a contract endpoint with native JS args](/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint) + calls the upgraded contract. - // stake full - setup - .world - .tx() - .from(USER_ADDRESS) - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .stake() - .egld(USER_BALANCE) - .run(); +--- - setup - .world - .query() - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .staking_position(USER_ADDRESS) - .returns(ExpectValue(USER_BALANCE)) - .run(); +### Upgrading smart contracts - setup.world.check_account(USER_ADDRESS).balance(0); - setup - .world - .check_account(STAKING_CONTRACT_ADDRESS) - .balance(USER_BALANCE); +## Sirius Mainnet Release - Version 1.6.0 - // unstake partial - setup - .world - .tx() - .from(USER_ADDRESS) - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .unstake(OptionalValue::Some(USER_BALANCE / 2)) - .run(); +:::important +The new Sirius Mainnet version 1.6.0 brings a significant update to how smart contracts can be upgraded. This release introduces a dedicated `upgrade` function, replacing the previous usage of the `init` function during contract upgrades. This change enhances the upgrade process and provides a clearer separation of concerns between contract initialization and subsequent upgrades. +**Note: Contracts deployed before version 1.6.0 will continue to function as before. The change in upgrade behavior applies only when upgrading the code of the contract. Existing contracts deployed only with an init function will still operate correctly without modifications.** +::: - setup - .world - .query() - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .staking_position(USER_ADDRESS) - .returns(ExpectValue(USER_BALANCE / 2)) - .run(); +Please take note of the following important information to ensure a smooth transition. - setup - .world - .check_account(USER_ADDRESS) - .balance(USER_BALANCE / 2); - setup - .world - .check_account(STAKING_CONTRACT_ADDRESS) - .balance(USER_BALANCE / 2); +For contracts developed on or after version 1.6.0, developers should ensure that the `upgrade` function is implemented to handle the necessary upgrade logic. The `init` function will no longer be called during contract upgrades. - // unstake full - setup - .world - .tx() - .from(USER_ADDRESS) - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .unstake(OptionalValue::None::) - .run(); +Let's look at an example of a simple Adder SC. - setup - .world - .query() - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .staking_position(USER_ADDRESS) - .returns(ExpectValue(0u8)) - .run(); +```rust +#[init] +fn init(&self, initial_value: u64) { + // Save the initial value in storage only if it is empty. + self.sum().set_if_empty(initial_value); +} - setup - .world - .check_account(USER_ADDRESS) - .balance(USER_BALANCE); - setup - .world - .check_account(STAKING_CONTRACT_ADDRESS) - .balance(0); +#[upgrade] +fn upgrade(&self, new_value: u64) { + self.sum().set(new_value); } ``` -We've added a `USER_ADDRESS` constant, which is initialized with `USER_BALANCE` EGLD in their account. - -:::note -For the test we're going to use small numbers for balances, since there is no reason to work with big numbers. For this test, we're using 1 EGLD for user balance. -::: - -Then, we've staked the user's entire balance, unstaked half, then unstaked fully. After each transaction, we've checked the smart contract's internal staking storage, and also the balance of the user and the smart contract respectively. +Let's assume we deploy the contract with the argument **1u64**, and then we upgrade it using the argument **2u64**. If before the new release, after upgrading the SC, we would have the value **1u64** in storage (as the `init` function would have been called, which saves the value in the storage only when it is empty), with the new release, the new value in the storage would be **2u64**. -### Running the test +## Deep diving into the Smart Contract Upgrade Process -To run a test, you can use click on the `Run Test` button from under the test name. +Upgrading a smart contract is a relatively easy process, but its implications are not exactly obvious. To upgrade a smart contract, simply run the following command: -![img](/developers/staking-contract-tutorial-img/running_rust_test.png) +``` +mxpy --verbose contract upgrade SC_ADDRESS \ + --pem=PEM_PATH --bytecode=WASM_PATH \ + --gas-limit=100000000 \ + --send --proxy=https://devnet-gateway.multiversx.com --chain=D +``` -There is also a `Debug` button, which can be used to debug smart contracts. More details on that [here](/developers/testing/sc-debugging/). +Replace SC_ADDRESS, PEM_PATH and WASM_PATH accordingly. Also, if you want to use testnet/mainnet, also change the proxy and chain ID. -Alternatively, you can run all the tests in the file by running the following command in the terminal, in the `staking-contract/` folder: +This will replace the given SC's code with the one from the provided file, but that is not all. Additionally, it will run the new code's `upgrade` function. So, if your `upgrade` function has any arguments, the command has to be run by also giving said arguments: -```bash -sc-meta test +``` +mxpy --verbose contract upgrade SC_ADDRESS \ + --pem=PEM_PATH --bytecode=WASM_PATH \ + --arguments arg1 arg2 arg3 + --gas-limit=100000000 \ + --send --proxy=https://devnet-gateway.multiversx.com --chain=D ``` +You might've seen in many of the MultiversX contracts, we often use the `set_if_empty` method in `init` and `upgrade` functions, instead of plain `set`. This is so we don't accidentally overwrite an important config value during the upgrade process. -## Staking Rewards - -Right now, there is no incentive to stake EGLD in this smart contract. Let's say we want to give every staker 10% APY (Annual Percentage Yield). For example, if someone staked 100 EGLD, they will receive a total of 10 EGLD per year. -For this, we're also going to need to save the time at which each user staked. Also, we can't simply make each user wait one year to get their rewards. We need a more fine-tuned solution, so we're going to calculate rewards per block instead of per year. +## What about the old contract's storage? -:::tip -You can also use rounds, timestamp, epochs etc. for time keeping in smart contracts, but number of blocks is the recommended approach. -::: +Storage is kept intact, except for the changes the `upgrade` function might do during upgrade. -### User-defined struct types +## Migrating storage or token attributes -A single `BigUint` for each user is not enough anymore. As stated before, we need to also store the stake block, and we need to update this block number on every action. +If you modify your core data's design, you will run into "Decode error: Input too short". Fear not, as there are ways to avoid that. -So, we're going to use a struct: +For example, let's assume you had the following struct type, which keeps track of user information: ```rust -pub struct StakingPosition { +#[derive(TypeAbi, TopEncode, TopDecode, NestedEncode, NestedDecode)] +pub struct UserData { pub stake_amount: BigUint, - pub last_action_block: u64, + pub last_update_block: u64, } ``` -:::note -Every managed type from the SpaceCraft needs a `ManagedTypeApi` implementation, which allows it to access the VM functions for performing operations. For example, adding two `BigUint` numbers, concatenating two `ManagedBuffers`, etc. Inside smart contract code, the `ManagedTypeApi` associated type is automatically added, but outside of it, we have to manually specify it. -::: - - -Additionally, since we need to store this in storage, we need to tell the Rust framework how to encode and decode this type. This can be done automatically by deriving (i.e. auto-implementing) these traits, via the `#[derive]` annotation: +In your new code, you decided it would be nice to also keep track of the last update epoch, not only block, so you changed the struct: ```rust -use multiversx_sc::derive_imports::*; - -#[type_abi] -#[derive(TopEncode, TopDecode, PartialEq, Debug)] -pub struct StakingPosition { +#[derive(TypeAbi, TopEncode, TopDecode, NestedEncode, NestedDecode)] +pub struct UserData { pub stake_amount: BigUint, - pub last_action_block: u64, + pub last_update_block: u64, + pub last_update_epoch: u64, } ``` -We've also added `#[type_abi]`, since this is required for ABI generation. ABIs are used by decentralized applications and such to decode custom smart contract types, but this is out of scope of this tutorial. - -Additionally, we've added `PartialEq` and `Debug` derives, for easier use within tests. This will not affect performance in any way, as the code for these is only used during testing/debugging. `PartialEq` allows us to use `==` for comparing instances, while `Debug` will pretty-print the struct, field by field, in case of errors. - -If you want to learn more about how such a structure is encoded, and the difference between top and nested encoding/decoding, you can read more [here](/developers/data/serialization-overview). - - -### Rewards formula - -A block is produced about every 6 seconds, so total blocks in a year would be seconds in year, divided by 6: - -```rust -pub const BLOCKS_IN_YEAR: u64 = 60 * 60 * 24 * 365 / 6; -``` - -More specifically: *60 seconds per minute* x *60 minutes per hour* x *24 hours per day* x *365 days*, divided by the 6-second block duration. +This will not work. The struct will decode `stake_amount`, it will then decode `last_update_epoch`, and it will have no bytes left to decode the `last_update_epoch`. -:::note -This is calculated and replaced with the exact value at compile time, so there is no performance penalty of having a constant with mathematical operations in its value definition. +:::caution +You always need to add new fields at the end of the struct, otherwise, this approach will not work. ::: - -Having defined this constant, rewards formula should look like this: - -```rust -let reward_amt = apy / 100 * user_stake * blocks_since_last_claim / BLOCKS_IN_YEAR; -``` - -Using 10% as the APY, and assuming exactly one year has passed since last claim. - -In this case, the reward should be calculated as: `10/100 * user_stake`, which is exactly 10% APY. - -However, there is something wrong with the current formula. We will always get `reward_amt` = 0. - - -### BigUint division - -BigUint division works the same as unsigned integer division. If you divide `x` by `y`, where `x < y`, you will always get `0` as result. So in our previous example, 10/100 is **NOT** `0.1`, but `0`. - -To fix this, we need to take care of our operation order: +To fix this, we need to manually implement the decoding traits, which were previously automatically added through the derives. ```rust -let reward_amt = user_stake * apy / 100 * blocks_since_last_claim / BLOCKS_IN_YEAR; -``` - +use multiversx_sc::codec::{NestedDecodeInput, TopDecodeInput}; -### How to express percentages like 50.45%? +#[derive(TypeAbi, TopEncode, NestedEncode)] +pub struct UserData { + pub stake_amount: BigUint, + pub last_update_block: u64, + pub last_update_epoch: u64, +} -In this case, we need to extend our precision by using fixed point precision. Instead of having `100` as the maximum percentage, we will extend it to `100_00`, and give `50.45%` as `50_45`. Updating our above formula results in this: +impl TopDecode for UserData { + fn top_decode(input: I) -> Result + where + I: TopDecodeInput, + { + let mut buffer = input.into_nested_buffer(); + Self::dep_decode(&mut buffer) + } +} -```rust -pub const MAX_PERCENTAGE: u64 = 100_00; +impl NestedDecode for UserData { + fn dep_decode(input: &mut I) -> Result { + let stake_amount = BigUint::dep_decode(input)?; + let last_update_block = u64::dep_decode(input)?; -let reward_amt = user_stake * apy / MAX_PERCENTAGE * blocks_since_last_claim / BLOCKS_IN_YEAR; -``` + let last_update_epoch = if !input.is_depleted() { + u64::dep_decode(input)? + } else { + 0 + }; -For example, let's assume the user stake is 100, and 1 year has passed. Using `50_45` as APY value, the formula would become: + if !input.is_depleted() { + return Result::Err(DecodeError::INPUT_TOO_LONG); + } -```rust -reward_amt = 100 * 50_45 / 100_00 = 5045_00 / 100_00 = 50 + Result::Ok(UserData { + stake_amount, + last_update_block, + last_update_epoch + }) + } +} ``` -:::note -Since we're still using the BigUint division, we don't get `50.45`, but `50`. This precision can be increased by using more zeroes for the `MAX_PERCENTAGE` and the respective APY, but this is also "inherently fixed" on the blockchain because we work with very big numbers for `user_stake`. -::: +With the above code, we manually decode each field, and then, if there are any bytes left, we decode the new fields we've added, using a default value (0 in this case) if there are no more bytes - as this means it's an encoded version of the old struct. +Remember to also check if there are any remaining bytes after that and throw an error, otherwise, your struct could potentially be decoded from almost any input bytes. -## Rewards functionality +### "But what if I want to remove a field?" -### Implementation +Unless you want to remove the very last field of the struct, and change nothing else, this is not possible, as you'd have almost no way of distinguishing between old and new data. -Now let's see how this would look in our Rust smart contract code. The smart contract looks like this after doing all the specified changes: +Assuming you simply want to remove `last_update_block` for the example above, the implementation would be as follows: ```rust -#![no_std] - -use multiversx_sc::derive_imports::*; -use multiversx_sc::imports::*; - -pub const BLOCKS_IN_YEAR: u64 = 60 * 60 * 24 * 365 / 6; -pub const MAX_PERCENTAGE: u64 = 10_000; +use multiversx_sc::codec::{NestedDecodeInput, TopDecodeInput}; -#[type_abi] -#[derive(TopEncode, TopDecode, PartialEq, Debug)] -pub struct StakingPosition { +#[derive(TypeAbi, TopEncode, NestedEncode)] +pub struct UserData { pub stake_amount: BigUint, - pub last_action_block: u64, } -#[multiversx_sc::contract] -pub trait StakingContract { - #[init] - fn init(&self, apy: u64) { - self.apy().set(apy); +impl TopDecode for UserData { + fn top_decode(input: I) -> Result + where + I: TopDecodeInput, + { + let mut buffer = input.into_nested_buffer(); + Self::dep_decode(&mut buffer) } +} - #[upgrade] - fn upgrade(&self) {} +impl NestedDecode for UserData { + fn dep_decode(input: &mut I) -> Result { + let stake_amount = BigUint::dep_decode(input)?; - #[payable("EGLD")] - #[endpoint] - fn stake(&self) { - let payment_amount = self.call_value().egld().clone(); - require!(payment_amount > 0, "Must pay more than 0"); + if input.is_depleted() { + return Result::Ok(UserData { stake_amount }); + } - let caller = self.blockchain().get_caller(); - self.staking_position(&caller).update(|staking_pos| { - self.claim_rewards_for_user(&caller, staking_pos); + let _last_update_block = u64::dep_decode(input)?; + if !input.is_depleted() { + return Result::Err(DecodeError::INPUT_TOO_LONG); + } - staking_pos.stake_amount += payment_amount - }); - self.staked_addresses().insert(caller); + Result::Ok(UserData { stake_amount }) } +} +``` - #[endpoint] - fn unstake(&self, opt_unstake_amount: OptionalValue) { - let caller = self.blockchain().get_caller(); - let stake_mapper = self.staking_position(&caller); - let mut staking_pos = stake_mapper.get(); +In this case, if there are any bytes left after we decode the `stake_amount`, we decode the old fields and ignore their values. Same as last time, remember to throw an error if there are bytes remaining still after that. - let unstake_amount = match opt_unstake_amount { - OptionalValue::Some(amt) => amt, - OptionalValue::None => staking_pos.stake_amount.clone(), - }; - require!( - unstake_amount > 0 && unstake_amount <= staking_pos.stake_amount, - "Invalid unstake amount" - ); - self.claim_rewards_for_user(&caller, &mut staking_pos); - staking_pos.stake_amount -= &unstake_amount; +## Conclusion - if staking_pos.stake_amount > 0 { - stake_mapper.set(&staking_pos); - } else { - stake_mapper.clear(); - self.staked_addresses().swap_remove(&caller); - } +This approach works for both stored instances, and token attributes. Keep in mind you'll have to keep adding more and more levels of partial decoding to the above implementation if you change the struct often, and new fields always have to be at the end. - self.tx().to(caller).egld(unstake_amount).transfer(); - } +--- - #[endpoint(claimRewards)] - fn claim_rewards(&self) { - let caller = self.blockchain().get_caller(); - let stake_mapper = self.staking_position(&caller); +### User-defined Smart Contracts - let mut staking_pos = stake_mapper.get(); - self.claim_rewards_for_user(&caller, &mut staking_pos); - stake_mapper.set(&staking_pos); - } +For user-defined Smart Contract deployments and function calls, the **actual gas consumption** of processing contains both of the previously mentioned cost components - though, while the **value movement and data handling** component is easily computable (using the previously depicted formula), the **contract execution** component is hard to determine precisely _a priori_. Therefore, for this component we have to rely on _simulations_ and _estimations_. - fn claim_rewards_for_user( - &self, - user: &ManagedAddress, - staking_pos: &mut StakingPosition, - ) { - let reward_amount = self.calculate_rewards(staking_pos); - let current_block = self.blockchain().get_block_nonce(); - staking_pos.last_action_block = current_block; +For **simulations**, we will start a local testnet using `mxpy` (detailed setup instructions can be found [here](/developers/setup-local-testnet)). Thus, before going further, make sure your local testnet is up and running. - if reward_amount > 0 { - self.tx().to(user).egld(&reward_amount).transfer(); - } - } - fn calculate_rewards(&self, staking_position: &StakingPosition) -> BigUint { - let current_block = self.blockchain().get_block_nonce(); - if current_block <= staking_position.last_action_block { - return BigUint::zero(); - } +## Contract deployments - let apy = self.apy().get(); - let block_diff = current_block - staking_position.last_action_block; +In order to get the required `gasLimit` (the **actual gas cost**) for a deployment transaction, one should use the well-known `mxpy contract deploy` command, but with the `--simulate` flag set. - &staking_position.stake_amount * apy / MAX_PERCENTAGE * block_diff / BLOCKS_IN_YEAR - } +At first, pass the maximum possible amount for `gas-limit` (no guessing). - #[view(calculateRewardsForUser)] - fn calculate_rewards_for_user(&self, addr: ManagedAddress) -> BigUint { - let staking_pos = self.staking_position(&addr).get(); - self.calculate_rewards(&staking_pos) +```bash +$ mxpy --verbose contract deploy --bytecode=./contract.wasm \ + --gas-limit=600000000 \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --simulate +``` + +In the output, look for `txGasUnits`. For example: + +```json +"txSimulation": { + ... + "cost": { + "txGasUnits": 1849711, + ... } +} +``` - #[view(getStakedAddresses)] - #[storage_mapper("stakedAddresses")] - fn staked_addresses(&self) -> UnorderedSetMapper; +:::note +The simulated cost `txGasUnits` contains both components of the cost. +::: - #[view(getStakingPosition)] - #[storage_mapper("stakingPosition")] - fn staking_position( - &self, - addr: &ManagedAddress, - ) -> SingleValueMapper>; +After that, check the cost simulation by running the simulation once again, but this time with the precise`gas-limit`: - #[view(getApy)] - #[storage_mapper("apy")] - fn apy(&self) -> SingleValueMapper; -} +```bash +$ mxpy --verbose contract deploy --bytecode=./contract.wasm \ + --gas-limit=1849711 \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --simulate ``` -Now, rebuild the contract and regenerate the proxy: +In the output, look for the `status` - it should be `success`: + +```json +"txSimulation": { + "execution": { + "result": { + "status": "success", + ... + }, + ... + } +``` +In the end, let's actually deploy the contract: ```bash -sc-meta all build -sc-meta all proxy +$ mxpy --verbose contract deploy --bytecode=./contract.wasm \ + --gas-limit=1849711 \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --send --wait-result ``` -Let's update our test, to use our new `StakingPosition` structure, and also provide the `APY` as an argument for the `init` function. +:::important +For deployments, the **execution** component of the cost is associated with instantiating the Smart Contract and calling its `init()` function. -```rust -mod staking_contract_proxy; -use multiversx_sc::{ - imports::OptionalValue, - types::{BigUint, TestAddress, TestSCAddress}, -}; -use multiversx_sc_scenario::imports::*; -use staking_contract_proxy::StakingPosition; +If the flow of `init()` is dependent on input arguments or it references blockchain data, then the cost will vary as well, depending on these variables. Make sure you simulate sufficient deployment scenarios and increase (decrease) the `gas-limit`. +::: -const OWNER_ADDRESS: TestAddress = TestAddress::new("owner"); -const USER_ADDRESS: TestAddress = TestAddress::new("user"); -const STAKING_CONTRACT_ADDRESS: TestSCAddress = TestSCAddress::new("staking-contract"); -const WASM_PATH: MxscPath = MxscPath::new("output/staking-contract.mxsc.json"); -const USER_BALANCE: u64 = 1_000_000_000_000_000_000; -const APY: u64 = 10_00; // 10% -struct ContractSetup { - pub world: ScenarioWorld, -} +## Contract calls -impl ContractSetup { - pub fn new() -> Self { - let mut world = ScenarioWorld::new(); - world.set_current_dir_from_workspace("staking-contract"); - world.register_contract(WASM_PATH, staking_contract::ContractBuilder); +In order to get the required `gasLimit` (the **actual gas cost**) for a contract call, one should first deploy the contract, then use the `mxpy contract call` command, with the `--simulate` flag set. - world.account(OWNER_ADDRESS).nonce(1).balance(0); - world.account(USER_ADDRESS).nonce(1).balance(USER_BALANCE); +:::important +If the contract makes further calls to other contracts, please read the next section. +::: - // simulate deploy - world - .tx() - .from(OWNER_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .init(APY) - .code(WASM_PATH) - .new_address(STAKING_CONTRACT_ADDRESS) - .run(); +Assuming we've already deployed the contract (see above) let's get the cost for calling one of its endpoints: - ContractSetup { world } +```bash +$ mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqygvvtlty3v7cad507v5z793duw9jjmlxd8sszs8a2y \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --function=increment\ + --gas-limit=600000000\ + --simulate +``` + +In the output, look for `txGasUnits`. For example: + +```json +"txSimulation": { + ... + "cost": { + "txGasUnits": 1225515, + ... } } +``` +In the end, let's actually call the contract: -#[test] -fn stake_unstake_test() { - let mut setup = ContractSetup::new(); +```bash +$ mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqygvvtlty3v7cad507v5z793duw9jjmlxd8sszs8a2y \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --function=increment\ + --gas-limit=1225515\ + --send --wait-result +``` - setup - .world - .check_account(USER_ADDRESS) - .balance(USER_BALANCE); - setup.world.check_account(OWNER_ADDRESS).balance(0); +:::important +If the flow of the called function is dependent on input arguments or it references blockchain data, then the cost will vary as well, depending on these variables. Make sure you simulate sufficient scenarios for the contract call and increase (decrease) the `gas-limit`. +::: - // stake full - setup - .world - .tx() - .from(USER_ADDRESS) - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .stake() - .egld(USER_BALANCE) - .run(); - let expected_result_1 = StakingPosition { - stake_amount: BigUint::from(USER_BALANCE), - last_action_block: 0, - }; +## Contracts calling (asynchronously) other contracts - setup - .world - .query() - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .staking_position(USER_ADDRESS) - .returns(ExpectValue(expected_result_1)) - .run(); +:::important +Documentation in this section is preliminary and subject to change. Furthermore, **only asynchronous calls are covered**. +::: - setup.world.check_account(USER_ADDRESS).balance(0); - setup - .world - .check_account(STAKING_CONTRACT_ADDRESS) - .balance(USER_BALANCE); +Before moving forward, make sure you first have a look over the following: - // unstake partial - setup - .world - .tx() - .from(USER_ADDRESS) - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .unstake(OptionalValue::Some(USER_BALANCE / 2)) - .run(); +- [Asynchronous calls between contracts](/learn/space-vm#asynchronous-calls-between-contracts) +- [Asynchronous calls (Rust framework)](/developers/transactions/tx-legacy-calls#asynchronous-calls) +- [Callbacks (Rust framework)](/developers/developer-reference/sc-annotations/#callbacks) - let expected_result_2 = StakingPosition { - stake_amount: BigUint::from(USER_BALANCE / 2), - last_action_block: 0, - }; - setup - .world - .query() - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .staking_position(USER_ADDRESS) - .returns(ExpectValue(expected_result_2)) - .run(); +Suppose we have two contracts: `A` and `B`, where `A::foo(addressOfB)` asynchronously calls `B::bar()` (e.g. using `asyncCall()`). - let staked_addresses_1 = setup - .world - .query() - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .staked_addresses() - .returns(ReturnsResultUnmanaged) - .run(); +Let's deploy the contracts `A` and `B`: - assert!(staked_addresses_1 - .into_vec() - .contains(&USER_ADDRESS.to_address())); +```bash +$ mxpy --verbose contract deploy --bytecode=./a.wasm \ + --gas-limit=5000000 \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --send --wait-result --outfile=a.json - setup - .world - .check_account(USER_ADDRESS) - .balance(USER_BALANCE / 2); - setup - .world - .check_account(STAKING_CONTRACT_ADDRESS) - .balance(USER_BALANCE / 2); +$ mxpy --verbose contract deploy --bytecode=./b.wasm \ + --gas-limit=5000000 \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --send --wait-result --outfile=b.json +``` - // unstake full - setup - .world - .tx() - .from(USER_ADDRESS) - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .unstake(OptionalValue::None::) - .run(); +Assuming `A` is deployed at `erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts`, and `B` is deployed at `erd1qqqqqqqqqqqqqpgqj5zftf3ef3gqm3gklcetpmxwg43rh8z2d8ss2e49aq`, let's **simulate** `A::foo(addressOfB)` (at first, pass a _large-enough_ or maximum `gas-limit`): - let staked_addresses_2 = setup - .world - .query() - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .staked_addresses() - .returns(ReturnsResultUnmanaged) - .run(); - assert!(staked_addresses_2.is_empty()); +```bash +$ export hexAddressOfB=0x$(mxpy wallet bech32 --decode erd1qqqqqqqqqqqqqpgqj5zftf3ef3gqm3gklcetpmxwg43rh8z2d8ss2e49aq) - setup - .world - .check_account(USER_ADDRESS) - .balance(USER_BALANCE); - setup - .world - .check_account(STAKING_CONTRACT_ADDRESS) - .balance(0); -} +$ mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --function=foo\ + --gas-limit=50000000\ + --arguments ${hexAddressOfB}\ + --simulate ``` -Now let's run the test... **it didn't work**. You should see the following error: +In the output, look for the simulated cost (as above): -```bash -Error: result code mismatch. -Tx id: '' -Want: "0" -Have: 4 -Message: storage decode error (key: stakingPositionuser____________________________): input too short +```json +"txSimulation": { + ... + "cost": { + "txGasUnits": 3473900, + ... + } +} ``` -But why? Everything worked fine before. +The simulated cost represents the **actual gas cost** for invoking `A::foo()`, `B::bar()` and `A::callBack()`. -This is because instead of using a simple `BigUint` for staking positions, we now use the `StakingPosition` structure. If you follow the error trace, you will see exactly where it failed: +**However, the simulated cost above isn't the value we are going to use as `gasLimit`.** If we were to do so, we would be presented the error `not enough gas`. -```bash -28: staking_contract::StakingContract::stake - at ./src/staking_contract.rs:33:9 -``` +Upon reaching the call to `B::bar()` inside `A::foo()`, the MultiversX VM inspects the **remaining gas _at runtime_** and **temporarily locks (reserves) a portion of it**, to allow for the execution of `A::callBack()` once the call to `B::bar()` returns. -Which leads to the following line: +With respect to the [VM Gas Schedule](https://github.com/multiversx/mx-chain-mainnet-config/tree/master/gasSchedules), the aforementioned **remaining gas _at runtime_** has to satisfy the following conditions in order for the **temporary gas lock reservation** to succeed: -```rust -self.staking_position(&caller).update(|staking_pos| { - self.claim_rewards_for_user(&caller, staking_pos); +```go +onTheSpotRemainingGas > gasToLockForCallback - staking_pos.stake_amount += payment_amount -}); +gasToLockForCallback = + costOf(AsyncCallStep) + + costOf(AsyncCallbackGasLock) + + codeSizeOf(callingContract) * costOf(AoTPreparePerByte) ``` -Because we're trying to add a new user, which has no staking entry yet, the decoding fails. - -For a simple `BigUint`, decoding from an empty storage yields the `0` value, which is exactly what we want, but for a struct type, it cannot give us any default value. +:::note +Subsequent asynchronous calls (asynchronous calls performed by an asynchronously-called contract) will require temporary gas locks as well. +::: -For this reason, we have to add some additional checks. The endpoint implementations will have to be changed to the following (the rest of the code remains the same): +For our example, where `A` has 453 bytes, the `gasToLockForCallback` would be (as of February 2022): -```rust -#[payable("EGLD")] -#[endpoint] -fn stake(&self) { - let payment_amount = self.call_value().egld().clone(); - require!(payment_amount > 0, "Must pay more than 0"); +``` +gasToLockForCallback = 100000 + 4000000 + 100 * 453 = 4145300 +``` - let caller = self.blockchain().get_caller(); - let stake_mapper = self.staking_position(&caller); +It follows that the value of `gasLimit` should be: - let new_user = self.staked_addresses().insert(caller.clone()); - let mut staking_pos = if !new_user { - stake_mapper.get() - } else { - let current_block = self.blockchain().get_block_epoch(); - StakingPosition { - stake_amount: BigUint::zero(), - last_action_block: current_block, - } - }; +``` +simulatedCost < gasLimit < simulatedCost + gasToLockForCallback +``` - self.claim_rewards_for_user(&caller, &mut staking_pos); - staking_pos.stake_amount += payment_amount; +For our example, that would be: - stake_mapper.set(&staking_pos); -} +``` +3473900 < gasLimit < 7619200 +``` -#[endpoint] -fn unstake(&self, opt_unstake_amount: OptionalValue) { - let caller = self.blockchain().get_caller(); - self.require_user_staked(&caller); +:::important +As of February 2022, for contracts that call other contracts, the lowest `gasLimit` required by a successful execution isn't easy to determine using **mxpy**. While this value can be determined by a careful inspection of the local testnet logs, for the moment, the recommended approach is to **start with the right-hand side of the inequality above (`simulatedCost + gasToLockForCallback`) and gradually decrease the value** while simulating the call and looking for a successful output. +::: - let stake_mapper = self.staking_position(&caller); - let mut staking_pos = stake_mapper.get(); +For our example, let's simulate using the following values for `gasLimit`: `7619200, 7000000, 6000000`: - let unstake_amount = match opt_unstake_amount { - OptionalValue::Some(amt) => amt, - OptionalValue::None => staking_pos.stake_amount.clone(), - }; - require!( - unstake_amount > 0 && unstake_amount <= staking_pos.stake_amount, - "Invalid unstake amount" - ); +```bash +$ mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --function=foo \ + --gas-limit=7619200 \ + --arguments ${hexAddressOfB} \ + --simulate - self.claim_rewards_for_user(&caller, &mut staking_pos); - staking_pos.stake_amount -= &unstake_amount; +... inspect output (possibly testnet logs); execution is successful - if staking_pos.stake_amount > 0 { - stake_mapper.set(&staking_pos); - } else { - stake_mapper.clear(); - self.staked_addresses().swap_remove(&caller); - } +mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --function=foo \ + --gas-limit=7000000 \ + --arguments ${hexAddressOfB} \ + --simulate - self.tx().to(caller).egld(unstake_amount).transfer(); -} +... inspect output (possibly testnet logs); execution is successful -#[endpoint(claimRewards)] -fn claim_rewards(&self) { - let caller = self.blockchain().get_caller(); - self.require_user_staked(&caller); +mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts \ + --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ + --function=foo \ + --gas-limit=6000000 \ + --arguments ${hexAddressOfB} \ + --simulate - let stake_mapper = self.staking_position(&caller); +... inspect output (possibly testnet logs); ERROR: out of gas when executing B::bar() - let mut staking_pos = stake_mapper.get(); - self.claim_rewards_for_user(&caller, &mut staking_pos); +``` - stake_mapper.set(&staking_pos); -} +Therefore, in our case, a reasonable value for `gasLimit` would be 7000000. -fn require_user_staked(&self, user: &ManagedAddress) { - require!(self.staked_addresses().contains(user), "Must stake first"); -} -``` +:::important +In case of a highly gas-demanding callback (not recommended) which would consume more than `gasToLockForCallback`, one should appropriately increase the right-hand side of the `gasLimit` inequation depicted above, by inspecting the contract call output and the testnet logs. +::: -For the `stake` endpoint, if the user was not previously staked, we provide a default entry. The `insert` method of `UnorderedSetMapper` returns `true` if the entry is new and `false` if the user already exists in the list. This means we can use that result directly, instead of checking for `stake_mapper.is_empty()`. +--- -For the `unstake` and `claimRewards` endpoints, we have to check if the user was already staked and return an error otherwise (as they'd have nothing to unstake/claim anyway). +### validators -Once you've applied all the suggested changes, **rebuilt the contract** and **regenerate the proxy**, running the test should work just fine now: +This page describes the structure of the `validators` index (Elasticsearch), and also depicts a few examples of how to query it. -```bash -running 1 test -test stake_unstake_test ... ok -``` -In order to apply these changes on devnet, you should build the contract, regenerate the interactor and then upgrade it. +## _id -:::warning -Whenever you regenerate the interactor, make sure that wallet_address points to the wallet you intend to use for executing transactions. +The `_id` field of this index is composed in this way: `{shardID}_{epoch}` (example: `1_123`) -By default, the interactor registers Alice's wallet, so you’ll need to update wallet_address manually if you’re using a different one. -::: -```bash -sc-meta all build -sc-meta all snippets -cd interactor/ -``` +## Fields -Set the `apy` variable inside the `upgrade` function from `staking-contract/interactor/src/interact.rs` to `100`, then run: -```bash -cargo run upgrade -``` +| Field | Description | +|-------------|-------------------------------------------------------------------------------------------------| +| publicKeys | The publicKeys field contains a list of all validators' public keys from an epoch and a shard. | -To verify the change, query the `apy` storage mapper by running: -```bash -cargo run getApy -``` +## Query examples -You should see the following output: -```bash -Result: 100 +#### Fetch all validators from a shard by epoch +In the example below, we fetch all the validators' public keys from shard 1, epoch 600. + +``` +curl --request GET \ + --url ${ES_URL}/validators/_search \ + --header 'Content-Type: application/json' \ + --data '{ + "query": { + "match": { + "_id":"1_600" + } + } +}' ``` +--- -### Testing +### Versions and Changelog -Now that we've implemented rewards logic, let's add the following test to `staking-contract/tests/staking_contract_blackbox_test.rs`: +## **Overview** -```rust -#[test] -fn rewards_test() { - let mut setup = ContractSetup::new(); +This page offers a high level overview of the important releases of the MultiversX Proxy API, along with recommendations (such as upgrade or transitioning recommendations) for API consumers. - // stake full - setup - .world - .tx() - .from(USER_ADDRESS) - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .stake() - .egld(USER_BALANCE) - .run(); +:::caution +Documentation in this section is preliminary and subject to change. +::: - let expected_result_1 = StakingPosition { - stake_amount: BigUint::from(USER_BALANCE), - last_action_block: 0, - }; - setup - .world - .query() - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .staking_position(USER_ADDRESS) - .returns(ExpectValue(&expected_result_1)) - .run(); +## **MultiversX Proxy HTTP API [v1.1.0](https://github.com/multiversx/mx-chain-proxy-go/releases/tag/v1.1.0)** - setup.world.current_block().block_nonce(BLOCKS_IN_YEAR); +This is the API launched at the Genesis. - // query rewards - setup - .world - .query() - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .calculate_rewards_for_user(USER_ADDRESS) - .returns(ExpectValue( - BigUint::from(USER_BALANCE) * APY / MAX_PERCENTAGE, - )) - .run(); - // claim rewards - setup - .world - .query() - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .staking_position(USER_ADDRESS) - .returns(ExpectValue(&expected_result_1)) - .run(); +## **MultiversX Proxy HTTP API [v1.1.1](https://github.com/multiversx/mx-chain-proxy-go/releases/tag/v1.1.1)** - setup - .world - .tx() - .from(USER_ADDRESS) - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .claim_rewards() - .run(); +This API version brought new features such as the [_hyperblock_-related endpoints](/sdk-and-tools/rest-api/blocks#get-hyperblock-by-nonce), useful for monitoring the blockchain. Furthermore, the `GET transaction` endpoint has been adjusted to include extra fields - for example, the so-called _hyperblock coordinates_ (the _nonce_ and the _hash_ of the containing hyperblock). - let expected_result_2 = StakingPosition { - stake_amount: BigUint::from(USER_BALANCE), - last_action_block: BLOCKS_IN_YEAR, - }; - setup - .world - .query() - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .staking_position(USER_ADDRESS) - .returns(ExpectValue(expected_result_2)) - .run(); +This API **has never been deployed to the central instance** of the MultiversX Proxy, available at [gateway.multiversx.com](https://gateway.multiversx.com/). However, until November 2020, **this API has been deployed on-premises** to several partners and 3rd party services (such as Exchange systems) - in the shape of [Observing Squads](/integrators/observing-squad), set up via the nodes scripts - [mx-chain-scripts](https://github.com/multiversx/mx-chain-scripts). - setup - .world - .check_account(USER_ADDRESS) - .balance(BigUint::from(USER_BALANCE) * APY / MAX_PERCENTAGE); +This version of the API requires Observer Nodes with tag [e1.1.0](https://github.com/multiversx/mx-chain-go/releases/tag/v1.1.6/releases/tag/e1.1.0) or greater. - // query rewards after claim - setup - .world - .query() - .to(STAKING_CONTRACT_ADDRESS) - .typed(staking_contract_proxy::StakingContractProxy) - .calculate_rewards_for_user(USER_ADDRESS) - .returns(ExpectValue(0u32)) - .run(); -} -``` +There are **no breaking changes** between API **v1.1.0** and API **v1.1.1** - neither in terms of structure and format of the requests / responses, nor in the scheme of the URL. **However**, in terms of semantics, the following fixes might lead to breaking changes for some API consumers: -In the test, we perform the following steps: +- `GET transaction` endpoint has been fixed to report the appropriate status **invalid** for actually _invalid transactions_ (e.g. not enough balance). In v1.1.0, the reported status was imprecise: **partially-executed**. -- Stake 1 EGLD; -- Set block nonce after 1 year (i.e. simulating 1 year worth of blocks passing); -- Querying rewards, which should give use 10% of 1 EGLD = 0.1 EGLD; -- Claiming said rewards and checking the internal state and user balance; -- Querying again after claim, to check that double-claim is not possible. +:::caution +As of November 2020, new API consumers are recommended to use a newer version of the API. -This test should work without any errors. +**v1.1.0 and v1.1.1 will be deprecated** once all existing API consumers are known to have been upgraded to a more recent version. +::: -## Conclusion +## **MultiversX Proxy HTTP API [v1.1.3](https://github.com/multiversx/mx-chain-proxy-go/releases/tag/v1.1.3)** -Currently, there is no way to deposit rewards into the smart contract, unless the owner makes it payable, which is generally bad practice, and not recommended. +This API version brought additions - new endpoints, such as `network/economics` or `address/shard`. Furthermore, the response of `vm-values` endpoints has been altered. Though, perhaps the most significant change is **the renaming of transaction statuses**. -As this is a fairly simple task compared to what we've done already, we'll leave this as an exercise to the reader. You'll have to add a `payable("EGLD")` endpoint, and additionally, a storage mapper that keeps track of the remaining rewards. +This version of the API requires Observer Nodes with tag [v1.1.6](https://github.com/multiversx/mx-chain-go/releases/tag/v1.1.6) or greater. -Good luck! +Between API **v1.1.1** and API **v1.1.3**, the API consumer would observe the following **breaking changes**: + +- All fields of `vm-values` endpoints has been renamed (changed casing, among others). +- The possible set of values for the transaction statuses has been changed: **executed** has been renamed to **success.** The statuses **received** and **partially-executed** have been merged under the status **pending**, while the status **not-executed** has been renamed to **fail**. For API consumers to not be affected by this change, they should follow the recommendations in [Querying the Blockchain](/integrators/querying-the-blockchain). + +:::important +As of November 2020, new API consumers are recommended to switch to this version (or a more recent one) of the API. + +For API consumers that use **on-premises Observing Squads**, an updated installer will be provided (on this matter, the work is in progress). +::: --- -### Storage Mappers +### Vote and close a proposal -The Rust framework provides various storage mappers you can use. Deciding which one to use for every situation is critical for performance. There will be a comparison section after each mapper is described. +After a proposal is created (see +[Create a governance proposal](/sdk-and-tools/sdk-js/cookbook/governance/create-proposal)), +it needs votes while its window is open, and closing once the window ends. This +recipe **votes** (yes / no / abstain / veto, weighted by staked voting power) and +**closes** a proposal, each via the controller and the factory, and reads a live +proposal's tallies and status first. -Note: All the storage mappers support additional key arguments. +The default `npm start` reads the latest real proposal on devnet, its vote counts, +whether it is closed, and whether it passed, so you see real governance data +without a wallet. +## Prerequisites -# General purpose mappers +- Node.js >= 20.13.1. +- For the default read and `payload` demos: devnet network access only. +- For an actual vote: a devnet PEM with staked or delegated EGLD (voting power). A + wallet with no stake has zero voting power and cannot vote. +## Install -## SingleValueMapper +```bash +git clone https://github.com/multiversx/cookbook.git +cd cookbook/recipes/governance-vote-close-proposal +npm install +npm run build +``` -Stores a single value. Examples: +## Voting and closing + +```ts title="src/voteClose.ts" +// src/voteClose.ts - the subject of this recipe: the rest of a governance +// proposal's life after it is created (see the create-proposal recipe): +// VOTING on it while its window is open, and CLOSING it once the window ends. +// +// vote -> cast yes / no / abstain / veto, weighted by your staked +// voting power. One transaction per voter. +// closeProposal -> after the end epoch, settle the proposal and release the +// fee back to the proposer (if it was not lost). +// +// Both go to the governance system contract and set their own gas limit from +// the SDK config. Voting requires staked or delegated EGLD - a wallet with no +// stake has zero voting power and its vote (and `getVotingPower`) is rejected. + +import { Account, Vote } from '@multiversx/sdk-core'; +import type { Address, DevnetEntrypoint, Transaction } from '@multiversx/sdk-core'; + +export interface ProposalView { + nonce: number; + commitHash: string; + issuer: string; + startVoteEpoch: number; + endVoteEpoch: number; + numYesVotes: bigint; + numNoVotes: bigint; + numAbstainVotes: bigint; + numVetoVotes: bigint; + isClosed: boolean; + isPassed: boolean; +} + +/** Read one proposal's tallies and status by its nonce. */ +export async function readProposal(entrypoint: DevnetEntrypoint, proposalNonce: number): Promise { + const controller = entrypoint.createGovernanceController(); + const info = await controller.getProposal(proposalNonce); + return { + nonce: info.nonce, + commitHash: info.commitHash, + issuer: info.issuer.toBech32(), + startVoteEpoch: info.startVoteEpoch, + endVoteEpoch: info.endVoteEpoch, + numYesVotes: info.numYesVotes, + numNoVotes: info.numNoVotes, + numAbstainVotes: info.numAbstainVotes, + numVetoVotes: info.numVetoVotes, + isClosed: info.isClosed, + isPassed: info.isPassed, + }; +} -```rust -fn single_value(&self) -> SingleValueMapper; -fn single_value_with_single_key_arg(&self, key_arg: Type1) -> SingleValueMapper; -fn single_value_with_multi_key_arg(&self, key_arg1: Type1, key_arg2: Type2) -> SingleValueMapper; +/** The address's current governance voting power (needs staked/delegated EGLD). */ +export async function readVotingPower(entrypoint: DevnetEntrypoint, address: Address): Promise { + const controller = entrypoint.createGovernanceController(); + return controller.getVotingPower(address); +} + +/** Vote on a proposal, path 1 - the controller. */ +export async function voteViaController( + entrypoint: DevnetEntrypoint, + voter: Account, + proposalNonce: number, + vote: Vote, +): Promise { + const controller = entrypoint.createGovernanceController(); + return controller.createTransactionForVoting(voter, voter.getNonceThenIncrement(), { proposalNonce, vote }); +} + +/** Vote on a proposal, path 2 - the factory (you set nonce + signature). */ +export async function voteViaFactory( + entrypoint: DevnetEntrypoint, + voter: Account, + proposalNonce: number, + vote: Vote, +): Promise { + const factory = entrypoint.createGovernanceTransactionsFactory(); + const transaction = await factory.createTransactionForVoting(voter.address, { proposalNonce, vote }); + transaction.nonce = voter.getNonceThenIncrement(); + transaction.signature = await voter.signTransaction(transaction); + return transaction; +} + +/** Close a proposal whose window has ended, path 1 - the controller. */ +export async function closeViaController( + entrypoint: DevnetEntrypoint, + closer: Account, + proposalNonce: number, +): Promise { + const controller = entrypoint.createGovernanceController(); + return controller.createTransactionForClosingProposal(closer, closer.getNonceThenIncrement(), { proposalNonce }); +} + +/** Close a proposal whose window has ended, path 2 - the factory. */ +export async function closeViaFactory( + entrypoint: DevnetEntrypoint, + closer: Account, + proposalNonce: number, +): Promise { + const factory = entrypoint.createGovernanceTransactionsFactory(); + const transaction = await factory.createTransactionForClosingProposal(closer.address, { proposalNonce }); + transaction.nonce = closer.getNonceThenIncrement(); + transaction.signature = await closer.signTransaction(transaction); + return transaction; +} + +export interface GovernancePayload { + function: string; + args: string[]; + receiver: string; + gasLimit: bigint; +} + +/** Decode a vote / closeProposal transaction's wire fields. */ +export function describeGovernancePayload(transaction: Transaction): GovernancePayload { + const parts = Buffer.from(transaction.data).toString().split('@'); + return { + function: parts[0] ?? '', + args: parts.slice(1), + receiver: transaction.receiver.toBech32(), + gasLimit: transaction.gasLimit, + }; +} ``` -Keep in mind there is no way of iterating over all `key_arg`s, so if you need to do that, consider using another mapper. +## Run it -Available methods: +```bash +# Read the latest live proposal (tallies + status) - no wallet, no funds: +npm start +# Inspect the vote and close wire payloads offline: +npm start -- payload -### get -```rust -fn get() -> Type +# Actually vote yes on the latest proposal (needs a staked devnet PEM); --close +# to close, --factory for the factory path, --no/--abstain/--veto to change vote: +npm start -- vote ./wallet.pem ``` -Reads the value from storage and deserializes it to the given `Type`. For numerical types and vector types, this will return the default value for empty storage, i.e. 0 and empty vec respectively. For custom structs, this will signal an error if trying to read from empty storage. +Output of the default read mode, and of `payload`: +```text +Latest proposal nonce: 138 + +Proposal #138 + commitHash: c8b6f734c248d5620aa6a045b8975b6d5e119314 + issuer: erd107uaynrvf80g4zuym4fqqh5pqzvaczdryj49zr2qew57wqe3mvusupj8xh + vote window: epoch 5133 .. 5133 + yes: 2633194709716022500000 + no: 120000000000000000000000 + abstain: 1349700998243378049064 + veto: 0 + closed: true passed: false + +vote: vote@8a@796573 (nonce hex | vote string hex, "yes" = 796573) gasLimit 5171000 +closeProposal: closeProposal@8a (nonce hex) gasLimit 50074000 +receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla (the governance system contract, for both) +``` + +## How it works + +**Controller vs factory.** +`controller.createTransactionForVoting(account, nonce, { proposalNonce, vote })` +builds, sets the nonce, and signs; the factory form only builds. `closeProposal` +is the same shape with just `{ proposalNonce }`. Neither takes an ABI, and both set +their own gas from the SDK config. + +**The vote is a typed enum.** Pass `Vote.YES` / `Vote.NO` / `Vote.ABSTAIN` / +`Vote.VETO` (imported from `@multiversx/sdk-core`). On the wire it is the utf8 +string of that value, `vote@@796573` is a yes vote (`796573` = "yes"). Your +vote's weight is your staked voting power, not one-address-one-vote. + +**Reading a proposal.** `getProposal(nonce)` returns the issuer, commit hash, vote +window, the four vote tallies, and `isClosed` / `isPassed`. The recipe reads +`getConfig().lastProposalNonce` to find the latest proposal, then reads it. + +## Pitfalls + +:::warning[Pitfall 1: voting needs voting power, and getVotingPower throws without it] +Governance votes are weighted by staked or delegated EGLD. An address with no stake +has zero voting power, and its vote is rejected on-chain. Worse, +`getVotingPower(address)` does not return `0` for such an address, it throws `not +enough stake/delegate to vote` (sdk-core v15.4.1). Guard the call, and stake before +voting. +::: -### set -```rust -fn set(value: &Type) -``` +:::note[Pitfall 2: vote only while open, close only after the window ends] +A vote lands only between `startVoteEpoch` and `endVoteEpoch`; `closeProposal` +works only after `endVoteEpoch`. Reading the proposal (as this recipe does) tells +you which phase it is in, `isClosed` and the vote window, before you spend gas. +::: -Sets the stored value to the provided `value` argument. For base Rust numerical types, the reference is not needed. +:::note[Pitfall 3: anyone can close, and closing releases the fee] +Closing is not restricted to the proposer; any account can close a proposal whose +window has ended. Closing settles the result and returns the proposal fee (minus +the lost-proposal fee if it did not pass) to the issuer. +::: +:::note[Pitfall 4: governance sets its own gas] +As with creating a proposal, `vote` and `closeProposal` set their gas limit +automatically (`gasLimitForVote` + a voting extra, and +`gasLimitForClosingProposal`). There is no `gasLimit` parameter to pass. +::: -### is_empty -```rust -fn is_empty() -> bool -``` +## See also -Returns `true` is the storage entry is empty. Usually used when storing struct types to prevent crashes on `get()`. +- [Create a governance proposal](/sdk-and-tools/sdk-js/cookbook/governance/create-proposal) + is where the proposal you vote on comes from. +- Delegated stake is one source of the voting power a vote needs, covered in the + delegation recipes. +--- -### set_if_empty -```rust -fn set_if_empty(value: &Type) -``` +### WalletConnect 2.0 Migration -Sets the value only if the storage for that value is currently empty. Usually used in #init functions to not overwrite values on contract upgrade. +## Using `sdk-dapp` +Using `@multiversx/sdk-dapp` >= `2.2.8` or `@elrondnetwork/dapp-core` >= `2.0.0` -### clear -```rust -fn clear() -``` +WalletConnect 2.0 is already integrated, only not enabled by default. -Clears the entry. +Follow [these steps](/sdk-and-tools/sdk-dapp/#walletconnect-setup) to generate and add a `walletConnectV2ProjectId` +--- -### update -```rust -fn update R>(f: F) -> R -``` +----------- -Takes a closure as argument, applies that closure to the currently stored value, saves the new value, and returns any value the given closure might return. Examples: -Incrementing a value: -```rust -fn my_value(&self) -> SingleValueMapper; +## Using `sdk-wallet-connect-provider` -self.my_value().update(|val| *val += 1); -``` +Using `@multiversx/sdk-wallet-connect-provider` >= 3.0.1 or `@elrondnetwork/erdjs-wallet-connect-provider` -Modifying a struct's field: -```rust -pub struct MyStruct { - pub field_1: u64, - pub field_2: u32 -} +Since the WalletConnect 2.0 implementation was complimentary to the existing 1.0 version, there should be no breaking changes to upgrade the library -fn my_value(&self) -> SingleValueMapper; +Check out some detailed examples [here](/sdk-and-tools/sdk-js/sdk-js-signing-providers/#the-walletconnect-provider). -self.my_value().update(|val| val.field1 = 5); -``` +To Set up the 2.0 functionality you can check out the examples here: [https://github.com/multiversx/mx-sdk-js-examples](https://github.com/multiversx/mx-sdk-js-examples) or a more advanced implementation on sdk-dapp here: [https://github.com/multiversx/mx-sdk-dapp/blob/main/src/hooks/login/useWalletConnectV2Login.ts](https://github.com/multiversx/mx-sdk-dapp/blob/main/src/hooks/login/useWalletConnectV2Login.ts) -Returning a value from the closure: -```rust -fn my_value(&self) -> SingleValueMapper; +The main difference between V1 and V2 is in the `login` method where we now have to first `connect()` to get the `uri` and the `approval` Promise -let new_val = self.my_value().update(|val| { - *val += 1; - *val -}); -``` +```js +await provider.init(); +const { uri, approval } = await provider.connect(); +await openModal(uri); -### raw_byte_length -```rust -fn raw_byte_length() -> usize +try { + await provider.login({ approval, token }); // optional token +} catch (err) { + console.log(err); + alert('Connection Proposal Refused') +} ``` -Returns the raw byte length of the stored value. This should be rarely used. +Another difference is in the `callbacks` where on WalletConnect 2.0 we have a third one for events: +```js +const callbacks = { + onClientLogin: async function () { + // closeModal() is defined above + closeModal(); + const address = await provider.getAddress(); + console.log("Address:", address); + }, + onClientLogout: async function () { + console.log("onClientLogout()"); + }, + onClientEvent: async function (event) { + console.log("onClientEvent()", event); + } +}; +``` -## VecMapper +`signTransaction`, `signTransactions`, `logout`, etc remain the same for the library user since the action happens in the provider directly. -Stores elements of the same type, each under their own storage key. Allows access by index for said items. Keep in mind indexes start at 1 for VecMapper. Examples: +--- -```rust -fn my_vec(&self) -> VecMapper; -fn my_vec_with_args(&self, arg: Type1) -> VecMapper; -``` +### Whitebox Framework -Available methods: +## Introduction +The Rust testing framework was developed as an alternative to manually writing scenario tests. This comes with many advantages: -### push -```rust -fn push(elem: &T) -``` +- being able to calculate values using variables +- type checking +- automatic serialization +- far less verbose +- semi-automatic generation of the scenario tests -Stores the element at index `len` and increments `len` afterwards. +The only disadvantage is that you need to learn something new! Jokes aside, keep in mind that this whole framework runs in a mocked environment. So while you get powerful testing and debugging tools, you are ultimately running a mock and have no guarantee that the contract will work identically with the current VM version deployed on the mainnet. +This is where the scenario generation part comes into play. The Rust testing framework allows you to generate scenarios with minimal effort, and then run said scenarios by invoking `cargo test`. There will be a bit of manual effort required on the developer's part, but we'll get to that in its specific section. -### get -```rust -fn get(index: usize) -> Type -``` +Please note that scenario generation is more of an experiment rather than a fully fledged implementation, which we might even remove in the future. Still, some examples are provided here if you still wish to attempt it. -Gets the element at the specific index. Valid indexes are 1 to `len`, both ends included. Attempting to read from an invalid index will signal an error. +## Prerequisites -### set -```rust -fn set(index: usize, value: &Type) -``` +You need to have the latest multiversx-sc version (at the time of writing this, the latest version is 0.39.0). You can check the latest version here: https://crates.io/crates/multiversx-sc -Sets the element at the given index. Index must be in inclusive range 1 to `len`. +Add `multiversx-sc-scenario` and required packages as dev-dependencies in your Cargo.toml: +```toml +[dev-dependencies.multiversx-sc-scenario] +version = "0.39.0" -### clear_entry -```rust -fn clear_entry(index: usize) +[dev-dependencies] +num-bigint = "0.4.2" +num-traits = "0.2" +hex = "0.4" ``` -Clears the entry at the given index. This does not decrease the length. +For this tutorial, we're going to use the crowdfunding SC, so it might be handy to have it open or clone the repository: https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples/crowdfunding +You need a `tests` and a `scenarios` folder in your contract. Create a `.rs` file in your `tests` folder. -### is_empty -```rust -fn is_empty() -> bool -``` +In your newly created test file, add the following code (adapt the `crowdfunding` namespace, the struct/variable names, and the contract wasm path according to your contract): -Returns `true` if the mapper has no elements stored. +```rust +use crowdfunding::*; +use multiversx_sc::{ + sc_error, + types::{Address, SCResult}, +}; +use multiversx_sc_scenario::{ + managed_address, managed_biguint, managed_token_id, rust_biguint, whitebox::*, + DebugApi, +}; +const WASM_PATH: &'static str = "crowdfunding/output/crowdfunding.wasm"; -### len -```rust -fn len() -> usize +struct CrowdfundingSetup +where + CrowdfundingObjBuilder: + 'static + Copy + Fn() -> crowdfunding::ContractObj, +{ + pub blockchain_wrapper: BlockchainStateWrapper, + pub owner_address: Address, + pub first_user_address: Address, + pub second_user_address: Address, + pub cf_wrapper: + ContractObjWrapper, CrowdfundingObjBuilder>, +} ``` -Returns the number of items stored in the mapper. +The `CrowdfundingSetup` struct isn't really needed, but it helps de-duplicating some code. You may add other fields in your struct if needed, but for now this is enough for our use-case. The only fields you'll need for any contract are `blockchain_wrapper` and `cf_wrapper`. The rest of the fields can be adapted according to your test scenario. + +And that's all you need to get started. -### extend_from_slice -```rust -fn extend_from_slice(slice: &[Type]) -``` +## Writing your first test -Pushes all elements from the given slice at the end of the mapper. More efficient than manual `for` of `push`, as the internal length is only read and updated once. +The first test you need to write is the one simulating the deploy of your smart contract. For that, you need a user address and a contract address. Then you simply call the `init` function of the smart contract. +Since we're going to be using the same token ID everywhere, let's add it as a constant (and while we're at it, have the deadline as a constant as well): -### swap_remove ```rust -fn swap_remove(index: usize) +const CF_TOKEN_ID: &[u8] = b"CROWD-123456"; +const CF_DEADLINE: u64 = 7 * 24 * 60 * 60; // 1 week in seconds ``` -Removes the element at `index`, moves the last element to `index` and decreases the `len` by 1. There is no way of removing an element and preserving the order. - +Let's create our initial setup: -### clear ```rust -fn clear() -``` +fn setup_crowdfunding( + cf_builder: CrowdfundingObjBuilder, +) -> CrowdfundingSetup +where + CrowdfundingObjBuilder: 'static + Copy + Fn() -> crowdfunding_esdt::ContractObj, +{ + let rust_zero = rust_biguint!(0u64); + let mut blockchain_wrapper = BlockchainStateWrapper::new(); + let owner_address = blockchain_wrapper.create_user_account(&rust_zero); + let first_user_address = blockchain_wrapper.create_user_account(&rust_zero); + let second_user_address = blockchain_wrapper.create_user_account(&rust_zero); + let cf_wrapper = blockchain_wrapper.create_sc_account( + &rust_zero, + Some(&owner_address), + cf_builder, + WASM_PATH, + ); -Clears all the elements from the mapper. This function can run out of gas for big collections. + blockchain_wrapper.set_esdt_balance(&first_user_address, CF_TOKEN_ID, &rust_biguint!(1_000)); + blockchain_wrapper.set_esdt_balance(&second_user_address, CF_TOKEN_ID, &rust_biguint!(1_000)); + + blockchain_wrapper + .execute_tx(&owner_address, &cf_wrapper, &rust_zero, |sc| { + let target = managed_biguint!(2_000); + let token_id = managed_token_id!(CF_TOKEN_ID); + + sc.init(target, CF_DEADLINE, token_id); + }) + .assert_ok(); + blockchain_wrapper.add_mandos_set_account(cf_wrapper.address_ref()); -### iter -```rust -fn iter() -> Iter + CrowdfundingSetup { + blockchain_wrapper, + owner_address, + first_user_address, + second_user_address, + cf_wrapper, + } +} ``` -Provides an iterator over all the elements. +The main object you're going to be interacting with is the `BlockchainStateWrapper`. It holds the entire (mocked) blockchain state at any given moment, and allows you to interact with the accounts. +As you can see in the above test, we use the said wrapper to create an owner account, two other user accounts, and the Crowdfunding smart contract account. -## SetMapper +Then, we set the ESDT balances for the two users, and deploy the smart contract, by using the `execute_tx` function of the `BlockchainStateWrapper` object. The arguments are: -Stores a set of values, with no duplicates being allowed. It also provides methods for checking if a value already exists in the set. Values order is given by their order of insertion. +- caller address +- contract wrapper (which contains the contract address and the contract object builder) +- EGLD payment amount +- a lambda function, which contains the actual execution -Unless you need to maintain the order of the elements, consider using `UnorderedSetMapper` or `WhitelistMapper` instead, as they're more efficient. +Since this is a SC deploy, we call the `init` function. Since the contract works with managed objects, we can't use the built-in Rust BigUint, so we use the one provided by `multiversx_sc` instead. To create managed types, we use the `managed_` functions. Alternatively, you can create those objects by: -Examples: ```rust -fn my_set(&self) -> SetMapper; +let target = BigUint::::from(2_000u32); ``` -Available methods: - +Keep in mind you can't create managed types outside of the `execute_tx` functions. -### insert -```rust -fn insert(value: Type) -> bool -``` +Some observations for the `execute_tx` function: -Insers the value into the set. Returns `false` if the item was already present. +- The return type for the lambda function is a `TxResult`, which has methods for checking for success or error: `assert_ok()` is used to check the tx worked. If you want to check error cases, you would use `assert_user_error("message")`. +- After running the `init` function, we add a `setState` step in the generated scenario, to simulate our deploy: `blockchain_wrapper.add_mandos_set_account(cf_wrapper.address_ref());` +To test the scenario and generate the trace file, you have to create a test function: -### remove ```rust -fn remove(value: &Type) +#[test] +fn init_test() { + let cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj); + cf_setup + .blockchain_wrapper + .write_mandos_output("_generated_init.scen.json"); +} ``` -Removes the value from the set. Returns `false` if the set did not contain the value. - +And you're done for this step. You successfully tested your contract's init function, and generated a scenario for it. -### contains -```rust -fn contains(value: &Type) -> bool -``` -Returns `true` if the mapper contains the given value. +## Testing transactions +Let's test the `fund` function. For this, we're going to use the previous setup, but now we use the `execute_esdt_transfer` method instead of `execute_tx`, because we're sending ESDT to the contract while calling `fund`: -### is_empty ```rust -fn is_empty() -> bool -``` - -Returns `true` if the mapper has no elements stored. +#[test] +fn fund_test() { + let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj); + let b_wrapper = &mut cf_setup.blockchain_wrapper; + let user_addr = &cf_setup.first_user_address; + b_wrapper + .execute_esdt_transfer( + user_addr, + &cf_setup.cf_wrapper, + CF_TOKEN_ID, + 0, + &rust_biguint!(1_000), + |sc| { + sc.fund(); -### len -```rust -fn len() -> usize + let user_deposit = sc.deposit(&managed_address!(user_addr)).get(); + let expected_deposit = managed_biguint!(1_000); + assert_eq!(user_deposit, expected_deposit); + }, + ) + .assert_ok(); +} ``` -Returns the number of items stored in the mapper. +As you can see, we can directly call the storage mappers (like `deposit`) from within the contract and compare with a local value. No need to encode anything. +If you also want to generate a scenario file for this transaction, this is where the bit of manual work comes in: -### clear ```rust -fn clear() -``` - -Clears all the elements from the mapper. This function can run out of gas for big collections. + let mut sc_call = ScCallMandos::new(user_addr, cf_setup.cf_wrapper.address_ref(), "fund"); + sc_call.add_esdt_transfer(CF_TOKEN_ID, 0, &rust_biguint!(1_000)); + let expect = TxExpectMandos::new(0); + b_wrapper.add_mandos_sc_call(sc_call, Some(expect)); -### iter -```rust -fn iter() -> Iter + cf_setup + .blockchain_wrapper + .write_mandos_output("_generated_fund.scen.json"); ``` -Returns an iterator over all the stored elements. +You have to add this at the end of your `fund_test`. The more complex the call, the more arguments you'll have to add and such. The `SCCallMandos` struct has the `add_argument` method so you don't have to do any encoding by yourself. -## UnorderedSetMapper +## Testing queries -Same as SetMapper, but does not guarantee the order of the items. More efficient than `SetMapper`, and should be used instead unless you need to maintain the order. Internally, `UnorderedSetMapper` uses a `VecMapper` to store the elements, and additionally, it stores each element's index to provide O(1) `contains`. +Testing queries is similar to testing transactions, just with less arguments (since there is no caller, and no payment, and any modifications are automatically reverted): -Examples: ```rust -fn my_set(&self) -> UnorderedSetMapper; -``` +#[test] +fn status_test() { + let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj); + let b_wrapper = &mut cf_setup.blockchain_wrapper; -Available methods: + b_wrapper + .execute_query(&cf_setup.cf_wrapper, |sc| { + let status = sc.status(); + assert_eq!(status, Status::FundingPeriod); + }) + .assert_ok(); -`UnorderedSetMapper` contains the same methods as `SetMapper`, the only difference being item removal. Instead of `remove`, we only have `swap_remove` available. + let sc_query = ScQueryMandos::new(cf_setup.cf_wrapper.address_ref(), "status"); + let mut expect = TxExpectMandos::new(0); + expect.add_out_value(&Status::FundingPeriod); + b_wrapper.add_mandos_sc_query(sc_query, Some(expect)); -### swap_remove -```rust -fn swap_remove(value: &Type) -> bool + cf_setup + .blockchain_wrapper + .write_mandos_output("_generated_query_status.scen.json"); +} ``` -Uses the internal `VecMapper`'s swap_remove method to remove the element. Additionally, it overwrites the last element's stored index with the removed value's index. Returns `false` if the element was not present in the set. - - -## WhitelistMapper - -Stores a whitelist of items. Does not provide any means of iterating over the elements, so if you need to iterate over the elements, use `UnorderedSetMapper` instead. Internally, this mapper simply stores a flag in storage for each item if they're whitelisted. - -Examples: -```rust -fn my_whitelist(&self) -> WhitelistMapper -``` -Available methods: +## Testing smart contract errors +In the previous transaction test, we've tested the happy flow. Now let's see how we can check for errors: -### add ```rust -fn add(value: &Type) -``` - -Adds the value to the whitelist. - +#[test] +fn test_sc_error() { + let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj); + let b_wrapper = &mut cf_setup.blockchain_wrapper; + let user_addr = &cf_setup.first_user_address; -### remove -```rust -fn remove(value: &Type) -``` + b_wrapper.set_egld_balance(user_addr, &rust_biguint!(1_000)); -Removes the value from the whitelist. + b_wrapper + .execute_tx( + user_addr, + &cf_setup.cf_wrapper, + &rust_biguint!(1_000), + |sc| { + sc.fund(); + }, + ) + .assert_user_error("wrong token"); + b_wrapper + .execute_tx(user_addr, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| { + let user_deposit = sc.deposit(&managed_address!(user_addr)).get(); + let expected_deposit = managed_biguint!(0); + assert_eq!(user_deposit, expected_deposit); + }) + .assert_ok(); -### contains -```rust -fn contains(value: &Type) -> bool -``` + let mut sc_call = ScCallMandos::new(user_addr, cf_setup.cf_wrapper.address_ref(), "fund"); + sc_call.add_egld_value(&rust_biguint!(1_000)); -Returns `true` if the mapper contains the given value. + let mut expect = TxExpectMandos::new(4); + expect.set_message("wrong token"); + b_wrapper.add_mandos_sc_call(sc_call, Some(expect)); -### require_whitelisted -```rust -fn require_whitelisted(value: &Type) + cf_setup + .blockchain_wrapper + .write_mandos_output("_generated_sc_err.scen.json"); +} ``` -Will signal an error if the item is not whitelisted. Does nothing otherwise. +Notice how we've changed the payment intentionally to an invalid token to check the error case. Also, we've changed the expected deposit to "0" instead of the previous "1_000". And lastly: the `.assert_user_error("wrong token")` call on the result. -## LinkedListMapper +## Testing a successful funding campaign -Stores a linked list, which allows fast insertion/removal of elements, as well as possibility to iterate over the whole list. +For this scenario, we need both users to fund the full amount, and then owner to claim the funds. For simplicity, we've left the scenario generation out of this one: -Examples: ```rust -fn my_linked_list(&self) -> LinkedListMapper -``` +#[test] +fn test_successful_cf() { + let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj); + let b_wrapper = &mut cf_setup.blockchain_wrapper; + let owner = &cf_setup.owner_address; + let first_user = &cf_setup.first_user_address; + let second_user = &cf_setup.second_user_address; -Available methods: + // first user fund + b_wrapper + .execute_esdt_transfer( + first_user, + &cf_setup.cf_wrapper, + CF_TOKEN_ID, + 0, + &rust_biguint!(1_000), + |sc| { + sc.fund(); + let user_deposit = sc.deposit(&managed_address!(first_user)).get(); + let expected_deposit = managed_biguint!(1_000); + assert_eq!(user_deposit, expected_deposit); + }, + ) + .assert_ok(); -### is_empty -```rust -fn is_empty() -> bool -``` + // second user fund + b_wrapper + .execute_esdt_transfer( + second_user, + &cf_setup.cf_wrapper, + CF_TOKEN_ID, + 0, + &rust_biguint!(1_000), + |sc| { + sc.fund(); -Returns `true` if the mapper has no elements stored. + let user_deposit = sc.deposit(&managed_address!(second_user)).get(); + let expected_deposit = managed_biguint!(1_000); + assert_eq!(user_deposit, expected_deposit); + }, + ) + .assert_ok(); + // set block timestamp after deadline + b_wrapper.set_block_timestamp(CF_DEADLINE + 1); -### len -```rust -fn len() -> usize -``` + // check status + b_wrapper + .execute_query(&cf_setup.cf_wrapper, |sc| { + let status = sc.status(); + assert_eq!(status, Status::Successful); + }) + .assert_ok(); -Returns the number of items stored in the mapper. + // user try claim + b_wrapper + .execute_tx(first_user, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| { + sc.claim(); + }) + .assert_user_error("only owner can claim successful funding"); + // owner claim + b_wrapper + .execute_tx(owner, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| { + sc.claim(); + }) + .assert_ok(); -### clear -```rust -fn clear() + b_wrapper.check_esdt_balance(owner, CF_TOKEN_ID, &rust_biguint!(2_000)); + b_wrapper.check_esdt_balance(first_user, CF_TOKEN_ID, &rust_biguint!(0)); + b_wrapper.check_esdt_balance(second_user, CF_TOKEN_ID, &rust_biguint!(0)); +} ``` -Clears all the elements from the mapper. This function can run out of gas for big collections. - +You've already seen most of the code in this test before already. The only new things are the `set_block_timestamp` and the `check_esdt_balance` methods of the wrapper. There are similar methods for setting block nonce, block random seed, etc., and the checking EGLD and SFT/NFT balances. -### iter -```rust -fn iter() -> Iter -``` -Returns an iterator over all the stored elements. +## Testing a failed funding campaign +This is simimlar to the previous one, but instead we have the users claim instead of the owner after deadline. -### iter_from_node_id ```rust -fn iter_from_node_id(node_id: u32) -> Iter -``` - -Returns an iterator starting from the given `node_id`. Useful when splitting iteration over multiple SC calls. +#[test] +fn test_failed_cf() { + let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj); + let b_wrapper = &mut cf_setup.blockchain_wrapper; + let owner = &cf_setup.owner_address; + let first_user = &cf_setup.first_user_address; + let second_user = &cf_setup.second_user_address; + // first user fund + b_wrapper + .execute_esdt_transfer( + first_user, + &cf_setup.cf_wrapper, + CF_TOKEN_ID, + 0, + &rust_biguint!(300), + |sc| { + sc.fund(); -### front -```rust -fn front() -> Option> -fn back() -> Option> -``` + let user_deposit = sc.deposit(&managed_address!(first_user)).get(); + let expected_deposit = managed_biguint!(300); + assert_eq!(user_deposit, expected_deposit); + }, + ) + .assert_ok(); -Returns the first/last element if the list is not empty, `None` otherwise. A `LinkedListNode` has the following format: -```rust -pub struct LinkedListNode { - value: Type, - node_id: u32, - next_id: u32, - prev_id: u32, -} + // second user fund + b_wrapper + .execute_esdt_transfer( + second_user, + &cf_setup.cf_wrapper, + CF_TOKEN_ID, + 0, + &rust_biguint!(600), + |sc| { + sc.fund(); -impl LinkedListNode { - pub fn get_value_cloned(&self) -> Type { - self.value.clone() - } + let user_deposit = sc.deposit(&managed_address!(second_user)).get(); + let expected_deposit = managed_biguint!(600); + assert_eq!(user_deposit, expected_deposit); + }, + ) + .assert_ok(); - pub fn get_value_as_ref(&self) -> &Type { - &self.value - } + // set block timestamp after deadline + b_wrapper.set_block_timestamp(CF_DEADLINE + 1); - pub fn into_value(self) -> Type { - self.value - } + // check status + b_wrapper + .execute_query(&cf_setup.cf_wrapper, |sc| { + let status = sc.status(); + assert_eq!(status, Status::Failed); + }) + .assert_ok(); - pub fn get_node_id(&self) -> u32 { - self.node_id - } + // first user claim + b_wrapper + .execute_tx(first_user, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| { + sc.claim(); + }) + .assert_ok(); - pub fn get_next_node_id(&self) -> u32 { - self.next_id - } + // second user claim + b_wrapper + .execute_tx(second_user, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| { + sc.claim(); + }) + .assert_ok(); - pub fn get_prev_node_id(&self) -> u32 { - self.prev_id - } + b_wrapper.check_esdt_balance(owner, CF_TOKEN_ID, &rust_biguint!(0)); + b_wrapper.check_esdt_balance(first_user, CF_TOKEN_ID, &rust_biguint!(1_000)); + b_wrapper.check_esdt_balance(second_user, CF_TOKEN_ID, &rust_biguint!(1_000)); } ``` -### pop_front/pop_back -```rust -fn pop_front(&mut self) -> Option> -fn pop_back(&mut self) -> Option> -``` +## Conclusion -Removes and returns the first/last element from the list. +These tests cover pretty much every flow in the crowdfunding smart contract. Keep in mind that code can be deduplicated even more by having functions similar to the `setup_crowdfunding` function, but for the sake of the example, we've kept this as simple as possible. We hope this will make writing tests and debugging a lot easier moving forward! +--- -### push_after/push_before -```rust -pub fn push_after(node: &mut LinkedListNode, element: Type) -> Option> -pub fn push_before(node: &mut LinkedListNode, element: Type) -> Option> -``` +### Whitebox Functions Reference -Inserts the given `element` into the list after/before the given `node`. Returns the newly inserted node if the insertion was successful, `None` otherwise. +## Introduction +This page contains a list of all the currently available functions for the Rust Testing Framework, specifically the ones that the `BlockchainStateWrapper` type provides. -### push_after_node_id/push_before_node_id -```rust -pub fn push_after_node_id(node_id: usize, element: Type) -> Option> -pub fn push_before_node_id(node_id: usize, element: Type) -> Option> -``` +Note: You will notice that most functions use the `num_bigint::BigUint` type for numbers. This is NOT the same as the BigUint type you use inside smart contracts. It comes from a Rust library, and they are different types. It is recommended to always use the Rust version outside of lambda functions, and only use the managed type when interacting with the sc directly. -Same as the methods above, but uses node_id instead of a full node struct. +## State-checking functions -### push_front/push_back -```rust -fn push_front(element: Type) -fn push_back(element: Type) -``` +These functions check the blockchain state. They will panic and the test will fail if the check is unsuccessful. -Pushes the given `element` at the front/back of the list. Can be seen as specialized versions of `push_before_node_id` and `push_after_node_id`. +### check_egld_balance -### set_node_value ```rust -fn set_node_value(mut node: LinkedListNode, new_value: Type) +check_egld_balance(&self, address: &Address, expected_balance: &num_bigint::BigUint) ``` -Sets a node's value, if the node exists in the list. - - -### set_node_value_by_id -```rust -fn set_node_value_by_id(node_id: usize, new_value: Type) -``` +Checks the EGLD balance for the given address. -Same as the method above, but uses node_id instead of a full node struct. +### check_esdt_balance -### remove_node ```rust -fn remove_node(node: &LinkedListNode) +check_esdt_balance(&self, address: &Address, token_id: &[u8], expected_balance: &num_bigint::BigUint) ``` -Removes the node from the list, if it exists. - - -### remove_node_by_id -```rust -fn remove_node(node_id: usize) -``` +Checks the fungible ESDT balance for the given address. -Same as the method above, but uses node_id instead of a full node struct. +### check_nft_balance -### iter ```rust -fn iter() -> Iter +check_nft_balance(&self, address: &Address, token_id: &[u8], nonce: u64, expected_balance: &num_bigint::BigUint, opt_expected_attributes: Option<&T>) ``` -Returns an iterator over all the stored elements. +Where T has to implement TopEncode, TopDecode, PartialEq, and core::fmt::Debug. This is usually done through `#[derive(TopEncode, TopDecode, PartialEq, Debug)]`. +This function checks the NFT balance for a specific nonce for an address, and optionally checks the NFT attributes as well. If you are only interested in the balance, pass `Option::None` for `opt_expected_attributes`. The Rust compiler might complain that it can't deduce the generic `T`, in which case, you can do one of the following: -### iter_from_node_id ```rust -fn iter_from_node_id(node_id: u32) -> Iter -``` +b_mock.check_nft_balance::(..., None); -Returns an iterator starting from the given `node_id`. Useful when splitting iteration over multiple SC calls. +b_mock.check_nft_balance(..., Option::::None); +``` +Where `...` are the rest of the arguments. -## MapMapper -Stores (key, value) pairs, while also allowing iteration over keys. This is the most expensive mapper to use, so make sure you really need to use it. Keys order is given by their order of insertion (same as `SetMapper`). +## State-getter functions -Examples: -```rust -fn my_map(&self) -> MapMapper -``` +These functions get the current state. They are generally used after a transaction to check that the tokens reached their intended destination. Most functions will panic if they're given an invalid address as argument. -Available methods: +### get_egld_balance -### is_empty ```rust -fn is_empty() -> bool +get_egld_balance(&self, address: &Address) -> num_bigint::BigUint ``` -Returns `true` if the mapper has no elements stored. - - -### len -```rust -fn len() -> usize -``` +Gets the EGLD balance for the given account. -Returns the number of items stored in the mapper. +### get_esdt_balance -### contains_key ```rust -fn contains_key(k: &KeyType) -> bool +get_esdt_balance(&self, address: &Address, token_id: &[u8], token_nonce: u64) -> num_bigint::BigUint ``` -Returns `true` if the mapper contains the given key. +Gets the ESDT balance for the given account. If you're interested in fungible token balance, set `token_nonce` to 0. -### get +### get_nft_attributes + ```rust -fn get(k: &KeyType) -> Option +get_nft_attributes(&self, address: &Address, token_id: &[u8], token_nonce: u64) -> Option ``` -Returns `Some(value)` if the key exists. Returns `None` if the key does not exist in the map. +Gets the NFT attributes for a token owned by the given address. Will return `Option::None` if there are no attributes. -### insert +### dump_state + ```rust -fn insert(k: KeyType, v: ValueType) -> Option +dump_state(&self) ``` -Inserts the given key, value pair into the map, and returns `Some(old_value)` if the key was already present. +Prints the current state to console. Useful for debugging. -### remove +### dump_state_for_account_hex_attributes + ```rust -fn remove(k: &KeyType) -> Option +dump_state_for_account_hex_attributes(&self, address: &Address) ``` -Removes the key and the corresponding value from the map, and returns the value. If the key was not present in the map, `None` is returned. +Similar to the function before, but dumps state only for the given account. -### keys/values/iter +### dump_state_for_account + ```rust -fn keys() -> Keys -fn values() -> Values -fn iter() -> Iter +dump_state_for_account(&self, address: &Address) ``` -Provides an iterator over all keys, values, and (key, value) pairs respectively. +Similar to the function before, but prints the attributes in a user-friendly format, given by the generic type given. This is useful for debugging NFT attributes. -# Specialized mappers +## State-altering functions +These functions alter the state in some way. -## FungibleTokenMapper -Stores a token identifier (like a `SingleValueMapper`) and provides methods for using this token ID directly with the most common API functions. Note that most method calls will fail if the token was not issued previously. +### create_user_account -Examples: ```rust -fn my_token_id(&self) -> FungibleTokenMapper +create_user_account(&mut self, egld_balance: &num_bigint::BigUint) -> Address ``` -Available methods: +Creates a new user account, with the given EGLD balance. The Address is pseudo-randomly generated by the framework. -### issue/issue_and_set_all_roles -```rust -fn issue(issue_cost: BigUint, token_display_name: ManagedBuffer, token_ticker: ManagedBuffer,initial_supply: BigUint, num_decimals: usize, opt_callback: Option>) -> ! +### create_user_account_fixed_address -fn issue_and_set_all_roles(issue_cost: BigUint, token_display_name: ManagedBuffer, token_ticker: ManagedBuffer,initial_supply: BigUint, num_decimals: usize, opt_callback: Option>) -> ! +```rust +create_user_account_fixed_address(&mut self, address: &Address, egld_balance: &num_bigint::BigUint) ``` -Issues a new fungible token. `issue_cost` is 0.05 EGLD (5000000000000000) at the time of writing this, but since this changed in the past, we've let it as an argument it case it changes again in the future. +Same as the function above, but it lets you create an account with a fixed address, for the few cases when it's needed. -This mapper allows only one issue, so trying to issue multiple types will signal an error. -`opt_callback` is an optional custom callback you can use for your issue call. We recommend using the default callback. To do so, you need to import multiversx-sc-modules in your Cargo.toml: -```toml -[dependencies.multiversx-sc-modules] -version = "0.39.0" +### create_sc_account + +```rust +create_sc_account(&mut self, egld_balance: &num_bigint::BigUint, owner: Option<&Address>, obj_builder: ContractObjBuilder, contract_wasm_path: &str) -> ContractObjWrapper ``` -Note: current released multiversx-sc version at the time of writing this was 0.39.0, upgrade if necessary. +Where: -Then you should import the `DefaultCallbacksModule` in your contract: ```rust -#[multiversx_sc::contract] -pub trait MyContract: multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule { - /* ... */ -} + CB: ContractBase + CallableContract + 'static, + ContractObjBuilder: 'static + Copy + Fn() -> CB, ``` -Additionally, pass `None` for `opt_callback`. +Creates a smart contract account. For `obj_builder`, you will have to pass `sc_namespace::contract_obj`. This function will return a `ContractObjWrapper`, which contains the address of the newly created SC, and the function that is used to create instances of your contract. -Note the "never" type `-> !` as return type for this function. This means this function will terminate the execution and launch the issue async call, so any code after this call will not be executed. +The `ContractObjWrapper` will be used whenever you interact with the SC, which will be through the execution functions. If you only need the address (for setting balance, for example), you can use the `address_ref` method to get a reference to the stored address. -Alternatively, if you want to issue and also have all roles set for the SC, you can use the `issue_and_set_all_roles` method instead. +`contract_wasm_path` is the path towards the wasm file. This path is relative to the `tests` folder that the current test file resides in. The most usual path will be `output/wasm_file_name.wasm`. -### mint +### create_sc_account_fixed_address + ```rust -fn mint(amount: BigUint) -> EsdtTokenPayment +create_sc_account_fixed_address(&mut self, address: &Address, egld_balance: &num_bigint::BigUint, owner: Option<&Address>, obj_builder: ContractObjBuilder, contract_wasm_path: &str) -> ContractObjWrapper ``` -Mints `amount` tokens for the stored token ID, using the `ESDTLocalMint` built-in function. Returns a payment struct, containing the token ID and the given amount. +Same as the function above, but the address can be set by the caller instead of being randomly generated. -### mint_and_send +### set_egld_balance + ```rust -fn mint_and_send(to: &ManagedAddress, amount: BigUint) -> EsdtTokenPayment +set_egld_balance(&mut self, address: &Address, balance: &num_bigint::BigUint) ``` -Same as the method above, but also sends the minted tokens to the given address. +Sets the EGLD balance for the given account. -### burn +### set_esdt_balance + ```rust -fn burn(amount: &BigUint) +set_esdt_balance(&mut self, address: &Address, token_id: &[u8], balance: &num_bigint::BigUint) ``` -Burns `amount` tokens, using the `ESDTLocalBurn` built-in function. +Sets the fungible token balance for the given account. -### get_balance +### set_nft_balance + ```rust -fn get_balance() -> BigUint +set_nft_balance(&mut self, address: &Address, token_id: &[u8], nonce: u64, balance: &num_bigint::BigUint, attributes: &T) ``` -Gets the current balance the SC has for the token. +Sets the non-fungible token balance for the given account, and the attributes. Attributes can be any serializable type. If you don't need attributes, you can pass "empty" in various ways: `&()`, `&Vec::::new()`, `BoxedBytes::empty()`, etc. -## NonFungibleTokenMapper +### set_esdt_local_roles -Similar to the `FungibleTokenMapper`, but is used for NFT, SFT and META-ESDT tokens. +```rust +set_esdt_local_roles(&mut self, address: &Address, token_id: &[u8], roles: &[EsdtLocalRole]) +``` +Sets the ESDT token roles for the given address and token. Usually used during setup steps. -### issue/issue_and_set_all_roles -```rust -fn issue(token_type: EsdtTokenType, issue_cost: BigUint, token_display_name: ManagedBuffer, token_ticker: ManagedBuffer,initial_supply: BigUint, num_decimals: usize, opt_callback: Option) -> ! -fn issue_and_set_all_roles(token_type: EsdtTokenType, issue_cost: BigUint, token_display_name: ManagedBuffer, token_ticker: ManagedBuffer,initial_supply: BigUint, num_decimals: usize, opt_callback: Option) -> ! +### set_block_epoch + +```rust +set_block_epoch(&mut self, block_epoch: u64) ``` -Same as the previous issue function, but also takes an `EsdtTokenType` enum as argument, to decide which type of token to issue. Accepted values are `EsdtTokenType::NonFungible`, `EsdtTokenType::SemiFungible` and `EsdtTokenType::Meta`. +### set_block_nonce -### nft_create/nft_create_named ```rust -fn nft_create(amount: BigUint, attributes: &T) -> EsdtTokenPayment - -fn nft_create_named(amount: BigUint, name: &ManagedBuffer, attributes: &T) -> EsdtTokenPayment +et_block_nonce(&mut self, block_nonce: u64) ``` -Creates an NFT (optionally with a display `name`) and returns the token ID, the created token's nonce, and the given amount in a payment struct. +### set_block_round -### nft_create_and_send/nft_create_and_send_named ```rust -fn nft_create_and_send(to: &ManagedAddress, amount: BigUint, attributes: &T,) -> EsdtTokenPayment - -fn nft_create_and_send_named(to: &ManagedAddress, amount: BigUint, name: &ManagedBuffer, attributes: &T,) -> EsdtTokenPayment +set_block_round(&mut self, block_round: u64) ``` -Same as the methods above, but also sends the created token to the provided address. +### set_block_timestamp -### nft_add_quantity ```rust -fn nft_add_quantity(token_nonce: u64, amount: BigUint) -> EsdtTokenPayment +set_block_timestamp(&mut self, block_timestamp: u64) ``` -Adds quantity for the given token nonce. This can only be used if one of the `nft_create` functions was used before AND the SC holds at least 1 token for the given nonce. +### set_block_random_seed -### nft_add_quantity_and_send ```rust -fn nft_add_quantity_and_send(to: &ManagedAddress, token_nonce: u64, amount: BigUint) -> EsdtTokenPayment +set_block_random_seed(&mut self, block_random_seed: Box<[u8; 48]>) ``` -Same as the method above, but also sends the tokens to the provided address. +Set various values for the current block info. -### nft_burn +### set_prev_block_epoch + ```rust -fn nft_burn(token_nonce: u64, amount: &BigUint) +set_prev_block_epoch(&mut self, block_epoch: u64) ``` -Burns `amount` tokens for the given nonce. +### set_prev_block_nonce -### get_all_token_data ```rust -fn get_all_token_data(token_nonce: u64) -> EsdtTokenData +set_prev_block_nonce(&mut self, block_nonce: u64) ``` -Gets all the token data for the given nonce. The SC must own the given nonce for this function to work. -`EsdtTokenData` contains the following fields: +### set_prev_block_round + ```rust -pub struct EsdtTokenData { - pub token_type: EsdtTokenType, - pub amount: BigUint, - pub frozen: bool, - pub hash: ManagedBuffer, - pub name: ManagedBuffer, - pub attributes: ManagedBuffer, - pub creator: ManagedAddress, - pub royalties: BigUint, - pub uris: ManagedVec>, -} +set_prev_block_round(&mut self, block_round: u64) ``` -### get_balance +### set_prev_block_timestamp + ```rust -fn get_balance(token_nonce: u64) -> BigUint +set_prev_block_timestamp(&mut self, block_timestamp: u64) ``` -Gets the SC's balance for the given token nonce. +### set_prev_block_random_seed -### get_token_attributes ```rust -fn get_token_attributes(token_nonce: u64) -> T +set_prev_block_random_seed(&mut self, block_random_seed: Box<[u8; 48]>) ``` -Gets the attributes for the given token nonce. The SC must own the given nonce for this function to work. +Same as the ones above, but sets the block info for the previous block. -## Common functions for FungibleTokenMapper and NonFungibleTokenMapper +## Smart Contract execution functions -Both mappers work similarly, so some functions have the same implementation for both. +These functions help interacting with smart contracts. While they would still fit into the state-altering category, we feel they deserve their own section. +Note: We will shorten the signatures by not specifying the complete types for the ContractObjWrapper. For reference, the contract wrapper is of type `ContractObjWrapper`, where -### is_empty ```rust -fn is_empty() -> bool + CB: ContractBase + CallableContract + 'static, + ContractObjBuilder: 'static + Copy + Fn() -> CB, ``` -Returns `true` if the token ID is not set yet. +### execute_tx -### get_token_id ```rust -fn get_token_id() -> TokenIdentifier +execute_tx(&mut self, caller: &Address, sc_wrapper: &ContractObjWrapper<...>, egld_payment: &num_bigint::BigUint, tx_fn: TxFn) -> TxResult ``` -Gets the stored token ID. +Executes a transaction towards the given SC (defined by the wrapper), with optional EGLD payment (pass 0 if you want no payment). `tx_fn` is a lambda function that accepts a contract object as an argument. For more details about how to write such a lambda, you can take a look at the [Crowdfunding test examples](/developers/testing/rust/whitebox-legacy). -### set_token_id +### execute_esdt_transfer + ```rust -fn set_token_id(token_id: &TokenIdentifier) +execute_esdt_transfer(&mut self, caller: &Address, sc_wrapper: &ContractObjWrapper<...>, token_id: &[u8], esdt_nonce: u64, esdt_amount: &num_bigint::BigUint, tx_fn: TxFn) -> TxResult ``` -Manually sets the token ID for this mapper. This can only be used once, and can not be overwritten afterwards. This will fail if the token was issue previously, as the token ID was automatically set. +Same as the function above, but executes an ESDT/NFT transfer instead of EGLD transfer. -### require_same_token/require_all_same_token +### execute_esdt_multi_transfer + ```rust -fn require_same_token(expected_token_id: &TokenIdentifier) -fn require_all_same_token(payments: &ManagedVec) +execute_esdt_multi_transfer(&mut self, caller: &Address, sc_wrapper: &ContractObjWrapper<...>, esdt_transfers: &[TxInputESDT], tx_fn: TxFn) -> TxResult ``` -Will signal an error if the provided token ID argument(s) differs from the stored token. Useful in `#[payable]` methods when you only want to this token as payment. +Same as the function above, but executes a MultiESDTNFT transfer instead. -### set_local_roles +### execute_query + ```rust -fn set_local_roles(roles: &[EsdtLocalRole], opt_callback: Option) -> ! +execute_query(&mut self, sc_wrapper: &ContractObjWrapper<...>, query_fn: TxFn) -> TxResult ``` -Sets the provided local roles for the token. By default, no callback is used for this call, but you may provide a custom callback if you want to. - -You don't need to call this function if you use `issue_and_set_all_roles` for issuing. +Executes a SCQuery on the SC. None of the changes are committed into the state, but it still needs to be a mutable function to perform the temporary changes. Just like on the real blockchain, there is no caller and no token transfer for queries. -Same as the issue function, this will terminate execution when called. +### execute_in_managed_environment -### set_local_roles_for_address ```rust -fn set_local_roles_for_address(address: &ManagedAddress, roles: &[EsdtLocalRole], opt_callback: Option) -> ! +execute_in_managed_environment(&self, f: Func) -> T ``` -Similar to the previous function, but sets the roles for a specific address instead of the SC address. +Executes an arbitrary function and returns its result. The result can be any type. This function is rarely used. It can be useful when you want to perform some checks that involve managed types and such. (since you cannot create managed types outside of the lambda functions). -## UniqueIdMapper +## Undocumented functions -A special mapper that holds the values from 1 to N, with the following property: if `mapper[i] == i`, then nothing is actually stored. +There are some scenario generation functions that have not been included, but we recommend not bothering with scenario generation. The process is very time-consuming and the results are some unreadable scenario files. If you need to write scenarios, we recommend writing them yourself. If you still want to dabble into the scenario generation, there are some examples in the [Crowdfunding test examples](/developers/testing/rust/whitebox-legacy). -This makes it so the mapper initialization is O(1) instead of O(N). Very useful when you want to have a list of available IDs, as its name suggests. +--- -Both the IDs and the indexes are `usize`. +### Writing and testing interactions -Note: If you want an in-memory version of this, you can use the `SparseArray` type provided by the framework. +Generally speaking, we recommended to use [sc-meta CLI](/developers/meta/sc-meta-cli) to [generate the boilerplate code for your contract interactions](/developers/meta/sc-meta-cli/#calling-snippets). -Examples: -```rust -fn my_id_mapper(&self) -> UniqueIdMapper -``` +Though, for writing contract interaction snippets in **JavaScript** or **TypeScript**, please refer to the [`sdk-js` cookbook](/sdk-and-tools/sdk-js/sdk-js-cookbook). If you'd like these snippets to function as system tests of your contract, a choice would be to structure them as Mocha or Jest tests - take the `*.local.net.spec.ts` tests in [`mx-sdk-js-core`](https://github.com/multiversx/mx-sdk-js-core) as examples. For writing contract interaction snippets in **Python**, please refer to the [`sdk-py` cookbook](/sdk-and-tools/sdk-py) - if desired, you can shape them as simple scripts, as Python unit tests, or as Jupyter notebooks. -Available methods: +You might also want to have a look over [**xSuite**](https://xsuite.dev), a toolkit to init, build, test, deploy contracts using JavaScript, made by the [Arda team](https://arda.run). +--- -### set_initial_len -```rust -fn set_initial_len(&mut self, len: usize) -``` +## Validators +### Convert An Existing Validator Into A Staking Provider -Sets the initial mapper length, i.e. the `N`. The length may only be set once. +Staking Phase 3.5 introduced the ability for an existing Validator to create a new delegation smart contract and have their validator node(s) added in the delegation smart contract directly. This is different from before, when in order to do this, a Validator node was to be unstaked, and then placed at the back of the queue. With Staking Phase 3.5, Validators can retain the place inside the 3,200 Validator nodes, and start accepting non-custodial delegations. +1. Create a new Delegation Smart Contract for an Existing Validator -### is_empty -```rust -fn is_empty() -> bool +Send the following transaction from the wallet you staked the Validator from: + +``` +Generate Contract for Validator transaction + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6 + Value: 0 + Gas Limit: 510000000 + Data: "makeNewContractFromValidatorData" + + "@" + "" + + "@" + "" ``` +*For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format).* -Returns `true` if the mapper has no elements stored. +Where: +Max cap = total delegation cap in EGLD, fully denominated, in hexadecimal encoding +For example, to obtain the fully denominated form of 7231.941 EGLD, the amount must be multiplied by 10^18, resulting in 7231941000000000000000. Do not encode the ASCII string "7231941000000000000000", but encode the integer 7231941000000000000000 itself. This would result in "01880b57b708cf408000". -### len -```rust -fn len() -> usize -``` +00 = uncapped -Returns the number of items stored in the mapper. +Fee: service fee as hundredths of percents, in hexadecimal encoding +For example, a service fee of 37.45% is expressed by the integer 3745. This integer must then be encoded hexadecimally (3745 becomes "0ea1"). -### get -```rust -fn get(index: usize) -> usize -``` +After successfully deploying your new delegation smart contract, make sure you manage it with the [Delegation Manager](/validators/delegation-manager). -Gets the value for the given index. If the entry is empty, then `index` is returned, as per the mapper's property. +:::caution +Whenever merging or converting direct staked keys into a staking provider pool, please be aware that the BLS key's signature will be altered automatically and re-staking an unbonded node is no longer possible. +In other words, if you attempt this flow of operations unStakeNodes->unBondNodes for a merged key, make sure you call also the removeNodes operation. Otherwise, the stakeNodes or reStakeUnStakedNodes will fail. +::: +--- -### set -```rust -fn set(&mut self, index: usize, id: usize) -``` +### FAQs -Sets the value at the given index. The mapper's internal property of `mapper[i] == i` if empty entry is maintained. +This page contains frequently asked questions about validators and nodes. -### swap_remove -```rust -fn swap_remove(index: usize) -> usize -``` +## FAQs -Removes the ID at the given `index` and returns it. Also, the value at `index` is now set the value of the last entry in the map. Length is decreased by 1. +**How to choose a VPS?** +Get started with a few popular VPS choices: -### iter -```rust -fn iter() -> Iter -``` +- Amazon - https://aws.amazon.com/lightsail/pricing/ +- DigitalOcean - https://www.digitalocean.com/pricing/ +- UpCloud - https://upcloud.com/pricing/ +- Vultr - https://www.vultr.com/products/cloud-compute/ +- Alibaba Cloud - https://www.alibabacloud.com/product/ecs#pricing +- Google Cloud - https://cloud.google.com/products/calculator +- Microsoft Azure - https://azure.microsoft.com/en-us/pricing/calculator/ -Provides an iterator over all the IDs. +Find more via a comparison calculator - https://www.vpsbenchmarks.com/screener +Recommendations from experienced validators: -## Comparisons between the different mappers +- Jose - https://medium.com/@josefcoap/elrond-vps-guide-ebfa6655cb9f + +:::caution +Consider the price of cheap services that offer same specs as other providers at half of the price, when running a validator - the protocol dynamically phases out low performance nodes, which will lead to fewer returns for operators. +::: -### SingleValueMapper vs old storage_set/storage_get pairs +**What if I have Windows?** -There is no difference between `SingleValueMapper` and the old-school setters/getters. In fact, `SingleValueMapper` is basically a combination between `storage_set`, `storage_get`, `storage_is_empty` and `storage_clear`. Use of `SingleValueMapper` is encouraged, as it's a lot more compact, and has no performance penalty (if, for example, you never use `is_empty()`, that code will be removed by the compiler). +Use a virtualization service to deploy a virtual Linux server on your Windows machine. This setup is recommended for testing purposes only. Try this tutorial, for example: https://itsfoss.com/install-linux-in-virtualbox/ -### SingleValueMapper vs VecMapper +**General note** -Storing a `ManagedVec` can be done in two ways: +There are hundreds of people actively engaging with MultiversX infrastructure. Engage with them over our official [Telegram channel](https://t.me/MultiversXValidators), and you will get your questions answered. Weigh in favor of people with “admin” in their name. Disregard any direct messages offering private support - those are most likely scam attempts. -```rust -#[storage_mapper("my_vec_single")] -fn my_vec_single(&self) -> SingleValueMapper> +--- -#[storage_mapper("my_vec_mapper")] -fn my_vec_mapper(&self) -> VecMapper; -``` +### How to use the Docker Image -Both of those approaches have their merits. The `SingleValueMapper` concatenates all elements and stores them under a single key, while the `VecMapper` stores each element under a different key. This also means that `SingleValueMapper` uses nested-encoding for each element, while `VecMapper` uses top-encoding. +This page will guide you through the process of using the Docker image to run a MultiversX node. -Use `SingleValueMapper` when: -- you need to read the whole array on every use -- the array is expected to be of small length -Use `VecMapper` when: -- you only require reading a part of the array -- `T`'s top-encoding is vastly more efficient than `T`'s nested-encoding (for example: `u64`) +## Docker node images +As an alternative to the recommended installation flow, one could choose to run a MultiversX Node using the official Docker images: [here](https://hub.docker.com/u/multiversx) +On the `dockerhub` there are Docker images for every chain (mainnet, devnet and testnet). -### VecMapper vs SetMapper +Images name: +- for mainnet: [chain-mainnet](https://hub.docker.com/r/multiversx/chain-mainnet) +- for devnet: [chain-devnet](https://hub.docker.com/r/multiversx/chain-devnet) +- for testnet: [chain-testnet](https://hub.docker.com/r/multiversx/chain-testnet) -The primary use for `SetMapper` is storing a whitelist of addresses, token ids, etc. A token ID whitelist can be stored in these two ways: +:::note Attention required -```rust -#[storage_mapper("my_vec_whitelist")] -fn my_vec_whitelist(&self) -> VecMapper +In order to get the latest tag for an image check the latest `RELEASE` from the config repository ([mainnet](https://github.com/multiversx/mx-chain-mainnet-config/releases), [devnet](https://github.com/multiversx/mx-chain-devnet-config/releases) or [testnet](https://github.com/multiversx/mx-chain-testnet-config/releases)). +::: -#[storage_mapper("my_set_mapper")] -fn my_set_mapper(&self) -> SetMapper; -``` -This might look very similar, but the implications of using `VecMapper` for this are very damaging to the potential gas costs. Checking for an item's existence in `VecMapper` is done in O(n), with each iteration requiring a new storage read! Worst case scenario is the Token ID is not in the whitelist and the whole Vec is read. +## How to pull a Docker image from Dockerhub for node ? +```docker +IMAGE_NAME=chain-mainnet +IMAGE_TAG=[latest_release_tag] +docker pull multiversx/${IMAGE_NAME}:${IMAGE_TAG} +``` -`SetMapper` is vastly more efficient than this, as it provides checking for a value in O(1). However, this does not come without a cost. This is how the storage looks for a `SetMapper` with two elements (this snippet is taken from a scenario test): -```rust -"str:tokenWhitelist.info": "u32:2|u32:1|u32:2|u32:2", -"str:tokenWhitelist.node_idEGLD-123456": "2", -"str:tokenWhitelist.node_idETH-123456": "1", -"str:tokenWhitelist.node_links|u32:1": "u32:0|u32:2", -"str:tokenWhitelist.node_links|u32:2": "u32:1|u32:0", -"str:tokenWhitelist.value|u32:2": "str:EGLD-123456", -"str:tokenWhitelist.value|u32:1": "str:ETH-123456" +## How to generate a BLS key ? +In order to generate a new BLS key one has to pull from `dockerhub` an image for the `chain-keygenerator` tool: ``` +# pull image from dockerhub +docker pull multiversx/chain-keygenerator:latest -A `SetMapper` uses 3 * N + 1 storage entries, where N is the number of elements. Checking for an element is very easy, as the only thing the mapper has to do is check the `node_id` entry for the provided token ID. +# create a folder for the bls key +BLS_KEY_FOLDER=~/bls-key +mkdir ${BLS_KEY_FOLDER} -Even so, for this particular case, `SetMapper` is way better than `VecMapper`. +# generate a new BLS key +docker run --rm --mount type=bind,source=${BLS_KEY_FOLDER},destination=/keys --workdir /keys multiversx/chain-keygenerator:latest +``` -### VecMapper vs LinkedListMapper +## How to run a node with Docker ? -`LinkedListMapper` can be seen as a specialization for the `VecMapper`. It allows insertion/removal only at either end of the list, known as pushing/popping. It's also storage-efficient, as it only requires 2 * N + 1 storage entries. The storage for such a mapper looks like this: +The following commands run a Node using the Docker image and map a container folder to a local one that holds the necessary configuration: -```rust -"str:list_mapper.node_links|u32:1": "u32:0|u32:2", -"str:list_mapper.node_links|u32:2": "u32:1|u32:0", -"str:list_mapper.value|u32:1": "123", -"str:list_mapper.value|u32:2": "111", -"str:list_mapper.info": "u32:2|u32:1|u32:2|u32:2" +```docker +PATH_TO_BLS_KEY_FILE=/absolute/path/to/bls-key +IMAGE_NAME=chain-mainnet +IMAGE_TAG=[latest_release_tag] + +docker run --mount type=bind,source=${PATH_TO_BLS_KEY_FILE}/,destination=/data multiversx/${IMAGE_NAME}:${IMAGE_TAG} \ + --validator-key-pem-file="/data/validatorKey.pem" ``` -This is one of the lesser used mappers, as its purpose is very specific, but it's very useful if you ever need to store a queue. +In the snippet above, make sure you adjust the path to a valid key file and also provide the appropriate command-line arguments to the Node. For more details go to [Node CLI](https://docs.multiversx.com/validators/node-cli). +:::note Attention required -### SingleValueMapper vs MapMapper +**Devnet** and **Testnet** validators **should carefully** specify the precise tag when using the Docker setup, always test the new releases themselves, and only deploy them once they understand and agree with the changes. +::: -Believe it or not, most of the time, `MapMapper` is not even needed, and can simply be replaced by a `SingleValueMapper`. For example, let's say you want to store an ID for every Address. It might be tempting to use `MapMapper`, which would look like this: -```rust -#[storage_mapper("address_id_mapper")] -fn address_id_mapper(&self) -> MapMapper; +:::tip For CentOS users +If the node's docker image runs on CentOS, the machine needs the `allow_execheap` flag to be enabled. + +In order to do this, run the command `sudo setsebool allow_execheap=true` +::: + +--- + +### Import DB + +This page will guide you through the process of starting a node in `import-db` mode, allowing the reprocessing of older transactions. + + +## Introduction + +The node is able to reprocess a previously produced database by providing the database and starting +the node with the import-db related flags explained in the section below. + +Possible use cases for the import-db process: + +- index in ElasticSearch (or something similar) all the data from genesis to present time; +- validate the blockchain state; +- make sure there aren't backwards compatibility issues with a new software version; +- check the blockchain state at a specified time (this includes additional code changes, but for example if you are + interested in the result of an API endpoint at the block 255255, you could use import db and force the node to stop + at the block corresponding to that date). + + +## How to start the process + +Let's suppose we have the following data structure: + +``` + ~/mx-chain-go/cmd/node ``` -This can be replaced with the following `SingleValueMapper`: -```rust -#[storage_mapper("address_id_mapper")] -fn address_id_mapper(&self, address: &ManagedAddress) -> SingleValueMapper; +the `node` binary is found in the above-mentioned path. +Now, we have a previously constructed database (from an observer that synced with the chain from the +genesis and never switched the shards). This database will be placed in a directory, let's presume +we will place it near the node's binary, yielding a data structure as follows: + +``` +. +├── config +│ ├── api.toml +│ ├── config.toml +│ ... +├── import-db +│ └── db +│ └── 1 +│ ├── Epoch_0 +│ │ └── Shard_1 +│ │ ├── BlockHeaders +│ │ │ ... +│ │ ├── BootstrapData +│ │ │ ... +│ │ ... +│ └── Static +│ └── Shard_1 +│ ... +├── node ``` -Both of them provide (almost) the same functionality. The difference is that the `SingleValueMapper` does not provide a way to iterate over all the keys, i.e. Addresses in this case, but it's also 4-5 times more efficient. +It is very important that the directory called `db` is a subdirectory (in our case of the `import-db`). +Also, please check that the `config` directory matches the one of the node that produced the `db` data +structure, including the `prefs.toml` file. -Unless you need to iterate over all the entries, `MapMapper` should be avoided, as this is the most expensive mapper. It uses 4 * N + 1 storage entries. The storage for a `MapMapper` looks like this: +:::caution +Please make sure the `/mx-chain-go/cmd/node/db` directory is empty so the import-db process will start +from the genesis up until the last epoch provided. +::: -```rust -"str:map_mapper.node_links|u32:1": "u32:0|u32:2", -"str:map_mapper.node_links|u32:2": "u32:1|u32:0", -"str:map_mapper.value|u32:1": "123", -"str:map_mapper.value|u32:2": "111", -"str:map_mapper.node_id|u32:123": "1", -"str:map_mapper.node_id|u32:111": "2", -"str:map_mapper.mapped|u32:123": "456", -"str:map_mapper.mapped|u32:111": "222", -"str:map_mapper.info": "u32:2|u32:1|u32:2|u32:2" +Next, the node can be started by using: + +``` + cd ~/mx-chain-go/cmd/node + ./node -use-log-view -log-level *:INFO -import-db ./import-db ``` -Keep in mind that all the mappers can have as many additional arguments for the main key. For example, you can have a `VecMapper` for every user pair, like this: -```rust -#[storage_mapper("list_per_user_pair")] -fn list_per_user_pair(&self, first_addr: &ManagedAddress, second_addr: &ManagedAddress) -> VecMapper; +:::note +Please note that the `-import-db` flag specifies the path to the directory containing the source db directory. The value provided in the example above assumes that the import db directory is called `import-db` and is located near the `node` executable file. +::: + +The node will start the reprocessing of the provided database. It will end with a message like: + +``` +import ended because data from epochs [x] or [y] does not exist ``` -Using the correct mapper for your situation can greatly decrease gas costs and complexity, so always remember to carefully evaluate your use-case. +:::tip +The import-db process can be sped up by skipping the block header's signature check if the import-db data comes from a trustworthy source. +In this case the node should be started with all previously mentioned flags, adding the `-import-db-no-sig-check` flag. +::: -## Accessing a value at an address +## Import-DB with populating an Elasticsearch cluster -Because of the way the storage mappers are structured, it is very easy to access a "remote" value, meaning a value stored under a key at a different address than the current one. +One of the use-cases for utilizing the `import-db` mechanism is to populate an Elasticsearch cluster with data that is +re-processed with the help of this process. -```rust -#[storage_mapper("key_example")] -fn content(&self) -> MapMapper; +:::tip +Import-DB for populating an Elasticsearch cluster should be used only for a full setup (a node in each Shard + a Metachain node) +::: -#[storage_mapper_from_address("key_example")] -fn content_from_address(&self, address: ManagedAddress, ...) -> SetMapper; -``` +The preparation implies the update of the `external.toml` file for each node. More details can be found [here](/sdk-and-tools/elastic-search/#setup). -The `content_from_address` function is used to create a **new map** for accessing the storage of another contract, identified by its address. +If everything is configured correctly, nodes will push the re-processed data into the Elasticsearch cluster. -The function can have any name, but it is necessary to be tagged with `#[storage_mapper_from_address("key_example")]`, where **"key_example"** is the **exact key** used by the storage they wish to access. +--- +### Installing a Validator Node -### Parameters +This page will guide you through the process of installing and updating a validator node. -- `&self`: reference to the current instance of the contract. -- `address: ManagedAddress`: required parameter; it specifies the address of the contract whose storage mapper you want to access. -- **optional** extra keys of any type. +## **Install your node(s)** -### Return type +After preparing the user permissions, the script configurations, and the keys, the actual node installation can begin. The Validator script is a multi-purpose tool for managing your node, it is accessible to Mainnet, Devnet or Testnet. -The function will return the desired mapper that will store the data. In addition, `ManagedAddress` will always be added to the end of the list of generics in the storage mapper. +Following these few steps, we will work on installing the MultiversX Network validator node to get it up and running on your local machine. -:::important important -This feature only works `intra-shard`. +For installation, one must start the scripts by: -Also note that a remote value found under a key at an address can only be `read`, not modified. -::: +```bash +cd ~/mx-chain-scripts +./script.sh +``` +After that, a menu will appear with the following options. Select the option `1` to install the node. -### Example +```bash + 1) install + 2) observing_squad + 3) multikey_group + 4) upgrade + 5) upgrade_multikey + 6) upgrade_squad + 7) upgrade_proxy + 8) remove_db + 9) start +10) start_all +11) stop +12) stop_all +13) cleanup +14) github_pull +15) add_nodes +16) get_logs +17) benchmark +18) quit + Please select an action:1 +``` -If a developer wanted, for example, to iterate over another contract's `SetMapper`, instead of retrieving the values through a call to an endpoint and then iterating, one could simply create a new `SetMapper` with a specific `address` parameter. Afterwards, the `iter` function can be called easily to accomplish the task. +:::note +As an alternative, the installation can be triggered by executing the following command: -```rust title=contract_to_be_called/lib.rs -#[storage_mapper("my_remote_mapper")] -fn my_set_mapper(&self) -> SetMapper +```bash +~/mx-chain-scripts/script.sh install ``` +::: -This is a simple `SetMapper` registered under the address of `contract_to_be_called`, and the value stored will be registered under the key provided, `my_remote_mapper`. If we wanted to iterate over the values of `my_set_mapper` intra-shard, we could write: +- When asked, indicate the number of nodes you want to run, i.e. `1` +- When asked, indicate the name of your validator, i.e. `Valar` +- Quit the menu without starting (we need keys first) by using `18 - quit` -```rust title=caller_contract/lib.rs -// the address of the contract containing the storage (contract_to_be_called) -#[storage_mapper("contract_address")] -fn contract_address(&self) -> SingleValueMapper; -// by creating the mapper with the address of the sc and exact storage key -// we get access to the value stored under that key -#[storage_mapper_from_address("contract_address")] -fn contract_from_address(&self, address: ManagedAddress) -> SetMapper; +### **Prepare your keys** -#[endpoint] -fn my_endpoint(&self) -> u32 { - let mut sum = 0u32; - let address = self.contract_address().get(); +Create a new folder "VALIDATOR_KEYS" to serve as a local backup when updating: - for number in self.contract_from_address(address).iter() { - sum += number - } +```bash +cd ~ +mkdir -p ~/VALIDATOR_KEYS - sum -} ``` -Calling `my_endpoint` will return the sum of the values stored in `contract_to_be_called`'s SetMapper. +Generate a certificate file containing your Validator key by running the `keygenerator`: -By specifying the expected type, storage key and address, the value can be read and used inside our logic. +```bash +./elrond-utils/keygenerator ---- +``` -### System Delegation Events +Copy the generated `validatorKey.pem` file to the `config` folder of your node(s), and repeat for each node. +```bash + cp validatorKey.pem ~/elrond-nodes/node-0/config/ ---- +``` -### System Smart Contracts +:::tip +Each node needs its unique `validatorKey.pem` file +::: -For transactions which call System Smart Contracts, the **actual gas cost** of processing contains the two previously mentioned cost components - and they are easily computable. +Then copy the `validatorKey.pem` file - in ZIP form - to the `$HOME/VALIDATOR_KEYS/` folder . This is important for your node to be able to restart correctly after an upgrade. -For more details, please follow: +```bash +zip node-0.zip validatorKey.pem +mv node-0.zip $HOME/VALIDATOR_KEYS/ - - [The Staking Smart Contract](/validators/staking/staking-smart-contract) - - [The Delegation Manager](/validators/delegation-manager) - - [ESDT tokens](/tokens/fungible-tokens) - - [NFT tokens](/tokens/nft-tokens) +``` ---- +Repeat the above process for all your “n” nodes. When complete, please refer to our Key management section for instructions about how to properly backup and protect your keys. -### tags -This page describes the structure of the `tags` index (Elasticsearch), and also depicts a few examples of how to query it. +### **Start the node(s)** +```bash +~/mx-chain-scripts/script.sh start +``` -## _id -The `_id` field of this index is represented by the tag name in a base64 encoding. +### **Start the node visual interface** +Once the node has started, you can check its progress, using the `TermUI` interface. Navigate to your `$HOME/elrond-utils` directory and start the `TermUI`, one for each of your nodes: -## Fields +```bash +cd $HOME/elrond-utils +./termui -address localhost:8080 +``` +:::tip -| Field | Description | -|-------|---------------------------------------------------------------------| -| count | The count field represents the number of NFTs with the current tag. | -| tag | This field represents the tag in an alphanumeric format. | +Your first node is called `node-0` and it is a REST API that will run on port `8080` by default. The next node is `node-1`on port `8081`, and so on. +::: -## Query examples +## **Update your node(s)** +Upgrade your node by running the script and selecting either of these options: -### Fetch NFTs count with a given tag +- `14 - github_pull` downloads the latest version of the scripts +- `4 - upgrade` +- `9 - start` +- `18 - quit` -``` -curl --request GET \ - --url ${ES_URL}/tags/_search \ - --header 'Content-Type: application/json' \ - --data '{ - "query": { - "match": { - "tag":"sport" - } - } -}' +```bash +~/mx-chain-scripts/script.sh ``` ---- +These are the basic steps. Please carefully read the on-screen instructions, refer to the scripts [readme file](https://github.com/multiversx/mx-chain-scripts/blob/master/README). You can also ask any questions in the MultiversX [Validators chat](https://t.me/MultiversXValidators) -### Test setup -[comment]: # "mx-abstract" +## **Mandatory: Backup your keys** -## Overview +Your private keys are needed to run your node. Losing them means losing control of your node. A 3rd party gaining access to them could result in loss of funds. + +Find them in `$HOME/elrond-nodes/node-0/config` [be mindful of your “`n`” nodes] +:::important +Create a safe backup for them on storage outside of the server running your node(s). +::: -### Registering contracts -Since we don't have native execution in the Rust backend yet, the only way to run contracts is to register the contract implementation for the given contract code identifier. In simpler words, we tell the environment "whenever you encounter this contract code, run this code that I've written instead". +## **Migration from old scripts** -Since this operation is specific to only the Rust debugger, it doesn't go through the mandos pipeline. +Before the release of the current `mx-chain-scripts`, there were the `elrond-go-scripts-testnet`, `elrond-go-scripts-devnet` and `elrond-go-scripts-mainnet` for setting up nodes +on the testnet, devnet and mainnet respectively. Those three repositories have been deprecated because `elrond-go-scripts` can be used to manage nodes regardless of their target network (`testnet`, `devnet` or `mainnet`). +If one wants to migrate from the old scripts to the new ones, it is generally possible to do so while preserving the validator keys, current node installation, DB and logs. +These are the steps to be followed: +- clone the `mx-chain-scripts` repo near the old one (`elrond-go-scripts-testnet`/`elrond-go-scripts-devnet`/`elrond-go-scripts-mainnet`); assuming the old scripts were located in the home directory, run the following: -### Setting accounts +``` +cd ~ +git clone https://github.com/multiversx/mx-chain-scripts +``` -Setting accounts in blackbox tests can be easily done by using a `SetStateBuilder`. In order to create an instance of the builder, we have to call the `.account(...)` method from `ScenarioWorld`. +- configure the new scripts as described in the sections above; +- make sure you set the new `ENVIRONMENT` variable declared within `~/mx-chain-scripts/config/variables.cfg`; it must contain one of `"testnet"`, `"devnet"` or `"mainnet"`; +- call the `migrate` operation on the scripts: -```rust title=blackbox_test.rs -world // ScenarioWorld struct - .account(USER_ADDRESS) // SetStateBuilder with USER_ADDRESS account - .nonce(1) // custom nonce - .balance(50) // egld balance - .esdt_balance(TRANSFER_TOKEN, 1000) // esdt balance - .esdt_nft_balance(NFT_TOKEN_ID, 1u64, 1u64, ManagedBuffer::new()); // nft balance +``` +cd ~/mx-chain-scripts +./script.sh migrate ``` -There are no mandatory fields, so we can only add the fields that we actually need. For example, if we only need to create the account and we don't care about other fields, `world.account(ADDRESS)` will compile. +Be careful as to not mix the previous installation network with the new one. This might lead to unpredictable results. -However, there is no possibility to upgrade and existing account, so there can only be one set block per account. -We can also chain the set state declarations (if useful) as such: +## **Choosing a custom configuration tag or branch** + +:::caution +This option should be only used when debugging or testing in advance of a pre-release tag. +Use this on your own risk! +::: + +The power of the scripts set has been leveraged with a new addition: the possibility to tell the scripts a specified tag +or branch (not recommended using a branch due to the fact that an unsigned commit might bring malicious code or configs) + +To accomplish this, edit the variables.cfg file -```rust title=blackbox_test.rs - world - .account(first) // SetStateBuilder for account `first` - .nonce(1) - .balance(100) - .account(second) // SetStateBuilder for `second`, ends set state for `first` - .nonce(2) - .balance(300) - .esdt_balance(TOKEN_ID, 500); // end set state for `second` +``` +cd ~/mx-chain-scripts/config +nano variables.cfg ``` +locate the `OVERRIDE_CONFIGVER` option and input a value there, something like `tags/T1.3.14.0`. +The `tags/` prefix will tell the scripts to use the tag and not search a branch called `T1.3.14.0`. +Call the `upgrade` command on the scripts to install the desired configuration version. -### Checking accounts +Resetting the value to `""` will make the scripts to use the released version. -Similar to setting accounts, the framework provides a `CheckStateBuilder` that we can use to check state values. The check builder is instantiated using `.check_account(...)` from `ScenarioWorld`. +:::caution +The `OVERRIDE_CONFIGVER` is not backed up when calling `github_pull` operation. +::: -```rust title=blackbox_test.rs - world - .check_account(first) // CheckStateBuilder for `first` - .nonce(3) - .balance(100); +## **Troubleshooting** + +If the node fails to start and the termui window shows messages like: +``` +termui websocket error, retrying in 10s... +termui websocket error, retrying in 10s... +termui websocket error, retrying in 10s... ``` -The same rules apply when chaining multiple account checks as for chaining accounts set. +a good method to check what the node is trying to do at startup (and fails) is to issue this command: -```rust title=blackbox_test.rs - world - .check_account(first) // CheckStateBuilder for `first` - .nonce(3) - .balance(100); - .check_account(second) // CheckStateBuilder for `second`, ends check state for `first` - .check_storage("str:sum", "6"); +```bash +sudo journalctl -f -u elrond-node-XXX.service ``` + by replacing `XXX` with the actual node instance on the machine: 0, 1, 2, 3... +--- -### Mandos trace +### Manage a validator node -A mandos [trace](../rust/mandos-trace) can quickly be generated by wrapping the integration test logic into the trace generation as such: +Your node will start as an observer. In order to make it into a validator, you will need to have 2500 xEGLD tokens. You can reach out to an admin in our [Telegram community](https://t.me/MultiversXValidators) who will gladly help. -```rust title=blackbox_test.rs - world.start_trace(); +Follow these steps to manage your validator node. - // integration test logic +Let’s begin! - world.write_scenario_trace("trace1.scen.json"); -``` +First, you need to create a MultiversX wallet. You can create this wallet on either the [mainnet](https://wallet.multiversx.com), [devnet](https://devnet-wallet.multiversx.com) or [testnet](https://testnet-wallet.multiversx.com). ---- +:::tip +For devnet and testnet only: -### Testing in Go +Share your wallet's public address (erd1...) with an admin on the Telegram community, and you will receive test xEGLD. If a smaller amount of tokens is needed, +you can use the [wallet's faucet](/wallet/web-wallet#testnet-and-devnet-faucet). +::: -At some point in the past we built some testing solutions in Go. They were never very popular, but are worth mentioning. +Once you have sufficient funds, you can use the wallet to send a stake transaction for your node, in order for it to become a Validator. -This page is currently a stub. If there is interest in this type of testing, we will expand it further. +In the wallet, navigate to the “Stake” section and click on the “Stake now” button at the top right of the page. +![img](https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlZkUHx72OLJMbqkEHff2%2Fuploads%2FlJg5GyCzq7NI9nmqiKJ5%2Fwalletelrond2.PNG?alt=media&token=136796b5-b10b-4df0-b83b-038419e57ed6) +Select the `validatorKey.pem` file you created for your node and proceed with the instructions displayed. -## **Embedding in Go** +![img](https://gblobscdn.gitbook.com/assets%2F-LhHlNldCYgbyqXEGXUS%2F-MKj4PGWn3kQ197_YcJQ%2F-MKjC2SwfiK2OdVWTz49%2Fimage.png?alt=media&token=9d38ba79-9d47-452e-8fb3-303f0edf5740) -Scenario steps can be embedded in Go, in order to program for more flexible behavior. One can even save dynamically generated scenarios. For a comprehensive example on how to do that, check out the [delegation contract fuzzer in MultiversX VM](https://github.com/multiversx/mx-chain-vm-go/tree/master/fuzz/delegation) or the [DNS contract deployment scenario test generator](https://github.com/multiversx/mx-chain-vm-go/tree/master/cmd/testgen/dns). Just a snippet from the fuzzer: +You can check the status of your Stake transaction and other information about the validator node in the explorer at [mainnet explorer](https://explorer.multiversx.com), [devnet explorer](https://devnet-explorer.multiversx.com) or the [testnet explorer](https://testnet-explorer.multiversx.com). Make sure to check out the Validators section too. -```rust -_, - (err = pfe.executeTxStep( - fmt.Sprintf( - ` - { - "step": "scDeploy", - "txId": "-deploy-", - "tx": { - "from": "''%s", - "value": "0", - "contractCode": "file:delegation.wasm", - "arguments": [ - "''%s", - "%d", - "%d", - "%d" - ], - "gasLimit": "100,000", - "gasPrice": "0" - }, - "expect": { - "out": [], - "status": "", - "logs": [], - "gas": "*", - "refund": "*" - } - }`, - string(pfe.ownerAddress), - string(pfe.auctionMockAddress), - args.serviceFee, - args.numBlocksBeforeForceUnstake, - args.numBlocksBeforeUnbond - ) - )); -``` +![img](https://gblobscdn.gitbook.com/assets%2F-LhHlNldCYgbyqXEGXUS%2F-MKj4PGWn3kQ197_YcJQ%2F-MKjCya_zwNCJWCZ4ryI%2Fimage.png?alt=media&token=7a1a0e1c-dc77-41ef-afcd-296dd23da18b) ---- +:::important +To distinguish between the mainnet and other networks (devnet and testnet), we have carefully created different addresses for the devnet tools, which are also presented in a predominantly black theme. Be cautious and know the difference, to avoid mistakes involving your mainnet validators and real EGLD tokens. +::: -### Testing Overview +--- -What does it mean to test a smart contract? +### Merging A Validator Into An Existing Delegation Smart Contract -Well, smart contracts are little programs that run on the blockchain, so the first step is to find an environment for them to run. +Introduced in Staking Phase 3.5, the ability of merging one or more existing standalone validator node into a staking provider gives more flexibility for staking provider operators. +There are two steps required for this action: The owner of the Delegation SC has to whitelist the wallet from which the Merging Validator was staked from. Then the Merging Validator has to send the merge transaction from the whitelisted wallet. -## Types of tests +1. Merging a Validator into an Existing Delegation Smart Contract -The simplest answer would be to test them directly on a blockchain. We advise against testing them on mainnet, but we have the public devnet and testnet especially made for this purpose. It is even possible to run a blockchain on your local machine, this way there is no interference with anyone. +From the Delegation Smart Contract owner's wallet, send a transaction with the following parameters: -But testing on a blockchain can be cumbersome. It's a great way to test a final product, but until our product reaches maturity, we want something else. Our testing solution should be: -- fast, -- local, -- something that can we can automate (continuous integration is important), -- something that we might also debug. +```rust +Whitelist Wallet For Merging + Sender: + Receiver: + Value: 0 + Gas Limit: 5000000 + Data: "whitelistForMerge" + + "@" + "" +``` -This is what integration tests are for. Conveniently, the MultiversX framework offers the possibility to run and debug smart contracts in a sandboxed environment that imitates a real blockchain. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -There are two flavors of integration tests: -- Black-box tests: execution imitates the blockchain, no access to private contract functions; -- White-box tests: the test has access to the inner workings of the contract. +:::tip +You can obtain the HEX format of an address by first converting its bech32 (erd1...) form into binary, and then converting the resulting binary into HEX. +::: -They both have their uses and we will see more about them further on. +2. The Merging Validator sends the merge transaction from the whitelisted wallet: -There is a third type of test: the unit test. This is the most underrated type of test. It is ideal for testing a small function, or a small component of a smart contract. They are quick to write and quick to run. A healthy project should contain plenty of unit tests. +```rust +Whitelist Wallet For Merging + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6 + Value: 0 + Gas Limit: 510000000 + Data: "mergeValidatorToDelegationWithWhitelist" + + "@" "" +``` -So, to recap, the ways to test a smart contract are as follows: -- On a blockchain, -- Integration tests: - - Black-box tests, - - White-box tests; -- Unit tests. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +:::caution +We advise against using this method to buy or sell validator slots - it requires the transfer of private keys (validatorKey.pem) which can't be changed. This puts the buyer at risk of slashing, should the seller deploy a node with the same key, either intentionally or by mistake. +::: -## What language should I use for my tests? +## **Merging own node(s)** -Since smart contracts are written in **Rust**, it is most convenient to have the tests also written in Rust. The Rust framework currently supports all types of testing mentioned above. +If the owner address of the node(s) and Delegation SC is the same use, whitelisting is not needed. -Let's, however, quickly go through all options available on MultiversX: -- On a blockchain: - - Rust interactor framework; - - [Any other SDK that can interact with the MultiversX blockchains](/sdk-and-tools/overview); - - Launching transactions from the wallet directly. -- Integration tests: - - Black-box tests: - - Rust, - - JSON, via the [JSON scenarios](/developers/testing/scenario/structure-json), - - Go, by building [around the Go VM tool](/developers/testing/testing-in-go); - - White-box tests: - - Rust only (since direct access to the contract code is needed); -- Unit tests: - - Rust only (since direct access to the contract code is needed). +```rust +Merge Own Nodes + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6 + Value: 0 + Gas Limit: 510000000 + Data: "mergeValidatorToDelegationSameOwner" + + "@" "" +``` +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -## Testing backends +:::caution +Whenever merging or converting direct staked keys into a staking provider pool, please be aware that the BLS key's signature will be altered automatically and re-staking an unbonded node is no longer possible. +In other words, if you attempt this flow of operations unStakeNodes->unBondNodes for a merged key, make sure you call also the removeNodes operation. Otherwise, the stakeNodes or reStakeUnStakedNodes will fail. +::: -A smart contract needs an environment to run. Any smart contract test needs to set up this environment. All these environments are modelled after the same blockchain, so their behavior is necessarily similar. This also means that in general, with some proper engineering, the same test should be able to run on any compatible infrastructure. +--- -```mermaid -graph TD - sc[Smart Contract] --> interact - interact[Interactor] --> blockchain["⚙️ Blockchain"] - sc --> bb[BlackBox Test] --> vm-go["⚙️ Go VM"] - bb -.-> vm-k["⚙️ K framework VM"] - sc --> wb[WhiteBox Test] --> vm-rust["⚙️ Rust VM (Debugger)"] - bb --> vm-rust - style vm-k stroke-dasharray: 5 5 -``` +### Multikey nodes management -The backends are as follows: -- A blockchain - - Can be thought of as the backend for blockchain interactions; - - Currently the only place to test cross-shard transactions and contract calls. -- The Go VM: - - The is the official VM, and currently the only VM implementation on a public MultiverX blockchain. - - This is currently the most complete implementation. If there is any difference in VM execution, the Go VM is considered the correct implementation. - - The only VM that can currently model gas. -- The Rust VM. - - Alternative implementation of the VM, written specifically for testing and debugging contracts. - - Is able to run contract code directly, without the need to compile contracts to WebAssembly. - - Supports step-by-step debugging of contract code, for all types of tests. - - Can provide code coverage. -- The K Framework VM specification. - - Currently in the works. Only test prototypes are currently available. - - It is a formal specification of WebAssembly and the MultiversX VM. - - It is an _executable_ specification, can therefore be used as backend for tests. - - Supports symbolic execution. +This page contains information about how to manage multiple keys on a group of nodes. -White-box and unit tests can only run on the Rust VM backend. For the black-box tests, however, all backends are available. -Interactors and black-box tests are very similar, so our goal is to at some point create a common test API that is compatible with all backends, including the real blockchain. We are half-way there. +## Multikey architecture overview ---- +Since the mainnet launch, and up until the release candidate RC/v1.6.0, each node could have managed only +one key. So the relationship between the nodes and staked validator keys is `1:1`, the node "following" the shard where +the managed key is assigned. +The multikey feature allows a node instance to hold more than one key. However, since MultiversX is a sharded blockchain +and a single node is only able to store the state of a single shard at a time, we need a group of nodes with exactly +one node in each shard, similar with what we have on observing squad. Also, in each epoch, the keys can be shuffled among shards. +This means that running multiple keys will require at least the number of shards + 1 node instances (one for each shard + metachain). +The same set of keys should be provided for all node instances. -### The dynamic allocation problem +This type of nodes used in multikey operation can be assimilated as a hybrid between an observer node and a validator. +It behaves as an observer by holding in the `validatorKey.pem` file a BLS key that will never be part of the consensus +group and the shard should be specified in the `prefs.toml` file, so the node will not change shards. +The node behaves also as a validator (or multiple validators) by monitoring and emitting consensus messages, +whenever required on the behalf of the managed keys set. -### Avoiding memory allocation +Since an observer already performs block validation in its own shard, it can be easily used to manage a group of validator +keys and propose or validate blocks on behalf of the keys it possesses. +To summarize, this type of node can use any provided keys, in any combination, to generate consensus messages provided +that those used keys are part of the consensus group in the current round. With the multikey feature, the relationship +now becomes `n:m`, providing that `n` is the number of keys managed by an entity and `m` is the number of shards + 1. -:::caution -**Smart contracts must avoid dynamic allocation**. Due to the performance penalty incurred by dynamic allocation, the MultiversX Virtual Machine is configured with hard limits and will stop a contract that attempts too much allocation. +:::important +This feature is purely optional. Normal `1:1` relationship between the keys and the nodes is still supported. The multikey +mode should optimize the costs when running a set of keys (check [Economics running multikey nodes](#economics-running-multikey-nodes) section) ::: -Here are a few simple guidelines you can use to ensure your contract performs efficiently. By following them, you might notice a considerable reduction of gas consumption when your contract is called. It is also likely that the WASM binary resulting from compilation may become smaller in size, thus faster and cheaper to call overall. +The following image describes the keys and nodes relationship between the single operation mode versus multikey operation mode. + +![img](/validators/multikey-diagram.drawio.png) + + +## General implementation details + +The nodes running with the multikey feature, beside deciding the consensus group (which is normally done on each node), +can access the provided keys set and use, in any combination, one or more keys, if the node detects that at least one managed +key is part of the consensus group. +The code changes to support multikey nodes affected mainly the `consensus`, `keyManagement` and `heartbeat` packages. + + +### Heartbeat information + +The group managing the set of keys (we will call them multikey nodes or multikey group), will pass the validators BLS +information tight to "virtual" peer IDs. A "virtual peer ID" is a generated p2p identity that the p2p network can not +connect to as it does not have a real address bind to. Consequently, this feature brings a new layer of security as +the multikey nodes will hide the relationship between the validator BLS keys and the host that manages those BLS keys. -### It's all about the types +### Redundancy -Many basic Rust types (like `String` and `Vec`) are dynamically allocated on the heap. In simple terms, it means the program (in this case, the smart contract) keeps asking for more and more memory from the runtime environment (the VM). For small collections, this doesn't matter much, but for bigger collection, this can become slow and the VM might even stop the contract and mark the execution as failed. +The redundancy sub-system has been upgraded to accommodate the multikey requirements keeping the multiple redundancy +fallback groups operation. A fallback multikey group will monitor each managed key for missed consensus activity **independent on +each managed node**. So, a bad configured main group, offline or stuck main group nodes should trigger fallback events on +the redundancy group. +Example: if main multikey group was set to manage the following key set `[key_0, key_1 ... key_e-1, key_e+1 ... key_n]` +(mind the missing `key_e`) and the redundancy fallback multikey group has the set `[key_0, key_1 ... key_e-1, key_e, key_e+1 ... key_n]`, +then, the fallback group, after `k` misses in the consensus activity (propose/sign block) will start using that `key_e` as it +was the only key assigned to the multikey group (`k` is the value defined in the `prefs.toml` file, `RedundancyLevel` option). -The main issue is that basic Rust types are quite eager with dynamic memory allocation: they ask for more memory than they actually need. For ordinary programs, this is great for performance, but for smart contracts, where every instruction costs gas, can be quite impactful, on both cost and even runtime failures. -The alternative is to use **managed types** instead of the usual Rust types. All managed types, like `BigUint`, `ManagedBuffer` etc. store all their contents inside the VM's memory, as opposed to the contract memory, so they have a great performance advantage. But you don't need to be concerned with "where" the contents are, because managed types automatically keep track of the contents with help from the VM. +## Economics running multikey nodes -The managed types work by only storing a `handle` within the contract memory, which is a `u32` index, while the actual payload resides in reserved VM memory. So whenever you have to add two `BigUint`s for example, the `+` operation in your code will only pass the three handles: the result, the first operand, and the second operand. This way, there is very little data being passed around, which in turn makes everything cheaper. And since these types only store a handle, their memory allocation is fixed in size, so it can be allocated on the stack instead of having to be allocated on the heap. +As for `n` managed keys we will need at least a group of nodes, there is a threshold that a staking operator +will want to consider when deciding to switch the operation towards the multikey mode. The switch becomes attractive for the +operator when the number of managed keys is greater than the number of shards. So, for the time being, when we have +at least 5 keys that are either *eligible* or *waiting*, the switch to multikey mode becomes feasible. :::caution -If you need to update older code to take advantage of managed types, please take the time to understand the changes you need to make. Such an update is important and cannot be done automatically. +Although there are no hard limits in the source code to impose a maximum number of keys for a multikey group, the MultiversX team +strongly recommends the node operators to not use more than 50 keys per group. The reason behind this recommendation is that a single node +controlling enough keys could cause damage to the chain as, in extreme cases, it could propose consecutive bad blocks, disrupting the +possibility of blocks synchronization or blocks cross-notarization. ::: -### Base Rust types vs managed types +## Usage -Below is a table of unmanaged types (basic Rust types) and their managed counterparts, provided by the MultiversX framework: -| Unmanaged (safe to use) | Unmanaged (allocates on the heap) | Managed | -| :---------------------: | :-------------------------------: | :------------------------------------------: | -| - | - | `BigUint` | -| `&[u8]` | - | `&ManagedBuffer` | -| - | `BoxedBytes` | `ManagedBuffer` | -| `ArrayVec`[^1] | `Vec` | `ManagedBuffer` | -| - | `String` | `ManagedBuffer` | -| - | - | `TokenIdentifier` | -| - | `MultiValueVec` | `MultiValueEncoded` / `MultiValueManagedVec` | -| `ArrayVec`[^1] | `Vec` | `ManagedVec` | -| `[T; N]`[^2] | `Box<[T; N]>` | `ManagedByteArray` | -| - | `Address` | `ManagedAddress` | -| - | `H256` | `ManagedByteArray<32>` | -| - | - | `EsdtTokenData` | -| - | - | `EsdtTokenPayment` | +### allValidatorsKeys.pem file +The switch towards the multikey operation will only require aggregating all BLS keys in a new file, called `allValidatorsKeys.pem` +that will be loaded by default from the same directory where the `validatorKey.pem` file resides. The path can be altered by using the +binary flag called `--all-validator-keys-pem-file`. +Example of an `allValidatorsKeys.pem` file: +``` +-----BEGIN PRIVATE KEY for e296e97524483e6b59bce00cb7a69ec8c0d1ac4227925f07fdd57b3ab4ec2f64b240728a0a3c5be2930aea570bf12c12314e25d942b106472800e51524add26ec9546475c1cfae91dd7e799f256d1b0758e17aaa3898c29d489bd87c86d04498----- +YzJlODM0NTdmOTVmYMDVjZGRiNzdiODc1N2YyZGEx +ZGRhYWY5MTI5Y2NlOWQyOQ== +-----END PRIVATE KEY for e296e97524483e6b59bce00cb7a69ec8c0d1ac4227925f07fdd57b3ab4ec2f64b240728a0a3c5be2930aea570bf12c12314e25d942b106472800e51524add26ec9546475c1cfae91dd7e799f256d1b0758e17aaa3898c29d489bd87c86d04498----- +-----BEGIN PRIVATE KEY for 5585ddceb6b7bf0d308162efd895d0717b22bab6b0412f09fb9cee234be73d197bfef8ae10064be5733472c573894015029672b70f63e0b58c7ab2e831ee0aff88b868e4d712bec0baf9a1cd1982e138af9b6cc55e4454b01cb8ad02a064f515----- +MzNlZjQyYTRhZDc3ZDBkZDk1M2JmNGIwNWE2MzczMmYxZWUy +ZWVkNzNiOGQ1ZDQ0NmEzMg== +-----END PRIVATE KEY for 5585ddceb6b7bf0d308162efd895d0717b22bab6b0412f09fb9cee234be73d197bfef8ae10064be5733472c573894015029672b70f63e0b58c7ab2e831ee0aff88b868e4d712bec0baf9a1cd1982e138af9b6cc55e4454b01cb8ad02a064f515----- +-----BEGIN PRIVATE KEY for 791c7e2bd6a5fb1371af18269267ad8ef9e56e264c4c95703c57526b16b84dd8df6347c0cc14f93d595a12316d38ae11264e05d2fa26d80387d12db52c1a98e93064d073d02549c71ec4e352d73724c21c02245b25d3643b532fac25d7580f0b----- +OTcxYjYyNWMzMzlkY2JhNTAyODMwNzZlYjMyY2MxMmYzNThiMjNiNzYz +NTA4YjFjMTVlYTIwNDYyMw== +-----END PRIVATE KEY for 791c7e2bd6a5fb1371af18269267ad8ef9e56e264c4c95703c57526b16b84dd8df6347c0cc14f93d595a12316d38ae11264e05d2fa26d80387d12db52c1a98e93064d073d02549c71ec4e352d73724c21c02245b25d3643b532fac25d7580f0b----- +``` -[^1]: `ArrayVec` allocates on the stack, and so it has a fixed capacity - it cannot grow indefinitely. You can make it as large as you please, but be warned that adding beyond this capacity results in a panic. Use `try_push` instead of `push` for more graceful error handling. -[^2]: Be careful when passing arrays around, since they get copied when returned from functions. This can add a lot of expensive memory copies in your contract. -In most cases, the managed types can be used as drop-in replacements for the basic Rust types. For a simple example, see [BigUint Operations](/developers/best-practices/biguint-operations). +### prefs.toml file -We also recommend _allocating Rust arrays directly on the stack_ (as local variables) whenever a contiguous area of useful memory is needed. Moreover, avoid allocating mutable global buffers for this purpose, which require `unsafe` code to work with. +The existing fields `NodeDisplayName` and `Identity` will be applied to all managed and loaded BLS keys. The `NodeDisplayName` will +be suffixed with an order index for all managed keys. +For example, suppose we have the above example of the `allValidatorsKeys.pem` file and the `NodeDisplayName` is set to `example`. +The name for the managed keys will be: +``` +example-0 e296e97524483e6b59... +example-1 585ddceb6b7bf0d308... +example-2 791c7e2bd6a5fb1371... +``` -Also, consider using `ArrayVec`, which provides the functionality of a `Vec`, but without allocation on the heap. Instead, it requires allocation of a block of memory directly on the stack, like a basic Rust local array, but retains the flexibility of `Vec`. +If part of the managed BLS keys will need to be run on a different identity and/or different naming, the file section called +`NamedIdentity` will be of great use. +Following the above example, if we need to use a different identity and/or node name for the `791c7e2bd6a5fb1371...` key, +we will need to define the section as: +``` +# NamedIdentity represents an identity that runs nodes on the multikey +# There can be multiple identities set on the same node, each one of them having different bls keys, just by duplicating the NamedIdentity +[[NamedIdentity]] + # Identity represents the keybase/GitHub identity for the current NamedIdentity + Identity = "identity2" + # NodeName represents the name that will be given to the names of the current identity + NodeName = "random" + # BLSKeys represents the BLS keys assigned to the current NamedIdentity + BLSKeys = [ + "791c7e2bd6a5fb1371af18269267ad8ef9e56e264c4c95703c57526b16b84dd8df6347c0cc14f93d595a12316d38ae11264e05d2fa26d80387d12db52c1a98e93064d073d02549c71ec4e352d73724c21c02245b25d3643b532fac25d7580f0b" + ] +``` -:::caution -Make sure you migrate to the managed types **incrementally** and **thoroughly test your code** before even considering deploying to the mainnet. -::: +which will generate the naming as: +``` +example-0 e296e97524483e6b59... +example-1 585ddceb6b7bf0d308... +random-0 791c7e2bd6a5fb1371... +``` -:::tip -You can use the `sc-meta report` command to verify whether your contract still requires dynamic allocation or not. -::: ---- +### Security notes for the multikey nodes -### The MultiversX Serialization Format +As stated above, the multikey feature is able to use any number of keys on a small group of nodes. +At the first sight, this can be seen as a security degradation in terms of means of attacking a large staking provider but there are ways to mitigate these concerns as explained in the following list: +1. use the recommendation found in this page regarding the maximum number of keys per multikey group; +2. for each main multikey group use at least one backup multikey group in case something bad happens with the main group; +3. use the `NamedIdentity` configuration explained above to obfuscate the BLS keys and their declared identity from the actual nodes that manage the keys. -In MultiversX, there is a specific serialization format for all data that interacts with a smart contract. The serialization format is central to any project because all values entering and exiting a contract are represented as byte arrays that need to be interpreted according to a consistent specification. +Regarding point 3, each managed BLS key will create a virtual p2p identity that no node from the network can connect to since it does not advertise the connection info but is only used to sign p2p messages. +Associated with a separate named identity, the system will make that BLS key virtually unreachable, and its origin hidden from the multikey nodes. Therefore, the node operators will need to apply the following changes on the `prefs.toml` file: +* in the `[Preference]` section, the 2 options called `NodeDisplayName` and `Identity` should be changed to something different used in the BLS' definitions to prevent easy matching. Generic names like `gateway` or `observer` are suitable for this section. +Also, completely random strings can be used as to be easier to identify the nodes in explorer. The `Identity` can be left empty; +* in the `[[NamedIdentity]]` section, the 2 options called `NodeName` and `Identity` will be changed to the real identities of the BLS keys: such as the staking provider brand names. **They should be different from the ones defined in the `[Preference]` section.** -In Rust, the **multiversx-sc-codec** crate ([crate](https://crates.io/crates/multiversx-sc-codec), [docs](https://docs.rs/multiversx-sc-codec/latest/multiversx_sc_codec/)) exclusively deals with this format. Both Go and Rust implementations of scenarios have a component that serializes to this format. DApp developers need to be aware of this format when interacting with the smart contract on the backend. +In this way, the operation will be somewhat similar to the *sentinel nodes* seen elsewhere. +The difference in our case is that the setup is greatly simplified as there is no separate network for the protected nodes that will need to be maintained. +The security of our setup (if points 1, 2 and 3 are applied) should be the same with a *sentinel setup*. -## Rationale +### Configuration example -We want the format to be somewhat readable and to interact with the rest of the blockchain ecosystem as easily as possible. This is why we have chosen **big endian representation for all numeric types.** +Let's suppose we have 5 BLS keys that belong to a staking provider called `testing-staking-provider` and we want to apply the security notes described above. +So, for the sake of the example, we generated 5 random BLS keys, the `allValidatorsKeys.pem` should contain something like this: +``` +-----BEGIN PRIVATE KEY for 15eb03756fae81d2fbae392a4d7d82abdf7618ce3056b89376c2a46bc6e8403ed3cc84e12bc819c0b088ee46e7c28302d2b666b011714cc8ea2b75488907d07e194a6e83f0f3d15c7699de412de425314be5cc3ce6ab2c594690006f9915dd15----- +NDA5MWVjODMwZjU3MDhkYmQwNzk5ZWEwNjg2MDc0MzUzYmZjNThjM2ZhYzU2Y2I1 +ZGRhMjY3YTY1NjhkZjI1YQ== +-----END PRIVATE KEY for 15eb03756fae81d2fbae392a4d7d82abdf7618ce3056b89376c2a46bc6e8403ed3cc84e12bc819c0b088ee46e7c28302d2b666b011714cc8ea2b75488907d07e194a6e83f0f3d15c7699de412de425314be5cc3ce6ab2c594690006f9915dd15----- +-----BEGIN PRIVATE KEY for ff12bc7f471e2e375c6e8b981f13ed823dcca857c41a2ffc3a0956283a8428a95754375dabc0b412df3ec41d2a51ef1490a8d23f4e4f9348787f9615093e0129969085488b59d2ab550467cd0d0fa33df22e2ed2d8c8c0c0f59042dafd0c1098----- +MTcwN2ZlMzFhMzk3Y2VjOWM4ZjdmMWU3Njg4MjY3YTAwOWU5ZjJmMWYxY2Y0ZjFl +MzI2Y2M5NGJiZGFjNGQwZA== +-----END PRIVATE KEY for ff12bc7f471e2e375c6e8b981f13ed823dcca857c41a2ffc3a0956283a8428a95754375dabc0b412df3ec41d2a51ef1490a8d23f4e4f9348787f9615093e0129969085488b59d2ab550467cd0d0fa33df22e2ed2d8c8c0c0f59042dafd0c1098----- +-----BEGIN PRIVATE KEY for 3dec570c02a4444197c1ed53fefd7e57acb9bc99ae47db7661cfbfb47170418702162a46ed40e113e3381d68b713e903e286ffaf9cac77fed8f9c79e83f2abb0ccd690ef4f689607b6414a6f893e0c0ced93d7456240bbccbf223f7603dd8e05----- +ZWMwYWRjYjNiYTQ0YmM4MGM5ZjhmNTlkNTU5YTRlMWJlMTI2ODFmMDlmM2JiNTM4 +MmMyYzdlYmNhYjNkNTk2MA== +-----END PRIVATE KEY for 3dec570c02a4444197c1ed53fefd7e57acb9bc99ae47db7661cfbfb47170418702162a46ed40e113e3381d68b713e903e286ffaf9cac77fed8f9c79e83f2abb0ccd690ef4f689607b6414a6f893e0c0ced93d7456240bbccbf223f7603dd8e05----- +-----BEGIN PRIVATE KEY for 38a93e3c00128c31769823710aa7deb145591b99a78c87dbd74c894afd540ade6de3906b45001d3f5a5882db34eaf30e412bef77ed43cf5a394edd0aa70254a74db1c80eef5d41342cae76fbbae596bc811fa491e00f16a7e011a836f7ceaa15----- +YWMzMDk2ZjY3NmExNjhiNTQ5ODQzM2JiM2NiZWFmNzkyYjQyYWZhZjJlZmMwNjNl +YzdhMWI5OGM1ZDdjODg1MQ== +-----END PRIVATE KEY for 38a93e3c00128c31769823710aa7deb145591b99a78c87dbd74c894afd540ade6de3906b45001d3f5a5882db34eaf30e412bef77ed43cf5a394edd0aa70254a74db1c80eef5d41342cae76fbbae596bc811fa491e00f16a7e011a836f7ceaa15----- +-----BEGIN PRIVATE KEY for 1fce426b632e5a5941d9989e4f8bbb93a0a08a0e85dfe16d4d65c08b351dfbff1a1104d5e75e1be7565b4bbc6a583103bfc4b4075727133a54fa421983d894e549576364694b3e8910359b3de5260360bfe9f9bea2fec1cb50c2cf79a3fd590d----- +ZmYzMjM2ODljODQwMDRiMDI1MGU0NjcyMzhjYjJlMDNlNzg0OGI0YzQ1ZTM0ZjQz +YTZkZDVmNTBjYjAwMjAyNg== +-----END PRIVATE KEY for 1fce426b632e5a5941d9989e4f8bbb93a0a08a0e85dfe16d4d65c08b351dfbff1a1104d5e75e1be7565b4bbc6a583103bfc4b4075727133a54fa421983d894e549576364694b3e8910359b3de5260360bfe9f9bea2fec1cb50c2cf79a3fd590d----- +``` -More importantly, the format needs to be as compact as possible, since each additional byte costs additional fees. +The staking operators that will create the actual `allValidatorsKeys.pem` file used on the chain should concatenate all keys from their `validatorKey.pem` files with a text editor. +The content should resemble the one depicted in this example. +For the `prefs.toml` file, we can have definitions like: -## The concept of top-level vs. nested objects +```toml +[Preferences] + # DestinationShardAsObserver represents the desired shard when running as observer + # value will be given as string. For example: "0", "1", "15", "metachain" + # if "disabled" is provided then the node will start in the corresponding shard for its public key or 0 otherwise + DestinationShardAsObserver = "0" -There is a perk that is central to the formatter: we know the size of the byte arrays entering the contract. All arguments have a known size in bytes, and we normally learn the length of storage values before loading the value itself into the contract. This gives us some additional data straight away that allows us to encode less. + # NodeDisplayName represents the friendly name a user can pick for his node in the status monitor when the node does not run in multikey mode + # In multikey mode, all bls keys not mentioned in NamedIdentity section will use this one as default + NodeDisplayName = "s14" -Imagine that we have an argument of type int32. During a smart contract call we want to transmit the value "5" to it. A standard deserializer might expect us to send the full 4 bytes `0x00000005`, but there is clearly no need for the leading zeroes. It's a single argument, and we know where to stop, there is no risk of reading too much. So sending `0x05` is enough. We saved 3 bytes. Here we say that the integer is represented in its **top-level form**, it exists on its own and can be represented more compactly. + # Identity represents the GitHub identity when the node does not run in multikey mode + # In multikey mode, all bls keys not mentioned in NamedIdentity section will use this one as default + Identity = "" -But now imagine that an argument that deserializes as a vector of int32. The numbers are serialized one after the other. We no longer have the possibility of having variable length integers because we won't know where one number begins and one ends. Should we interpret `0x0101` as`[1, 1]` or `[257]`? So the solution is to always represent each integer in its full 4-byte form. `[1, 1]` is thus represented as `0x0000000100000001` and`[257]`as `0x00000101`, there is no more ambiguity. The integers here are said to be in their **nested form**. This means that because they are part of a larger structure, the length of their representation must be apparent from the encoding. + # RedundancyLevel represents the level of redundancy used by the node (-1 = disabled, 0 = main instance (default), + # 1 = first backup, 2 = second backup, etc.) + RedundancyLevel = 0 -But what about the vector itself? Its representation must always be a multiple of 4 bytes in length, so from the representation we can always deduce the length of the vector by dividing the number of bytes by 4. If the encoded byte length is not divisible by 4, this is a deserialization error. Because the vector is top-level we don't have to worry about encoding its length, but if the vector itself gets embedded into an even larger structure, this can be a problem. If, for instance, the argument is a vector of vectors of int32, each nested vector also needs to have its length encoded before its data. + # FullArchive, if enabled, will make the node able to respond to requests from past, old epochs. + # It is highly recommended to enable this flag on an observer (not on a validator node) + FullArchive = false + # PreferredConnections holds an array containing valid ips or peer ids from nodes to connect with (in top of other connections) + # Example: + # PreferredConnections = [ + # "127.0.0.10", + # "16Uiu2HAm6yvbp1oZ6zjnWsn9FdRqBSaQkbhELyaThuq48ybdorrr" + # ] + PreferredConnections = [] -## A note about the value zero + # ConnectionWatcherType represents the type of the connection watcher needed. + # possible options: + # - "disabled" - no connection watching should be made + # - "print" - new connection found will be printed in the log file + ConnectionWatcherType = "disabled" -We are used to writing the number zero as "0" or "0x00", but if we think about it, we don't need 1 byte for representing it, 0 bytes or an "empty byte array" represent the number 0 just as well. In fact, just like in `0x0005`, the leading 0 byte is superfluous, so is the byte `0x00` just like an unnecessary leading 0. + # OverridableConfigTomlValues represents an array of items to be overloaded inside other configuration files, which can be helpful + # so that certain config values need to remain the same during upgrades. + # (for example, an Elasticsearch user wants external.toml->ElasticSearchConnector.Enabled to remain true all the time during upgrades, while the default + # configuration of the node has the false value) + # The Path indicates what value to change, while Value represents the new value in string format. The node operator must make sure + # to follow the same type of the original value (ex: uint32: "37", float32: "37.0", bool: "true") + # File represents the file name that holds the configuration. Currently, the supported files are: config.toml, external.toml, p2p.toml and enableEpochs.toml + # ------------------------------- + # Un-comment and update the following section in order to enable config values overloading + # ------------------------------- + # OverridableConfigTomlValues = [ + # { File = "config.toml", Path = "StoragePruning.NumEpochsToKeep", Value = "4" }, + # { File = "config.toml", Path = "MiniBlocksStorage.Cache.Name", Value = "MiniBlocksStorage" }, + # { File = "external.toml", Path = "ElasticSearchConnector.Enabled", Value = "true" } + #] -With this being said, the format always encodes zeroes of any type as empty byte arrays. +# BlockProcessingCutoff can be used to stop processing blocks at a certain round, nonce or epoch. +# This can be useful for snapshotting different stuff and also for debugging purposes. +[BlockProcessingCutoff] + # If set to true, the node will stop at the given coordinate + Enabled = false + # Mode represents the cutoff mode. possible values: "pause" or "process-error". + # "pause" mode will halt the processing at the block with the given coordinates. Useful for snapshots/analytics + # "process-error" will return an error when processing the block with the given coordinates. Useful for debugging + Mode = "pause" -## How each type gets serialized + # CutoffTrigger represents the kind of coordinate to look after when cutting off the processing. + # Possible values: "round", "nonce", or "epoch" + CutoffTrigger = "round" -This guide is split into several sections: -- [Simple values, such as numbers, strings, etc.](/developers/data/simple-values) -- [Lists, tuples, Option](/developers/data/composite-values) -- [Custom types defined in the contracts.](/developers/data/custom-types) -- [Var-args and other multi-values](/developers/data/multi-values) -- [The code metadata flag](/developers/data/code-metadata) + # The minimum value of the cutoff. For example, if CutoffType is set to "round", and Value to 20, then the node will stop processing at round 20+ + Value = 0 -There is a special section about [uninitialized data and how defaults relate to serialization](/developers/data/defaults). +# NamedIdentity represents an identity that runs nodes on the multikey +# There can be multiple identities set on the same node, each one of them having different bls keys, just by duplicating the NamedIdentity +[[NamedIdentity]] + # Identity represents the GitHub identity for the current NamedIdentity + Identity = "testing-staking-provider" + # NodeName represents the name that will be given to the names of the current identity + NodeName = "tsp" + # BLSKeys represents the BLS keys assigned to the current NamedIdentity + BLSKeys = [ + "15eb03756fae81d2fbae392a4d7d82abdf7618ce3056b89376c2a46bc6e8403ed3cc84e12bc819c0b088ee46e7c28302d2b666b011714cc8ea2b75488907d07e194a6e83f0f3d15c7699de412de425314be5cc3ce6ab2c594690006f9915dd15", + "ff12bc7f471e2e375c6e8b981f13ed823dcca857c41a2ffc3a0956283a8428a95754375dabc0b412df3ec41d2a51ef1490a8d23f4e4f9348787f9615093e0129969085488b59d2ab550467cd0d0fa33df22e2ed2d8c8c0c0f59042dafd0c1098", + "3dec570c02a4444197c1ed53fefd7e57acb9bc99ae47db7661cfbfb47170418702162a46ed40e113e3381d68b713e903e286ffaf9cac77fed8f9c79e83f2abb0ccd690ef4f689607b6414a6f893e0c0ced93d7456240bbccbf223f7603dd8e05", + "38a93e3c00128c31769823710aa7deb145591b99a78c87dbd74c894afd540ade6de3906b45001d3f5a5882db34eaf30e412bef77ed43cf5a394edd0aa70254a74db1c80eef5d41342cae76fbbae596bc811fa491e00f16a7e011a836f7ceaa15", + "1fce426b632e5a5941d9989e4f8bbb93a0a08a0e85dfe16d4d65c08b351dfbff1a1104d5e75e1be7565b4bbc6a583103bfc4b4075727133a54fa421983d894e549576364694b3e8910359b3de5260360bfe9f9bea2fec1cb50c2cf79a3fd590d" + ] +``` ---- +:::important +These 2 configuration files `allValidatorsKeys.pem` and `prefs.toml` should be copied on all n nodes that assemble the multikey group of nodes. -### Time-related Types +**Do not forget to change the `DestinationShardAsObserver` accordingly for each node.** +::: -The Supernova release introduces increased block frequency and encourages transitioning to millisecond timestamps, instead of seconds. To support this, the SpaceCraft SDK (starting with `v0.63.0`) provides strong type wrappers for time values to prevent common bugs. +After starting the multikey nodes, in ~10 minutes, the explorer will reflect the changes. All n nodes that run the multikey group will broadcast their identity as an empty string and their names will be `s14`. +The BLS keys' identities, on the other hand will have the following names & identities: +| Key | Name | Identity | +|--------------|--------|--------------------------| +| 15eb03756... | tsp-00 | testing-staking-provider | +| ff12bc7f4... | tsp-01 | testing-staking-provider | +| 3dec570c0... | tsp-02 | testing-staking-provider | +| 38a93e3c0... | tsp-03 | testing-staking-provider | +| 1fce426b6... | tsp-04 | testing-staking-provider | -## Overview +### Migration guide from single-key operation to multikey -Traditionally, smart contracts used plain `u64` values to represent timestamps and durations. As the ecosystem transitions to millisecond precision, mixing seconds and milliseconds becomes error-prone. Runtime errors are difficult to detect, so the compiler now assists by enforcing type correctness. +:::warning +This guide can lead to potential node jailing if done incorrectly. Make sure that you understand completely all the steps involved. +We strongly suggest to practice this process first on the public testnet. You should gather invaluable experience and know how. +::: +Whenever deciding to switch from single-key operation to multikey, the following steps on how to execute this process can be considered: +1. create your `allValidatorsKeys.pem` by manually (or through a text tool) concatenate all your `validatorKey.pem` files; +2. start a multikey group, **configure it as a backup group**, provide the `allValidatorsKeys.pem` file to all the nodes forming the group; +3. let this backup multikey group nodes sync and go the next step **after all these nodes are synced**; +4. switch off your single-key backup nodes (if you previously had ones); +5. create a new multikey group, configure it as main group and let it sync. **Do not provide the `allValidatorsKeys.pem` keys yet!**. Go the next step **after all these nodes are synced**; +6. after the main group nodes are synced, copy the `allValidatorsKeys.pem` file to all nodes from the main group, switch off the main single-key nodes and restart the multikey nodes from the main group, so they will load the `allValidatorsKeys.pem` file; +7. closely monitor all your nodes in the explorer, should be online and with their rating status increasing/at 100%. Repeat this step for a few times at 10 minutes interval. -## The Four Time-Related Types +Make sure that all operations from step 6 are made as quickly as possible. In case this step takes a long time, the backup multikey group should take over. -The framework introduces four new types: +:::caution +Always attempt this process while closely monitor your nodes. If done correctly, your nodes might experience a brief rating drop (until the backup group takes over - if necessary) +::: -* **`TimestampSeconds`** — moment in time measured in seconds -* **`TimestampMillis`** — moment in time measured in milliseconds -* **`DurationSeconds`** — duration measured in seconds -* **`DurationMillis`** — duration measured in milliseconds +--- -Each is a **newtype wrapper around `u64`**, with an identical underlying representation but distinct meaning. +### MultiversX Node upgrades +As opposed to a hard fork, which is a change in the protocol that is not backward compatible, MultiversX performs regular node upgrades, which are changes in the protocol +that are backward compatible and bring new features, improvements and bugs fixes. Nodes operators must be aware of the upgrade process and the steps they need to take in order +to avoid any downtime. -## Why These Types Exist +## **Introduction** -Using plain `u64` makes it easy to: +Once a new node's binary is ready to be deployed on one of the networks (mainnet, testnet or devnet), nodes operators must +perform the upgrade to the newest version. These releases are always announced on MultiversX [Validators chat](https://t.me/MultiversXValidators) +plus via other communication channels, depending on the case. -* accidentally mix seconds and milliseconds -* add timestamps together (nonsensical) -* subtract mismatched units -* perform invalid arithmetic without realizing it -The new types make such mistakes **fail at compile time**. +## **When to upgrade** +Subscribe to email notifications for new releases from the official GitHub repositories that hold the chain configuration ([mx-chain-mainnet-config](https://github.com/multiversx/mx-chain-mainnet-config), [mx-chain-testnet-config](https://github.com/multiversx/mx-chain-mainnet-config) and [mx-chain-devnet-config](https://github.com/multiversx/mx-chain-mainnet-config). Also, make sure to join the Validators Telegram channel. +Setup monitoring and get alerts for new updates. As last resort, check the status of your validator software version using the relevant Explorer section https://explorer.multiversx.com/nodes - outdated versions will be marked with ⚠ -## Supported Operations -The framework implements common operators only where they make sense. +## **Types of upgrades** -Examples: +Currently, we have the following types of upgrades: -``` -TimestampMillis - TimestampMillis → DurationMillis -TimestampSeconds + DurationSeconds → TimestampSeconds -DurationSeconds + DurationSeconds → DurationSeconds -``` +A. - **all nodes need to upgrade**: upgrades that involve processing changes with an activation epoch (as explained below) +and have to be performed by all nodes operators in order to keep the same view over the network and not cause service disruptions. -Examples of invalid operations (will not compile): +B. - **optional upgrades**: upgrades that, for example, simply add a new Rest API endpoint or improve the trie syncing timing +are not critical from a processing point of view, and they are optional. If the nodes operators think the new feature will help them, +they can proceed with the upgrade without losing the compatibility with the network. -``` -TimestampMillis - TimestampSeconds -TimestampMillis + TimestampMillis -``` +C. - **only validators need to upgrade**: upgrades that, for example, include new features that only trigger validators (ratings changes, +transactions selection improvements and so on). Observers (nodes that don't have a stake attached) don't need to perform the upgrade +(but can upgrade nonetheless if desired). +## **Activation epochs** -## Codec and ABI Support +In order to make the upgrades as smooth as possible and to ensure that each node has the same view over the network at a given moment, +MultiversX has a so called _activation epoch_ mechanism that allows the node to implement both behaviors of the protocol - +the old (current) one, and the new one, planned for activation at a specific epoch. This mechanisms ensures that, +**until the protocol change becomes active**, nodes with an upgraded codebase / binary remain compatible with nodes that +did not perform the upgrade, and consensus is held. Although this happens in 99.9% cases, this is not 100% guaranteed due +to the unforeseen consequences when upgrading the code base in parallel with executing transactions generated by 3rd parties (MultiversX blockchain users) -All four types: -* support codec and ABI traits -* can be used in storage -* can be used in events and arguments -* behave like `u64` for serialization +### **Deterministic time / height for upgrades** -This makes them safe to adopt even in existing contracts. +As compared to other protocols that perform upgrades that start at a specific block height, releases for MultiversX nodes +don't have a specific block height where the new updates become effective, but rather the first block in the +activation epoch will make the nodes proceed with the updated versions of the components. +Since the height of the first block in an epoch cannot be known in advance (due to possible roll-backs), the network height +where a feature becomes effective cannot be calculated. +However, the time when a new feature of a bugfix becomes effective can be calculated, as epochs have fixed lengths in rounds. +Currently, MultiversX Mainnet has epochs of `14,400` rounds and a round is `6 sec`. This results in a `24h` epoch. However, +there can be delays of a few rounds, due to rollbacks of the start of epoch metablocks. -## When to Use Seconds vs. Milliseconds -* **Seconds**: acceptable when your contract already stores timestamps in seconds or relies on existing storage/metadata -* **Milliseconds**: recommended for all new contracts and for future-proof logic +### _Activation epoch example_ -The key rule: **Never mix seconds and milliseconds without explicit conversion.** +For example, let's say that we want to introduce a feature so that smart contracts can receive a `PayableBySC` metadata that +will allow them to receive EGLD or other tokens from other smart contracts. +_Timeline example_ +- the MultiversX Mainnet is at epoch `590`. +- currently, the node binary doesn't know about the `PayableBySC` metadata so if one wants to try it, an error like `invalid metadata` + will be returned. +- at epoch `600`, we release a new node binary that contains the `PayableBySC` metadata that will become active starting with epoch `613`. +- all nodes operators perform the upgrade. +- when epoch `613` begins, the new feature activates and the new metadata is recognized and accepted. +- if one wants to issue a smart contract that is `PayableBySC`, it will work. -## Testing +- nodes that didn't perform the upgrade will produce a different output of the transaction (as compared to the majority) + and won't be able to keep up with the rest of the chain. -Time-related types also work in testing frameworks. +_Backwards compatibility explained_: +If one wants to process all the blocks since genesis (via `full archive` or via `import-db`) with the released binary +it will behave this way: -In blackbox tests, one can write: +- if for example, in epoch `455` there was a transaction that tried to set the `PayableBySC` metadata, it will process it + as `invalid metadata` +- for transactions in epochs newer than `613` it will process the new metadata. -```rust - let block_timestamp_ms = TimestampMillis::new(123_000_000); +| | Epoch < 613 | Epoch >= 613 | +| ------------- | ------------------ | ------------ | +| IsPayableBySC | `invalid metadata` | `successful` | - world - .epoch_start_block() - .block_timestamp_ms(block_timestamp_ms) - .block_nonce(15_000) - .block_round(17_000); -``` +--- +### Node CLI -Mandos supports both `blockTimestamp` and the newer `blockTimestampMs`. Set both if they are used together for backward compatibility. +This page will guide you through the CLI fields available for the node and other tools from the `mx-chain-go` repository. +## **Introduction** -## Summary +Command Line Interface for the Node and the associated Tools -* Time-related newtypes enforce correctness at compile time -* They eliminate a class of subtle bugs related to timestamp unit mismatches -* Both seconds and milliseconds will continue to work, but milliseconds are recommended for new code +The **Command Line Interface** of the **Node** and its associated **Tools** is described at the following locations: -Use these types to ensure your contract remains safe and consistent across all future protocol updates. +- [Node](https://github.com/multiversx/mx-chain-go/blob/master/cmd/node/CLI.md) +- [SeedNode](https://github.com/multiversx/mx-chain-go/blob/master/cmd/seednode/CLI.md) +- [Keygenerator](https://github.com/multiversx/mx-chain-go/blob/master/cmd/keygenerator/CLI.md) +- [TermUI](https://github.com/multiversx/mx-chain-go/blob/master/cmd/termui/CLI.md) +- [Logviewer](https://github.com/multiversx/mx-chain-go/blob/master/cmd/logviewer/CLI.md) ---- -### tokens +## **Examples** -This page describes the structure of the `tokens` index (Elasticsearch), and also depicts a few examples of how to query it. +For example, the following command starts an **Observer Node** in **Shard 0**: +``` +./node --rest-api-interface=localhost:8080 \ + --log-save --log-level=*:DEBUG --log-logger-name \ + --destination-shard-as-observer=0 --start-in-epoch\ + --validator-key-pem-file=observer0.pem +``` -## _id +While the following starts a Node as a **Metachain Observer**: -The `_id` field of this index is represented by token identifier of an ESDT token. +``` +./node --rest-api-interface=localhost:8080 \ + --use-log-view --log-save --log-level=*:DEBUG --log-logger-name \ + --destination-shard-as-observer=metachain --start-in-epoch\ + --validator-key-pem-file=observerMetachain.pem +``` +--- -## Fields +### Node Configuration +## Introduction -| Field | Description | -|---------------|-----------------------------------------------------------------------------------------------------------------------------------| -| name | The name field holds the name of the token. It contains alphanumeric characters only. | -| ticker | The ticker field represents the token's ticker (uppercase alphanumeric characters). | -| token | The token field is composed of the `ticker` field and a random sequence generated when the token is created(e.g. `ABCD-012345`). | -| issuer | The issuer field holds the bech32 encoded address of the token's issuer. | -| currentOwner | The currentOwner field holds the address in a bech32 format of the current owner of the token. | -| type | The type field holds the type of the token. It can be `FungibleESDT`, `NonFungibleESDT`, `SemiFungibleESDT`, or `MetaESDT`. | -| timestamp | The timestamp field represents the timestamp of the block in which the token was created. | -| ownersHistory | The ownersHistory field holds a list of all the owners of a token. | -| paused | The paused field is true if the token is paused. | +The node relies on some configuration files that are meant to allow the node operator to easily change some values +that won't require a code change, a new release, or so on. -## Query examples +## Configuration files +All the configuration files are located by default in the `config` directory that resides near the node's binary. The paths can be changed +by using the node's CLI flags. -### Fetch details of a token +:::important +Not all configuration values can be user-defined. For example, it is perfectly fine if a node operator increases the size of a cacher or sets an Elasticsearch instance, but changing the genesis total supply, for example, will lead to an inconsistent state as compared to the Network. +::: + +Below you can find an example of how the configuration files look like for the `v1.5.8` node. ``` -curl --request GET \ - --url ${ES_URL}/tokens/_search \ - --header 'Content-Type: application/json' \ - --data '{ - "query": { - "match": { - "_id":"ABCD-012345" - } - } -}' +├── api.toml +├── config.toml +├── economics.toml +├── enableEpochs.toml +├── enableRounds.toml +├── external.toml +├── gasSchedules +│ ├── gasScheduleV1.toml +│ ├── gasScheduleV2.toml +│ ├── gasScheduleV3.toml +│ ├── gasScheduleV4.toml +│ ├── gasScheduleV5.toml +│ ├── gasScheduleV6.toml +│ └── gasScheduleV7.toml +├── genesisContracts +│ ├── delegation.wasm +│ └── dns.wasm +├── genesis.json +├── genesisSmartContracts.json +├── nodesSetup.json +├── p2p.toml +├── prefs.toml +├── ratings.toml +├── systemSmartContractsConfig.toml +├── testKeys +│ ├── delegationWalletKey.pem +│ ├── dnsWalletKey.pem +│ ├── esdtWalletKey.pem +│ └── protocolSustainabilityWalletKey.pem +└── upgradeContracts + └── dns + └── v3.0 + ├── deploy.json + └── dns.wasm + ``` ---- +- `api.toml` contains the Rest API endpoints configuration (open or closed endpoints, logging and so on) +- `config.toml` contains the main configuration of the node (storers & cachers type and size, type of hasher, type of marshaller, and so on) +- `economics.toml` contains the economics configuration (such as genesis total supply, inflation per year, developer fees, and so on) +- `enableEpochs.toml` contains a list of new features or bugfixes and their activation epoch +- `enableRounds.toml` contains a list of new features or bugfixes and their activation epoch +- `external.toml` contains external drivers' configuration (for example: Elasticsearch or event notifier) +- `gasSchedules` is the directory that contains the gas consumption configuration to be used for SC execution, depending on activation epochs specified on enableEpochs.toml -> GasSchedule -> GasScheduleByEpochs +- `genesisContracts` is the directory that contains the WASM contracts that were deployed at the genesis +- `genesis.json` contains all the addresses and their balance/active delegation at the genesis +- `genesisSmartContracts.json` specifies the SCs to be deployed at Genesis time, alongside additional parameters +- `nodesSetup.json` holds all the Genesis nodes' public keys, alongside their wallet address +- `p2p.toml` contains peer-to-peer configurable values, such as the number of peers to connect to +- `prefs.toml` contains a set of custom configuration values, that should not be replaced from an upgrade to another +- `ratings.toml` contains the parameters used for the nodes' rating mechanism, for example, the start rating, decrease steps, and so on +- `systemSmartContractsConfig.toml` contains System Smart Contracts configurable values, such as parameters for Staking, ESDT, or Governance + -### Toolchain Setup +### Overriding config.toml values -## Installing Rust and sc-meta +As mentioned in the above descriptions, `prefs.toml` is not overwritten by the installation scripts when performing an upgrade. -:::note -`sc-meta` is universal smart contract management tool. Please follow [this](/developers/meta/sc-meta) for more information. -::: +However, there are some more custom values that nodes operators use (antiflood disabled or with fewer constraints, db lookup extension, and so on) +and they don't want these values to be changed during an upgrade. +For this use-case, release `v1.4.x` introduces the `OverridableConfigTomlValues` setting inside `prefs.toml` that is able to override certain configuration +values from `config.toml`. -For systems running Ubuntu or Windows with WSL, you should first ensure the following system-level dependencies required by Rust and sc-meta are in place: +Here's how to use it: -```bash -sudo apt-get install build-essential pkg-config libssl-dev ``` - -Install Rust as recommended on [rust-lang.org](https://www.rust-lang.org/tools/install): - -```bash -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + OverridableConfigTomlValues = [ + { Path = "StoragePruning.NumEpochsToKeep", Value = "4" }, + { Path = "MiniBlocksStorage.Cache.Name", Value = "MiniBlocksStorage" } + ] ``` -Then, choose **Proceed with installation (default)**. - -:::tip -Generally speaking, you should install Rust `v1.85.0` (stable channel) or later. -::: +Therefore, after each upgrade, the node will override these values to the newly provided values. The path points to an entry +in `config.toml` file before setting a new overridable value. -```bash -rustup update -rustup default stable -``` +--- -Afterwards, open a new terminal (shell) and install `sc-meta`: +### Node Databases -```bash -cargo install multiversx-sc-meta --locked -``` +This page will describe the databases used by the Node. These are simple key-value storage units that will hold different types of data, as described below. -Once `sc-meta` is ready, install the `wasm32` target (for the Rust compiler), `wasm-opt`, and others dependencies as follows: -```bash -# Installs `wasm32`, `wasm-opt`, and others in one go: -sc-meta install all +## **Node databases** -cargo install twiggy -``` +Nodes use simple Key-Value type databases. +Nodes use Serial LevelDB databases to persist processed blocks, transactions, and so on. -### Within Continuous Integration / Continuous Delivery +The data can be removed or not, depending on the pruning flags that can be enabled or not in `config.toml`. +The flags used to specify if a node should delete databases or not are `ValidatorCleanOldEpochsData` and `ObserverCleanOldEpochsData`. +Older versions of the configuration only have one flag `CleanOldEpochsData`. If set to false, then old databases won't be removed. -For automated environments like CI/CD pipelines, start by installing the necessary foundational libraries. On platforms such as Ubuntu (or WSL), this means installing: +By default, validators only keep the last 4 epochs and delete older ones for freeing disk space. -```bash -sudo apt-get install build-essential pkg-config libssl-dev +The default databases directory is `/db` and it's content should match the following structure: +``` +/db +└── + ├── Epoch_X + │ └── Shard_X + │ ├── BlockHeaders + │ │ ├── 000001.log + │ │ ├── CURRENT + │ │ ├── LOCK + │ │ ├── LOG + │ │ └── MANIFEST-000000 + │ ├── BootstrapData + │ │ ├── 000001.log + | ............. + └── Static + └── Shard_X + ├── AccountsTrie + │ └── MainDB + │ ├── 000001.log + ............. ``` -For CI / CD, install Rust as follows: +Nodes will fetch the state from an existing database if one is detected during the startup process. If it does not match +the current network height, it will sync the rest of the data from the network, until fully synced. -```bash -wget -O rustup.sh https://sh.rustup.rs && \ - chmod +x rustup.sh && \ - ./rustup.sh --verbose --default-toolchain stable -y -cargo install multiversx-sc-meta --locked +## **Starting a node with existent databases** -sc-meta install wasm32 -``` +There are use-cases when a node can receive the entire database from other node that is fully synced in order to speed up the process. +In order to perform this, one has to copy the entire database directory to the new node. This is as simple as copying the `db/` +directory from one node to the other one. +The configuration files must be the same as the old node, except the BLS key which is independent of databases. -## Check your Rust installation +Two nodes in the same shard generate the same databases. These databases are interchangeable between them. However, starting +a node as observer and setting the `--destination-shard-as-observer` so it will join a pre-set shard, requires that it's database +is from the same shard. So starting an observer in shard 1 with a database of a shard 0 node will result in ignoring the database +and network-only data fetch. -You can check your Rust installation by invoking `rustup show`: +If the configuration and the database's shard are the same, then the node should have the full state from the database and +start to sync with the network only remaining items. If, for instance, a node starts with a database of 255 epochs, and the current epoch is +256, then it will only sync from network the data from the missing epoch. -```sh -$ rustup show +--- -Default host: x86_64-unknown-linux-gnu -rustup home: /home/ubuntu/.rustup +### Node operation modes -installed toolchains --------------------- -stable-x86_64-unknown-linux-gnu (default) -[...] +Without configuration changes, nodes will start by using the default settings. However, there are several ways to configure the node, depending on the desired operation mode. +Instead of manually (or programmatically via `sed`s for example) editing the `toml` files, you can use the `--operation-mode` CLI flag described below to specify a custom +operation mode that will result in config changes. -active toolchain ----------------- -name: stable-x86_64-unknown-linux-gnu -installed targets: - wasm32-unknown-unknown - wasm32v1-none - x86_64-unknown-linux-gnu ---- +## Introduction -### Tooling Overview +Starting with `v1.4.x` release, a new CLI flag has been introduced to the node. It is `--operation-mode` and its purpose +is to override some configuration values that will allow the node to act differently, depending on the use-case. -## Introduction -We have developed a universal smart contract management tool, called `multiversx-sc-meta` (`sc-meta` in short). +## List of available operation modes -It is called that, because it provides a layer of meta-programming over the regular smart contract development. It can read and interact with some of the code written by developers. +Below you can find a list of operation modes that are supported: -You can find it on [crates.io](https://crates.io/crates/multiversx-sc-meta) [![crates.io](https://img.shields.io/crates/v/multiversx-sc-meta.svg?style=flat)](https://crates.io/crates/multiversx-sc-meta) -To install it, simply call +### Full archive +Usage: ``` -cargo install multiversx-sc-meta --locked +./node --operation-mode full-archive ``` -After that, try calling `sc-meta help` or `sc-meta -h` to see the CLI docs. - -:::note endure dependencies -Ubuntu users have to ensure the existence of the `build_essential` package installed in their system. -::: - - -## Standalone tool vs. contract tool +The `full-archive` operation mode will change the node's configuration in order to make it able to sync from genesis and also +be able to serve historical requests. +Syncing a node from genesis might take some time since there aren't that many full archive peers to sync from. -The unusual thing about this tool is that it comes in two flavors. One of them is the standalone tool, installed as above. The other is a tool that gets provided specifically for every contract, and which helps with building. -The contract tool lies in the `meta` folder under each contract. It just contains these 3 lines of code: +### Db Lookup Extension -```rust -fn main() { - multiversx_sc_meta::cli_main::(); -} +Usage: +``` +./node --operation-mode db-lookup-extension ``` -... but they are important, because they link the contract tool to the contract code, via the [ABI](/developers/data/abi). - -The contract tool is required in order to build contracts, because it is the only tool that we have that calls the ABI generator, manages the wasm crate and the multi-contract config, and has the data on how to build the contract. +The `db-lookup-extension` operation mode will change the node's configuration in order to support extended databases that are +able to store more data that is to be used in further Rest API requests, such as logs, links between blocks and epoch, and so on. -Therefore, all the functionality that needs the ABI goes into the contract tool, whereas the rest in the standalone tool. +For example, the proxy's `hyperblock` endpoint relies on the fact that its observers have this setting enabled. Other examples +are `/network/esdt/supply/:tokenID` or `/transaction/:txhash?withResults=true`. -To see the contract meta CLI docs, `cd` into the `/meta` crate and call `cargo run help` or `cargo run -- -h`. +### Historical balances -## Contract functionality +Usage: +``` +./node --operation-mode historical-balances +``` -Currently the contract functionality is: - - `abi` Generates the contract ABI and nothing else. - - `build` Builds contract(s) for deploy on the blockchain. - - `build-dbg` Builds contract(s) with symbols and WAT. - - `twiggy` Builds contract(s) and generate twiggy reports. - - `clean` Clean the Rust project and the output folder. - - `update` Update the Cargo.lock files in all wasm crates. - - `snippets` Generates a snippets project, based on the contract ABI. +The `historical-balances` operation mode will change the node's configuration in order to support historical balances queries. +By setting this mode, the node won't perform the usual trie pruning, resulting in a more disk usage, but also in +the ability to query the balance or the nonce of an address at blocks that were proposed long time ago. -To learn more about the smart contract ABI and ABI-based individual contract tools, see [the CLI reference](/developers/meta/sc-meta-cli). +### Snapshotless observers -## Standalone functionality +Usage: +``` +./node --operation-mode snapshotless-observer +``` -The standalone functionality is: - - `info` General info about the contract an libraries residing in the targeted directory. - - `all` Calls the meta crates for all contracts under given path with the given arguments. - - `new` Creates a new smart contract from a template. - - `templates` Lists the available templates. - - `upgrade` Upgrades a contract to the latest version. Multiple contract crates are allowed. - - `local-deps` Generates a report on the local dependencies of contract crates. Will explore indirect dependencies too. +The `snapshotless-observer` operation mode will change the node's configuration in order to make it efficient for real-time requests +by disabling the trie snapshotting mechanism and making sure that older data is removed. -All the standalone tools take an optional `--path` argument. if not provided, it will be the current directory. +A use-case for such an observer would be serving live balances requests, or broadcasting transactions, eliminating the costly operations +of the trie snapshotting. --- -### Tools for signing - -In order to sign a transaction without actually dispatching it, several tools can be used. One of the most popular ones is [mxpy](/sdk-and-tools/sdk-py). +### Node redundancy +MultiversX Validator Nodes can be configured to have one or more hot-standby nodes. +This means additional nodes will run on different servers, in sync with the Main Validator node. +Their role is to stand in for the Main Validator node in case it fails, to ensure high availability. -## **Sign using [mxpy](/sdk-and-tools/sdk-py/) (Command Line Interface)** -Using a **pem** file: +This is a redundancy mechanism which allows the Main Validator operator to start additional 'n' hot-standby nodes, +each of them running the same 'validatorKey.pem' file. The difference between +configurations consists on an option inside the `prefs.toml` file. -``` -$ mxpy tx new --nonce=41 --data="Hello, World" --gas-limit=70000 \ - --receiver=erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz \ - --pem=aliceKey.pem --pem-index=0 --outfile=myTransaction.json +Hot standby nodes are configured using the 'RedundancyLevel' option in the 'prefs.toml' configuration file: -``` +- a 0 value will represent that the node is the Main Validator. + The value 0 will be the default, therefore if the option is missing it will still make that node the Main Validator by default. With consideration to backwards compatibility, the already-running Validators are not affected by the addition of this option. Moreover, we never overwrite the `prefs.toml` files during the node's upgrade. -Using a JSON wallet key (and its password): +The values of `RedundancyLevel` are interpreted as follows: -``` -mxpy tx new --nonce=41 --data="Hello, World" --gas-limit=70000 \ - --receiver=erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz \ - --keyfile=walletKeyOfAlice.json --passfile=passwordOfAlice.txt \ - --outfile=myTransaction.json +- a positive value will represent the "order of the hot-standby node" in the automatic fail-over sequence. + Example: suppose we have 3 nodes running with the same BLS key. One has the redundancy level set to 0, + another has 1 and another with 3. The node with level 0 will propose and sign blocks. The other 2 will + sync data with the same shard as the Main Validator (and shuffle in and out of the same shards) but will + not sign anything. If the Main Validator fails, the hot-standby node + with level 1 will start producing/signing blocks after `level*5` missed rounds. So, after 5 + missed rounds by the Main Validator, the hot-standby node with level 1 will take the turn. + If hot-standby node 1 is down as well, hot-standby node 2 will step in + after `3*5 = 15 rounds` after the Main Validator failed and 10 rounds after the failed hot-standby node 1 + should have been produced a block. +- a large value for this level option (say 1 million), or a negative value (say -1) will mean that the + hot-standby nodes won't get the chance to produce/sign blocks but will sync with the network and + shuffle between shards just as the Main Validator will. -``` +:::tip +The hot-standby nodes will advertise on the network a different public key (autogenerated at start-up) and thus, concealing the real public key that will be used when signing the header blocks. +::: -In either case, the output file looks like this: +:::tip +If the Main Validator (RedundancyLevel 0) gets back online, the hot-standby node(s) revert to standby mode. +::: -``` -{ - "tx": { - "nonce": 41, - "value": "0", - "receiver": "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz", - "sender": "erd1aedmqfsflx4rhwvs7v9z52e7eylkevz4w342jzuaa9ezy5unsc5qqy963v", - "gasPrice": 1000000000, - "gasLimit": 70000, - "data": "SGVsbG8sIFdvcmxk", - "chainID": "1596807148", - "version": 123, - "signature": "f432442ebfee6edf4518c10d006ab571d8ecbd6f2601995554c75d3402b424364908235d45449ba5dd28575e4a8129271020e4718cf8a4c6f44e22c0885ac40a" - }, - "hash": "", - "data": "Hello, World" -} -``` +:::caution +Do not use the same redundancy level on more than one node. Otherwise, the nodes with the same `RedundancyLevel` value will start signing blocks in parallel in the same time. Although the protocol is not negatively affected by double signing, in the near future the BLS key that will perform double signing will have its stake slashed. +::: -## **Other signing tools** +The random BLS key on hot-standby nodes has the following purposes: -Each SDK includes functions for signing and broadcasting transactions. Please refer to [SDKs & Tools](/sdk-and-tools/overview) for the full list. +- the hot-standby node(s) will not cause BLS signature re-verification when idle. +- it slightly prevents DDoS attacks as an attacker can not find all IPs behind a targeted BLS public key: + when an attacker takes down the Main Validator, the hot-standby nodes will advertise the public key when they + will need to sign blocks, but not sooner. --- -### Transaction Overview +### Protecting your keys -A big part of the life of a blockchain developer is to create and launch blockchain transactions. +This page contains information about how to protect your validator and wallet keys. -Whether it's an off-chain tool, smart contract code, or a testing scenario, it is important that we have a powerful syntax to express any conceivable transaction. -During the process of creating the development framework, we realized that the following are equivalent to a large extent and could be expressed using the same syntax: -- transactions launched from smart contracts, -- blackbox integration tests, -- off-chain calls. +## How sensitive are your keys -We called this "unified transaction syntax" or "unified syntax", and the first version of it was released in [multiversx-sc version 0.49.0](https://github.com/multiversx/mx-sdk-rs/releases/tag/v0.49.0). +Validator Keys are very sensitive: -In this documentation, you will get a complete explanation of all the features of this syntax, organized around the various components of a blockchain transaction. +- if you lose them and your node crashes irreparably (i.e. you delete the virtual machine, your VPS provider deletes/loses it), you lose access to that node, you won't be able to bring it back up online and will thus stop earning money with it +- if someone steals them and maliciously uses them in the MultiversX network, they can engage in bad behavior such as double-signing, produce bad blocks, inject fake transactions, mint new coins, etc. - all of those actions are slashable, meaning you can lose your EGLD stake - all 2500! +Wallet Keys are extremely sensitive because: +- if you lose the keys, you can't recover your stake or claim your rewards -> you lose all the money +- if someone steals your keys, they can send an unstake transaction from it and claim the EGLD -> the bad guys steal your money -## Motivation and design -Transactions can be fairly complex, and in order to get them to use the same syntax in very different environments, we had a few challenges: +## How to protect your keys -1. Transactions have many fields, which can be configured in many ways. It is important to be able to configure most of them independently, otherwise the framework becomes too large, or unreliable. -2. Since this syntax is also used in contracts, it was essential to choose a design that adds almost no runtime and code size overhead. So the syntax must make heavy use of generics, to resolve at compile-time all type checks, conversions, and restrictions. -3. Also, because syntax is used in contracts, it had to rely on managed types. -4. We wanted as much type safety as possible, so we had to find a way to rely on the ABI to produce type checks both for contract inputs and outputs. +How to protect them: +- make multiple safe backups of the private keys & files + - paper + - hardware + - encrypted physical storage + - distributed cloud storage, etc + - [some hints](https://coinsutra.com/bitcoin-private-key/) +:::tip +Wallet Keys are not required on host running the Node. Store them on a different location. +::: -## The `Tx` object -We decided to model all transactions using a single object, but with exactly 7 generic arguments, one for each of the transaction fields. +## How to secure your node -```mermaid -graph LR - subgraph Tx - Env - From - To - Payment - Gas - Data - rh[Result Handler] - end -``` +Secure your MultiversX node -
+- no ports should open in the firewall except for the ones used by the node's normal operation +(the port range can be checked [here](/validators/system-requirements).) +- don't run the node as `root` +- use encryption, all other measures +- [some hints ](https://www.liquidweb.com/kb/security-for-your-linux-server/) -Each one of these 7 fields has a trait that governs what types are allowed to occupy the respective position. +--- -All of these positions (except the environment `Env`) can be empty, uninitialized. This is signaled at compile time by the unit type, `()`. In fact, a transaction always starts out with all fields empty, except for the environment. +### Rating -For instance, if we are in a contract and write `self.tx()`, the universal start of a transaction, the resulting type will be `Tx, (), (), (), (), (), ()>`, where `TxScEnv` is simply the smart contract call environment. Of course, the transaction at this stage is unusable, it is up to the developer to add the required fields and send it. +This page exposes the rating system used for MultiversX validators. +## **Introduction** +Each individual validator has a **rating score**, which expresses its overall reliability, performance and responsiveness. It is an important value, and node operators should be always mindful of the rating of their validators. -## The `Tx` fields +Rating influences the probability of a validator to be selected for consensus in each round. A performant validator will be preferred in consensus, as opposed to a validator which sometimes fails to contribute or which is not always online. -We have dedicated a page to each of these 7 fields: +:::tip +Observer nodes do not have a rating score. Only validator nodes do. +::: -| Field | Description | -| ------------------------------------- | --------------------------------------------------------------------------------------------------------- | -| [Environment](tx-env) | Some representation of the environment where the transaction runs. | -| [From](tx-from) | The transaction sender. Implicit for SC calls (the contract is the sender), but mandatory anywhere else. | -| [To](tx-to) | The receiver. Needs to be specified for any transaction expect for deploys. | -| [Payment](tx-payment) | Optional, can be EGLD, single or multi-ESDT. We also have some payment types that get decided at runtime. | -| [Gas](tx-gas) | Some transactions need explicit gas, others don't. | -| [Data](tx-data) | [Proxies](tx-proxies) (ideally) or raw | The main part of the payload. Can be inhabited by a function call, deploy data, upgrade data, or nothing, in the case of direct transfers. | -| [Result Handlers](tx-result-handlers) | Anything that deals with results, from callbacks to decoding logic. | +When validators join the network immediately after staking, they start with an initial score of `50` points. -We could also group them in three broad categories: -- The [environment](tx-env) is its own category, pertaining to both **inputs and outputs**. -- 5 **inputs**: [From](tx-from), [To](tx-to), [Payment](tx-payment), [Gas](tx-gas), [Data](tx-data). -- 1 field dealing with the **output**: [the result handlers](tx-result-handlers). +Validators gain or lose rating points in a round depending on their role in that round (consensus proposer vs. consensus validator) and on their behavior within that role. Rating penalties are currently set to be `4` times as large as the corresponding gains. This means that a validator has to perform an action correctly 4 times in order to compensate for performing it once incorrectly. Moreover, consecutive losses are _compounding_, which means that the rating penalty increases with each transgression. See [Rating shard validators](/validators/rating#rating-shard-validators) and [Rating metashard validators](/validators/rating#rating-metashard-validators) for details on the calculations. +:::tip +Rating gains and losses on the metashard are different from the gains and losses on the normal shards. +::: +The past and current rating of an individual validator can be found in the MultiversX Network Explorer at https://explorer.multiversx.com/nodes. Use the "Search" box to find a validator and click on its entry in the list. The "Node Details" page opens, which contains status information about the validator. -## Transaction builder +The "Node Details" page displays a plot of the validator rating during the past epochs: -Now that we've seen what the contents of a transaction are, let's see how we can specify them in code. +![img](https://gblobscdn.gitbook.com/assets%2F-LhHlNldCYgbyqXEGXUS%2F-MA1wJCHfE7ffob9gOjE%2F-MA1we9u12mvMRF1PU9y%2Fplot-rating.png?alt=media&token=6a1f0071-66d0-4aec-8192-2a8f716e67bb) -Of course, the developer shouldn't access the fields of the transaction directly. There is sort of a builder pattern when constructing a transaction. +The X-axis represent the epochs, and the Y-axis represents the rating. -In its most basic form, a transaction might be constructed as follows: -```rust -self.tx() - .from(from) - .to(to) - .payment(payment) - .gas(gas) - .raw_call("function") - .with_result(result_handler) -``` +## **The jail** -:::caution Important -While this may look like a traditional OOP builder pattern, there is one important aspect to point out: +For the overall health of the network, if the rating of a validator drops below `10` points, it will be **jailed**. Being jailed means that the validator will be taken out of the shards, it will not participate in consensus, and thus it will not earn any rewards. -*Each of these setters outputs a type that is different from its input.* +There is an exception to jailing, though. If the network finds itself in a situation where jailing a validator would reduce the size of a shard below an allowed limit, the validator will not be jailed. -We are not merely setting new data into an existing field, we are also specifying the type of each field, at compile-time. At each step we are not only specifying the data, but also the types. +:::important +Jailing only occurs at the end of an epoch. This means that a validator with low rating still has until the end of the epoch to recover. If the validator fails to recover, and its rating remains below `10` at the end of the epoch, then it will start the new epoch in jail. ::: -Even though these look like simple setters, we are first of all constructing a type using this syntax. - -Also note that the way these methods are set up, it is impossible to call most of them twice. They only work when the respective field is not yet set (of unit type `()`), so writing something like `self.tx().from(a).from(b)` causes a compiler error. - -Here we have some of the most common methods used to construct a transaction: - -| Field | Filed name | Initialize with | -| ------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------- | -| [Environment](tx-env) | `env` | `.tx()` | -| [From](tx-from) | `from` | `.from(...)` | -| [To](tx-to) | `to` | `.to(...)` | -| [Payment](tx-payment) | `payment` | `.payment(...)` | -| [Gas](tx-gas) | `gas` | `.gas(...)` | -| [Data](tx-data) | `data` | `.typed(...)` (ideally)
`.raw_call(...)`
`.raw_deploy()`
`.raw_upgrade()` | -| [Result Handlers](tx-result-handlers) | `result_handler` | `.callback(...)`
`.returns(...)`
`.with_result(...)` | - -The list is by no means exhaustive, it is just an initial overview. Please find the full documentation on each of the linked pages. +To reinstate a jailed validator, its operator must submit an **unjail** transaction to the Staking SmartContract. This causes the validator to be taken out of the "jail" and added to the network as if it were a new validator. +A reinstated validator will be passive during the epoch of its unjailing. In the immediately following epoch, the validator will be assigned to a shard, where it must wait the entire epoch and spend it to synchronize with its new shard. -## Execution +:::note rating reset +Rating is **not** reset to 50 due to shard shuffling. The rating of a validator is retained when changing shards due to shuffling. +::: -Ultimately, the purpose of a transaction is to be executed. Simply constructing a transaction has no effect in itself. So we must finalize each transaction construct with a call that sends it to the blockchain. +:::tip +The only way to increase the rating of a validator is to keep it up-to-date, keep it well-connected and make sure it is running on hardware that conforms to the [System requirements](/validators/system-requirements). +::: -The allowed execution methods depend on the environment. More specifically: -- In a contract, the options are `.transfer()`, `.transfer_execute()`, `.async_call_and_exit()`, `.sync_call()`, etc. -- In tests just `.run()` is universal. -- In interactors, we call `.run().await`. It's the same method name, but this time it's asynchronous Rust, so it can only be called in an `async` function, and `.await` is necessary. +:::note multiple validators on the same machine +Running **multiple validators on a single machine** will impact your rating and consequently _your rewards,_ if the machine doesn't have the as many times the minimum requirements as there are validators running on it. +::: -More on this in the [launch page](tx-run). +## **Consensus probabilities** -## Map of the fields types +Rating affects the probability of a validator to be selected in the consensus group of a round. This is done by applying **rating modifiers** on the probability of selection for each validator. -This is a graph of what the common types are that fit in each of the transaction fields. +Without rating, all validators of a shard would have the same probability of being chosen for consensus. But rating modifiers will alter the probability of individual validators based on their rating score, in order to give performant validators an edge over the average validators, and also to diminish the probability of selecting weak validators. +The following table shows how the rating of a validator influences its probability of being chosen for consensus: -```mermaid -graph LR - subgraph Tx - Env - From - To - Payment - Gas - Data - rh[Result Handler] - end - Env --> TxScEnv - Env --> ScenarioEnvExec - Env --> ScenarioEnvQuery - Env --> InteractorEnvExec - Env --> InteractorEnvQuery - From --> from-unit["()"] - From --> from-man-address[ManagedAddress] - From --> from-address[Address] - From --> from-bech32[Bech32Address] - From --> from-test-addr[TestAddress] - To --> to-unit["()"] - To --> to-man-address[ManagedAddress] - To --> to-address[Address] - To --> to-bech32[Bech32Address] - To --> to-test-addr[TestAddress] - To --> to-test-sc[TestSCAddress] - To --> to-caller[ToCaller] - To --> to-self[ToSelf] - Payment --> payment-unit["()"] - Payment --> egld["Egld(EgldValue)"] - egld --> egld-biguint["Egld(BigUint)"] - egld --> egld-u64["Egld(u64)"] - Payment --> EsdtTokenPayment - Payment --> EsdtTokenPaymentRefs - Payment --> MultiEsdtPayment - Payment --> EgldOrEsdtTokenPaymentRefs - Payment --> EgldOrMultiEsdtPayment - Gas --> gas-unit["()"] - Gas --> gas-explicit["ExplicitGas(u64)"] - Gas --> GasLeft - Data --> data-unit["()"] - Data --> deploy["DeployCall<()>"] - deploy --> deploy-from-source["DeployCall<FromSource<ManagedAddress>>"] - deploy --> deploy-code["DeployCall<Code<ManagedBuffer>>"] - Data --> upgrade["UpgradeCall<()>"] - upgrade --> upgrade-from-source["UpgradeCall<CodeSource<ManagedAddress>>"] - upgrade --> upgrade-code["UpgradeCall<Code<ManagedBuffer>>"] - Data --> fc[FunctionCall] - rh --> rh-unit("()") - rh --> rh-ot("OriginalTypeMarker") - rh --> CallbackClosure --> CallbackClosureWithGas - rh --> Decoder -``` +| Rating interval | Modifier | +|-----------------|----------| +| [0-10] | -100% | +| (10-20] | -20% | +| (20-30] | -15% | +| (30-40] | -10% | +| (40-50] | -5% | +| (50-60] | 0% | +| (60-70] | +5% | +| (70-80] | +10% | +| (80-90] | +15% | +| (90-100] | +20% | +:::important +The algorithm that selects validators for consensus treats these modified selection probabilities as being relative to each other. +::: -## Map of the setters -Constructing a transaction is similar to exploring a map, or running a finite state machine. You start with an initial position, and then get to choose in which direction to go. +## **Calibration** -Choosing a path at one point closes off many other options. The compiler is always guiding us and preventing us from ending up with an invalid transaction. +Assuming a **24-hour-long epoch**, the rating mechanism has been calibrated with the following intentions: -Here is a map of all the paths you can take when configuring a transaction. The fields are mostly independent, so the map is split into 7 sections. See more details on each of their respective pages. +- A new validator requires **approx. 72 hours** to reach maximum rating, assuming it remains in the same shard and won't be shuffled out (therefore it will be productive all the time, without any waiting time). +- The amount of rating gains earned as a block validator should be in balance with the amount of rating gains earned as a block proposer. This balance must take into account the fact that being selected as proposer is considerably less likely than being selected in consensus as block validator. -```mermaid -graph LR - subgraph Environment - sc-code["SC Code"] -->|"self.tx()"| sc-env[TxScEnv] - test-code["Test Code"] -->|"world.tx()"| ScenarioEnvExec - test-code["Test Code"] -->|"world.query()"| ScenarioEnvQuery - intr-code["Interactor Code"] -->|"interactor.tx()"| InteractorEnvExec - intr-code["Interactor Code"] -->|"interactor.query()"| InteractorEnvQuery - end -``` +## **Rating shard validators** -```mermaid -graph LR - subgraph From - from-unit["()"] - from-unit -->|from| from-man-address[ManagedAddress] - from-unit -->|from| from-address[Address] - from-unit -->|from| from-bech32[Bech32Address] - end -``` -```mermaid -graph LR - subgraph To - to-unit["()"] - to-unit -->|to| to-man-address[ManagedAddress] - to-unit -->|to| to-address[Address] - to-unit -->|to| to-bech32[Bech32Address] - to-unit -->|to| to-esdt-system-sc[ESDTSystemSCAddress] - to-unit -->|to| to-caller[ToCaller] - to-unit -->|to| to-self[ToSelf] - end -``` +### **Rating the shard block proposer** -```mermaid -graph LR - subgraph Payment - payment-unit["()"] - payment-unit -->|egld| egld-biguint["Egld(BigUint)"] - payment-unit -->|egld| egld-u64["Egld(u64)"] - payment-unit -->|esdt| EsdtTokenPayment - EsdtTokenPayment -->|esdt| MultiEsdtPayment - MultiEsdtPayment -->|esdt| MultiEsdtPayment - payment-unit -->|payment| MultiEsdtPayment - payment-unit -->|multi_esdt| EsdtTokenPayment - payment-unit -->|payment| EgldOrEsdtTokenPaymentRefs - payment-unit -->|egld_or_single_esdt| EgldOrEsdtTokenPaymentRefs - payment-unit -->|payment| EgldOrMultiEsdtPayment - payment-unit -->|egld_or_multi_esdt| EgldOrMultiEsdtPayment - end -``` +The node chosen to propose the block for a specific round will: -```mermaid -graph LR - subgraph Gas - gas-unit["()"] - gas-unit -->|gas| gas-explicit["ExplicitGas(u64)"] - gas-unit -->|gas| GasLeft - end -``` +- Gain `0.23148` points for a successful proposal: (1) the block is built correctly, (2) it is accepted by the consensus validators and (3) the proposer applies the final signature and propagates the block throughout the network; +- Lose `0.92592` points for an unsuccessful proposal. -```mermaid -graph LR - subgraph Data - data-unit["()"] - data-unit ----->|raw_deploy| deploy["DeployCall<()>"] - deploy -->|from_source| deploy-from-source["DeployCall<FromSource<_>>"] - deploy -->|code| deploy-code["DeployCall<Code<_>>"] - deploy -->|code_metadata| deploy - data-unit ----->|raw_upgrade| upgrade["UpgradeCall<()>"] - upgrade -->|from_source| upgrade-from-source["UpgradeCall<CodeSource<_>>"] - upgrade -->|code| upgrade-code["UpgradeCall<Code<_>>"] - upgrade -->|code_metadata| upgrade - data-unit ---->|raw_call| fc[FunctionCall] - data-unit -->|typed| Proxy - Proxy -->|init| deploy - Proxy -->|upgrade| upgrade - Proxy -->|endpoint| fc[Function Call] - end -``` +Observe that the loss is 4 times larger than the gain, which means that a proposer must succeed 4 times to gain the points lost for a single missed block. -```mermaid -graph LR - subgraph Result Handlers - rh-unit("()") - rh-unit -->|original_type| rh-ot("OriginalTypeMarker") - rh-ot -->|callback| CallbackClosure -->|gas_for_callback| CallbackClosureWithGas - dh[Decode Handler] - rh-unit -->|"returns
with_result"| dh - rh-ot -->|"returns
with_result"| dh - dh -->|"returns
with_result"| dh - end -``` +Rating for proposers is even stricter: there is a compounding penalty rule, which makes the rating of a node drop even faster when it proposes unsuccessfully. ---- +The amount of `0.92592` points is deducted from the rating of the proposer on the first unsuccessful proposal, but the second unsuccessful proposal will be penalized by `0.92592 × 1.1`. The third, by `0.92592 × 1.1 × 1.1`. The general formula is: -### transactions +`0.92592 × 1.1^{cfp-1}0.92592×1.1^cfp^−1` -This page describes the structure of the `transactions` index (Elasticsearch), and also depicts a few examples of how to query it. +where `cfp` is the number of consecutive failed proposals. +This compounding penalty has the effect of quickly jailing repeatedly unsuccessful proposers. -## _id -:::warning Important +### **Rating the shard block validator** -**The `transactions` index will be deprecated and removed in the near future.** -We recommend using the [operations](/sdk-and-tools/indices/es-index-operations) index, which contains all the transaction data. -The only change required in your queries is to include the `type` field with the value `normal` to fetch all transactions. +The nodes that take part in the consensus of a round (other than the proposer) will: -Please make the necessary updates to ensure a smooth transition. -If you need further assistance, feel free to reach out. -::: +- Gain `0.00367` points for a successful validation: (1) the proposer has built and proposed a block, (2) the validator appears as a "signer" on that block; being a "signer" of a block means that the validator has approved of the block and was fast enough to be among the first ⅔ + 1 validators to have its signature received by the block proposer; +- Lose `0.01469` points for an unsuccessful proposal. -The `_id` field for this index is composed of hex-encoded transaction hash. -(example: `cad4692a092226d68fde24840586bdf36b30e02dc4bf2a73516730867545d53c`) +Observe that the first bulled mentions "the proposer has built and proposed a block". This sentence implies that _all validators will lose rating_ if the proposer fails to propose in the respective round. +Moreover, the validator must have been a "signer" in at least 1% of the previous blocks, otherwise it will not gain rating. In other words: if the validator has been performing poorly in the past, it will have to perform well for a while until it can start receiving any gains. -## Fields +## **Rating metashard validators** -| Field | Description | -|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| miniBlockHash | The miniBlockHash field represents the hash of the miniblock in which the transaction was included. | -| nonce | The nonce field represents the transaction sequence number of the sender address. | -| round | The round field represents the round of the block when the transaction was executed. | -| value | The value field represents the amount of EGLD to be sent from the sender to the receiver. | -| receiver | The receiver field represents the destination address of the transaction. | -| sender | The sender field represents the address of the transaction sender. | -| receiverShard | The receiverShard field represents the shard ID of the receiver address. | -| senderShard | The senderShard field represents the shard ID of the sender address. | -| gasPrice | The gasPrice field represents the amount to be paid for each gas unit. | -| gasLimit | The gasLimit field represents the maximum gas units the sender is willing to pay for. | -| gasUsed | The gasUsed field represents the amount of gas used by the transaction. | -| fee | The fee field represents the amount of EGLD the sender paid for the transaction. | -| initialPaidFee | The initialPaidFee field represents the initial amount of EGLD the sender paid for the transaction, before the refund. | -| data | The data field holds additional information for a transaction. It can contain a simple message, a function call, an ESDT transfer payload, and so on. | -| signature | The signature of the transaction, hex-encoded. | -| timestamp | The timestamp field represents the timestamp of the block in which the transaction was executed. | -| status | The status field represents the status of the transaction. | -| senderUserName | The senderUserName field represents the username of the sender address. | -| receiverUserName | The receiverUserName field represents the username of the receiver address. | -| hasScResults | The hasScResults field is true if the transaction has smart contract results. | -| isScCall | The isScCall field is true if the transaction is a smart contract call. | -| hasOperations | The hasOperations field is true if the transaction has smart contract results. | -| tokens | The tokens field contains a list of ESDT tokens that are transferred based on the data field. The indices from the `tokens` list are linked with the indices from `esdtValues` list. | -| esdtValues | The esdtValues field contains a list of ESDT values that are transferred based on the data field. | -| receivers | The receivers field contains a list of receiver addresses in case of ESDTNFTTransfer or MultiESDTTransfer. | -| receiversShardIDs | The receiversShardIDs field contains a list of receiver addresses shard IDs. | -| type | The type field represents the type of the transaction based on the data field. | -| operation | The operation field represents the operation of the transaction based on the data field. | -| function | The function field holds the name of the function that is called in case of a smart contract call. | -| isRelayed | The isRelayed field is true if the transaction is a relayed transaction. | -| version | The version field represents the version of the transaction. | -| hasLogs | The hasLogs field is true if the transaction has logs. | +The rating mechanism for the metashard is identical with the rating mechanism of the normal shards, but the gain / loss values themselves are configured differently. -## Query examples +### **Rating the metashard block proposer** +The metachain proposer will: -### Fetch the latest transactions of an address +- Gain `0.23148` points for a successful proposal; +- Lose `0.92592` points for an unsuccessful proposal. -``` -curl --request GET \ - --url ${ES_URL}/transactions/_search \ - --header 'Content-Type: application/json' \ - --data '{ - "query": { - "bool": { - "should": [ - { - "match": { - "sender": "erd..." - } - }, - { - "match": { - "receiver": "erd..." - } - }, - { - "match": { - "receivers": "erd..." - } - } - ] - } - }, - "sort": [ - { - "timestamp": { - "order": "desc" - } - } - ] -}' -``` +The compounding penalty rule also applies to block proposers of the metachain. See [Rating the shard block proposer](#rating-the-shard-block-proposer) for details. ---- -### Upgrading smart contracts +### **Rating the metashard block validator** -## Sirius Mainnet Release - Version 1.6.0 +A validator taking part in consensus on the metachain will: -:::important -The new Sirius Mainnet version 1.6.0 brings a significant update to how smart contracts can be upgraded. This release introduces a dedicated `upgrade` function, replacing the previous usage of the `init` function during contract upgrades. This change enhances the upgrade process and provides a clearer separation of concerns between contract initialization and subsequent upgrades. -**Note: Contracts deployed before version 1.6.0 will continue to function as before. The change in upgrade behavior applies only when upgrading the code of the contract. Existing contracts deployed only with an init function will still operate correctly without modifications.** -::: +- Gain `0.00057` points for a successful validation; +- Lose `0.00231` points for an unsuccessful validation. -Please take note of the following important information to ensure a smooth transition. +The rules from [Rating the shard block validator](#rating-the-shard-block-validator) apply for the metashard validators as well. -For contracts developed on or after version 1.6.0, developers should ensure that the `upgrade` function is implemented to handle the necessary upgrade logic. The `init` function will no longer be called during contract upgrades. +--- -Let's look at an example of a simple Adder SC. +### Scripts & User config -```rust -#[init] -fn init(&self, initial_value: u64) { - // Save the initial value in storage only if it is empty. - self.sum().set_if_empty(initial_value); -} +MultiversX provides scripts designed to streamline the process of installing a MultiversX node. This validator script is a general script for accessing the Mainnet, Devnet and Testnet networks. -#[upgrade] -fn upgrade(&self, new_value: u64) { - self.sum().set(new_value); -} +To get started, you will begin by getting a copy of the latest version of the scripts from Github and configure it to match your local setup. + +:::caution +Nodes scripts should not be run as a root user. Such usage is not supported and may result in unexpected behavior. +::: + + +## **Download the MultiversX Scripts** + +```bash +cd ~ +git clone https://github.com/multiversx/mx-chain-scripts ``` -Let's assume we deploy the contract with the argument **1u64**, and then we upgrade it using the argument **2u64**. If before the new release, after upgrading the SC, we would have the value **1u64** in storage (as the `init` function would have been called, which saves the value in the storage only when it is empty), with the new release, the new value in the storage would be **2u64**. +## **Configure the scripts correctly** -## Deep diving into the Smart Contract Upgrade Process +The scripts require a few configurations to be set in order to work correctly. -Upgrading a smart contract is a relatively easy process, but its implications are not exactly obvious. To upgrade a smart contract, simply run the following command: +First and foremost, you need your exact username on your local machine. You can find out your current username by running the `whoami` command, which will print it out: -``` -mxpy --verbose contract upgrade SC_ADDRESS \ - --pem=PEM_PATH --bytecode=WASM_PATH \ - --gas-limit=100000000 \ - --send --proxy=https://devnet-gateway.multiversx.com --chain=D +```bash +whoami ``` -Replace SC_ADDRESS, PEM_PATH and WASM_PATH accordingly. Also, if you want to use testnet/mainnet, also change the proxy and chain ID. +Next, in the `variables.cfg` file, edit and add your username in the following variables: -This will replace the given SC's code with the one from the provided file, but that is not all. Additionally, it will run the new code's `upgrade` function. So, if your `upgrade` function has any arguments, the command has to be run by also giving said arguments: +- `ENVIRONMENT`: The MultiversX network to be used: mainnet, testnet or devnet. +- `CUSTOM_HOME`: This refers to the folder on the computer in which you will install your node. +- `CUSTOM_USER`: which is the username on the computer under which you will run the installation, upgrade, and other processes -``` -mxpy --verbose contract upgrade SC_ADDRESS \ - --pem=PEM_PATH --bytecode=WASM_PATH \ - --arguments arg1 arg2 arg3 - --gas-limit=100000000 \ - --send --proxy=https://devnet-gateway.multiversx.com --chain=D +Open `variables.cfg` in the `nano` editor: + +```bash +cd ~/mx-chain-scripts/config +nano variables.cfg ``` -You might've seen in many of the MultiversX contracts, we often use the `set_if_empty` method in `init` and `upgrade` functions, instead of plain `set`. This is so we don't accidentally overwrite an important config value during the upgrade process. +Change the variables `ENVIRONMENT`, `CUSTOM_HOME` and `CUSTOM_USER` as highlighted in the image below: +![img](/validators/scripts/variables.png) -## What about the old contract's storage? +For `CUSTOM_USER` variable, use the output of the `whoami` command that was run earlier. -Storage is kept intact, except for the changes the `upgrade` function might do during upgrade. +Save the file and exit: +- If you’re editing with **nano**, press `Ctrl+X`, then `y`, and `Enter` +- If you’re editing with **vi** or **vim**, hold down `Shift` and press `z` twice. -## Migrating storage or token attributes -If you modify your core data's design, you will run into "Decode error: Input too short". Fear not, as there are ways to avoid that. +## **Ensure user privileges** -For example, let's assume you had the following struct type, which keeps track of user information: +Ensure your user has `sudo` enabled and accessible so that it doesn't ask for a password every time it executes something. -```rust -#[derive(TypeAbi, TopEncode, TopDecode, NestedEncode, NestedDecode)] -pub struct UserData { - pub stake_amount: BigUint, - pub last_update_block: u64, -} +If you are certain this is already done, feel free to skip forward. Otherwise, you will need to add your username to a special list. + +So let's add it to the overrides: + +```bash +sudo visudo -f /etc/sudoers.d/myOverrides ``` -In your new code, you decided it would be nice to also keep track of the last update epoch, not only block, so you changed the struct: +Now, navigate to the end of the file by pressing `Shift + G`. Next, press `o` to add a new line, and type the following, replacing `username` with the output of the `whoami` command that was run earlier. -```rust -#[derive(TypeAbi, TopEncode, TopDecode, NestedEncode, NestedDecode)] -pub struct UserData { - pub stake_amount: BigUint, - pub last_update_block: u64, - pub last_update_epoch: u64, -} +```bash +yourusername ALL=(ALL) NOPASSWD:ALL ``` -This will not work. The struct will decode `stake_amount`, it will then decode `last_update_epoch`, and it will have no bytes left to decode the `last_update_epoch`. +Conclude by pressing `Esc`, then save and close the file by holding down `Shift` while pressing `z` twice. -:::caution -You always need to add new fields at the end of the struct, otherwise, this approach will not work. -::: +Your user should now be able to execute `sudo` commands. -To fix this, we need to manually implement the decoding traits, which were previously automatically added through the derives. +--- -```rust -use multiversx_sc::codec::{NestedDecodeInput, TopDecodeInput}; +### Staking & Unstaking -#[derive(TypeAbi, TopEncode, NestedEncode)] -pub struct UserData { - pub stake_amount: BigUint, - pub last_update_block: u64, - pub last_update_epoch: u64, -} +This page will guide you through the process of staking and unstaking nodes. -impl TopDecode for UserData { - fn top_decode(input: I) -> Result - where - I: TopDecodeInput, - { - let mut buffer = input.into_nested_buffer(); - Self::dep_decode(&mut buffer) - } -} -impl NestedDecode for UserData { - fn dep_decode(input: &mut I) -> Result { - let stake_amount = BigUint::dep_decode(input)?; - let last_update_block = u64::dep_decode(input)?; +## **Introduction** - let last_update_epoch = if !input.is_depleted() { - u64::dep_decode(input)? - } else { - 0 - }; +Before staking, a node is a mere observer. After staking, the node becomes a validator, which means that it will be eligible for consensus and will earn rewards. Validators play a central role in the operation of the network. - if !input.is_depleted() { - return Result::Err(DecodeError::INPUT_TOO_LONG); - } +**Staking** is the process by which the operator of the node sends a sum of 2500 EGLD to be locked in a system SmartContract. Multiple nodes can be staked at once, and their operator must lock 2500 EGLD for each of the nodes. This sum acts as a collateral, and it will be released back to the node operator through the process of **unstaking**, with a final step called **unbonding**. - Result::Ok(UserData { - stake_amount, - last_update_block, - last_update_epoch - }) - } -} -``` +A validator node produces rewards, which are transferred to the node operator at their **reward address** of choice, decided upon during the staking process. The reward address may be changed after staking as well. -With the above code, we manually decode each field, and then, if there are any bytes left, we decode the new fields we've added, using a default value (0 in this case) if there are no more bytes - as this means it's an encoded version of the old struct. +Each staking or unstaking process requires a transaction to be sent to the Staking Smart Contract. These transactions must contain all the required information, encoded properly, and must provide a high enough gas limit to allow for successful execution. These details are described in the following pages. -Remember to also check if there are any remaining bytes after that and throw an error, otherwise, your struct could potentially be decoded from almost any input bytes. +There are currently 2 supported methods of constructing and submitting these transactions to the Staking SmartContract: +- Manually constructing the transaction, then submitting it to [wallet.multiversx.com](https://wallet.multiversx.com/); +- Automatically constructing the transaction and submitting it using the `mxpy` command-line tool. -### "But what if I want to remove a field?" +The following pages will describe both approaches in each specific case. -Unless you want to remove the very last field of the struct, and change nothing else, this is not possible, as you'd have almost no way of distinguishing between old and new data. -Assuming you simply want to remove `last_update_block` for the example above, the implementation would be as follows: +## **Prerequisites** -```rust -use multiversx_sc::codec::{NestedDecodeInput, TopDecodeInput}; +In order to submit a staking transaction, you must have the following: -#[derive(TypeAbi, TopEncode, NestedEncode)] -pub struct UserData { - pub stake_amount: BigUint, -} +- 2500 EGLD for each node and 0.006 EGLD per node as transaction fee +- A unique `validatorKey.pem` file of each node -impl TopDecode for UserData { - fn top_decode(input: I) -> Result - where - I: TopDecodeInput, - { - let mut buffer = input.into_nested_buffer(); - Self::dep_decode(&mut buffer) - } -} +You have the option of staking through the online Wallet at [https://wallet.multiversx.com](https://wallet.multiversx.com/) or by using `mxpy`. -impl NestedDecode for UserData { - fn dep_decode(input: &mut I) -> Result { - let stake_amount = BigUint::dep_decode(input)?; - if input.is_depleted() { - return Result::Ok(UserData { stake_amount }); - } +## **Staking through the Wallet** - let _last_update_block = u64::dep_decode(input)?; - if !input.is_depleted() { - return Result::Err(DecodeError::INPUT_TOO_LONG); - } +1. Go to https://wallet.multiversx.com and log into your wallet +2. Go to the Validate section +3. Press "Stake now" - Result::Ok(UserData { stake_amount }) - } -} -``` +![staking1](/validators/staking1.png) -In this case, if there are any bytes left after we decode the `stake_amount`, we decode the old fields and ignore their values. Same as last time, remember to throw an error if there are bytes remaining still after that. +4. Navigate to the location of the .pem file or drag & drop it +5. Press "Continue" +![staking2](/validators/staking2.png) -## Conclusion +6. The staking transaction data is automatically populated using the public key in the .pem certificate you provided. The private key is not touched and the data does not leave your browser. Only the transaction with this public information will be sent to the network once you press Confirm +7. Press "Confirm" -This approach works for both stored instances, and token attributes. Keep in mind you'll have to keep adding more and more levels of partial decoding to the above implementation if you change the struct often, and new fields always have to be at the end. +![staking3](/validators/staking3.png) ---- +8. The status of the transaction will be displayed on screen, together with a success message. Click "Done" once you see the Success message. -### User-defined Smart Contracts +![staking4](/validators/staking4.png) -For user-defined Smart Contract deployments and function calls, the **actual gas consumption** of processing contains both of the previously mentioned cost components - though, while the **value movement and data handling** component is easily computable (using the previously depicted formula), the **contract execution** component is hard to determine precisely _a priori_. Therefore, for this component we have to rely on _simulations_ and _estimations_. +9. You can review the transaction in your history. Based on the current staking capacity of the network, you will get an OK message indicating that your node has become a validator, or a response indicating that the network staking is at capacity and your node has been put in the Queue. -For **simulations**, we will start a local testnet using `mxpy` (detailed setup instructions can be found [here](/developers/setup-local-testnet)). Thus, before going further, make sure your local testnet is up and running. +![staking5](/validators/staking5.png) + +10. The information about the staked nodes from the current wallet will be updated +11. You can further interact with your node(s) by clicking on the three vertical dots next to the public key, which brings up a menu for performing actions such as Unjail, Unstake and Unbond. +![staking6](/validators/staking6.png) -## Contract deployments -In order to get the required `gasLimit` (the **actual gas cost**) for a deployment transaction, one should use the well-known `mxpy contract deploy` command, but with the `--simulate` flag set. +## **Staking through mxpy** -At first, pass the maximum possible amount for `gas-limit` (no guessing). +Submitting the staking transaction using `mxpy` avoids having to write the "Data" field manually. Instead, the staking transaction is constructed automatically by `mxpy` and submitted to the network directly, in a single command. + +Make sure `mxpy` is installed by issuing this command on a terminal: ```bash -$ mxpy --verbose contract deploy --bytecode=./contract.wasm \ - --gas-limit=600000000 \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --simulate +mxpy --version ``` -In the output, look for `txGasUnits`. For example: +If `mxpy` is not installed (`command not found`), please follow [these instructions](/sdk-and-tools/mxpy/installing-mxpy). + +Make sure `mxpy` is installed and has the latest version before continuing. + + +## **Your Wallet PEM file** + +To send transactions on your behalf _without_ using the online MultiversX Wallet, `mxpy` must be able to sign for you. For this reason, you have to generate a PEM file using your Wallet mnemonic. + +Please follow the guide [Deriving the Wallet PEM file](/sdk-and-tools/mxpy/mxpy-cli#converting-a-wallet). Make sure you know exactly where the PEM file was generated, because you'll need to reference its path in the `mxpy` commands. + +After the PEM file was generated, you can issue transactions from `mxpy` directly. + + +## **The staking transaction** + +The following commands assume that the PEM file for your Wallet was saved with the name `walletKey.pem` in the current folder, where you are issuing the commands from. + +The command to submit a staking transaction with `mxpy` is this: + +```bash +mxpy --verbose validator stake --pem=walletKey.pem --value="" --validators-file= --proxy=https://gateway.multiversx.com +``` + +Notice that we are using the `walletKey.pem` file. Moreover, before executing this command, you need to replace the following: + +- Replace `` with the amount you are staking. You need to calculate this value with respect to the number of nodes you are staking for. See the [beginning of the "Staking through the Wallet"](/validators/staking#staking-through-the-wallet) section for info on how to do it. +- Replace `` with the JSON file that lists the nodes you are staking for. This JSON file should look like this: ```json -"txSimulation": { - ... - "cost": { - "txGasUnits": 1849711, - ... +{ + "validators": [ + { + "pemFile": "valPem1.pem" + }, + { + "pemFile": "valPem2.pem" + }, + { + "pemFile": "valPem3.pem" } + ] } ``` -:::note -The simulated cost `txGasUnits` contains both components of the cost. -::: +The `pemFile` field should point to valid Validator PEM file. **Note that paths must be relative to the JSON file itself.** + +Here's an example for a staking command for one node: + +``` +mxpy --verbose validator stake --pem=walletKey.pem --value="2500000000000000000000" --validators-file=my-validators.json --proxy=https://gateway.multiversx.com +``` + +:::note important +You must take **denomination** into account when specifying the `value` parameter in **mxpy**. +::: + +For two nodes, it becomes this: + +``` +mxpy --verbose validator stake --pem=walletKey.pem --value="5000000000000000000000" --validators-file=my-validators.json --proxy=https://gateway.multiversx.com +``` + + +## **The --reward-address parameter** + +When you submit a staking transaction, the Staking SmartContract remembers the wallet you sent it from, and the rewards from your staked validators will go to that wallet. This is the _default_ behavior. In this case, it will be the wallet which you used to generate the `walletKey.pem` file in the earlier subsection ["Your Wallet PEM file"](/validators/staking#your-wallet-pem-file). -After that, check the cost simulation by running the simulation once again, but this time with the precise`gas-limit`: +Alternatively, you can tell `mxpy` to specify another wallet to which your rewards should be transferred. You will need the **address of your reward wallet** (it looks like `erd1xxxxx…`) for this, which you will pass to `mxpy` using the `--reward-address` parameter. -```bash -$ mxpy --verbose contract deploy --bytecode=./contract.wasm \ - --gas-limit=1849711 \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --simulate +For example, a staking command for a single node, with a reward address specified, looks like this: + +``` +mxpy --verbose validator stake --reward-address="erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" --pem=walletKey.pem --value="2500000000000000000000" --validators-file=my-validators.json --proxy=https://gateway.multiversx.com ``` -In the output, look for the `status` - it should be `success`: +The above command will submit a staking command and will also inform the Staking SmartContract that the rewards should be transferred to the wallet `erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th` . -```json -"txSimulation": { - "execution": { - "result": { - "status": "success", - ... - }, - ... - } -``` -In the end, let's actually deploy the contract: +--- -```bash -$ mxpy --verbose contract deploy --bytecode=./contract.wasm \ - --gas-limit=1849711 \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --send --wait-result +### Staking Providers + +```mdx-code-block +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; ``` -:::important -For deployments, the **execution** component of the cost is associated with instantiating the Smart Contract and calling its `init()` function. -If the flow of `init()` is dependent on input arguments or it references blockchain data, then the cost will vary as well, depending on these variables. Make sure you simulate sufficient deployment scenarios and increase (decrease) the `gas-limit`. -::: +## Introducing staking providers +A **staking provider** is defined as a custom delegation smart contract, the associated nodes and the funds staked in the pool by participants. **Node operators** may wish to set up a staking provider for their nodes, which can then be funded by anyone in exchange for a proportion of the validator rewards. This form of funding the stake for validators is called **delegation**. -## Contract calls +Staking providers bridge the gap between node operators, who need funds to stake for their nodes, and fund holders who wish to earn rewards by staking their funds, but are not interested in managing validator nodes. -In order to get the required `gasLimit` (the **actual gas cost**) for a contract call, one should first deploy the contract, then use the `mxpy contract call` command, with the `--simulate` flag set. +Node operators can set up a staking provider to manage one or more validator nodes. For this purpose, they may use the **delegation manager** built into the MultiversX Protocol to create their own **delegation contract**. A delegation contract automates certain tasks required for the management of a staking provider, such as keeping track of every account that has funded the staking provider, keeping track of the nodes themselves, as well as providing information to the delegators. :::important -If the contract makes further calls to other contracts, please read the next section. +A staking provider requires 1250 EGLD deposited by the node operator at the moment of its creation. However, 2500 EGLD is required to stake a single validator node and start earning rewards. ::: -Assuming we've already deployed the contract (see above) let's get the cost for calling one of its endpoints: +This page describes how to request a new delegation contract from the delegation manager and how to use it. It will focus on the delegation _contract_ more than the delegation _manager_, but the two concepts are intimately linked. However, it is important to remember that it is the delegation _contract_ which handles the staking provider and the nodes associated with it. -```bash -$ mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqygvvtlty3v7cad507v5z793duw9jjmlxd8sszs8a2y \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --function=increment\ - --gas-limit=600000000\ - --simulate -``` +Note that the delegation manager is not required to set up a staking provider. For example, it is also possible to set up delegation using a regular smart contract, although that is a more complex process and is not discussed here. -In the output, look for `txGasUnits`. For example: +Node operators may also choose to set up a delegation dashboard, although they may use any user interface or none whatsoever. As an example, the boilerplate for such a delegation dashboard can be found here: https://github.com/multiversx/mx-delegation-dapp. Alternatively, the old boilerplate is located here: https://github.com/multiversx/mx-deprecated-starter-dapp/tree/master/react-delegationdashboard. -```json -"txSimulation": { - ... - "cost": { - "txGasUnits": 1225515, - ... - } -} -``` -In the end, let's actually call the contract: +A detailed description of the delegation process can be consulted at https://github.com/multiversx/mx-specs/blob/main/sc-delegation-specs.md. -```bash -$ mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqygvvtlty3v7cad507v5z793duw9jjmlxd8sszs8a2y \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --function=increment\ - --gas-limit=1225515\ - --send --wait-result -``` -:::important -If the flow of the called function is dependent on input arguments or it references blockchain data, then the cost will vary as well, depending on these variables. Make sure you simulate sufficient scenarios for the contract call and increase (decrease) the `gas-limit`. -::: +## Creating a new delegation contract +The delegation contract for a new staking provider can be created by issuing a request to the delegation manager. This is done by submitting a transaction of the following form: -## Contracts calling (asynchronously) other contracts +```rust +NewDelegationContractTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6 + Value: 1250000000000000000000 (1250 EGLD) + GasLimit: 60000000 + Data: "createNewDelegationContract" + + "@" + + + "@" + +} +``` -:::important -Documentation in this section is preliminary and subject to change. Furthermore, **only asynchronous calls are covered**. -::: +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -Before moving forward, make sure you first have a look over the following: +The `Receiver` address is set to `erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6`, which is the fixed address of the delegation manager, located on the Metachain. -- [Asynchronous calls between contracts](/learn/space-vm#asynchronous-calls-between-contracts) -- [Asynchronous calls (Rust framework)](/developers/transactions/tx-legacy-calls#asynchronous-calls) -- [Callbacks (Rust framework)](/developers/developer-reference/sc-annotations/#callbacks) +The `Value` is set to 1250 EGLD, which will be automatically added into the funds of the newly created delegation contract, i.e. this is the initial amount of EGLD in the staking provider. This amount of EGLD always belongs to the owner of the delegation contract. -Suppose we have two contracts: `A` and `B`, where `A::foo(addressOfB)` asynchronously calls `B::bar()` (e.g. using `asyncCall()`). +:::important +The initial 1250 EGLD count towards the total delegation cap, like all the funds in the staking provider. -Let's deploy the contracts `A` and `B`: +The initial amount of 1250 EGLD added to the pool makes the owner the first delegator of the staking provider. This means that the owner is also entitled to a proportion of the rewards, which can be claimed like any other delegator. +::: -```bash -$ mxpy --verbose contract deploy --bytecode=./a.wasm \ - --gas-limit=5000000 \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --send --wait-result --outfile=a.json +In the `Data field`, the first argument passed to `createNewDelegationContract` is the total delegation cap (the maximum possible size of the staking provider). It is expressed as a fully denominated amount of EGLD, meaning that it is the number of $10^{-18}$ subdivisions of the EGLD, and not the actual number of EGLD tokens. The fully denominated total delegation cap must then be encoded hexadecimally. Make sure not to encode the ASCII string representing the total delegation cap. -$ mxpy --verbose contract deploy --bytecode=./b.wasm \ - --gas-limit=5000000 \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --send --wait-result --outfile=b.json -``` +:::tip +For example, to obtain the fully denominated form of 7231.941 EGLD, the amount must be multiplied by $10^{18}$, resulting in 7231941000000000000000. Do not encode the ASCII string `"7231941000000000000000"`, but encode the integer 7231941000000000000000 itself. This would result in `"01880b57b708cf408000"`. +::: -Assuming `A` is deployed at `erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts`, and `B` is deployed at `erd1qqqqqqqqqqqqqpgqj5zftf3ef3gqm3gklcetpmxwg43rh8z2d8ss2e49aq`, let's **simulate** `A::foo(addressOfB)` (at first, pass a _large-enough_ or maximum `gas-limit`): +Setting the total delegation cap to 0 ("00" in hexadecimal) specifies an unlimited total delegation amount. It can always be modified later (see [Delegation cap](/validators/delegation-manager#delegation-cap)). -```bash -$ export hexAddressOfB=0x$(mxpy wallet bech32 --decode erd1qqqqqqqqqqqqqpgqj5zftf3ef3gqm3gklcetpmxwg43rh8z2d8ss2e49aq) +The second argument passed to `createNewDelegationContract` is the service fee that will be reserved for the owner of the delegation contract. It is computed as a proportion of the total rewards earned by the validator nodes. The remaining rewards apart from this proportion will be available to delegators to either claim or redelegate. The service fee is expressed as hundredths of a percent. -$ mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --function=foo\ - --gas-limit=50000000\ - --arguments ${hexAddressOfB}\ - --simulate -``` +:::tip +For example, a service fee of 37.45% is expressed by the integer 3745. This integer must then be encoded hexadecimally (3745 becomes `"0ea1"`). +::: -In the output, look for the simulated cost (as above): +Setting the service fee to 0 (`"00"` in hexadecimal) specifies that no rewards are reserved for the owner of the delegation contract - all rewards will be available to the delegators. The service fee can always be modified later (see [Service fee](/validators/delegation-manager#service-fee)). -```json -"txSimulation": { - ... - "cost": { - "txGasUnits": 3473900, - ... - } +The following is a complete example of a transaction requesting the creation of a new delegation contract: + +```rust +NewDelegationContractTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6 + Value: 1250000000000000000000 + GasLimit: 60000000 + Data: "createNewDelegationContract" + + "@01880b57b708cf408000" + + "@0ea1" } ``` -The simulated cost represents the **actual gas cost** for invoking `A::foo()`, `B::bar()` and `A::callBack()`. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -**However, the simulated cost above isn't the value we are going to use as `gasLimit`.** If we were to do so, we would be presented the error `not enough gas`. +The above transaction creates a new delegation contract owned by the sender, with total delegation cap of 7231.941 EGLD and service fee of 37.45% from the rewards. Moreover, the newly created delegation contract will start with a staking provider of 1250 EGLD. -Upon reaching the call to `B::bar()` inside `A::foo()`, the MultiversX VM inspects the **remaining gas _at runtime_** and **temporarily locks (reserves) a portion of it**, to allow for the execution of `A::callBack()` once the call to `B::bar()` returns. -With respect to the [VM Gas Schedule](https://github.com/multiversx/mx-chain-mainnet-config/tree/master/gasSchedules), the aforementioned **remaining gas _at runtime_** has to satisfy the following conditions in order for the **temporary gas lock reservation** to succeed: +## Configuring the delegation contract -```go -onTheSpotRemainingGas > gasToLockForCallback +The owner of the delegation contract has a number of operations at their disposal. -gasToLockForCallback = - costOf(AsyncCallStep) + - costOf(AsyncCallbackGasLock) + - codeSizeOf(callingContract) * costOf(AoTPreparePerByte) -``` -:::note -Subsequent asynchronous calls (asynchronous calls performed by an asynchronously-called contract) will require temporary gas locks as well. -::: +### Metadata -For our example, where `A` has 453 bytes, the `gasToLockForCallback` would be (as of February 2022): +The delegation contract can store information that identifies the staking provider: its human-readable name, its website and its associated GitHub identity. +```rust +SetMetadataTransaction { + Sender: + Receiver:
+ Value: 0 + GasLimit: 2000000 + Data: "setMetaData" + "@" + + + "@" + + + "@" + +} ``` -gasToLockForCallback = 100000 + 4000000 + 100 * 453 = 4145300 -``` - -It follows that the value of `gasLimit` should be: -``` -simulatedCost < gasLimit < simulatedCost + gasToLockForCallback -``` +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -For our example, that would be: +An example for the `Data` field that sets the name to `"Test Mx Provider"`, the website to `"testmx.provider"` and the GitHub identifier to `"testmxprovider"` is: -``` -3473900 < gasLimit < 7619200 +```rust + "setMetaData" + + "@54657374204d782050726f7669646572" // Test Mx Provider + "@746573746d782e70726f7669646572" // testmx.provider + "@746573746d7870726f7669646572" // testmxprovider ``` :::important -As of February 2022, for contracts that call other contracts, the lowest `gasLimit` required by a successful execution isn't easy to determine using **mxpy**. While this value can be determined by a careful inspection of the local testnet logs, for the moment, the recommended approach is to **start with the right-hand side of the inequality above (`simulatedCost + gasToLockForCallback`) and gradually decrease the value** while simulating the call and looking for a successful output. +Setting the identity of the staking provider in the metadata is the **first step** in connecting the delegation contract and a GitHub identity. The second step is explained in the next section [Display information](/validators/delegation-manager#display-information) where the inverse connection is made: from the GitHub identity to the delegation contract address. ::: -For our example, let's simulate using the following values for `gasLimit`: `7619200, 7000000, 6000000`: -```bash -$ mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --function=foo \ - --gas-limit=7619200 \ - --arguments ${hexAddressOfB} \ - --simulate +### Display information -... inspect output (possibly testnet logs); execution is successful +:::caution +As of January 2024, the only accepted way to customize the information of a delegation contract is via MultiversX Assets. -mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --function=foo \ - --gas-limit=7000000 \ - --arguments ${hexAddressOfB} \ - --simulate +Provisioning of information from `keybase` or your `GitHub` repository is no longer accepted. -... inspect output (possibly testnet logs); execution is successful +The only accepted changes are on `mx-assets` identities. -mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts \ - --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \ - --function=foo \ - --gas-limit=6000000 \ - --arguments ${hexAddressOfB} \ - --simulate +You can safely delete the `keybase` profile and also the `multiversx` repository inside your organization's `GitHub`. +::: -... inspect output (possibly testnet logs); ERROR: out of gas when executing B::bar() +To customize the information for your delegation contract, which will be available inside the MultiversX ecosystem (such as Explorer, Web Wallet, xPortal, and so on), +some additional information has to be added to the MultiversX assets repository. -``` +In order to do so, the owner of the nodes/staking provider must open a Pull Request against https://github.com/multiversx/mx-assets (`master` branch). -Therefore, in our case, a reasonable value for `gasLimit` would be 7000000. +**Step 1: choose the environment** -:::important -In case of a highly gas-demanding callback (not recommended) which would consume more than `gasToLockForCallback`, one should appropriately increase the right-hand side of the `gasLimit` inequation depicted above, by inspecting the contract call output and the testnet logs. -::: +- for mainnet go to `mx-assets/identities` +- for testnet go to `mx-assets/testnet/identities` +- for devnet go to `mx-assets/devnet/identities` ---- +**Step 2: create a directory with an unique name** -### validators +Must be lowercase, alphanumeric, with no spaces. For example, `my-new-identity` -This page describes the structure of the `validators` index (Elasticsearch), and also depicts a few examples of how to query it. +**Step 3: add a `info.json` file and a logo.png** +Create a `info.json` file where you specify the owned address(es), the name, the description and social links. +Example: +```json +{ + "description": "This is my new identity", + "name": "New identity", + "website": "http://newidentity.com", + "twitter": "https://twitter.com/newidentity", + "location": ", ", + "owners": [ + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqxgeryqxzqjkh", // staking provider address + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" // the address of regular validator nodes owner + ] +} +``` -## _id +Inside the owners array: +- if you want to define an identity of a Staking Provider, add its SC address +- if you want to define an identity of simple nodes, add the owner address (all the keys owned by that address will have that identity) +- if you want to define the same identity for both a Staking Provider and simple nodes, set both the address of the Staking Provider and of the owner of the nodes -The `_id` field of this index is composed in this way: `{shardID}_{epoch}` (example: `1_123`) +Also, upload a `logo.png` that will be displayed near the identity. +**Step 4: Open PR and validate the ownership** -## Fields +After opening the PR, you also have to validate the ownership. Please refer to the [this](#assets-ownership-validation) section. +**Done** -| Field | Description | -|-------------|-------------------------------------------------------------------------------------------------| -| publicKeys | The publicKeys field contains a list of all validators' public keys from an epoch and a shard. | +After the PR is merged, the specified information together with the **service fee, percentage filled** and **APR** (for Staking Providers) will be displayed across the ecosystem. +If this information cannot be found a generic logo and the address is displayed. +An example of how the delegation contract will be displayed based on the information provided in the GitHub is provided below. -## Query examples +![stakingpool](/img/stakingpool.png) +:::note +Before MultiversX Assets, nodes owners needed to update the `Identity` field inside the `prefs.toml` file of each node. This is not required anymore now and you can safely leave the identity empty on your node configuration. +::: -#### Fetch all validators from a shard by epoch -In the example below, we fetch all the validators' public keys from shard 1, epoch 600. +### Assets ownership validation -``` -curl --request GET \ - --url ${ES_URL}/validators/_search \ - --header 'Content-Type: application/json' \ - --data '{ - "query": { - "match": { - "_id":"1_600" - } - } -}' -``` +This applies for both adding of updating an identity over the MultiversX assets. ---- +In order to validate the ownership of a staking provider or validator nodes, one will need to sign a message by using +the owner wallet and then submit a comment on the PR with the branding of a validator node or a staking provider. +In case of a staking provider, its owner must perform this message signing. -### Versions and Changelog +Message signing can be performed either via [Web Wallet](https://wallet.multiversx.com), either via [MultiversX Utils](https://utils.multiversx.com/sign-message). +The message to be signed is the commit hash of the latest commit in the PR. Inside the PR, go to the `Commits` tab and copy the latest commit hash (the bottom one is the latest commit). -## **Overview** +![commit-hash](/img/commit-hash.png) -This page offers a high level overview of the important releases of the MultiversX Proxy API, along with recommendations (such as upgrade or transitioning recommendations) for API consumers. +Then, by using a message signer (Web Wallet of MultiversX Utils), sign that commit hash (make sure you don't have additional spaces or other characters) by using the owner wallet. -:::caution -Documentation in this section is preliminary and subject to change. -::: +![commit-hash-sign](/img/commit-hash-sign.png) +After that, leave a comment on that PR with the resulted signature. -## **MultiversX Proxy HTTP API [v1.1.0](https://github.com/multiversx/mx-chain-proxy-go/releases/tag/v1.1.0)** +![commit-hash-sig-comm](/img/commit-hash-sig-comm.png) -This is the API launched at the Genesis. +That's it. A GitHub workflow will validate the signature and if everything is ok, the PR will merged by a MultiversX responsible and the identity will be live anytime soon. -## **MultiversX Proxy HTTP API [v1.1.1](https://github.com/multiversx/mx-chain-proxy-go/releases/tag/v1.1.1)** +### Service fee -This API version brought new features such as the [_hyperblock_-related endpoints](/sdk-and-tools/rest-api/blocks#get-hyperblock-by-nonce), useful for monitoring the blockchain. Furthermore, the `GET transaction` endpoint has been adjusted to include extra fields - for example, the so-called _hyperblock coordinates_ (the _nonce_ and the _hash_ of the containing hyperblock). +The service fee is a percentage of the validator rewards that will be reserved for the owner of the delegation contract. The rest of the rewards will be available to delegators to either claim or redelegate. -This API **has never been deployed to the central instance** of the MultiversX Proxy, available at [gateway.multiversx.com](https://gateway.multiversx.com/). However, until November 2020, **this API has been deployed on-premises** to several partners and 3rd party services (such as Exchange systems) - in the shape of [Observing Squads](/integrators/observing-squad), set up via the nodes scripts - [mx-chain-scripts](https://github.com/multiversx/mx-chain-scripts). +The service fee can be changed at any time using a transaction of the form: -This version of the API requires Observer Nodes with tag [e1.1.0](https://github.com/multiversx/mx-chain-go/releases/tag/v1.1.6/releases/tag/e1.1.0) or greater. +```rust +ChangeServiceFeeTransaction { + Sender: + Receiver:
+ Value: 0 + GasLimit: 2000000 + Data: "changeServiceFee" + + "@" + +} +``` -There are **no breaking changes** between API **v1.1.0** and API **v1.1.1** - neither in terms of structure and format of the requests / responses, nor in the scheme of the URL. **However**, in terms of semantics, the following fixes might lead to breaking changes for some API consumers: +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -- `GET transaction` endpoint has been fixed to report the appropriate status **invalid** for actually _invalid transactions_ (e.g. not enough balance). In v1.1.0, the reported status was imprecise: **partially-executed**. +In the `Data` field, the only argument passed to `changeServiceFee` is the new value of the service fee, expressed as hundredths of a percent. -:::caution -As of November 2020, new API consumers are recommended to use a newer version of the API. +Setting the service fee to 0 (`"00"` in hexadecimal) specifies that no rewards are reserved for the owner of the delegation contract - all rewards will be available to the delegators. The service fee can always be modified later. -**v1.1.0 and v1.1.1 will be deprecated** once all existing API consumers are known to have been upgraded to a more recent version. +:::tip +For example, a service fee of 37.45% is expressed by the integer 3745. This integer must then be encoded hexadecimally (3745 becomes `"0ea1"`). + +Finally, a `Data` field containing `changeServiceFee@0ea1` will change the service fee to 37.45%. ::: -## **MultiversX Proxy HTTP API [v1.1.3](https://github.com/multiversx/mx-chain-proxy-go/releases/tag/v1.1.3)** +### Automatic activation -This API version brought additions - new endpoints, such as `network/economics` or `address/shard`. Furthermore, the response of `vm-values` endpoints has been altered. Though, perhaps the most significant change is **the renaming of transaction statuses**. +When automatic activation is enabled, the delegation contract will activate (stake) inactive nodes as soon as funds have become available in sufficient amount. Consequently, any [delegation transaction](/validators/delegation-manager#delegating-funds) can potentially trigger the activation of inactive nodes, assuming the transaction has sufficient gas. -This version of the API requires Observer Nodes with tag [v1.1.6](https://github.com/multiversx/mx-chain-go/releases/tag/v1.1.6) or greater. +Automatic activation can be enabled or disabled using a transaction of the form: -Between API **v1.1.1** and API **v1.1.3**, the API consumer would observe the following **breaking changes**: +```rust +SetAutomaticActivationTransaction { + Sender: + Receiver:
+ Value: 0 + GasLimit: 2000000 + Data: "setAutomaticActivation" + + "@" + <"true" or "false" in hexadecimal encoding> +} +``` -- All fields of `vm-values` endpoints has been renamed (changed casing, among others). -- The possible set of values for the transaction statuses has been changed: **executed** has been renamed to **success.** The statuses **received** and **partially-executed** have been merged under the status **pending**, while the status **not-executed** has been renamed to **fail**. For API consumers to not be affected by this change, they should follow the recommendations in [Querying the Blockchain](/integrators/querying-the-blockchain). +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -:::important -As of November 2020, new API consumers are recommended to switch to this version (or a more recent one) of the API. +The only argument passed to `setAutomaticActivation` is either `true` or `false`, as an ASCII string encoded hexadecimally. For reference, `true` is `"74727565"` and `false` is `"66616c7365"`. -For API consumers that use **on-premises Observing Squads**, an updated installer will be provided (on this matter, the work is in progress). +:::tip +For example, a `Data` field containing `"setAutomaticActivation@74727565"` enables automatic activation. ::: ---- -### WalletConnect 2.0 Migration +### Delegation cap -## Using `sdk-dapp` +The total delegation cap is the maximum possible size amount of EGLD which can be held by the delegation contract. After reaching the total delegation cap, the contract will reject any subsequent funds. -Using `@multiversx/sdk-dapp` >= `2.2.8` or `@elrondnetwork/dapp-core` >= `2.0.0` +The total delegation cap can be modified at any time using a transaction of the form: -WalletConnect 2.0 is already integrated, only not enabled by default. +```rust +ModifyTotalDelegationCapTransaction { + Sender: + Receiver:
+ Value: 0 + GasLimit: 2000000 + Data: "modifyTotalDelegationCap" + + "@" + +} +``` -Follow [these steps](/sdk-and-tools/sdk-dapp/#walletconnect-setup) to generate and add a `walletConnectV2ProjectId` +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ ---- +In the `Data` field, the only argument passed to `modifyTotalDelegationCap` is the new value for the delegation cap. It is expressed as a fully denominated amount of EGLD, meaning that it is the number of $10^{-18}$ subdivisions of the EGLD, and not the actual number of EGLD tokens. Take sure not to encode the ASCII string representing the total delegation cap. ------------ +:::tip +For example, to obtain the fully denominated form of 7231.941 EGLD, the amount must be multiplied by the denomination factor $10^{18}$, resulting in 7231941000000000000000. Do not encode the ASCII string `"7231941000000000000000"`, but encode the integer 7231941000000000000000 itself. This would result in "01880b57b708cf408000". + +Finally, a `Data` field containing `"modifyTotalDelegationCap@01880b57b708cf408000"` will change the total delegation cap to 7231.941 EGLD. +::: +Setting the total delegation cap to 0 (`"00"` in hexadecimal) specifies an unlimited total delegation amount. It can always be modified later. -## Using `sdk-wallet-connect-provider` +:::important +The total delegation cap cannot be set to a value lower than the amount staked for currently active nodes. It must be either higher than that amount or set to 0 (infinite cap). +::: -Using `@multiversx/sdk-wallet-connect-provider` >= 3.0.1 or `@elrondnetwork/erdjs-wallet-connect-provider` -Since the WalletConnect 2.0 implementation was complimentary to the existing 1.0 version, there should be no breaking changes to upgrade the library +## Managing nodes -Check out some detailed examples [here](/sdk-and-tools/sdk-js/sdk-js-signing-providers/#the-walletconnect-provider). -To Set up the 2.0 functionality you can check out the examples here: [https://github.com/multiversx/mx-sdk-js-examples](https://github.com/multiversx/mx-sdk-js-examples) or a more advanced implementation on sdk-dapp here: [https://github.com/multiversx/mx-sdk-dapp/blob/main/src/hooks/login/useWalletConnectV2Login.ts](https://github.com/multiversx/mx-sdk-dapp/blob/main/src/hooks/login/useWalletConnectV2Login.ts) +### Adding nodes -The main difference between V1 and V2 is in the `login` method where we now have to first `connect()` to get the `uri` and the `approval` Promise +When a delegation contract is first created, it contains no information about nodes. The owner of the contract must then register nodes into the contract, so that they can be later activated. Any newly added node is "inactive" by default. -```js -await provider.init(); -const { uri, approval } = await provider.connect(); +Adding nodes requires the BLS key pairs belonging to each of them, which the owner of the contract uses to prove that they have access to the nodes. This proof consists of signing the address of the delegation contract itself with the secret BLS key of each node, individually. This results in as many signed messages as there are nodes. -await openModal(uri); +Adding `N` nodes to the delegation contract is done by submitting a transaction with the values set as follows: -try { - await provider.login({ approval, token }); // optional token -} catch (err) { - console.log(err); - alert('Connection Proposal Refused') +```rust +AddNodesTransaction { + Sender: + Receiver:
+ Value: 0 + GasLimit: 1000000 + N·6000000 + Data: "addNodes" + + "@" + + + "@" +
+ + "@" + + + "@" +
+ + <...> + + "@" + + + "@" +
} ``` -Another difference is in the `callbacks` where on WalletConnect 2.0 we have a third one for events: +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -```js -const callbacks = { - onClientLogin: async function () { - // closeModal() is defined above - closeModal(); - const address = await provider.getAddress(); - console.log("Address:", address); - }, - onClientLogout: async function () { - console.log("onClientLogout()"); - }, - onClientEvent: async function (event) { - console.log("onClientEvent()", event); - } -}; -``` +As shown above, the `Data` field contains an enumeration of `N` pairs. Such a pair consists of the public BLS key of a node along with the message produced by signing the address of the delegation contract with the secret BLS key of the respective node. There are as many pairs as there are nodes to add. -`signTransaction`, `signTransactions`, `logout`, etc remain the same for the library user since the action happens in the provider directly. ---- +### Staking nodes -### Whitebox Framework +When the staking provider held by the delegation contract contains a sufficient amount of EGLD, the inactive (non-staked) nodes can be staked (activated). This promotes the nodes to the status of **validator**, which means they participate in consensus and earn rewards. -## Introduction +This subsection describes the _manual_ staking (activation) of nodes. To automatically stake (activate) nodes when funds become available, [automatic activation](/validators/delegation-manager#automatic-activation) can be enabled. -The Rust testing framework was developed as an alternative to manually writing scenario tests. This comes with many advantages: +To stake specific nodes manually, a transaction of the following form can be submitted: -- being able to calculate values using variables -- type checking -- automatic serialization -- far less verbose -- semi-automatic generation of the scenario tests +```rust +StakeNodesTransaction { + Sender: + Receiver:
+ Value: 0 + GasLimit: 1000000 + N·6000000 + Data: "stakeNodes" + + "@" + + + "@" + + + <...> + + "@" + + +} +``` -The only disadvantage is that you need to learn something new! Jokes aside, keep in mind that this whole framework runs in a mocked environment. So while you get powerful testing and debugging tools, you are ultimately running a mock and have no guarantee that the contract will work identically with the current VM version deployed on the mainnet. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -This is where the scenario generation part comes into play. The Rust testing framework allows you to generate scenarios with minimal effort, and then run said scenarios by invoking `cargo test`. There will be a bit of manual effort required on the developer's part, but we'll get to that in its specific section. +The `Data` field contains an enumeration of `N` public BLS keys corresponding to the nodes to be staked. -Please note that scenario generation is more of an experiment rather than a fully fledged implementation, which we might even remove in the future. Still, some examples are provided here if you still wish to attempt it. +### Unstaking nodes + +Validator nodes that are already staked (active) can be manually unstaked. -## Prerequisites +:::important +Validators are demoted to observer status at the beginning of the next epoch _after unstaking_. This means that they stop receiving rewards. -You need to have the latest multiversx-sc version (at the time of writing this, the latest version is 0.39.0). You can check the latest version here: https://crates.io/crates/multiversx-sc +Unstaking _does not_ mean that the staked amount returns to the staking provider (see [undelegating](/validators/delegation-manager#undelegating-funds) and [withdrawing](/validators/delegation-manager#withdrawing)). +::: -Add `multiversx-sc-scenario` and required packages as dev-dependencies in your Cargo.toml: +To cancel the deactivation before the unstaking is complete, the nodes can be [restaked](/validators/delegation-manager#restaking-nodes). -```toml -[dev-dependencies.multiversx-sc-scenario] -version = "0.39.0" +To begin the deactivation process for a selection of validator nodes, a transaction of the following form is used: -[dev-dependencies] -num-bigint = "0.4.2" -num-traits = "0.2" -hex = "0.4" +```rust +UnstakeNodesTransaction { + Sender: + Receiver:
+ Value: 0 + GasLimit: 1000000 + N·6000000 + Data: "unStakeNodes" + + "@" + + + "@" + + + <...> + + "@" + + +} ``` -For this tutorial, we're going to use the crowdfunding SC, so it might be handy to have it open or clone the repository: https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples/crowdfunding +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -You need a `tests` and a `scenarios` folder in your contract. Create a `.rs` file in your `tests` folder. +The `Data` field contains an enumeration of `N` public BLS keys corresponding to the nodes to be unstaked. -In your newly created test file, add the following code (adapt the `crowdfunding` namespace, the struct/variable names, and the contract wasm path according to your contract): -```rust -use crowdfunding::*; -use multiversx_sc::{ - sc_error, - types::{Address, SCResult}, -}; -use multiversx_sc_scenario::{ - managed_address, managed_biguint, managed_token_id, rust_biguint, whitebox::*, - DebugApi, -}; +### Restaking nodes -const WASM_PATH: &'static str = "crowdfunding/output/crowdfunding.wasm"; +Validator nodes that have been unstaked can be restaked (reactivated) before their deactivation is complete. To cancel their deactivation, a transaction of the following form is used: -struct CrowdfundingSetup -where - CrowdfundingObjBuilder: - 'static + Copy + Fn() -> crowdfunding::ContractObj, -{ - pub blockchain_wrapper: BlockchainStateWrapper, - pub owner_address: Address, - pub first_user_address: Address, - pub second_user_address: Address, - pub cf_wrapper: - ContractObjWrapper, CrowdfundingObjBuilder>, +```rust +RestakeNodesTransaction { + Sender: + Receiver:
+ Value: 0 + GasLimit: 1000000 + N·6000000 + Data: "reStakeUnStakedNodes" + + "@" + + + "@" + + + <...> + + "@" + + } ``` -The `CrowdfundingSetup` struct isn't really needed, but it helps de-duplicating some code. You may add other fields in your struct if needed, but for now this is enough for our use-case. The only fields you'll need for any contract are `blockchain_wrapper` and `cf_wrapper`. The rest of the fields can be adapted according to your test scenario. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -And that's all you need to get started. +The `Data` field contains an enumeration of `N` public BLS keys corresponding to the nodes to be restaked. -## Writing your first test +### Unbonding nodes -The first test you need to write is the one simulating the deploy of your smart contract. For that, you need a user address and a contract address. Then you simply call the `init` function of the smart contract. +Nodes that have been [unstaked](/validators/delegation-manager#unstaking-nodes) can be completely deactivated, a process called **unbonding**. -Since we're going to be using the same token ID everywhere, let's add it as a constant (and while we're at it, have the deadline as a constant as well): +:::important +Validators are demoted to observer status at the beginning of the next epoch _after unstaking_, not unbonding. See [unstaking](/validators/delegation-manager#unstaking-nodes) above. +::: + +Validator nodes that have been unbonded cannot be restaked (reactivated). They must be staked anew. ```rust -const CF_TOKEN_ID: &[u8] = b"CROWD-123456"; -const CF_DEADLINE: u64 = 7 * 24 * 60 * 60; // 1 week in seconds +UnbondNodesTransaction { + Sender: + Receiver:
+ Value: 0 + GasLimit: 1000000 + N·6000000 + Data: "unBondNodes" + + "@" + + + "@" + + + <...> + + "@" + + +} ``` -Let's create our initial setup: +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -```rust -fn setup_crowdfunding( - cf_builder: CrowdfundingObjBuilder, -) -> CrowdfundingSetup -where - CrowdfundingObjBuilder: 'static + Copy + Fn() -> crowdfunding_esdt::ContractObj, -{ - let rust_zero = rust_biguint!(0u64); - let mut blockchain_wrapper = BlockchainStateWrapper::new(); - let owner_address = blockchain_wrapper.create_user_account(&rust_zero); - let first_user_address = blockchain_wrapper.create_user_account(&rust_zero); - let second_user_address = blockchain_wrapper.create_user_account(&rust_zero); - let cf_wrapper = blockchain_wrapper.create_sc_account( - &rust_zero, - Some(&owner_address), - cf_builder, - WASM_PATH, - ); +The `Data` field contains an enumeration of `N` public BLS keys corresponding to the nodes to be unbonded. - blockchain_wrapper.set_esdt_balance(&first_user_address, CF_TOKEN_ID, &rust_biguint!(1_000)); - blockchain_wrapper.set_esdt_balance(&second_user_address, CF_TOKEN_ID, &rust_biguint!(1_000)); - blockchain_wrapper - .execute_tx(&owner_address, &cf_wrapper, &rust_zero, |sc| { - let target = managed_biguint!(2_000); - let token_id = managed_token_id!(CF_TOKEN_ID); +### Removing nodes - sc.init(target, CF_DEADLINE, token_id); - }) - .assert_ok(); +Inactive (not staked, unbonded) nodes can be removed from the delegation contract by the owner at any time. Neither active (staked) nor unstaked nodes cannot be removed. - blockchain_wrapper.add_mandos_set_account(cf_wrapper.address_ref()); +Unlike [adding nodes](/validators/delegation-manager#adding-nodes), this step does not require the BLS key pairs of the nodes. - CrowdfundingSetup { - blockchain_wrapper, - owner_address, - first_user_address, - second_user_address, - cf_wrapper, - } +Removing `N` nodes from the delegation contract is done by submitting a transaction with the values set as follows: + +```rust +RemoveNodesTransaction { + Sender: + Receiver:
+ Value: 0 + GasLimit: 1000000 + N·6000000 + Data: "removeNodes" + + "@" + + + "@" + + + <...> + + "@" + + } ``` -The main object you're going to be interacting with is the `BlockchainStateWrapper`. It holds the entire (mocked) blockchain state at any given moment, and allows you to interact with the accounts. - -As you can see in the above test, we use the said wrapper to create an owner account, two other user accounts, and the Crowdfunding smart contract account. - -Then, we set the ESDT balances for the two users, and deploy the smart contract, by using the `execute_tx` function of the `BlockchainStateWrapper` object. The arguments are: +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -- caller address -- contract wrapper (which contains the contract address and the contract object builder) -- EGLD payment amount -- a lambda function, which contains the actual execution +The `Data` field contains an enumeration of `N` public BLS keys corresponding to the nodes to be removed. -Since this is a SC deploy, we call the `init` function. Since the contract works with managed objects, we can't use the built-in Rust BigUint, so we use the one provided by `multiversx_sc` instead. To create managed types, we use the `managed_` functions. Alternatively, you can create those objects by: -```rust -let target = BigUint::::from(2_000u32); -``` +### Unjailing nodes -Keep in mind you can't create managed types outside of the `execute_tx` functions. +When active validator nodes perform poorly or to the detriment of the network, they are penalized by having their rating reduced. Rating is essential to earning rewards, because it directly determines the likelihood of a validator to be [selected for consensus](/learn/consensus). -Some observations for the `execute_tx` function: +However, it can happen that rating of a validator might drop under the acceptable threshold. As a consequence, the validator will begin its next epoch **jailed**, which prevents it from participating in consensus. -- The return type for the lambda function is a `TxResult`, which has methods for checking for success or error: `assert_ok()` is used to check the tx worked. If you want to check error cases, you would use `assert_user_error("message")`. -- After running the `init` function, we add a `setState` step in the generated scenario, to simulate our deploy: `blockchain_wrapper.add_mandos_set_account(cf_wrapper.address_ref());` +:::important +A jailed validator does not lose its stake nor its status. It remains active, but it cannot earn rewards while in jail. +::: -To test the scenario and generate the trace file, you have to create a test function: +Recovering a validator from jail and restoring it is called **unjailing**, for which a fine of 2.5 EGLD must be paid. Multiple validators can be recovered from jail at the same time by paying 2.5 EGLD for each validator. The format of the unjailing transaction is as follows: ```rust -#[test] -fn init_test() { - let cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj); - cf_setup - .blockchain_wrapper - .write_mandos_output("_generated_init.scen.json"); +UnjailNodesTransaction { + Sender: + Receiver:
+ Value: 2.5 EGLD × + GasLimit: 1000000 + N·6000000 + Data: "unJailNodes" + + "@" + + + "@" + + + <...> + + "@" + + } ``` -And you're done for this step. You successfully tested your contract's init function, and generated a scenario for it. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +Note that the `Value` field depends on `N`, the number of validators to unjail. -## Testing transactions +The `Data` field contains an enumeration of `N` public BLS keys corresponding to the nodes to be unjailed. -Let's test the `fund` function. For this, we're going to use the previous setup, but now we use the `execute_esdt_transfer` method instead of `execute_tx`, because we're sending ESDT to the contract while calling `fund`: -```rust -#[test] -fn fund_test() { - let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj); - let b_wrapper = &mut cf_setup.blockchain_wrapper; - let user_addr = &cf_setup.first_user_address; +## Delegating and managing delegated funds - b_wrapper - .execute_esdt_transfer( - user_addr, - &cf_setup.cf_wrapper, - CF_TOKEN_ID, - 0, - &rust_biguint!(1_000), - |sc| { - sc.fund(); +Accounts that delegate their own funds to the staking provider are called **delegators**. The delegation contract offers them a set of actions as well. This means that these actions are available to the owner of the delegation contract as well. - let user_deposit = sc.deposit(&managed_address!(user_addr)).get(); - let expected_deposit = managed_biguint!(1_000); - assert_eq!(user_deposit, expected_deposit); - }, - ) - .assert_ok(); -} -``` -As you can see, we can directly call the storage mappers (like `deposit`) from within the contract and compare with a local value. No need to encode anything. +### Delegating funds -If you also want to generate a scenario file for this transaction, this is where the bit of manual work comes in: +Accounts become delegators by funding the staking provider, i.e. they delegate their funds. The delegators are rewarded for their contribution with a proportion of the rewards earned by the validator nodes. By default, the owner of the delegation contract is the first delegator, having already contributed 1250 EGLD to the staking provider at its creation. -```rust - let mut sc_call = ScCallMandos::new(user_addr, cf_setup.cf_wrapper.address_ref(), "fund"); - sc_call.add_esdt_transfer(CF_TOKEN_ID, 0, &rust_biguint!(1_000)); +:::important +Extra funds received by the delegation contract from delegators will be immediately used to top-up the stake of the existing active validators, consequently increasing their rewards. +::: - let expect = TxExpectMandos::new(0); - b_wrapper.add_mandos_sc_call(sc_call, Some(expect)); +Submitting a delegation transaction takes into account the status of [automatic activation](/validators/delegation-manager#automatic-activation): if the delegated funds cause the amount in the staking provider to become sufficient for the staking of extra nodes, it can trigger their activation automatically. This happens only if the transaction contains enough gas. - cf_setup - .blockchain_wrapper - .write_mandos_output("_generated_fund.scen.json"); +But if gas is insufficient, or if automatic activation is disabled, the amount received through the delegation transaction simply becomes top-up for the stake of already active validators. Subsequent [manual staking](/validators/delegation-manager#staking-nodes) will be necessary to use the funds for staking, assuming they are sufficient. + +Funds can be delegated by any fund holder by submitting a transaction of the following form: + +```rust +DelegateTransaction { + Sender: + Receiver:
+ Value: minimum 1 EGLD + GasLimit: 12000000 + Data: "delegate" +} ``` -You have to add this at the end of your `fund_test`. The more complex the call, the more arguments you'll have to add and such. The `SCCallMandos` struct has the `add_argument` method so you don't have to do any encoding by yourself. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ + +If the transaction is successful, the funds' holder has become a delegator and the funds either become a top-up amount for the stake of active validators, or may trigger the staking of inactive nodes, as described above. -## Testing queries +### Claiming rewards -Testing queries is similar to testing transactions, just with less arguments (since there is no caller, and no payment, and any modifications are automatically reverted): +A portion of the rewards earned by validator nodes is reserved for each delegator. To claim the rewards, a delegator may issue a transaction of the following form: ```rust -#[test] -fn status_test() { - let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj); - let b_wrapper = &mut cf_setup.blockchain_wrapper; +ClaimRewardsTransaction { + Sender: + Receiver:
+ Value: 0 + Gas: 6000000 + Data: "claimRewards" +} +``` - b_wrapper - .execute_query(&cf_setup.cf_wrapper, |sc| { - let status = sc.status(); - assert_eq!(status, Status::FundingPeriod); - }) - .assert_ok(); +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - let sc_query = ScQueryMandos::new(cf_setup.cf_wrapper.address_ref(), "status"); - let mut expect = TxExpectMandos::new(0); - expect.add_out_value(&Status::FundingPeriod); +If the transaction is successful, the delegator receives the proportion of rewards they are entitled to. - b_wrapper.add_mandos_sc_query(sc_query, Some(expect)); - cf_setup - .blockchain_wrapper - .write_mandos_output("_generated_query_status.scen.json"); -} -``` +### Redelegating rewards +Current delegation rewards can also be immediately delegated instead of [claimed](/validators/delegation-manager#claiming-rewards). This makes it an operation very similar to [delegation](/validators/delegation-manager#delegating-funds). -## Testing smart contract errors +:::important +Just like delegation, redelegation of rewards takes into account the status of [automatic activation](/validators/delegation-manager#automatic-activation): if the redelegated rewards cause the amount in the staking provider to become sufficient for the staking of extra nodes, it can trigger their activation automatically (requires sufficient gas in the redelegation transaction). +::: -In the previous transaction test, we've tested the happy flow. Now let's see how we can check for errors: +Rewards are redelegated using a transaction of the form: ```rust -#[test] -fn test_sc_error() { - let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj); - let b_wrapper = &mut cf_setup.blockchain_wrapper; - let user_addr = &cf_setup.first_user_address; +RedelegateRewardsTransaction { + Sender: + Receiver:
+ Value: 0 + Gas: 12000000 + Data: "reDelegateRewards" +} +``` - b_wrapper.set_egld_balance(user_addr, &rust_biguint!(1_000)); +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - b_wrapper - .execute_tx( - user_addr, - &cf_setup.cf_wrapper, - &rust_biguint!(1_000), - |sc| { - sc.fund(); - }, - ) - .assert_user_error("wrong token"); +If the transaction is successful, the delegator does not receive any EGLD at the moment, but the rewards they were entitled to will be added to their delegated amount. - b_wrapper - .execute_tx(user_addr, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| { - let user_deposit = sc.deposit(&managed_address!(user_addr)).get(); - let expected_deposit = managed_biguint!(0); - assert_eq!(user_deposit, expected_deposit); - }) - .assert_ok(); - let mut sc_call = ScCallMandos::new(user_addr, cf_setup.cf_wrapper.address_ref(), "fund"); - sc_call.add_egld_value(&rust_biguint!(1_000)); +### Undelegating funds - let mut expect = TxExpectMandos::new(4); - expect.set_message("wrong token"); +Delegators may express the intent to withdraw a specific amount of EGLD from the staking provider. However, this process cannot happen at once and may take a few epochs before the amount is actually available for withdrawal, because the funds may already be used to stake for active validators and this means that unstaking of nodes may be necessary. - b_wrapper.add_mandos_sc_call(sc_call, Some(expect)); +:::important +If the amount to undelegate requested by the delegator will cause the staking provider to drop below the sufficient amount required to keep all the current validators active, some validators will inevitably end up unstaked. The owner of the delegation contract may intervene and add extra funds to prevent such situations. +::: - cf_setup - .blockchain_wrapper - .write_mandos_output("_generated_sc_err.scen.json"); -} -``` +Funds that have been previously used as stake for validators have been transferred into a separate system smart contract at the moment of staking, therefore the delegation contract itself does not hold these funds. But submitting an undelegation request will cause the delegation contract to attempt their retrieval. -Notice how we've changed the payment intentionally to an invalid token to check the error case. Also, we've changed the expected deposit to "0" instead of the previous "1_000". And lastly: the `.assert_user_error("wrong token")` call on the result. +The delegation contract may receive the funds immediately if they're not currently used as stake; this makes them available for subsequent [withdrawal](/validators/delegation-manager#withdrawing). This is the case where previously delegated funds acted as top-up to the stake of existing validators. +On the other hand, if the requested funds are currently in use as stake, the delegation contract cannot receive them yet. -## Testing a successful funding campaign +:::important +Funds used as stake can only be retrieved after 144000 blocks have been built on the Metachain (a little over 10 chronological epochs). It doesn't matter whether the validator was already demoted to observer, or whether it has been decommissioned entirely - the funds may not return until the aforementioned time has passed. -For this scenario, we need both users to fund the full amount, and then owner to claim the funds. For simplicity, we've left the scenario generation out of this one: +After 144000 blocks, the funds can be [withdrawn](/validators/delegation-manager#withdrawing) normally by their rightful owner. +::: + +To express the intention of future withdrawal of funds from the staking provider, a delegator may submit the following transaction: ```rust -#[test] -fn test_successful_cf() { - let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj); - let b_wrapper = &mut cf_setup.blockchain_wrapper; - let owner = &cf_setup.owner_address; - let first_user = &cf_setup.first_user_address; - let second_user = &cf_setup.second_user_address; +UndelegateTransaction { + Sender: + Receiver:
+ Value: 0 + Gas: 12000000 + Data: "unDelegate" + "@" + +} +``` - // first user fund - b_wrapper - .execute_esdt_transfer( - first_user, - &cf_setup.cf_wrapper, - CF_TOKEN_ID, - 0, - &rust_biguint!(1_000), - |sc| { - sc.fund(); +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - let user_deposit = sc.deposit(&managed_address!(first_user)).get(); - let expected_deposit = managed_biguint!(1_000); - assert_eq!(user_deposit, expected_deposit); - }, - ) - .assert_ok(); +In the `Data` field, the only argument passed to `unDelegate` is the desired amount of EGLD to undelegate and later withdraw. It is expressed as a fully denominated amount of EGLD, meaning that it is the number of $10^{-18}$ subdivisions of the EGLD, and not the actual number of EGLD tokens. The fully denominated amount must then be encoded hexadecimally. Make sure not to encode the ASCII string representing the amount. - // second user fund - b_wrapper - .execute_esdt_transfer( - second_user, - &cf_setup.cf_wrapper, - CF_TOKEN_ID, - 0, - &rust_biguint!(1_000), - |sc| { - sc.fund(); - let user_deposit = sc.deposit(&managed_address!(second_user)).get(); - let expected_deposit = managed_biguint!(1_000); - assert_eq!(user_deposit, expected_deposit); - }, - ) - .assert_ok(); +### Withdrawing - // set block timestamp after deadline - b_wrapper.set_block_timestamp(CF_DEADLINE + 1); +After submitting an [undelegation transaction](/validators/delegation-manager#undelegating-funds), a delegator may finally withdraw funds from the staking provider. - // check status - b_wrapper - .execute_query(&cf_setup.cf_wrapper, |sc| { - let status = sc.status(); - assert_eq!(status, Status::Successful); - }) - .assert_ok(); +:::important +Funds must always be [undelegated](/validators/delegation-manager#undelegating-funds) first. They cannot be directly withdrawn. +::: - // user try claim - b_wrapper - .execute_tx(first_user, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| { - sc.claim(); - }) - .assert_user_error("only owner can claim successful funding"); +This action withdraws _all the currently undelegated funds_ belonging to the specific delegator. - // owner claim - b_wrapper - .execute_tx(owner, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| { - sc.claim(); - }) - .assert_ok(); +Withdrawing funds is done using a transaction of the following form: - b_wrapper.check_esdt_balance(owner, CF_TOKEN_ID, &rust_biguint!(2_000)); - b_wrapper.check_esdt_balance(first_user, CF_TOKEN_ID, &rust_biguint!(0)); - b_wrapper.check_esdt_balance(second_user, CF_TOKEN_ID, &rust_biguint!(0)); +```rust +WithdrawTransaction { + Sender: + Receiver:
+ Value: 0 + GasLimit: 12000000 + Data: "withdraw" } ``` -You've already seen most of the code in this test before already. The only new things are the `set_block_timestamp` and the `check_esdt_balance` methods of the wrapper. There are similar methods for setting block nonce, block random seed, etc., and the checking EGLD and SFT/NFT balances. - +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -## Testing a failed funding campaign +If the transaction is successful, the delegator receives all the EGLD they have previously requested to undelegate. The amount is removed from the staking provider. -This is simimlar to the previous one, but instead we have the users claim instead of the owner after deadline. -```rust -#[test] -fn test_failed_cf() { - let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj); - let b_wrapper = &mut cf_setup.blockchain_wrapper; - let owner = &cf_setup.owner_address; - let first_user = &cf_setup.first_user_address; - let second_user = &cf_setup.second_user_address; +## Delegation contract view functions - // first user fund - b_wrapper - .execute_esdt_transfer( - first_user, - &cf_setup.cf_wrapper, - CF_TOKEN_ID, - 0, - &rust_biguint!(300), - |sc| { - sc.fund(); +The delegation contract can be queried using the following view functions. These queries should be done on a local proxy on the `/vm-values/query` endpoint. - let user_deposit = sc.deposit(&managed_address!(first_user)).get(); - let expected_deposit = managed_biguint!(300); - assert_eq!(user_deposit, expected_deposit); - }, - ) - .assert_ok(); +The following documentation sections only show the value of the relevant `returnData` field and omit the other fields for simplicity. - // second user fund - b_wrapper - .execute_esdt_transfer( - second_user, - &cf_setup.cf_wrapper, - CF_TOKEN_ID, - 0, - &rust_biguint!(600), - |sc| { - sc.fund(); +```json +{ + "data": { + "data": { + "returnData": [], + "returnCode": "ok", + "returnMessage": "", + "gasRemaining": 0, + "gasRefund": 0, + "outputAccounts": null, + "deletedAccounts": null, + "touchedAccounts": null, + "logs": [] + } + }, + "error": "", + "code": "successful" +} +``` - let user_deposit = sc.deposit(&managed_address!(second_user)).get(); - let expected_deposit = managed_biguint!(600); - assert_eq!(user_deposit, expected_deposit); - }, - ) - .assert_ok(); - // set block timestamp after deadline - b_wrapper.set_block_timestamp(CF_DEADLINE + 1); +### POST Contract config {#contract-config} - // check status - b_wrapper - .execute_query(&cf_setup.cf_wrapper, |sc| { - let status = sc.status(); - assert_eq!(status, Status::Failed); - }) - .assert_ok(); +The response contains an array of the properties in a fixed order (base64 encoded): owner address, service fee, maximum delegation cap, initial owner funds, automatic activation, with delegation cap, can change service fee, check cap on redelegate, nonce on creation and unbond period. - // first user claim - b_wrapper - .execute_tx(first_user, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| { - sc.claim(); - }) - .assert_ok(); + + - // second user claim - b_wrapper - .execute_tx(second_user, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| { - sc.claim(); - }) - .assert_ok(); +```bash +https://proxy:port/vm-values/query +``` - b_wrapper.check_esdt_balance(owner, CF_TOKEN_ID, &rust_biguint!(0)); - b_wrapper.check_esdt_balance(first_user, CF_TOKEN_ID, &rust_biguint!(1_000)); - b_wrapper.check_esdt_balance(second_user, CF_TOKEN_ID, &rust_biguint!(1_000)); +```json +{ + "scAddress": "
", + "funcName": "getContractConfig" } ``` + + -## Conclusion +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -These tests cover pretty much every flow in the crowdfunding smart contract. Keep in mind that code can be deduplicated even more by having functions similar to the `setup_crowdfunding` function, but for the sake of the example, we've kept this as simple as possible. We hope this will make writing tests and debugging a lot easier moving forward! +```json +{ + "returnData": [ + "", + "", + "", + "", + "", + "" + ] +} +``` ---- + -### Whitebox Functions Reference +Request -## Introduction +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", + "funcName": "getContractConfig" +} +``` -This page contains a list of all the currently available functions for the Rust Testing Framework, specifically the ones that the `BlockchainStateWrapper` type provides. +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) -Note: You will notice that most functions use the `num_bigint::BigUint` type for numbers. This is NOT the same as the BigUint type you use inside smart contracts. It comes from a Rust library, and they are different types. It is recommended to always use the Rust version outside of lambda functions, and only use the managed type when interacting with the sc directly. +```json +{ + "returnData": [ + "gKzHUD288mzScNX6nEmkGm4CHneMdrrPhJyPET9iGA8=", + null, + "", + "Q8M8GTdWSAAA", + "dHJ1ZQ==", + "ZmFsc2U=", + "dHJ1ZQ==", + "AuU=", + "+g==" + ] +} +``` + + -## State-checking functions -These functions check the blockchain state. They will panic and the test will fail if the check is unsuccessful. +### POST Contract metadata {#contract-metadata} +The response contains an array of the properties in a fixed order (base64 encoded): staking provider name, website and identifier. -### check_egld_balance + + -```rust -check_egld_balance(&self, address: &Address, expected_balance: &num_bigint::BigUint) +```bash +https://proxy:port/vm-values/query ``` -Checks the EGLD balance for the given address. +```json +{ + "scAddress": "
", + "funcName": "getMetaData" +} +``` + + -### check_esdt_balance +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -```rust -check_esdt_balance(&self, address: &Address, token_id: &[u8], expected_balance: &num_bigint::BigUint) +```json +{ + "returnData": [ + "", + "", + "" + ] +} ``` -Checks the fungible ESDT balance for the given address. + +Request -### check_nft_balance +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", + "funcName": "getMetaData" +} +``` -```rust -check_nft_balance(&self, address: &Address, token_id: &[u8], nonce: u64, expected_balance: &num_bigint::BigUint, opt_expected_attributes: Option<&T>) +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) + +```json +{ + "returnData": [ + "U3Rha2luZyBwcm92aWRlciB0ZXN0", + "d3d3LmVscm9uZHN0YWtpbmcuY29t", + "dGVzdEtleWJhc2VJZGVudGlmaWVy" + ] +} ``` -Where T has to implement TopEncode, TopDecode, PartialEq, and core::fmt::Debug. This is usually done through `#[derive(TopEncode, TopDecode, PartialEq, Debug)]`. + + -This function checks the NFT balance for a specific nonce for an address, and optionally checks the NFT attributes as well. If you are only interested in the balance, pass `Option::None` for `opt_expected_attributes`. The Rust compiler might complain that it can't deduce the generic `T`, in which case, you can do one of the following: -```rust -b_mock.check_nft_balance::(..., None); +### POST Number of delegators {#number-of-delegators} -b_mock.check_nft_balance(..., Option::::None); +The response contains a value representing the number of delegators in base64 encoding of the hex encoding. + + + + +```bash +https://proxy:port/vm-values/query ``` -Where `...` are the rest of the arguments. +```json +{ + "scAddress": "
", + "funcName": "getNumUsers" +} +``` + + -## State-getter functions +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -These functions get the current state. They are generally used after a transaction to check that the tokens reached their intended destination. Most functions will panic if they're given an invalid address as argument. +```json +{ + "returnData": [ + "" + ] +} +``` + -### get_egld_balance +Request -```rust -get_egld_balance(&self, address: &Address) -> num_bigint::BigUint +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", + "funcName": "getNumUsers" +} ``` -Gets the EGLD balance for the given account. +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +```json +{ + "returnData": ["BQ=="] +} +``` -### get_esdt_balance + + -```rust -get_esdt_balance(&self, address: &Address, token_id: &[u8], token_nonce: u64) -> num_bigint::BigUint -``` -Gets the ESDT balance for the given account. If you're interested in fungible token balance, set `token_nonce` to 0. +### POST Number of nodes {#number-of-nodes} +The response contains the number of nodes in base64 encoding of the hex encoding. -### get_nft_attributes + + -```rust -get_nft_attributes(&self, address: &Address, token_id: &[u8], token_nonce: u64) -> Option +```bash +https://proxy:port/vm-values/query ``` -Gets the NFT attributes for a token owned by the given address. Will return `Option::None` if there are no attributes. +```json +{ + "scAddress": "
", + "funcName": "getNumNodes" +} +``` + + -### dump_state +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -```rust -dump_state(&self) +```json +{ + "returnData": [ + "" + ] +} ``` -Prints the current state to console. Useful for debugging. + +Request -### dump_state_for_account_hex_attributes +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", + "funcName": "getNumNodes" +} +``` -```rust -dump_state_for_account_hex_attributes(&self, address: &Address) +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) + +```json +{ + "returnData": ["Dg=="] +} ``` -Similar to the function before, but dumps state only for the given account. + + -### dump_state_for_account +### POST Nodes states {#nodes-states} -```rust -dump_state_for_account(&self, address: &Address) +The response contains an enumeration of alternating status codes and BLS keys. Each status code is followed by the BLS key of the node it describes. Both status codes and BLS keys are encoded in base64. + + + + +```bash +https://proxy:port/vm-values/query ``` -Similar to the function before, but prints the attributes in a user-friendly format, given by the generic type given. This is useful for debugging NFT attributes. +```json +{ + "scAddress": "
", + "funcName": "getAllNodeStates" +} +``` + + -## State-altering functions +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -These functions alter the state in some way. +```json +{ + "returnData": [ + "", + "" + ] +} +``` + -### create_user_account +Request -```rust -create_user_account(&mut self, egld_balance: &num_bigint::BigUint) -> Address +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", + "funcName": "getAllNodeStates" +} ``` -Creates a new user account, with the given EGLD balance. The Address is pseudo-randomly generated by the framework. +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +```json +{ + "returnData": [ + "c3Rha2Vk", + "KJ6auG3rKQydktc9soWvyBOa5UPA7DYezttTqlS6JIIvsvOaH8ghs2Qruc4aXLUXNJ1if7Ot9gbt5dNUrmNfkLtZl1hpLvPllrGmFP4bKCzZ25UNiTratwOMcXhhCmSD", + "bm90U3Rha2Vk", + "7gJzQ3GQ4htSx6CYvOkXPDdwGfzdahuDY4agZkGhIAMfB44K08FP6z3wLQEnn2IULfZ8/Hds38LEu3Xq+mJZ4FktF0vm8C1T34b5uAEpZWtDZLICAEFCuQZrqS5Qb1CR", + "vTyNQ/vDxg0L8LmoGuKP+4/wsbyWv8RaqeQ+WH+xrMvk1m7Q3wjheOpjYtQPz80YZ1CrwKj6ObsCUejP4uuvi3MQ1oMEGKg5yh3kRgybRb4TXAWEpAPszYMLIQhrIn2P", + "9TbGQCcrbyXH9HBAhzIWOuH/cdSNO1dwxO5foM2L28tWU0p9Kos6DKsPMtKMx4sAeRal08K3Dk0gQxeTSAvC2fb3DAQt01rmPSAqCSXZetSX12BVcTi+pYGUHaXKJ/OW" + ] +} +``` -### create_user_account_fixed_address + + -```rust -create_user_account_fixed_address(&mut self, address: &Address, egld_balance: &num_bigint::BigUint) -``` -Same as the function above, but it lets you create an account with a fixed address, for the few cases when it's needed. +### POST Total active stake {#total-active-stake} +The response contains a value representing the total active stake in base64 encoding of the hex encoding. -### create_sc_account + + -```rust -create_sc_account(&mut self, egld_balance: &num_bigint::BigUint, owner: Option<&Address>, obj_builder: ContractObjBuilder, contract_wasm_path: &str) -> ContractObjWrapper +```bash +https://proxy:port/vm-values/query ``` -Where: - -```rust - CB: ContractBase + CallableContract + 'static, - ContractObjBuilder: 'static + Copy + Fn() -> CB, +```json +{ + "scAddress": "
", + "funcName": "getTotalActiveStake" +} ``` -Creates a smart contract account. For `obj_builder`, you will have to pass `sc_namespace::contract_obj`. This function will return a `ContractObjWrapper`, which contains the address of the newly created SC, and the function that is used to create instances of your contract. + + -The `ContractObjWrapper` will be used whenever you interact with the SC, which will be through the execution functions. If you only need the address (for setting balance, for example), you can use the `address_ref` method to get a reference to the stored address. +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -`contract_wasm_path` is the path towards the wasm file. This path is relative to the `tests` folder that the current test file resides in. The most usual path will be `output/wasm_file_name.wasm`. +```json +{ + "returnData": [""] +} +``` + -### create_sc_account_fixed_address +Request -```rust -create_sc_account_fixed_address(&mut self, address: &Address, egld_balance: &num_bigint::BigUint, owner: Option<&Address>, obj_builder: ContractObjBuilder, contract_wasm_path: &str) -> ContractObjWrapper +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", + "funcName": "getTotalActiveStake" +} ``` -Same as the function above, but the address can be set by the caller instead of being randomly generated. +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +```json +{ + "returnData": ["ArXjrxaxiAAA"] +} +``` -### set_egld_balance + + -```rust -set_egld_balance(&mut self, address: &Address, balance: &num_bigint::BigUint) -``` -Sets the EGLD balance for the given account. +### POST Total unstaked stake {#total-unstaked-stake} +The response contains a value representing the total unstaked stake in base64 encoding of the hex encoding. -### set_esdt_balance + + -```rust -set_esdt_balance(&mut self, address: &Address, token_id: &[u8], balance: &num_bigint::BigUint) +```bash +https://proxy:port/vm-values/query ``` -Sets the fungible token balance for the given account. +```json +{ + "scAddress": "
", + "funcName": "getTotalUnStaked" +} +``` + + -### set_nft_balance +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -```rust -set_nft_balance(&mut self, address: &Address, token_id: &[u8], nonce: u64, balance: &num_bigint::BigUint, attributes: &T) +```json +{ + "returnData": [ + "" + ] +} ``` -Sets the non-fungible token balance for the given account, and the attributes. Attributes can be any serializable type. If you don't need attributes, you can pass "empty" in various ways: `&()`, `&Vec::::new()`, `BoxedBytes::empty()`, etc. + +Request -### set_esdt_local_roles +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", + "funcName": "getTotalUnStaked" +} +``` -```rust -set_esdt_local_roles(&mut self, address: &Address, token_id: &[u8], roles: &[EsdtLocalRole]) +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) + +```json +{ + "returnData": ["ArXjrxaxiAAA"] +} ``` -Sets the ESDT token roles for the given address and token. Usually used during setup steps. + + -### set_block_epoch +### POST Total cumulated rewards {#total-cumulated-rewards} -```rust -set_block_epoch(&mut self, block_epoch: u64) -``` +The response contains a value representing the sum of all accumulated rewards in base64 encoding of the hex encoding. + + -### set_block_nonce +```bash +https://proxy:port/vm-values/query +``` -```rust -et_block_nonce(&mut self, block_nonce: u64) +```json +{ + "scAddress": "
", + "funcName": "getTotalCumulatedRewards", + "caller": "erd1qqqqqqqqqqqqqqqpqqqqqqqqlllllllllllllllllllllllllllsr9gav8" +} ``` + + -### set_block_round +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -```rust -set_block_round(&mut self, block_round: u64) +```json +{ + "returnData": [ + "" + ] +} ``` + -### set_block_timestamp +Request -```rust -set_block_timestamp(&mut self, block_timestamp: u64) +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", + "funcName": "getTotalCumulatedRewards", + "caller": "erd1qqqqqqqqqqqqqqqpqqqqqqqqlllllllllllllllllllllllllllsr9gav8" +} ``` +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) -### set_block_random_seed - -```rust -set_block_random_seed(&mut self, block_random_seed: Box<[u8; 48]>) +```json +{ + "returnData": ["czSCSSYZr8E="] +} ``` -Set various values for the current block info. + + -### set_prev_block_epoch +### POST Delegator claimable rewards {#delegator-claimable-rewards} -```rust -set_prev_block_epoch(&mut self, block_epoch: u64) -``` +The response contains a value representing the total claimable rewards for the delegator in base64 encoding of the hex encoding. + + -### set_prev_block_nonce +```bash +https://proxy:port/vm-values/query +``` -```rust -set_prev_block_nonce(&mut self, block_nonce: u64) +```json +{ + "scAddress": "
", + "funcName": "getClaimableRewards", + "args": [ + "" + ] +} ``` + + -### set_prev_block_round +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -```rust -set_prev_block_round(&mut self, block_round: u64) +```json +{ + "returnData": [ + "" + ] +} ``` + -### set_prev_block_timestamp +Request -```rust -set_prev_block_timestamp(&mut self, block_timestamp: u64) +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", + "funcName": "getClaimableRewards", + "args": ["ebfd923cd251f857ed7639e87143ac83f12f423827abc4a0cdde0119c3e37915"] +} ``` +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) -### set_prev_block_random_seed - -```rust -set_prev_block_random_seed(&mut self, block_random_seed: Box<[u8; 48]>) +```json +{ + "returnData": ["Ft9RZzF7Dyc"] +} ``` -Same as the ones above, but sets the block info for the previous block. + + -## Smart Contract execution functions +### POST Delegator total accumulated rewards {#delegator-total-accumulated-rewards} -These functions help interacting with smart contracts. While they would still fit into the state-altering category, we feel they deserve their own section. +The response contains a value representing the total accumulated rewards for the delegator in base64 encoding of the hex encoding. -Note: We will shorten the signatures by not specifying the complete types for the ContractObjWrapper. For reference, the contract wrapper is of type `ContractObjWrapper`, where + + -```rust - CB: ContractBase + CallableContract + 'static, - ContractObjBuilder: 'static + Copy + Fn() -> CB, +```bash +https://proxy:port/vm-values/query ``` - -### execute_tx - -```rust -execute_tx(&mut self, caller: &Address, sc_wrapper: &ContractObjWrapper<...>, egld_payment: &num_bigint::BigUint, tx_fn: TxFn) -> TxResult +```json +{ + "scAddress": "
", + "funcName": "getTotalCumulatedRewardsForUser", + "args": [ + "" + ] +} ``` -Executes a transaction towards the given SC (defined by the wrapper), with optional EGLD payment (pass 0 if you want no payment). `tx_fn` is a lambda function that accepts a contract object as an argument. For more details about how to write such a lambda, you can take a look at the [Crowdfunding test examples](/developers/testing/rust/whitebox-legacy). - + + -### execute_esdt_transfer +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -```rust -execute_esdt_transfer(&mut self, caller: &Address, sc_wrapper: &ContractObjWrapper<...>, token_id: &[u8], esdt_nonce: u64, esdt_amount: &num_bigint::BigUint, tx_fn: TxFn) -> TxResult +```json +{ + "returnData": [ + "" + ] +} ``` -Same as the function above, but executes an ESDT/NFT transfer instead of EGLD transfer. - + -### execute_esdt_multi_transfer +Request -```rust -execute_esdt_multi_transfer(&mut self, caller: &Address, sc_wrapper: &ContractObjWrapper<...>, esdt_transfers: &[TxInputESDT], tx_fn: TxFn) -> TxResult +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", + "funcName": "getTotalCumulatedRewardsForUser", + "args": ["ebfd923cd251f857ed7639e87143ac83f12f423827abc4a0cdde0119c3e37915"] +} ``` -Same as the function above, but executes a MultiESDTNFT transfer instead. +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +```json +{ + "returnData": ["Ft9RZzF7Dyc"] +} +``` -### execute_query + + -```rust -execute_query(&mut self, sc_wrapper: &ContractObjWrapper<...>, query_fn: TxFn) -> TxResult -``` -Executes a SCQuery on the SC. None of the changes are committed into the state, but it still needs to be a mutable function to perform the temporary changes. Just like on the real blockchain, there is no caller and no token transfer for queries. +### POST Delegator active stake {#delegator-active-stake} +The response contains a value representing the active stake for the delegator in base64 encoding of the hex encoding. -### execute_in_managed_environment + + -```rust -execute_in_managed_environment(&self, f: Func) -> T +```bash +https://proxy:port/vm-values/query ``` -Executes an arbitrary function and returns its result. The result can be any type. This function is rarely used. It can be useful when you want to perform some checks that involve managed types and such. (since you cannot create managed types outside of the lambda functions). +```json +{ + "scAddress": "
", + "funcName": "getUserActiveStake", + "args": [ + "" + ] +} +``` + + -## Undocumented functions +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -There are some scenario generation functions that have not been included, but we recommend not bothering with scenario generation. The process is very time-consuming and the results are some unreadable scenario files. If you need to write scenarios, we recommend writing them yourself. If you still want to dabble into the scenario generation, there are some examples in the [Crowdfunding test examples](/developers/testing/rust/whitebox-legacy). +```json +{ + "returnData": [""] +} +``` ---- + -### Writing and testing interactions +Request -Generally speaking, we recommended to use [sc-meta CLI](/developers/meta/sc-meta-cli) to [generate the boilerplate code for your contract interactions](/developers/meta/sc-meta-cli/#calling-snippets). +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", + "funcName": "getUserActiveStake", + "args": ["ebfd923cd251f857ed7639e87143ac83f12f423827abc4a0cdde0119c3e37915"] +} +``` -Though, for writing contract interaction snippets in **JavaScript** or **TypeScript**, please refer to the [`sdk-js` cookbook](/sdk-and-tools/sdk-js/sdk-js-cookbook). If you'd like these snippets to function as system tests of your contract, a choice would be to structure them as Mocha or Jest tests - take the `*.local.net.spec.ts` tests in [`mx-sdk-js-core`](https://github.com/multiversx/mx-sdk-js-core) as examples. For writing contract interaction snippets in **Python**, please refer to the [`sdk-py` cookbook](/sdk-and-tools/sdk-py) - if desired, you can shape them as simple scripts, as Python unit tests, or as Jupyter notebooks. +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) -You might also want to have a look over [**xSuite**](https://xsuite.dev), a toolkit to init, build, test, deploy contracts using JavaScript, made by the [Arda team](https://arda.run). +```json +{ + "returnData": ["slsrv1so8QAA"] +} +``` ---- + + -## Validators -### Convert An Existing Validator Into A Staking Provider -Staking Phase 3.5 introduced the ability for an existing Validator to create a new delegation smart contract and have their validator node(s) added in the delegation smart contract directly. This is different from before, when in order to do this, a Validator node was to be unstaked, and then placed at the back of the queue. With Staking Phase 3.5, Validators can retain the place inside the 3,200 Validator nodes, and start accepting non-custodial delegations. +### POST Delegator unstaked stake {#delegator-unstaked-stake} -1. Create a new Delegation Smart Contract for an Existing Validator +The response contains a value representing the unstaked stake for the delegator in base64 encoding of the hex encoding. -Send the following transaction from the wallet you staked the Validator from: + + +```bash +https://proxy:port/vm-values/query ``` -Generate Contract for Validator transaction - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6 - Value: 0 - Gas Limit: 510000000 - Data: "makeNewContractFromValidatorData" + - "@" + "" + - "@" + "" + +```json +{ + "scAddress": "
", + "funcName": "getUserUnStakedValue", + "args": [ + "" + ] +} ``` -*For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format).* -Where: -Max cap = total delegation cap in EGLD, fully denominated, in hexadecimal encoding + + -For example, to obtain the fully denominated form of 7231.941 EGLD, the amount must be multiplied by 10^18, resulting in 7231941000000000000000. Do not encode the ASCII string "7231941000000000000000", but encode the integer 7231941000000000000000 itself. This would result in "01880b57b708cf408000". +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -00 = uncapped +```json +{ + "returnData": [ + "" + ] +} +``` -Fee: service fee as hundredths of percents, in hexadecimal encoding + -For example, a service fee of 37.45% is expressed by the integer 3745. This integer must then be encoded hexadecimally (3745 becomes "0ea1"). +Request -After successfully deploying your new delegation smart contract, make sure you manage it with the [Delegation Manager](/validators/delegation-manager). +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", + "funcName": "getUserUnStakedValue", + "args": ["ebfd923cd251f857ed7639e87143ac83f12f423827abc4a0cdde0119c3e37915"] +} +``` -:::caution -Whenever merging or converting direct staked keys into a staking provider pool, please be aware that the BLS key's signature will be altered automatically and re-staking an unbonded node is no longer possible. -In other words, if you attempt this flow of operations unStakeNodes->unBondNodes for a merged key, make sure you call also the removeNodes operation. Otherwise, the stakeNodes or reStakeUnStakedNodes will fail. -::: +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) ---- +```json +{ + "returnData": ["ARWORgkT0AAA"] +} +``` -### FAQs + + -This page contains frequently asked questions about validators and nodes. +### POST Delegator unbondable stake {#delegator-unbondable-stake} -## FAQs +The response contains a value representing the unbondable stake in base64 encoding of the hex encoding. -**How to choose a VPS?** + + -Get started with a few popular VPS choices: +```bash +https://proxy:port/vm-values/query +``` -- Amazon - https://aws.amazon.com/lightsail/pricing/ -- DigitalOcean - https://www.digitalocean.com/pricing/ -- UpCloud - https://upcloud.com/pricing/ -- Vultr - https://www.vultr.com/products/cloud-compute/ -- Alibaba Cloud - https://www.alibabacloud.com/product/ecs#pricing -- Google Cloud - https://cloud.google.com/products/calculator -- Microsoft Azure - https://azure.microsoft.com/en-us/pricing/calculator/ +```json +{ + "scAddress": "
", + "funcName": "getUserUnBondable", + "args": [ + "" + ] +} +``` -Find more via a comparison calculator - https://www.vpsbenchmarks.com/screener + + -Recommendations from experienced validators: +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -- Jose - https://medium.com/@josefcoap/elrond-vps-guide-ebfa6655cb9f +```json +{ + "returnData": [ + "" + ] +} +``` -:::caution -Consider the price of cheap services that offer same specs as other providers at half of the price, when running a validator - the protocol dynamically phases out low performance nodes, which will lead to fewer returns for operators. -::: + +Request -**What if I have Windows?** +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", + "funcName": "getUserUnBondable", + "args": ["ebfd923cd251f857ed7639e87143ac83f12f423827abc4a0cdde0119c3e37915"] +} +``` -Use a virtualization service to deploy a virtual Linux server on your Windows machine. This setup is recommended for testing purposes only. Try this tutorial, for example: https://itsfoss.com/install-linux-in-virtualbox/ +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +```json +{ + "returnData": ["ARWORgkT0AAA"] +} +``` -**General note** + + -There are hundreds of people actively engaging with MultiversX infrastructure. Engage with them over our official [Telegram channel](https://t.me/MultiversXValidators), and you will get your questions answered. Weigh in favor of people with “admin” in their name. Disregard any direct messages offering private support - those are most likely scam attempts. ---- +### POST Delegator undelegated stake {#delegator-undelegated-stake} -### How to use the Docker Image +The response contains an enumeration representing the different undelegated stake values in base64 encoding of the hex encoding. -This page will guide you through the process of using the Docker image to run a MultiversX node. + + +```bash +https://proxy:port/vm-values/query +``` -## Docker node images -As an alternative to the recommended installation flow, one could choose to run a MultiversX Node using the official Docker images: [here](https://hub.docker.com/u/multiversx) +```json +{ + "scAddress": "
", + "funcName": "getUserUnDelegatedList", + "args": [ + "" + ] +} +``` -On the `dockerhub` there are Docker images for every chain (mainnet, devnet and testnet). + + -Images name: -- for mainnet: [chain-mainnet](https://hub.docker.com/r/multiversx/chain-mainnet) -- for devnet: [chain-devnet](https://hub.docker.com/r/multiversx/chain-devnet) -- for testnet: [chain-testnet](https://hub.docker.com/r/multiversx/chain-testnet) +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -:::note Attention required +```json +{ + "returnData": [""] +} +``` -In order to get the latest tag for an image check the latest `RELEASE` from the config repository ([mainnet](https://github.com/multiversx/mx-chain-mainnet-config/releases), [devnet](https://github.com/multiversx/mx-chain-devnet-config/releases) or [testnet](https://github.com/multiversx/mx-chain-testnet-config/releases)). -::: + +Request -## How to pull a Docker image from Dockerhub for node ? -```docker -IMAGE_NAME=chain-mainnet -IMAGE_TAG=[latest_release_tag] -docker pull multiversx/${IMAGE_NAME}:${IMAGE_TAG} +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", + "funcName": "getUserUnDelegatedList", + "args": ["ebfd923cd251f857ed7639e87143ac83f12f423827abc4a0cdde0119c3e37915"] +} ``` +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) -## How to generate a BLS key ? -In order to generate a new BLS key one has to pull from `dockerhub` an image for the `chain-keygenerator` tool: +```json +{ + "returnData": ["Q8M8GTdWSAAA", "iscjBInoAAA="] +} ``` -# pull image from dockerhub -docker pull multiversx/chain-keygenerator:latest -# create a folder for the bls key -BLS_KEY_FOLDER=~/bls-key -mkdir ${BLS_KEY_FOLDER} + + -# generate a new BLS key -docker run --rm --mount type=bind,source=${BLS_KEY_FOLDER},destination=/keys --workdir /keys multiversx/chain-keygenerator:latest -``` +### POST Delegator funds data {#delegator-funds-data} -## How to run a node with Docker ? +The response contains an enumeration for the delegator encoded base64 of the hexadecimal encoding of the following: active stake, unclaimed rewards, unstaked stake and unbondable stake. -The following commands run a Node using the Docker image and map a container folder to a local one that holds the necessary configuration: + + -```docker -PATH_TO_BLS_KEY_FILE=/absolute/path/to/bls-key -IMAGE_NAME=chain-mainnet -IMAGE_TAG=[latest_release_tag] +```bash +https://proxy:port/vm-values/query +``` -docker run --mount type=bind,source=${PATH_TO_BLS_KEY_FILE}/,destination=/data multiversx/${IMAGE_NAME}:${IMAGE_TAG} \ - --validator-key-pem-file="/data/validatorKey.pem" +```json +{ + "scAddress": "
", + "funcName": "getDelegatorFundsData", + "args": [ + "" + ] +} ``` -In the snippet above, make sure you adjust the path to a valid key file and also provide the appropriate command-line arguments to the Node. For more details go to [Node CLI](https://docs.multiversx.com/validators/node-cli). + + -:::note Attention required +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -**Devnet** and **Testnet** validators **should carefully** specify the precise tag when using the Docker setup, always test the new releases themselves, and only deploy them once they understand and agree with the changes. -::: +```json +{ + "returnData": [ + "", + "", + "", + "" + ] +} +``` + -:::tip For CentOS users -If the node's docker image runs on CentOS, the machine needs the `allow_execheap` flag to be enabled. +Request -In order to do this, run the command `sudo setsebool allow_execheap=true` -::: +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", + "funcName": "getUserUnDelegatedList", + "args": ["ebfd923cd251f857ed7639e87143ac83f12f423827abc4a0cdde0119c3e37915"] +} +``` ---- +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) -### Import DB +```json +{ + "returnData": ["REAihYg4zAAA", "Q8M8GTdWSAAA", "REAihYg4zAAA", "Q8M8GTdWSAAA"] +} +``` -This page will guide you through the process of starting a node in `import-db` mode, allowing the reprocessing of older transactions. + + -## Introduction +### POST Get reward data for epoch {#get-reward-data-for-epoch} -The node is able to reprocess a previously produced database by providing the database and starting -the node with the import-db related flags explained in the section below. +The response contains an enumeration for the specified epoch representing the base64 encoding of the hexadecimal encoding for the rewards to distribute, total active stake and service fee. -Possible use cases for the import-db process: + + -- index in ElasticSearch (or something similar) all the data from genesis to present time; -- validate the blockchain state; -- make sure there aren't backwards compatibility issues with a new software version; -- check the blockchain state at a specified time (this includes additional code changes, but for example if you are - interested in the result of an API endpoint at the block 255255, you could use import db and force the node to stop - at the block corresponding to that date). +```bash +https://proxy:port/vm-values/query +``` +```json +{ + "scAddress": "
", + "funcName": "getRewardData", + "args": [""] +} +``` -## How to start the process + + -Let's suppose we have the following data structure: +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -``` - ~/mx-chain-go/cmd/node +```json +{ + "returnData": [ + "", + "", + "" + ] +} ``` -the `node` binary is found in the above-mentioned path. -Now, we have a previously constructed database (from an observer that synced with the chain from the -genesis and never switched the shards). This database will be placed in a directory, let's presume -we will place it near the node's binary, yielding a data structure as follows: + +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqp0llllswfeycs", + "funcName": "getRewardData", + "args": ["fc2b"] +} ``` -. -├── config -│ ├── api.toml -│ ├── config.toml -│ ... -├── import-db -│ └── db -│ └── 1 -│ ├── Epoch_0 -│ │ └── Shard_1 -│ │ ├── BlockHeaders -│ │ │ ... -│ │ ├── BootstrapData -│ │ │ ... -│ │ ... -│ └── Static -│ └── Shard_1 -│ ... -├── node + +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) + +```json +{ + "returnData": ["REAihYg4zAAA", "Q8M8GTdWSAAA", "REAihYg4zAAA"] +} ``` -It is very important that the directory called `db` is a subdirectory (in our case of the `import-db`). -Also, please check that the `config` directory matches the one of the node that produced the `db` data -structure, including the `prefs.toml` file. + + -:::caution -Please make sure the `/mx-chain-go/cmd/node/db` directory is empty so the import-db process will start -from the genesis up until the last epoch provided. -::: -Next, the node can be started by using: +## Delegation manager view functions -``` - cd ~/mx-chain-go/cmd/node - ./node -use-log-view -log-level *:INFO -import-db ./import-db -``` -:::note -Please note that the `-import-db` flag specifies the path to the directory containing the source db directory. The value provided in the example above assumes that the import db directory is called `import-db` and is located near the `node` executable file. -::: +### POST All contract addresses {#all-contract-addresses} -The node will start the reprocessing of the provided database. It will end with a message like: +The response contains an enumeration of bech32 keys bytes in base64 encoding. -``` -import ended because data from epochs [x] or [y] does not exist -``` + + -:::tip -The import-db process can be sped up by skipping the block header's signature check if the import-db data comes from a trustworthy source. -In this case the node should be started with all previously mentioned flags, adding the `-import-db-no-sig-check` flag. -::: +```bash +https://proxy:port/vm-values/query +``` +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + "funcName": "getAllContractAddresses" +} +``` -## Import-DB with populating an Elasticsearch cluster + + -One of the use-cases for utilizing the `import-db` mechanism is to populate an Elasticsearch cluster with data that is -re-processed with the help of this process. +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -:::tip -Import-DB for populating an Elasticsearch cluster should be used only for a full setup (a node in each Shard + a Metachain node) -::: +```json +{ + "returnData": [ + "
" + ] +} +``` -The preparation implies the update of the `external.toml` file for each node. More details can be found [here](/sdk-and-tools/elastic-search/#setup). + -If everything is configured correctly, nodes will push the re-processed data into the Elasticsearch cluster. +Request ---- +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + "funcName": "getAllContractAddresses" +} +``` -### Installing a Validator Node +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) -This page will guide you through the process of installing and updating a validator node. +```json +{ + "returnData": [ + "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAL///8=", + "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAP///8=", + "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAT///8=", + "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAX///8=", + "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAb///8=", + "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAf///8=", + "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAj///8=", + "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAn///8=", + "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAr///8=" + ] +} +``` + + -## **Install your node(s)** -After preparing the user permissions, the script configurations, and the keys, the actual node installation can begin. The Validator script is a multi-purpose tool for managing your node, it is accessible to Mainnet, Devnet or Testnet. +### POST Contract config {#contract-config-1} -Following these few steps, we will work on installing the MultiversX Network validator node to get it up and running on your local machine. +The response contains an enumeration of the properties in a fixed order (base64 encoded): current number of contracts, last created contract address, minimum and maximum service fee, minimum deposit and delegation. -For installation, one must start the scripts by: + + ```bash -cd ~/mx-chain-scripts -./script.sh +https://proxy:port/vm-values/query ``` -After that, a menu will appear with the following options. Select the option `1` to install the node. +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + "funcName": "getContractConfig" +} +``` -```bash - 1) install - 2) observing_squad - 3) multikey_group - 4) upgrade - 5) upgrade_multikey - 6) upgrade_squad - 7) upgrade_proxy - 8) remove_db - 9) start -10) start_all -11) stop -12) stop_all -13) cleanup -14) github_pull -15) add_nodes -16) get_logs -17) benchmark -18) quit - Please select an action:1 + + + +Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response + +```json +{ + "returnData": [ + "", + "", + "", + "", + "", + "" + ] +} ``` -:::note -As an alternative, the installation can be triggered by executing the following command: + -```bash -~/mx-chain-scripts/script.sh install +Request + +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + "funcName": "getContractConfig" +} ``` -::: -- When asked, indicate the number of nodes you want to run, i.e. `1` -- When asked, indicate the name of your validator, i.e. `Valar` -- Quit the menu without starting (we need keys first) by using `18 - quit` +Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +```json +{ + "returnData": [ + "Gw==", + "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAABz///8=", + "", + "JxA=", + "Q8M8GTdWSAAA", + "iscjBInoAAA=" + ] +} +``` -### **Prepare your keys** + + -Create a new folder "VALIDATOR_KEYS" to serve as a local backup when updating: +--- -```bash -cd ~ -mkdir -p ~/VALIDATOR_KEYS +### Staking Providers APR -``` +Staking Providers are entities that allow their delegators, plus the owner's funds to deploy validators on the Network. The Staking Provider owner +is responsible for the nodes' up-time, as well as for configuring the contract's parameters. According to the contract's configuration and operations, the delegation cap, +the contract fee, the number of tokens kept as top-up or the number of nodes to be deployed are all variables in computing the final APR (Annual Percentage Return) +of the provider. -Generate a certificate file containing your Validator key by running the `keygenerator`: -```bash -./elrond-utils/keygenerator +## Introduction -``` +By using the [Delegation Manager](/validators/delegation-manager/) system smart contract, a new staking provider can be +set up. According to the initial deposits (half of the minimum node stake) + delegations from other users (or even the owner itself) +the staking contract can spawn new nodes. Currently, the minimum node cost is 2500 EGLD, so, for example, if a staking contract +gathered 7500 EGLD it can spawn 3 new nodes. -Copy the generated `validatorKey.pem` file to the `config` folder of your node(s), and repeat for each node. -```bash - cp validatorKey.pem ~/elrond-nodes/node-0/config/ +### Base stake and top-up -``` +As stated, a validator node requires at least 2500 EGLD. So multiple nodes would mean at least _2500 multiplied by the number of nodes_ EGLD in the contract. +The difference is considered top-up. Also, the staking provider owner can choose to keep the tokens as top-up, even +if the top-up is enough to spawn a new validator node. -:::tip -Each node needs its unique `validatorKey.pem` file -::: +Let's take some examples: -Then copy the `validatorKey.pem` file - in ZIP form - to the `$HOME/VALIDATOR_KEYS/` folder . This is important for your node to be able to restart correctly after an upgrade. +a). A Staking Contract has 2550 EGLD. This would mean a base stake of 2500 EGLD + 50 EGLD top-up -```bash -zip node-0.zip validatorKey.pem -mv node-0.zip $HOME/VALIDATOR_KEYS/ +b). A Staking Contract has 5200 EGLD. This could mean: -``` +- a base stake of 2500 EGLD (1 node) + 2700 EGLD top-up +- a base stake of 5000 EGLD (2 nodes) + 200 EGLD top-up -Repeat the above process for all your “n” nodes. When complete, please refer to our Key management section for instructions about how to properly backup and protect your keys. +Network-wise, the base stake is currently limited to 8,000,000 EGLD (3200 nodes \* 2500 EGLD / node). However, current staking +metrics indicate that the total EGLD staked is around 13,000,000 EGLD, resulting in a base stake of 8 millions EGLD + ~5 millions EGLD top-up. -### **Start the node(s)** +### Service Fee -```bash -~/mx-chain-scripts/script.sh start -``` +At each epoch change, the delegation contract receives the rewards in accordance to its stake. Over those rewards, +a service fee applies so the owner can cover the hosting and nodes management costs. +So for example, if the rewards are 10 EGLD in an epoch, and the service fee is set to 10%, the owner of the staking +contract will be eligible for 1 EGLD, while the difference (9 EGLD) will be allocated to the delegators. -### **Start the node visual interface** -Once the node has started, you can check its progress, using the `TermUI` interface. Navigate to your `$HOME/elrond-utils` directory and start the `TermUI`, one for each of your nodes: +### Inflation Rate -```bash -cd $HOME/elrond-utils -./termui -address localhost:8080 -``` +MultiversX's economics model is based on an inflation rate that decreases each year. More about this can be read on the +[blog](https://multiversx.com/blog/the-wealth-of-crypto-networks-elrond-economics-paper). -:::tip +This means that for each year the estimated rewards for a validator will change. This has to be taken into account +when computing the APR. -Your first node is called `node-0` and it is a REST API that will run on port `8080` by default. The next node is `node-1`on port `8081`, and so on. -::: +The configuration for the inflation rate can be found [here](https://github.com/multiversx/mx-chain-mainnet-config/blob/master/economics.toml) (`YearSettings`). +The protocol doesn't take leap years into consideration, but rather approximate each year at 365 days. -## **Update your node(s)** +The approximated inflation rate is as follows: -Upgrade your node by running the script and selecting either of these options: +| Year | Start Date | Inflation rate | +|------|-------------|----------------| +| 1 | 2020.07.30 | 10.84% | +| 2 | 2021.07.30 | 9.7% | +| 3 | 2022.07.30 | 8.56% | +| 4 | 2023.07.30 | 7.42% | +| 5 | 2024.07.29 | 6.27% | +| 6 | 2025.07.29 | 5.13% | +| 7 | 2026.07.29 | 3.99% | +| 8 | 2027.07.29 | 2.85% | +| 9 | 2028.07.28 | 1.71% | +| 10 | 2029.07.28 | 0.57% | +| 11 | 2030.07.28 | 0% | -- `14 - github_pull` downloads the latest version of the scripts -- `4 - upgrade` -- `9 - start` -- `18 - quit` -```bash -~/mx-chain-scripts/script.sh -``` +### Protocol Sustainability -These are the basic steps. Please carefully read the on-screen instructions, refer to the scripts [readme file](https://github.com/multiversx/mx-chain-scripts/blob/master/README). You can also ask any questions in the MultiversX [Validators chat](https://t.me/MultiversXValidators) +In accordance to the Mainnet's [configuration](https://github.com/multiversx/mx-chain-mainnet-config/blob/master/economics.toml#L35) (`ProtocolSustainabilityPercentage`). +at each epoch change, when new tokens are distributed among the validators, 10% of the value goes to Protocol Sustainability Address. +This also has to be taken into account when calculating the APR. -## **Mandatory: Backup your keys** -Your private keys are needed to run your node. Losing them means losing control of your node. A 3rd party gaining access to them could result in loss of funds. +## Rewards calculation -Find them in `$HOME/elrond-nodes/node-0/config` [be mindful of your “`n`” nodes] +When wanting to calculate the APR (Annual Percentage Return) of a Staking Provider, there are multiple factors that have +to be taken into account, such as total value locked at Network-level, the inflation based on the current year, the +staking provider base stake and top-up stake, and so on. -:::important -Create a safe backup for them on storage outside of the server running your node(s). -::: +### Network Top-Up rewards -## **Migration from old scripts** +The formula for determining the rewards received by the validators for the top-up in a given epoch is: -Before the release of the current `mx-chain-scripts`, there were the `elrond-go-scripts-testnet`, `elrond-go-scripts-devnet` and `elrond-go-scripts-mainnet` for setting up nodes -on the testnet, devnet and mainnet respectively. Those three repositories have been deprecated because `elrond-go-scripts` can be used to manage nodes regardless of their target network (`testnet`, `devnet` or `mainnet`). +$$ +topUpRewards(e) = \frac{2 * topUpRewardLimit(e)}{\pi} * atan(\frac{eligibleCumulatedTopUp(e)}{p}) +$$ -If one wants to migrate from the old scripts to the new ones, it is generally possible to do so while preserving the validator keys, current node installation, DB and logs. -These are the steps to be followed: +Where: -- clone the `mx-chain-scripts` repo near the old one (`elrond-go-scripts-testnet`/`elrond-go-scripts-devnet`/`elrond-go-scripts-mainnet`); assuming the old scripts were located in the home directory, run the following: +- `e` represents the given epoch +- `topUpRewardLimit(e)` represents the maximum top-up rewards that can be distributed in the given epoch. This can be viewed + as the maximum value out of the epoch rewards that can be distributed as rewards for the top-up stake, and depends + on the total rewards to be distributed in the epoch and a configured network parameter that defines the proportion out of the total rewards. +- `eligibleCumulatedTopUp(e)` represents the rewards distributed in the epoch for signing and proposing blocks. + This does not include the protocol sustainability rewards, developer fees or the penalty for missed blocks. +- `p` represents a chosen parameter to control the gradient of top-up rewards. It can be viewed as the cumulated top-up stake + where the given top-up rewards reach ½ of the top-up rewards set limit. It is currently set to 2M EGLD. -``` -cd ~ -git clone https://github.com/multiversx/mx-chain-scripts -``` -- configure the new scripts as described in the sections above; -- make sure you set the new `ENVIRONMENT` variable declared within `~/mx-chain-scripts/config/variables.cfg`; it must contain one of `"testnet"`, `"devnet"` or `"mainnet"`; -- call the `migrate` operation on the scripts: +### Network base rewards -``` -cd ~/mx-chain-scripts -./script.sh migrate -``` +$$ +baseRewards(e) = blocksRewards(e) - topUpReward(e) +$$ -Be careful as to not mix the previous installation network with the new one. This might lead to unpredictable results. +Where: +- `blocksRewards(e)` represents the rewards received by the validators by either signing or proposing blocks during the epoch that are now + part of the canonical chain +- `topUpReward(e)` is computed above -## **Choosing a custom configuration tag or branch** -:::caution -This option should be only used when debugging or testing in advance of a pre-release tag. -Use this on your own risk! -::: +## APR calculation -The power of the scripts set has been leveraged with a new addition: the possibility to tell the scripts a specified tag -or branch (not recommended using a branch due to the fact that an unsigned commit might bring malicious code or configs) +After determining the base and the top-up rewards for an epoch, the APR can be calculated for a Staking Provider. -To accomplish this, edit the variables.cfg file +First, we have to determine the maximum rewards that can be reached in ideal situations (no missed block in an epoch). -``` -cd ~/mx-chain-scripts/config -nano variables.cfg -``` -locate the `OVERRIDE_CONFIGVER` option and input a value there, something like `tags/T1.3.14.0`. -The `tags/` prefix will tell the scripts to use the tag and not search a branch called `T1.3.14.0`. -Call the `upgrade` command on the scripts to install the desired configuration version. +### Staking Provider base stake rewards -Resetting the value to `""` will make the scripts to use the released version. +We have to calculate the estimated rewards received by a Staking Provider in one epoch for the base stake. -:::caution -The `OVERRIDE_CONFIGVER` is not backed up when calling `github_pull` operation. -::: +This is done by calculating the share of the total rewards in accordance to the provider's number of nodes. -## **Troubleshooting** +Therefore, if a Staking Provider has 320 nodes out of 3200 nodes, it will receive 10% of the base rewards. Note that the base rewards +are calculated after decreasing the protocol sustainability rewards, as well as the computed top-up rewards. -If the node fails to start and the termui window shows messages like: -``` -termui websocket error, retrying in 10s... -termui websocket error, retrying in 10s... -termui websocket error, retrying in 10s... -``` +$$ +stakingProviderBaseStakeRewards(e) = \frac{stakingProviderNumberOfNodes}{totalNumberOfNodesInNetwork} * baseRewards(e) +$$ -a good method to check what the node is trying to do at startup (and fails) is to issue this command: -```bash -sudo journalctl -f -u elrond-node-XXX.service -``` - by replacing `XXX` with the actual node instance on the machine: 0, 1, 2, 3... +### Top-Up rewards ---- +Similar to base stake rewards, the rewards for top-up are estimated by computing the share of the provider's top-up in +accordance to the network's total top-up. -### Manage a validator node +$$ +stakingProviderTopUpRewards(e) = \frac{stakingProviderTopUpAmount}{networkTotalTopUp} * topUpRewards(e) +$$ -Your node will start as an observer. In order to make it into a validator, you will need to have 2500 xEGLD tokens. You can reach out to an admin in our [Telegram community](https://t.me/MultiversXValidators) who will gladly help. -Follow these steps to manage your validator node. +### APR calculation -Let’s begin! +In order to obtain the estimation of the APR, we first need to calculate the share of the provider's earnings in an epoch as compared +to it's total stake locked. Then we will multiply by 365 (the number of days in a year) and get the result. -First, you need to create a MultiversX wallet. You can create this wallet on either the [mainnet](https://wallet.multiversx.com), [devnet](https://devnet-wallet.multiversx.com) or [testnet](https://testnet-wallet.multiversx.com). +$$ +aprWithoutFee = \frac{stakingProviderBaseStakeRewards(e) + stakingProviderTopUpRewards(e)}{providerTotalStake} * 365 +$$ -:::tip -For devnet and testnet only: +The last step is to decrease the fee deducted by the staking provider owner: -Share your wallet's public address (erd1...) with an admin on the Telegram community, and you will receive test xEGLD. If a smaller amount of tokens is needed, -you can use the [wallet's faucet](/wallet/web-wallet#testnet-and-devnet-faucet). -::: +$$ +apr = \frac{100 - fee}{100} * aprWithoutFee +$$ -Once you have sufficient funds, you can use the wallet to send a stake transaction for your node, in order for it to become a Validator. -In the wallet, navigate to the “Stake” section and click on the “Stake now” button at the top right of the page. -![img](https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlZkUHx72OLJMbqkEHff2%2Fuploads%2FlJg5GyCzq7NI9nmqiKJ5%2Fwalletelrond2.PNG?alt=media&token=136796b5-b10b-4df0-b83b-038419e57ed6) +## Example -Select the `validatorKey.pem` file you created for your node and proceed with the instructions displayed. +The formulas and all the mathematics involved might be quite complicated, so let's take an example. -![img](https://gblobscdn.gitbook.com/assets%2F-LhHlNldCYgbyqXEGXUS%2F-MKj4PGWn3kQ197_YcJQ%2F-MKjC2SwfiK2OdVWTz49%2Fimage.png?alt=media&token=9d38ba79-9d47-452e-8fb3-303f0edf5740) +Let's say we have the following parameters: -You can check the status of your Stake transaction and other information about the validator node in the explorer at [mainnet explorer](https://explorer.multiversx.com), [devnet explorer](https://devnet-explorer.multiversx.com) or the [testnet explorer](https://testnet-explorer.multiversx.com). Make sure to check out the Validators section too. +Network parameters -![img](https://gblobscdn.gitbook.com/assets%2F-LhHlNldCYgbyqXEGXUS%2F-MKj4PGWn3kQ197_YcJQ%2F-MKjCya_zwNCJWCZ4ryI%2Fimage.png?alt=media&token=7a1a0e1c-dc77-41ef-afcd-296dd23da18b) +``` +genesisTotalSupply = 20M EGLD +inflationRate = 9.7% (year 2) +p = 2M EGLD +totalNodes = 3200 +eligibleCumulatedTopUp = 2.6M +totalCumulatedTopUp = 5.2M +protocolSustainabilityRewards = 10% +numDaysInAYear = 365 +topUpFactor = 0.5 +``` -:::important -To distinguish between the mainnet and other networks (devnet and testnet), we have carefully created different addresses for the devnet tools, which are also presented in a predominantly black theme. Be cautious and know the difference, to avoid mistakes involving your mainnet validators and real EGLD tokens. -::: +Staking provider parameters: ---- +``` +stakingProviderNumberOfNodes = 10 +stakingProviderBaseStake = 25,000 EGLD +stakingProviderTopUpAmount = 6,472 EGLD +stakingProviderTotalStake = 31,472 EGLD +fee = 2% +``` -### Merging A Validator Into An Existing Delegation Smart Contract +For a random day, the maximum rewards that can be distributed is: -Introduced in Staking Phase 3.5, the ability of merging one or more existing standalone validator node into a staking provider gives more flexibility for staking provider operators. +``` +maximumRewardsInADay(e) = inflationRate * genesisTotalSupply / numDaysInAYear = 9.7% * 20M / 365 = 5315 EGLD +``` -There are two steps required for this action: The owner of the Delegation SC has to whitelist the wallet from which the Merging Validator was staked from. Then the Merging Validator has to send the merge transaction from the whitelisted wallet. +We have to decrease the protocol sustainability rewards, resulting in: -1. Merging a Validator into an Existing Delegation Smart Contract +``` +maximumRewardsInADay(e) = 4783 EGLD +``` -From the Delegation Smart Contract owner's wallet, send a transaction with the following parameters: +The maximum top-up reward for the epoch is: -```rust -Whitelist Wallet For Merging - Sender: - Receiver: - Value: 0 - Gas Limit: 5000000 - Data: "whitelistForMerge" + - "@" + "" +``` +topUpRewardLimit(e) = topUpFactor * maximumRewardsInADay(e) = 0.5 * 4783 =~ 2391 EGLD ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +Therefore, the network top-up would be: -:::tip -You can obtain the HEX format of an address by first converting its bech32 (erd1...) form into binary, and then converting the resulting binary into HEX. -::: +``` +topUpRewards(e) = (2 * topUpRewardLimit(e) / pi) * atan(eligibleCumulatedTopUp(e) / p) = (2 * 2391 / pi) * atan(2.6M / 2M) =~ 1522 * 0.91 =~ 1385 EGLD +``` -2. The Merging Validator sends the merge transaction from the whitelisted wallet: +The base rewards would be: -```rust -Whitelist Wallet For Merging - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6 - Value: 0 - Gas Limit: 510000000 - Data: "mergeValidatorToDelegationWithWhitelist" + - "@" "" +``` +baseRewards(e) = blocksRewards(e) - topUpReward(e) = maximumRewardsInADay(e) - topUpReward(e) = 4783 - 1385 = 3398 EGLD ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - -:::caution -We advise against using this method to buy or sell validator slots - it requires the transfer of private keys (validatorKey.pem) which can't be changed. This puts the buyer at risk of slashing, should the seller deploy a node with the same key, either intentionally or by mistake. -::: +Moving to the staking provider: -## **Merging own node(s)** +``` +stakingProviderBaseStakeRewards(e) = stakingProviderNumberOfNodes / totalNodes * baseRewards(e) = 10 / 3200 * 3398 = 10.61 EGLD +stakingProviderTopUpRewards(e) = stakingProviderTopUpAmount / totalCumulatedTopUp * topUpRewards(e) = 6472 / 5.2M * 1385 = 1.72 EGLD +``` -If the owner address of the node(s) and Delegation SC is the same use, whitelisting is not needed. +And, finally, calculating the APR: -```rust -Merge Own Nodes - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6 - Value: 0 - Gas Limit: 510000000 - Data: "mergeValidatorToDelegationSameOwner" + - "@" "" +``` +aprWithoutFee = (stakingProviderBaseStakeRewards(e) + stakingProviderTopUpRewards(e)) / providerTotalStake * 365 = (10.61 + 1.72) / 31472 * 365 = 14.29 % ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +After deducting the fee: -:::caution -Whenever merging or converting direct staked keys into a staking provider pool, please be aware that the BLS key's signature will be altered automatically and re-staking an unbonded node is no longer possible. -In other words, if you attempt this flow of operations unStakeNodes->unBondNodes for a merged key, make sure you call also the removeNodes operation. Otherwise, the stakeNodes or reStakeUnStakedNodes will fail. -::: +``` +apr = (100 - fee) / 100 * aprWithoutFee = (100 - 2) / 100 * 14.29 = 14.00 % +``` --- -### Multikey nodes management +### Staking v4 -This page contains information about how to manage multiple keys on a group of nodes. +# **Introduction** +Staking and delegation are processes that evolve over time. No system has to remain static. Our assumptions about how +the market works and reacts can change, just as user behavior and market dynamics may evolve. Currently, we have +approximately 400 validators, with some acting as staking providers and others as individual validator operators. While +most nodes have a comfortable top-up on the base stake of 2.500 eGLD, some do not contribute to the network's security by +adding more top-up. -## Multikey architecture overview -Since the mainnet launch, and up until the release candidate RC/v1.6.0, each node could have managed only -one key. So the relationship between the nodes and staked validator keys is `1:1`, the node "following" the shard where -the managed key is assigned. -The multikey feature allows a node instance to hold more than one key. However, since MultiversX is a sharded blockchain -and a single node is only able to store the state of a single shard at a time, we need a group of nodes with exactly -one node in each shard, similar with what we have on observing squad. Also, in each epoch, the keys can be shuffled among shards. -This means that running multiple keys will require at least the number of shards + 1 node instances (one for each shard + metachain). -The same set of keys should be provided for all node instances. +# **Limitations of the Current Implementation** -This type of nodes used in multikey operation can be assimilated as a hybrid between an observer node and a validator. -It behaves as an observer by holding in the `validatorKey.pem` file a BLS key that will never be part of the consensus -group and the shard should be specified in the `prefs.toml` file, so the node will not change shards. -The node behaves also as a validator (or multiple validators) by monitoring and emitting consensus messages, -whenever required on the behalf of the managed keys set. +- Limiting the number of nodes to 3200, creating an additional queue. New validators can join the network only if + someone leaves the system. +- Concentration of power among large providers, hindering decentralization. Top 11 staking agencies control 33%. -Since an observer already performs block validation in its own shard, it can be easily used to manage a group of validator -keys and propose or validate blocks on behalf of the keys it possesses. -To summarize, this type of node can use any provided keys, in any combination, to generate consensus messages provided -that those used keys are part of the consensus group in the current round. With the multikey feature, the relationship -now becomes `n:m`, providing that `n` is the number of keys managed by an entity and `m` is the number of shards + 1. +One of our primary objectives is to eliminate the additional queue and leverage the top-up value per node to determine +the best nodes. This ensures that we do not restrict the entry of new validators, as the current system requires an old +validator to leave for a new one to enter. The market will determine the actual node price, operating as a soft auction +where anyone paying the node price (2.500 eGLD) can register, but the node becomes a validator only if it has sufficient +top-up. The shuffling from the waiting list to the eligible list remains unaffected, focusing solely on selecting nodes +from the auction list to the waiting list. -:::important -This feature is purely optional. Normal `1:1` relationship between the keys and the nodes is still supported. The multikey -mode should optimize the costs when running a set of keys (check [Economics running multikey nodes](#economics-running-multikey-nodes) section) -::: +This approach, known as a _Soft Auction_, democratizes the validator system, transforming it into a free market. -The following image describes the keys and nodes relationship between the single operation mode versus multikey operation mode. - -![img](/validators/multikey-diagram.drawio.png) +The selection process for the best X nodes from the auction into the "to be shuffled into waiting" list can be executed +in two ways. +**The first** and easy version is a strict selection where all nodes from the auction list are sorted based on their +topUp, and only the first ones are selected. -## General implementation details +This approach is strict, requiring validators to manage their number of nodes if their topUp is close to the selection +threshold. The topUp is computed as TotalStake/NumberOfNodes - 2.500 eGLD. If a validator registers more nodes, their +topUp per node decreases. For example: -The nodes running with the multikey feature, beside deciding the consensus group (which is normally done on each node), -can access the provided keys set and use, in any combination, one or more keys, if the node detects that at least one managed -key is part of the consensus group. -The code changes to support multikey nodes affected mainly the `consensus`, `keyManagement` and `heartbeat` packages. +- 10 nodes, 50.000 eGLD = 2.500 eGLD topUp per node +- 20 nodes, 50.000 eGLD = 0 eGLD topUp per node +In this scenario, if a staking provider registers all 20 nodes, they will be at the end of the list in case of an +auction selection. If only 320 nodes out of 340 can be selected, and every other node has at least 1 eGLD topUp on each +of them, none of the 20 nodes from this staking provider will be selected. -### Heartbeat information +Since this selection occurs at the end of every epoch, staking providers near the topUp limit must continually monitor +and adjust their nodes, unstaking or staking nodes based on the topUp per node of other providers. Sorting nodes based +on topUp does not provide adequate protection for staking providers, requiring constant supervision and action. -The group managing the set of keys (we will call them multikey nodes or multikey group), will pass the validators BLS -information tight to "virtual" peer IDs. A "virtual peer ID" is a generated p2p identity that the p2p network can not -connect to as it does not have a real address bind to. Consequently, this feature brings a new layer of security as -the multikey nodes will hide the relationship between the validator BLS keys and the host that manages those BLS keys. +**The second version**, currently implemented and explained in the following chapters, addresses the shortcomings of the +first version. -### Redundancy +# **Current Implementation** -The redundancy sub-system has been upgraded to accommodate the multikey requirements keeping the multiple redundancy -fallback groups operation. A fallback multikey group will monitor each managed key for missed consensus activity **independent on -each managed node**. So, a bad configured main group, offline or stuck main group nodes should trigger fallback events on -the redundancy group. -Example: if main multikey group was set to manage the following key set `[key_0, key_1 ... key_e-1, key_e+1 ... key_n]` -(mind the missing `key_e`) and the redundancy fallback multikey group has the set `[key_0, key_1 ... key_e-1, key_e, key_e+1 ... key_n]`, -then, the fallback group, after `k` misses in the consensus activity (propose/sign block) will start using that `key_e` as it -was the only key assigned to the multikey group (`k` is the value defined in the `prefs.toml` file, `RedundancyLevel` option). +:::note +Please note that the numbers below are indicative and only used to better exemplify the model. +::: +In the current implementation (staking 3.5), we have: -## Economics running multikey nodes +1. A capped number of 3200 **nodes in the network**, including: + - 1600 active/eligible validators globally, split into 400 nodes per shard + - 1600 waiting validators globally, split into 400 nodes per shard +2. An uncapped `FIFO` queue where newly staked nodes are placed and await participation in the network. -As for `n` managed keys we will need at least a group of nodes, there is a threshold that a staking operator -will want to consider when deciding to switch the operation towards the multikey mode. The switch becomes attractive for the -operator when the number of managed keys is greater than the number of shards. So, for the time being, when we have -at least 5 keys that are either *eligible* or *waiting*, the switch to multikey mode becomes feasible. +:::important + +Currently, a queued node can participate in the network only if an existing node is either unstaked or jailed. -:::caution -Although there are no hard limits in the source code to impose a maximum number of keys for a multikey group, the MultiversX team -strongly recommends the node operators to not use more than 50 keys per group. The reason behind this recommendation is that a single node -controlling enough keys could cause damage to the chain as, in extreme cases, it could propose consecutive bad blocks, disrupting the -possibility of blocks synchronization or blocks cross-notarization. ::: +![Current Staking](/validators/stakingV4/current-staking.png) -## Usage +Nodes are distributed in the following steps: +1. Randomly shuffle out 80 eligible validators for each shard, resulting in 320 (80 validators per 4 shards) + shuffled-out validators. +2. Select these 320 shuffled-out validators to be randomly but evenly distributed at the end of each shard's waiting + list. +3. For each shard, replace the previously shuffled-out validators with 80 waiting validators from the top of each + shard's waiting list. -### allValidatorsKeys.pem file -The switch towards the multikey operation will only require aggregating all BLS keys in a new file, called `allValidatorsKeys.pem` -that will be loaded by default from the same directory where the `validatorKey.pem` file resides. The path can be altered by using the -binary flag called `--all-validator-keys-pem-file`. -Example of an `allValidatorsKeys.pem` file: -``` ------BEGIN PRIVATE KEY for e296e97524483e6b59bce00cb7a69ec8c0d1ac4227925f07fdd57b3ab4ec2f64b240728a0a3c5be2930aea570bf12c12314e25d942b106472800e51524add26ec9546475c1cfae91dd7e799f256d1b0758e17aaa3898c29d489bd87c86d04498----- -YzJlODM0NTdmOTVmYMDVjZGRiNzdiODc1N2YyZGEx -ZGRhYWY5MTI5Y2NlOWQyOQ== ------END PRIVATE KEY for e296e97524483e6b59bce00cb7a69ec8c0d1ac4227925f07fdd57b3ab4ec2f64b240728a0a3c5be2930aea570bf12c12314e25d942b106472800e51524add26ec9546475c1cfae91dd7e799f256d1b0758e17aaa3898c29d489bd87c86d04498----- ------BEGIN PRIVATE KEY for 5585ddceb6b7bf0d308162efd895d0717b22bab6b0412f09fb9cee234be73d197bfef8ae10064be5733472c573894015029672b70f63e0b58c7ab2e831ee0aff88b868e4d712bec0baf9a1cd1982e138af9b6cc55e4454b01cb8ad02a064f515----- -MzNlZjQyYTRhZDc3ZDBkZDk1M2JmNGIwNWE2MzczMmYxZWUy -ZWVkNzNiOGQ1ZDQ0NmEzMg== ------END PRIVATE KEY for 5585ddceb6b7bf0d308162efd895d0717b22bab6b0412f09fb9cee234be73d197bfef8ae10064be5733472c573894015029672b70f63e0b58c7ab2e831ee0aff88b868e4d712bec0baf9a1cd1982e138af9b6cc55e4454b01cb8ad02a064f515----- ------BEGIN PRIVATE KEY for 791c7e2bd6a5fb1371af18269267ad8ef9e56e264c4c95703c57526b16b84dd8df6347c0cc14f93d595a12316d38ae11264e05d2fa26d80387d12db52c1a98e93064d073d02549c71ec4e352d73724c21c02245b25d3643b532fac25d7580f0b----- -OTcxYjYyNWMzMzlkY2JhNTAyODMwNzZlYjMyY2MxMmYzNThiMjNiNzYz -NTA4YjFjMTVlYTIwNDYyMw== ------END PRIVATE KEY for 791c7e2bd6a5fb1371af18269267ad8ef9e56e264c4c95703c57526b16b84dd8df6347c0cc14f93d595a12316d38ae11264e05d2fa26d80387d12db52c1a98e93064d073d02549c71ec4e352d73724c21c02245b25d3643b532fac25d7580f0b----- -``` +In the current implementation, each node, regardless of its top-up, has equal chances of participating in the consensus. +Starting with staking phase 4, the probability of validators entering the validation process will be significantly +influenced by the amount of their staked top-up. Validators with a higher staked top-up will have considerably greater +chances of participation, while those with little or no top-up will find their chances of entering into validation +markedly reduced. -### prefs.toml file +# **Staking V4** -The existing fields `NodeDisplayName` and `Identity` will be applied to all managed and loaded BLS keys. The `NodeDisplayName` will -be suffixed with an order index for all managed keys. -For example, suppose we have the above example of the `allValidatorsKeys.pem` file and the `NodeDisplayName` is set to `example`. -The name for the managed keys will be: -``` -example-0 e296e97524483e6b59... -example-1 585ddceb6b7bf0d308... -example-2 791c7e2bd6a5fb1371... -``` +Staking phase 4 will unfold in three consecutive steps, each corresponding to a specific epoch. -If part of the managed BLS keys will need to be run on a different identity and/or different naming, the file section called -`NamedIdentity` will be of great use. -Following the above example, if we need to use a different identity and/or node name for the `791c7e2bd6a5fb1371...` key, -we will need to define the section as: -``` -# NamedIdentity represents an identity that runs nodes on the multikey -# There can be multiple identities set on the same node, each one of them having different bls keys, just by duplicating the NamedIdentity -[[NamedIdentity]] - # Identity represents the keybase/GitHub identity for the current NamedIdentity - Identity = "identity2" - # NodeName represents the name that will be given to the names of the current identity - NodeName = "random" - # BLSKeys represents the BLS keys assigned to the current NamedIdentity - BLSKeys = [ - "791c7e2bd6a5fb1371af18269267ad8ef9e56e264c4c95703c57526b16b84dd8df6347c0cc14f93d595a12316d38ae11264e05d2fa26d80387d12db52c1a98e93064d073d02549c71ec4e352d73724c21c02245b25d3643b532fac25d7580f0b" - ] -``` -which will generate the naming as: -``` -example-0 e296e97524483e6b59... -example-1 585ddceb6b7bf0d308... -random-0 791c7e2bd6a5fb1371... -``` +## Staking v4. Step 1. +In the first step, we will completely **remove the staking queue** and **unstake all nodes from the staking queue**. +This process will occur automatically at the end of the epoch and requires no interaction from validators. +Nodes' distribution remains unchanged. -### Security notes for the multikey nodes +For owners which had **unstaked** nodes, these can be **restaked** using 'RestakeUnstakedNodes' complete details [here](https://docs.multiversx.com/validators/delegation-manager/#restaking-nodes) -As stated above, the multikey feature is able to use any number of keys on a small group of nodes. -At the first sight, this can be seen as a security degradation in terms of means of attacking a large staking provider but there are ways to mitigate these concerns as explained in the following list: -1. use the recommendation found in this page regarding the maximum number of keys per multikey group; -2. for each main multikey group use at least one backup multikey group in case something bad happens with the main group; -3. use the `NamedIdentity` configuration explained above to obfuscate the BLS keys and their declared identity from the actual nodes that manage the keys. +![Staking V4 Step 1](/validators/stakingV4/stakingV4-step1.png) -Regarding point 3, each managed BLS key will create a virtual p2p identity that no node from the network can connect to since it does not advertise the connection info but is only used to sign p2p messages. -Associated with a separate named identity, the system will make that BLS key virtually unreachable, and its origin hidden from the multikey nodes. Therefore, the node operators will need to apply the following changes on the `prefs.toml` file: -* in the `[Preference]` section, the 2 options called `NodeDisplayName` and `Identity` should be changed to something different used in the BLS' definitions to prevent easy matching. Generic names like `gateway` or `observer` are suitable for this section. -Also, completely random strings can be used as to be easier to identify the nodes in explorer. The `Identity` can be left empty; -* in the `[[NamedIdentity]]` section, the 2 options called `NodeName` and `Identity` will be changed to the real identities of the BLS keys: such as the staking provider brand names. **They should be different from the ones defined in the `[Preference]` section.** +:::important Important notes -In this way, the operation will be somewhat similar to the *sentinel nodes* seen elsewhere. -The difference in our case is that the setup is greatly simplified as there is no separate network for the protected nodes that will need to be maintained. -The security of our setup (if points 1, 2 and 3 are applied) should be the same with a *sentinel setup*. +Starting with this epoch: +- Every **newly staked** node will be placed in the **auction list**. +- Every **unjailed** node will be placed in the **auction list**. +- The global **number of new nodes** that can take part in the system is **uncapped**. +- Owners with **insufficient base staked EGLD** for their nodes will have them **removed** at the end of the epoch in + the following order: `auction` -> `waiting` -> `eligible`. -### Configuration example +::: -Let's suppose we have 5 BLS keys that belong to a staking provider called `testing-staking-provider` and we want to apply the security notes described above. -So, for the sake of the example, we generated 5 random BLS keys, the `allValidatorsKeys.pem` should contain something like this: -``` ------BEGIN PRIVATE KEY for 15eb03756fae81d2fbae392a4d7d82abdf7618ce3056b89376c2a46bc6e8403ed3cc84e12bc819c0b088ee46e7c28302d2b666b011714cc8ea2b75488907d07e194a6e83f0f3d15c7699de412de425314be5cc3ce6ab2c594690006f9915dd15----- -NDA5MWVjODMwZjU3MDhkYmQwNzk5ZWEwNjg2MDc0MzUzYmZjNThjM2ZhYzU2Y2I1 -ZGRhMjY3YTY1NjhkZjI1YQ== ------END PRIVATE KEY for 15eb03756fae81d2fbae392a4d7d82abdf7618ce3056b89376c2a46bc6e8403ed3cc84e12bc819c0b088ee46e7c28302d2b666b011714cc8ea2b75488907d07e194a6e83f0f3d15c7699de412de425314be5cc3ce6ab2c594690006f9915dd15----- ------BEGIN PRIVATE KEY for ff12bc7f471e2e375c6e8b981f13ed823dcca857c41a2ffc3a0956283a8428a95754375dabc0b412df3ec41d2a51ef1490a8d23f4e4f9348787f9615093e0129969085488b59d2ab550467cd0d0fa33df22e2ed2d8c8c0c0f59042dafd0c1098----- -MTcwN2ZlMzFhMzk3Y2VjOWM4ZjdmMWU3Njg4MjY3YTAwOWU5ZjJmMWYxY2Y0ZjFl -MzI2Y2M5NGJiZGFjNGQwZA== ------END PRIVATE KEY for ff12bc7f471e2e375c6e8b981f13ed823dcca857c41a2ffc3a0956283a8428a95754375dabc0b412df3ec41d2a51ef1490a8d23f4e4f9348787f9615093e0129969085488b59d2ab550467cd0d0fa33df22e2ed2d8c8c0c0f59042dafd0c1098----- ------BEGIN PRIVATE KEY for 3dec570c02a4444197c1ed53fefd7e57acb9bc99ae47db7661cfbfb47170418702162a46ed40e113e3381d68b713e903e286ffaf9cac77fed8f9c79e83f2abb0ccd690ef4f689607b6414a6f893e0c0ced93d7456240bbccbf223f7603dd8e05----- -ZWMwYWRjYjNiYTQ0YmM4MGM5ZjhmNTlkNTU5YTRlMWJlMTI2ODFmMDlmM2JiNTM4 -MmMyYzdlYmNhYjNkNTk2MA== ------END PRIVATE KEY for 3dec570c02a4444197c1ed53fefd7e57acb9bc99ae47db7661cfbfb47170418702162a46ed40e113e3381d68b713e903e286ffaf9cac77fed8f9c79e83f2abb0ccd690ef4f689607b6414a6f893e0c0ced93d7456240bbccbf223f7603dd8e05----- ------BEGIN PRIVATE KEY for 38a93e3c00128c31769823710aa7deb145591b99a78c87dbd74c894afd540ade6de3906b45001d3f5a5882db34eaf30e412bef77ed43cf5a394edd0aa70254a74db1c80eef5d41342cae76fbbae596bc811fa491e00f16a7e011a836f7ceaa15----- -YWMzMDk2ZjY3NmExNjhiNTQ5ODQzM2JiM2NiZWFmNzkyYjQyYWZhZjJlZmMwNjNl -YzdhMWI5OGM1ZDdjODg1MQ== ------END PRIVATE KEY for 38a93e3c00128c31769823710aa7deb145591b99a78c87dbd74c894afd540ade6de3906b45001d3f5a5882db34eaf30e412bef77ed43cf5a394edd0aa70254a74db1c80eef5d41342cae76fbbae596bc811fa491e00f16a7e011a836f7ceaa15----- ------BEGIN PRIVATE KEY for 1fce426b632e5a5941d9989e4f8bbb93a0a08a0e85dfe16d4d65c08b351dfbff1a1104d5e75e1be7565b4bbc6a583103bfc4b4075727133a54fa421983d894e549576364694b3e8910359b3de5260360bfe9f9bea2fec1cb50c2cf79a3fd590d----- -ZmYzMjM2ODljODQwMDRiMDI1MGU0NjcyMzhjYjJlMDNlNzg0OGI0YzQ1ZTM0ZjQz -YTZkZDVmNTBjYjAwMjAyNg== ------END PRIVATE KEY for 1fce426b632e5a5941d9989e4f8bbb93a0a08a0e85dfe16d4d65c08b351dfbff1a1104d5e75e1be7565b4bbc6a583103bfc4b4075727133a54fa421983d894e549576364694b3e8910359b3de5260360bfe9f9bea2fec1cb50c2cf79a3fd590d----- -``` +For example, if an owner has insufficient base stake for their nodes, the nodes will be removed from the network at the +end of the epoch based on the order: `auction` -> `waiting` -> `eligible`. This ensures that nodes contributing to the +ecosystem with a healthy top-up will not be adversely affected. -The staking operators that will create the actual `allValidatorsKeys.pem` file used on the chain should concatenate all keys from their `validatorKey.pem` files with a text editor. -The content should resemble the one depicted in this example. +Below is an example of how nodes are unstaked based on insufficient base stake. Suppose an owner has four nodes: -For the `prefs.toml` file, we can have definitions like: +- 1 eligible: `node1` +- 2 waiting: `node2`, `node3` +- 1 auction: `node4` -```toml -[Preferences] - # DestinationShardAsObserver represents the desired shard when running as observer - # value will be given as string. For example: "0", "1", "15", "metachain" - # if "disabled" is provided then the node will start in the corresponding shard for its public key or 0 otherwise - DestinationShardAsObserver = "0" +Assuming a minimum price of 2500 EGLD per staked node, the owner should have a minimum base stake of 10,000 EGLD (4 * +2500 EGLD). If, during the epoch, the owner unstakes 4000 EGLD, resulting in a base stake of 6000 EGLD, only two staked +nodes can be covered. At the end of the epoch, the nodes `node4` and `node3` will be unstaked in the specified order. - # NodeDisplayName represents the friendly name a user can pick for his node in the status monitor when the node does not run in multikey mode - # In multikey mode, all bls keys not mentioned in NamedIdentity section will use this one as default - NodeDisplayName = "s14" - # Identity represents the GitHub identity when the node does not run in multikey mode - # In multikey mode, all bls keys not mentioned in NamedIdentity section will use this one as default - Identity = "" +## Staking v4. Step 2. - # RedundancyLevel represents the level of redundancy used by the node (-1 = disabled, 0 = main instance (default), - # 1 = first backup, 2 = second backup, etc.) - RedundancyLevel = 0 +In the second step, all **shuffled-out** nodes from the **eligible list** will be sent to the **auction list**. Waiting +lists will not be filled by any shuffled-out nodes. - # FullArchive, if enabled, will make the node able to respond to requests from past, old epochs. - # It is highly recommended to enable this flag on an observer (not on a validator node) - FullArchive = false +Using the example above, this will resize each waiting list per shard from 400 nodes to 320 nodes. - # PreferredConnections holds an array containing valid ips or peer ids from nodes to connect with (in top of other connections) - # Example: - # PreferredConnections = [ - # "127.0.0.10", - # "16Uiu2HAm6yvbp1oZ6zjnWsn9FdRqBSaQkbhELyaThuq48ybdorrr" - # ] - PreferredConnections = [] +![Staking V4 Step 2](/validators/stakingV4/stakingV4-step2.png) - # ConnectionWatcherType represents the type of the connection watcher needed. - # possible options: - # - "disabled" - no connection watching should be made - # - "print" - new connection found will be printed in the log file - ConnectionWatcherType = "disabled" - # OverridableConfigTomlValues represents an array of items to be overloaded inside other configuration files, which can be helpful - # so that certain config values need to remain the same during upgrades. - # (for example, an Elasticsearch user wants external.toml->ElasticSearchConnector.Enabled to remain true all the time during upgrades, while the default - # configuration of the node has the false value) - # The Path indicates what value to change, while Value represents the new value in string format. The node operator must make sure - # to follow the same type of the original value (ex: uint32: "37", float32: "37.0", bool: "true") - # File represents the file name that holds the configuration. Currently, the supported files are: config.toml, external.toml, p2p.toml and enableEpochs.toml - # ------------------------------- - # Un-comment and update the following section in order to enable config values overloading - # ------------------------------- - # OverridableConfigTomlValues = [ - # { File = "config.toml", Path = "StoragePruning.NumEpochsToKeep", Value = "4" }, - # { File = "config.toml", Path = "MiniBlocksStorage.Cache.Name", Value = "MiniBlocksStorage" }, - # { File = "external.toml", Path = "ElasticSearchConnector.Enabled", Value = "true" } - #] +## Staking v4. Step 3. -# BlockProcessingCutoff can be used to stop processing blocks at a certain round, nonce or epoch. -# This can be useful for snapshotting different stuff and also for debugging purposes. -[BlockProcessingCutoff] - # If set to true, the node will stop at the given coordinate - Enabled = false +Starting with this epoch: - # Mode represents the cutoff mode. possible values: "pause" or "process-error". - # "pause" mode will halt the processing at the block with the given coordinates. Useful for snapshots/analytics - # "process-error" will return an error when processing the block with the given coordinates. Useful for debugging - Mode = "pause" +- Maximum number of nodes in the network will be changed from 3200 to 2880 (3200 - 320), consisting of: + - a global number of 1600 active/eligible validators, split into 400 nodes/shard + - a global number of 1280 waiting validators to join the active list, split into 320 nodes/shard +- All **shuffled out** nodes from the eligible list will be sent to the auction list to take part in the auction + selection. The more topUp an owner has, the higher the chances of having their auction nodes selected will be. +- Based on the _soft auction selection_ (see the next section), all **qualified** nodes from the **auction** will be + distributed to the waiting list (depending on the `available slots`). The other **unqualified** nodes will remain in + the auction and wait to be selected in the next epoch if possible. The number of available slots is based on the + number of shuffled-out nodes and other nodes leaving the network (e.g.: `unstake/jail`). It guarantees that the + waiting list is filled, and the nodes' configuration is maintained. +- Distribution from the `waiting` to the `eligible` list will remain unchanged. - # CutoffTrigger represents the kind of coordinate to look after when cutting off the processing. - # Possible values: "round", "nonce", or "epoch" - CutoffTrigger = "round" +![](/validators/stakingV4/stakingV4-step3.png) - # The minimum value of the cutoff. For example, if CutoffType is set to "round", and Value to 20, then the node will stop processing at round 20+ - Value = 0 -# NamedIdentity represents an identity that runs nodes on the multikey -# There can be multiple identities set on the same node, each one of them having different bls keys, just by duplicating the NamedIdentity -[[NamedIdentity]] - # Identity represents the GitHub identity for the current NamedIdentity - Identity = "testing-staking-provider" - # NodeName represents the name that will be given to the names of the current identity - NodeName = "tsp" - # BLSKeys represents the BLS keys assigned to the current NamedIdentity - BLSKeys = [ - "15eb03756fae81d2fbae392a4d7d82abdf7618ce3056b89376c2a46bc6e8403ed3cc84e12bc819c0b088ee46e7c28302d2b666b011714cc8ea2b75488907d07e194a6e83f0f3d15c7699de412de425314be5cc3ce6ab2c594690006f9915dd15", - "ff12bc7f471e2e375c6e8b981f13ed823dcca857c41a2ffc3a0956283a8428a95754375dabc0b412df3ec41d2a51ef1490a8d23f4e4f9348787f9615093e0129969085488b59d2ab550467cd0d0fa33df22e2ed2d8c8c0c0f59042dafd0c1098", - "3dec570c02a4444197c1ed53fefd7e57acb9bc99ae47db7661cfbfb47170418702162a46ed40e113e3381d68b713e903e286ffaf9cac77fed8f9c79e83f2abb0ccd690ef4f689607b6414a6f893e0c0ced93d7456240bbccbf223f7603dd8e05", - "38a93e3c00128c31769823710aa7deb145591b99a78c87dbd74c894afd540ade6de3906b45001d3f5a5882db34eaf30e412bef77ed43cf5a394edd0aa70254a74db1c80eef5d41342cae76fbbae596bc811fa491e00f16a7e011a836f7ceaa15", - "1fce426b632e5a5941d9989e4f8bbb93a0a08a0e85dfe16d4d65c08b351dfbff1a1104d5e75e1be7565b4bbc6a583103bfc4b4075727133a54fa421983d894e549576364694b3e8910359b3de5260360bfe9f9bea2fec1cb50c2cf79a3fd590d" - ] +## Staking v4. Soft Auction Selection Mechanism + +Nodes from the auction list will be selected to be distributed in the waiting list based on the **soft auction** +mechanism. For each owner, based on their topUp, we compute how many validators they would be able to run by +distributing their total topUp per fewer nodes (considering we would not select all of their auction nodes, but only a +part of them). This mechanism ensures that for each owner, we select as many nodes as possible, based on the **minimum +required topUp** to fill the`available slots`. This is a global selection, not per shard. We preselect the best global +nodes at the end of the epoch. + +Suppose we have the following auction list, and 3 available slots: + +![](/validators/stakingV4/soft-auction1.png) + +``` ++--------+------------------+------------------+-------------------+--------------+-----------------+-------------------------+ +| Owner | Num staked nodes | Num active nodes | Num auction nodes | Total top up | Top up per node | Auction list nodes | ++--------+------------------+------------------+-------------------+--------------+-----------------+-------------------------+ +| owner1 | 3 | 2 | 1 | 3669 | 1223 | pubKey1 | +| owner2 | 3 | 1 | 2 | 2555 | 851 | pubKey2, pubKey3 | +| owner3 | 2 | 1 | 1 | 2446 | 1223 | pubKey4 | +| owner4 | 4 | 1 | 3 | 2668 | 667 | pubKey5, pubKe6, pubKe7 | ++--------+------------------+------------------+-------------------+--------------+-----------------+-------------------------+ ``` -:::important -These 2 configuration files `allValidatorsKeys.pem` and `prefs.toml` should be copied on all n nodes that assemble the multikey group of nodes. +For the configuration above: -**Do not forget to change the `DestinationShardAsObserver` accordingly for each node.** -::: +- Minimum possible `topUp per node` = 667, considering `owner4` will have all of his **3 auction nodes selected** + - owner4's total top up/(1 active node + 3 auction nodes) = 2668 / 4 = 667 +- Maximum possible `topUp per node` = 1334, considering `owner4` will only have **one of his auction nodes selected** + - owner4's total top up/(1 active node + 1 auction node) = 2668 / 2 = 1334 -After starting the multikey nodes, in ~10 minutes, the explorer will reflect the changes. All n nodes that run the multikey group will broadcast their identity as an empty string and their names will be `s14`. -The BLS keys' identities, on the other hand will have the following names & identities: +Based on the above interval: `[667, 1334]`, we compute the `minimum required topUp per node` to be qualified from the +auction list. We gradually increase from min to max possible topUp per node with a step, such that we can fill +the `available slots`. At each step, we compute for each owner what's the maximum number of nodes that they could run by +distributing their total topUp per fewer auction nodes, leaving their other nodes as unqualified in the auction list. +This is a soft auction selection mechanism, since it is dynamic at each step and does not require owners to "manually +unstake" their nodes so that their topUp per node would be redistributed (and higher). This threshold ensures that we +maximize the number of owners that will be selected, as well as their number of auction nodes. -| Key | Name | Identity | -|--------------|--------|--------------------------| -| 15eb03756... | tsp-00 | testing-staking-provider | -| ff12bc7f4... | tsp-01 | testing-staking-provider | -| 3dec570c0... | tsp-02 | testing-staking-provider | -| 38a93e3c0... | tsp-03 | testing-staking-provider | -| 1fce426b6... | tsp-04 | testing-staking-provider | +In this example, if we use a step of 10 EGLD in the `[667, 1334]` interval, the `minimum required topUp per node` would +be 1216, such that: +![](/validators/stakingV4/soft-auction2.png) -### Migration guide from single-key operation to multikey +``` ++--------+------------------+----------------+--------------+-------------------+-----------------------------+------------------+---------------------------+-----------------------------+ +| Owner | Num staked nodes | TopUp per node | Total top up | Num auction nodes | Num qualified auction nodes | Num active nodes | Qualified top up per node | Selected auction list nodes | ++--------+------------------+----------------+--------------+-------------------+-----------------------------+------------------+---------------------------+-----------------------------+ +| owner1 | 3 | 1223 | 3669 | 1 | 1 | 2 | 1223 | pubKey1 | +| owner2 | 3 | 851 | 2555 | 2 | 1 | 1 | 1277 | pubKey2 | +| owner3 | 2 | 1223 | 2446 | 1 | 1 | 1 | 1223 | pubKey4 | +| owner4 | 4 | 667 | 2668 | 3 | 1 | 1 | 1334 | pubKey5 | ++--------+------------------+----------------+--------------+-------------------+-----------------------------+------------------+---------------------------+-----------------------------+ +``` -:::warning -This guide can lead to potential node jailing if done incorrectly. Make sure that you understand completely all the steps involved. +- `owner1` possesses one auction node, `pubKey1`, with a qualified topUp per node of 1223, surpassing the threshold of + 1216. +- `owner2` holds two auction nodes, `pubKey2` and `pubKey3`, with a topUp per node of 851. By leaving one + node (`pubKey3`) in the auction while selecting only one (`pubKey2`), the topUp per node is rebalanced to 1277 ( + 2555/2), exceeding the minimum threshold of 1216. +- `owner3` has one auction node, `pubKey4`, with a qualified topUp per node of 1223, surpassing the threshold of 1216. +- `owner4` possesses three auction nodes, `pubKey5`, `pubKey6`, and `pubKey7`, with a topUp per node of 667. By leaving + two nodes (`pubKey6`, `pubKey7`) in the auction while selecting only one (`pubKey5`), the topUp per node is rebalanced + to 1334 (2668/2), exceeding the minimum threshold of 1216. -We strongly suggest to practice this process first on the public testnet. You should gather invaluable experience and know how. -::: +If the threshold were increased by one more step from `1216` to `1226`, only two nodes, `pubKey2` and `pubKey5`, would +qualify, which is insufficient to fill all slots. -Whenever deciding to switch from single-key operation to multikey, the following steps on how to execute this process can be considered: -1. create your `allValidatorsKeys.pem` by manually (or through a text tool) concatenate all your `validatorKey.pem` files; -2. start a multikey group, **configure it as a backup group**, provide the `allValidatorsKeys.pem` file to all the nodes forming the group; -3. let this backup multikey group nodes sync and go the next step **after all these nodes are synced**; -4. switch off your single-key backup nodes (if you previously had ones); -5. create a new multikey group, configure it as main group and let it sync. **Do not provide the `allValidatorsKeys.pem` keys yet!**. Go the next step **after all these nodes are synced**; -6. after the main group nodes are synced, copy the `allValidatorsKeys.pem` file to all nodes from the main group, switch off the main single-key nodes and restart the multikey nodes from the main group, so they will load the `allValidatorsKeys.pem` file; -7. closely monitor all your nodes in the explorer, should be online and with their rating status increasing/at 100%. Repeat this step for a few times at 10 minutes interval. +:::note -Make sure that all operations from step 6 are made as quickly as possible. In case this step takes a long time, the backup multikey group should take over. +If an owner has multiple nodes in the auction, but only a portion is selected for distribution in the waiting list, the +selection will be based on sorting the BLS keys. -:::caution -Always attempt this process while closely monitor your nodes. If done correctly, your nodes might experience a brief rating drop (until the backup group takes over - if necessary) ::: ---- +:::note -### MultiversX Node upgrades +The minimum required topUp per node, along with the real-time auction list, is accessible in the explorer at all +times. This allows owners to determine the optimal strategy for maximizing the number of selected auction nodes. -As opposed to a hard fork, which is a change in the protocol that is not backward compatible, MultiversX performs regular node upgrades, which are changes in the protocol -that are backward compatible and bring new features, improvements and bugs fixes. Nodes operators must be aware of the upgrade process and the steps they need to take in order -to avoid any downtime. +::: +Finally, validators are sorted based on the qualified topUp per node, and the selection is made considering available +slots. In instances where two or more validators share the same topUp (e.g., `pubKey1` and `pubKey4`), the selection +process is random but deterministic. The selection involves an XOR operation between the validators' public keys and the +current block's randomness. This mechanism prevents validators from "minting" their BLS keys to gain an advantage in +selection, as the randomness is only revealed at the time of selection. -## **Introduction** +``` + +--------+----------------+--------------------------+ + | Owner | Registered key | Qualified TopUp per node | + +--------+----------------+--------------------------+ + | owner4 | pubKey5 | 1334 | + | owner2 | pubKey2 | 1277 | + | owner1 | pubKey1 | 1223 | + +--------+----------------+--------------------------+ + | owner3 | pubKey4 | 1223 | + +--------+----------------+--------------------------+ +``` -Once a new node's binary is ready to be deployed on one of the networks (mainnet, testnet or devnet), nodes operators must -perform the upgrade to the newest version. These releases are always announced on MultiversX [Validators chat](https://t.me/MultiversXValidators) -plus via other communication channels, depending on the case. +Following the example above, there are two nodes with a qualified top-up of 1223 per node: +- `owner1` with 1 BLS key = `pubKey1` +- `owner3` with 1 BLS key = `pubKey4` -## **When to upgrade** +Assuming the result of the XOR operation between their BLS keys and randomness is: -Subscribe to email notifications for new releases from the official GitHub repositories that hold the chain configuration ([mx-chain-mainnet-config](https://github.com/multiversx/mx-chain-mainnet-config), [mx-chain-testnet-config](https://github.com/multiversx/mx-chain-mainnet-config) and [mx-chain-devnet-config](https://github.com/multiversx/mx-chain-mainnet-config). Also, make sure to join the Validators Telegram channel. +- `XOR1` = `pubKey1` XOR `randomness` = `[143...]` +- `XOR2` = `pubKey4` XOR `randomness` = `[131...]` -Setup monitoring and get alerts for new updates. As last resort, check the status of your validator software version using the relevant Explorer section https://explorer.multiversx.com/nodes - outdated versions will be marked with ⚠ +Since `XOR1` > `XOR2`, `pubKey1` will be selected, while `pubKey4` remains in the auction list. -## **Types of upgrades** +## Introducing Node Limitations for Enhanced Decentralization -Currently, we have the following types of upgrades: +In tandem with the upcoming staking v4 feature, we are implementing a crucial change aimed at fostering +decentralization, increasing the Nakamoto coefficient, and reinforcing the principles of a decentralized network. -A. - **all nodes need to upgrade**: upgrades that involve processing changes with an activation epoch (as explained below) -and have to be performed by all nodes operators in order to keep the same view over the network and not cause service disruptions. -B. - **optional upgrades**: upgrades that, for example, simply add a new Rest API endpoint or improve the trie syncing timing -are not critical from a processing point of view, and they are optional. If the nodes operators think the new feature will help them, -they can proceed with the upgrade without losing the compatibility with the network. +### Dynamic Node Limitation -C. - **only validators need to upgrade**: upgrades that, for example, include new features that only trigger validators (ratings changes, -transactions selection improvements and so on). Observers (nodes that don't have a stake attached) don't need to perform the upgrade -(but can upgrade nonetheless if desired). +To achieve our decentralization goals, a cap on the number of nodes an owner can have will be introduced. This +limitation is dynamic, recalculated at each epoch, ensuring adaptability to the evolving network conditions. + + +### Impact and Considerations +This restriction primarily affects scenarios where users wish to stake new nodes. If an individual already possesses +more nodes than the specified threshold, their existing nodes will not be affected. However, they won't be able to stake +additional nodes beyond the limit; only unstaking will be allowed. -## **Activation epochs** -In order to make the upgrades as smooth as possible and to ensure that each node has the same view over the network at a given moment, -MultiversX has a so called _activation epoch_ mechanism that allows the node to implement both behaviors of the protocol - -the old (current) one, and the new one, planned for activation at a specific epoch. This mechanisms ensures that, -**until the protocol change becomes active**, nodes with an upgraded codebase / binary remain compatible with nodes that -did not perform the upgrade, and consensus is held. Although this happens in 99.9% cases, this is not 100% guaranteed due -to the unforeseen consequences when upgrading the code base in parallel with executing transactions generated by 3rd parties (MultiversX blockchain users) +### Decentralization in Action +This initiative encourages staking providers to critically evaluate their node count. For larger providers, having an +excessive number of nodes may lead to a decrease in overall APR. Achieving enough top-up to select numerous nodes from +the auction could become challenging. -### **Deterministic time / height for upgrades** -As compared to other protocols that perform upgrades that start at a specific block height, releases for MultiversX nodes -don't have a specific block height where the new updates become effective, but rather the first block in the -activation epoch will make the nodes proceed with the updated versions of the components. +### Proactive Measures -Since the height of the first block in an epoch cannot be known in advance (due to possible roll-backs), the network height -where a feature becomes effective cannot be calculated. +Staking providers are encouraged to strategize accordingly. For instance, they might choose to unstake some nodes +themselves or explore collaboration with other small providers. Merging resources can enhance their chances of being +selected in the auction, especially for those with limited top-up. -However, the time when a new feature of a bugfix becomes effective can be calculated, as epochs have fixed lengths in rounds. -Currently, MultiversX Mainnet has epochs of `14,400` rounds and a round is `6 sec`. This results in a `24h` epoch. However, -there can be delays of a few rounds, due to rollbacks of the start of epoch metablocks. +No immediate action is required from users; however, thoughtful consideration of their node portfolio and strategic +decisions will play a pivotal role in navigating this shift toward a more decentralized network. -### _Activation epoch example_ +# **FAQ** -For example, let's say that we want to introduce a feature so that smart contracts can receive a `PayableBySC` metadata that -will allow them to receive EGLD or other tokens from other smart contracts. -_Timeline example_ +## How much topUp should I have as a validator? -- the MultiversX Mainnet is at epoch `590`. -- currently, the node binary doesn't know about the `PayableBySC` metadata so if one wants to try it, an error like `invalid metadata` - will be returned. -- at epoch `600`, we release a new node binary that contains the `PayableBySC` metadata that will become active starting with epoch `613`. -- all nodes operators perform the upgrade. -- when epoch `613` begins, the new feature activates and the new metadata is recognized and accepted. -- if one wants to issue a smart contract that is `PayableBySC`, it will work. +The required topUp for validators depends on various factors, including the number of nodes in the auction and the soft +auction selection mechanism. The soft auction selection dynamically computes the minimum required topUp per node to +qualify for distribution from the auction to the waiting list. To maximize the chances of having auction nodes selected, +validators are encouraged to maintain a competitive topUp. Real-time auction list information and the minimum required +topUp per node is available in the explorer, allowing validators to strategize effectively. -- nodes that didn't perform the upgrade will produce a different output of the transaction (as compared to the majority) - and won't be able to keep up with the rest of the chain. -_Backwards compatibility explained_: -If one wants to process all the blocks since genesis (via `full archive` or via `import-db`) with the released binary -it will behave this way: +## What happens if there are fewer nodes in the auction than available slots? -- if for example, in epoch `455` there was a transaction that tried to set the `PayableBySC` metadata, it will process it - as `invalid metadata` -- for transactions in epochs newer than `613` it will process the new metadata. +In this case, all nodes will be selected, regardless of their topUp. -| | Epoch < 613 | Epoch >= 613 | -| ------------- | ------------------ | ------------ | -| IsPayableBySC | `invalid metadata` | `successful` | ---- +## One of my nodes was sent to auction during stakingV4 step 2. Will I lose rewards? -### Node CLI +If one of your nodes is shuffled out into the auction list during step2, it will enter into competition with the other +existing nodes. If you have enough topUp, nothing changes, and no rewards will be lost. For owners contributing to the +ecosystem and maintaining a sufficient topUp, this change will not have any negative impact. However, if you have low +topUp or close to zero, your nodes might be unqualified and remain in the auction list. -This page will guide you through the CLI fields available for the node and other tools from the `mx-chain-go` repository. +## Why downsize the waiting list? -## **Introduction** +Short answer: _to keep the APR unchanged_. -Command Line Interface for the Node and the associated Tools +Before stakingV4, if a node was shuffled out and moved to the waiting list, it was guaranteed to be "idle" (not +participating in consensus) for 5 epochs. During this time, the node would not gain any rewards. -The **Command Line Interface** of the **Node** and its associated **Tools** is described at the following locations: +During stakingV4 step2, no node from the waiting list is moved to active. If we were to keep the same configuration, a +shuffled out node from this step would have to wait 6 epochs until eligible (if selected from the auction) and +therefore decreasing the overall APR: -- [Node](https://github.com/multiversx/mx-chain-go/blob/master/cmd/node/CLI.md) -- [SeedNode](https://github.com/multiversx/mx-chain-go/blob/master/cmd/seednode/CLI.md) -- [Keygenerator](https://github.com/multiversx/mx-chain-go/blob/master/cmd/keygenerator/CLI.md) -- [TermUI](https://github.com/multiversx/mx-chain-go/blob/master/cmd/termui/CLI.md) -- [Logviewer](https://github.com/multiversx/mx-chain-go/blob/master/cmd/logviewer/CLI.md) +## How does the dynamic node limitation work? -## **Examples** +The dynamic node limitation is determined by the `NodeLimitPercentage`, which defines a percentage of +the `TotalNumOfEligibleNodes` from the current epoch. For example, if `NodeLimitPercentage` is set to 0.005 (0.5%) and +the `TotalNumOfEligibleNodes` for a given epoch is 1600 nodes, this means owners cannot exceed having more than 8 nodes. +The specific parameters, including the initial limit and `NodeLimitPercentage`, can be decided through a governance +vote. This ensures community involvement in determining the rules governing node ownership. -For example, the following command starts an **Observer Node** in **Shard 0**: +The actual limit is 50 nodes per provider with the calculation details from the 'systemSmartContractsConfig.toml' [here](https://github.com/multiversx/mx-chain-mainnet-config/blob/2ca2da07427c5a802202d1ed364a923f0e366f13/systemSmartContractsConfig.toml#L15) -``` -./node --rest-api-interface=localhost:8080 \ - --log-save --log-level=*:DEBUG --log-logger-name \ - --destination-shard-as-observer=0 --start-in-epoch\ - --validator-key-pem-file=observer0.pem -``` +--- -While the following starts a Node as a **Metachain Observer**: +### System Requirements -``` -./node --rest-api-interface=localhost:8080 \ - --use-log-view --log-save --log-level=*:DEBUG --log-logger-name \ - --destination-shard-as-observer=metachain --start-in-epoch\ - --validator-key-pem-file=observerMetachain.pem -``` +This page provides the system requirements for running a MultiversX node. ---- -### Node Configuration +## **MultiversX Nodes explained** -## Introduction +Nodes are computers running the MultiversX software, so they contribute to the MultiversX network by relaying information and validating it. Each node needs to stake 2500 EGLD to become a **Validator** and is rewarded for its service. Nodes without a stake are called **Observers** - they are connected to the network and relay information, but they have no role in processing transactions and thus do not earn rewards. -The node relies on some configuration files that are meant to allow the node operator to easily change some values -that won't require a code change, a new release, or so on. +## **Minimum System Requirements for running 1 MultiversX Node** -## Configuration files +- 4 x dedicated/physical CPUs, either Intel or AMD, **with `SSE4.1` and `SSE4.2` flags** (use [lscpu](https://manpages.ubuntu.com/manpages/trusty/man1/lscpu.1.html) to verify) +- 8 GB RAM +- 200 GB SSD +- 100 Mbit/s always-on internet connection, at least 4 TB/month data plan +- Linux OS (Ubuntu 22.04/Debian 12 minimum) / MacOS -All the configuration files are located by default in the `config` directory that resides near the node's binary. The paths can be changed -by using the node's CLI flags. +:::caution +1. The CPUs must be `SSE4.1` and `SSE4.2` capable, otherwise the node won't be able to use the Wasmer 2 VM available through the VM 1.5 (and above) and the node will not be able to sync blocks from the network. +2. If the system chosen to host the node is a VPS, the host must have dedicated CPUs. Using shared CPUs can hinder your node's performance that will result in a decrease of node's rating and eventually the node might get jailed. +3. If you run multiple MultiversX Nodes on the same machine, the host running those nodes should have the specs at least equal to the minimum system requirements multiplied by the number of nodes running on that host. +::: -:::important -Not all configuration values can be user-defined. For example, it is perfectly fine if a node operator increases the size of a cacher or sets an Elasticsearch instance, but changing the genesis total supply, for example, will lead to an inconsistent state as compared to the Network. +:::tip +We are promoting using processors that support the `fma` or `fma3` instruction set since it is widely used by our VM. Displaying the available CPU instruction set can be done using the Linux shell command `sudo lshw` or `lscpu` ::: -Below you can find an example of how the configuration files look like for the `v1.5.8` node. -``` -├── api.toml -├── config.toml -├── economics.toml -├── enableEpochs.toml -├── enableRounds.toml -├── external.toml -├── gasSchedules -│ ├── gasScheduleV1.toml -│ ├── gasScheduleV2.toml -│ ├── gasScheduleV3.toml -│ ├── gasScheduleV4.toml -│ ├── gasScheduleV5.toml -│ ├── gasScheduleV6.toml -│ └── gasScheduleV7.toml -├── genesisContracts -│ ├── delegation.wasm -│ └── dns.wasm -├── genesis.json -├── genesisSmartContracts.json -├── nodesSetup.json -├── p2p.toml -├── prefs.toml -├── ratings.toml -├── systemSmartContractsConfig.toml -├── testKeys -│ ├── delegationWalletKey.pem -│ ├── dnsWalletKey.pem -│ ├── esdtWalletKey.pem -│ └── protocolSustainabilityWalletKey.pem -└── upgradeContracts - └── dns - └── v3.0 - ├── deploy.json - └── dns.wasm +## **ARM Architecture Support** -``` +Processors with ARM architecture are now supported, starting from mainnet epoch 1265 (related to [this](https://github.com/multiversx/mx-chain-mainnet-config/releases/tag/v1.6.7.0) release). +Synchronization from genesis to epoch 1265 is not possible on ARM processors. -- `api.toml` contains the Rest API endpoints configuration (open or closed endpoints, logging and so on) -- `config.toml` contains the main configuration of the node (storers & cachers type and size, type of hasher, type of marshaller, and so on) -- `economics.toml` contains the economics configuration (such as genesis total supply, inflation per year, developer fees, and so on) -- `enableEpochs.toml` contains a list of new features or bugfixes and their activation epoch -- `enableRounds.toml` contains a list of new features or bugfixes and their activation epoch -- `external.toml` contains external drivers' configuration (for example: Elasticsearch or event notifier) -- `gasSchedules` is the directory that contains the gas consumption configuration to be used for SC execution, depending on activation epochs specified on enableEpochs.toml -> GasSchedule -> GasScheduleByEpochs -- `genesisContracts` is the directory that contains the WASM contracts that were deployed at the genesis -- `genesis.json` contains all the addresses and their balance/active delegation at the genesis -- `genesisSmartContracts.json` specifies the SCs to be deployed at Genesis time, alongside additional parameters -- `nodesSetup.json` holds all the Genesis nodes' public keys, alongside their wallet address -- `p2p.toml` contains peer-to-peer configurable values, such as the number of peers to connect to -- `prefs.toml` contains a set of custom configuration values, that should not be replaced from an upgrade to another -- `ratings.toml` contains the parameters used for the nodes' rating mechanism, for example, the start rating, decrease steps, and so on -- `systemSmartContractsConfig.toml` contains System Smart Contracts configurable values, such as parameters for Staking, ESDT, or Governance +:::caution +This update comes after extensive testing to ensure compatibility and functionality. However, we advise caution with its use in production environments. +::: + +Usage recommendations: +- Testnet/Devnet Validators: ARM processors can be utilized effectively as validator nodes on Testnet or Devnet. +- Mainnet Observers: ARM processors can be utilized effectively as observer nodes that can provide API support to non-critical services. +- Mainnet Validators: Despite successful testing, **it is NOT recommended to use ARM processors as mainnet validators** at this time due to potential performance and reliability concerns. +We will continue to monitor and improve support for ARM architecture, and we encourage the community to provide [feedback](https://t.me/MultiversXValidators) on their experiences. -### Overriding config.toml values -As mentioned in the above descriptions, `prefs.toml` is not overwritten by the installation scripts when performing an upgrade. +### **Networking** -However, there are some more custom values that nodes operators use (antiflood disabled or with fewer constraints, db lookup extension, and so on) -and they don't want these values to be changed during an upgrade. +In order for a node to be reachable by other nodes several conditions have to be met: -For this use-case, release `v1.4.x` introduces the `OverridableConfigTomlValues` setting inside `prefs.toml` that is able to override certain configuration -values from `config.toml`. +1. The port opened by the node on the interfaces must not be blocked by a firewall that denies inbound connections on it +2. If behind a NAT device, the node must be able to use the UPnP protocol to successfully negotiate a port that the NAT device will forward the incoming connections to (in other words, the router should be UPnP compatible) +3. There must be maximum 1 NAT device between the node and the Internet at large. Otherwise, the node will not be reachable by other nodes, even if it can connect itself to them. -Here's how to use it: +To make sure the required ports are open, use the following command before continuing: ``` - OverridableConfigTomlValues = [ - { Path = "StoragePruning.NumEpochsToKeep", Value = "4" }, - { Path = "MiniBlocksStorage.Cache.Name", Value = "MiniBlocksStorage" } - ] +sudo ufw allow 37373:38383/tcp ``` -Therefore, after each upgrade, the node will override these values to the newly provided values. The path points to an entry -in `config.toml` file before setting a new overridable value. +:::note +The above ports need to be open in order to allow the node to communicate with other nodes via p2p. The configuration for the port range is set [here](https://github.com/multiversx/mx-chain-go/blob/master/cmd/node/config/p2p.toml#L7). +::: ---- +:::caution +In case a firewall for outgoing traffic is used please make sure traffic to ports 10000 (p2p seeder) as well as 123 (NTP) is explicitly allowed. +::: -### Node Databases +--- -This page will describe the databases used by the Node. These are simple key-value storage units that will hold different types of data, as described below. +### The Staking Smart Contract +This page will guide you through the the operations of the Staking System Smart Contract. -## **Node databases** -Nodes use simple Key-Value type databases. +## **Staking** -Nodes use Serial LevelDB databases to persist processed blocks, transactions, and so on. +Nodes are _promoted_ to the role of **validator** when their operator sends a _staking transaction_ to the Staking smart contract. Through this transaction, the operator locks ("stakes") an amount of their own EGLD for each node that becomes a validator. A single staking transaction contains the EGLD and the information needed to stake for one or more nodes. Such a transaction contains the following: -The data can be removed or not, depending on the pruning flags that can be enabled or not in `config.toml`. -The flags used to specify if a node should delete databases or not are `ValidatorCleanOldEpochsData` and `ObserverCleanOldEpochsData`. -Older versions of the configuration only have one flag `CleanOldEpochsData`. If set to false, then old databases won't be removed. +- The number of nodes that the operator is staking for +- The concatenated list of BLS keys belonging to the individual nodes +- The stake amount for each individual node, namely the number of nodes × 2500 EGLD +- A gas limit of 6 000 000 gas units × the number of nodes +- Optionally, a separate address may be specified, to which the rewards should be transferred, instead of the address from which the transaction itself originates. The reward address must be first decoded to bytes from the Bech32 representation, then re-encoded to base16 (hexadecimal). -By default, validators only keep the last 4 epochs and delete older ones for freeing disk space. +For example, if an operator manages two individual nodes with the 96-byte-long BLS keys `45e7131ba....294812f004` and `ecf6fdbf5....70f1d251f7`, then the staking transaction would be built as follows: -The default databases directory is `/db` and it's content should match the following structure: -``` -/db -└── - ├── Epoch_X - │ └── Shard_X - │ ├── BlockHeaders - │ │ ├── 000001.log - │ │ ├── CURRENT - │ │ ├── LOCK - │ │ ├── LOG - │ │ └── MANIFEST-000000 - │ ├── BootstrapData - │ │ ├── 000001.log - | ............. - └── Static - └── Shard_X - ├── AccountsTrie - │ └── MainDB - │ ├── 000001.log - ............. +```rust +StakingTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l + Value: 5000 EGLD + GasLimit: 12000000 + Data: "stake" + + "@0002" + + "@45e7131ba....294812f004" + + "@67656e65736973" + "@ecf6fdbf5....70f1d251f7" + + "@67656e65736973" + "@optional_reward_address_HEX_ENCODED" +} ``` -Nodes will fetch the state from an existing database if one is detected during the startup process. If it does not match -the current network height, it will sync the rest of the data from the network, until fully synced. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +Because this transaction is a call to the Staking smart contract, it passes information via the `Data` field: -## **Starting a node with existent databases** +- `stake` is the name of the smart contract function to be called; +- `0002` is the number of nodes (unsigned integer, hex-encoded); +- 45e7131ba....294812f004 is the BLS key of the first node, represented as a 192-character-long hexadecimal string; +- `67656e65736973` is a reserved placeholder, required after each BLS key; +- `ecf6fdbf5....70f1d251f7` is the BLS key of the second node, represented as a 192-character-long hexadecimal string; +- `67656e65736973` is the aforementioned reserved placeholder, repeated; +- `optional_reward_address_HEX_ENCODED` is the address of the account which will receive the rewards for the staked nodes (decoded from its usual Bech32 representation into binary, then re-encoded to a hexadecimal string). -There are use-cases when a node can receive the entire database from other node that is fully synced in order to speed up the process. -In order to perform this, one has to copy the entire database directory to the new node. This is as simple as copying the `db/` -directory from one node to the other one. -The configuration files must be the same as the old node, except the BLS key which is independent of databases. +## **Changing the reward address** -Two nodes in the same shard generate the same databases. These databases are interchangeable between them. However, starting -a node as observer and setting the `--destination-shard-as-observer` so it will join a pre-set shard, requires that it's database -is from the same shard. So starting an observer in shard 1 with a database of a shard 0 node will result in ignoring the database -and network-only data fetch. +Validator nodes produce rewards, which are then transferred to an account. By default, this account is the same one from which the staking transaction was submitted (see the section above). In the staking transaction, the node operator has the option to select a different reward address. -If the configuration and the database's shard are the same, then the node should have the full state from the database and -start to sync with the network only remaining items. If, for instance, a node starts with a database of 255 epochs, and the current epoch is -256, then it will only sync from network the data from the missing epoch. +The reward address can also be changed at a later time, with a special transaction to the Staking smart contract. It is essential to know exactly how many nodes were specified in the original staking transaction, in order to properly compute the gas limit for changing the reward address. ---- +- An amount of 0 EGLD +- A gas limit of 6 000 000 gas units × the nodes for which the reward address is changed (as specified by the original staking transaction). +- The new reward address. The reward address must be first decoded into binary from its normal Bech32 representation, then re-encoded to base16 (hexadecimal). -### Node operation modes +For example, changing the reward address for two nodes requires the following transaction: -Without configuration changes, nodes will start by using the default settings. However, there are several ways to configure the node, depending on the desired operation mode. -Instead of manually (or programmatically via `sed`s for example) editing the `toml` files, you can use the `--operation-mode` CLI flag described below to specify a custom -operation mode that will result in config changes. +```rust +ChangeRewardAddressTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l + Value: 0 EGLD + Data: "changeRewardAddress@reward_address_HEX_ENCODED" + GasLimit: 12000000 +} +``` +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -## Introduction -Starting with `v1.4.x` release, a new CLI flag has been introduced to the node. It is `--operation-mode` and its purpose -is to override some configuration values that will allow the node to act differently, depending on the use-case. +## **Unstaking** +A node operator may _demote_ their validator nodes back to **observer** status by sending an _unstaking transaction_ to the Staking smart contract, containing the following: -## List of available operation modes +- An amount of 0 EGLD +- The concatenated list of the BLS keys belonging to the individual nodes which are to be demoted from validator status +- A gas limit of 6 000 000 gas units × the number of nodes -Below you can find a list of operation modes that are supported: +Note that the demotion does not happen instantaneously: the unstaked nodes will remain validators until the network releases them, a process which is subject to various influences. +Moreover, the amount of EGLD which was previously locked as stake will not return instantaneously. It will only be available after a predetermined number of rounds, after which the node operator may claim back the amount with a third special transaction (see the following section). -### Full archive +Continuing the example in the previous section, an unstaking transaction for the two nodes contains the following: -Usage: -``` -./node --operation-mode full-archive +```rust +UnstakingTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l + Value: 0 EGLD + GasLimit: 12000000 + Data: "unStake" + + "@45e7131ba....294812f004" + + "@ecf6fdbf5....70f1d251f7" +} ``` -The `full-archive` operation mode will change the node's configuration in order to make it able to sync from genesis and also -be able to serve historical requests. -Syncing a node from genesis might take some time since there aren't that many full archive peers to sync from. - - -### Db Lookup Extension - -Usage: -``` -./node --operation-mode db-lookup-extension -``` +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -The `db-lookup-extension` operation mode will change the node's configuration in order to support extended databases that are -able to store more data that is to be used in further Rest API requests, such as logs, links between blocks and epoch, and so on. +Note that: -For example, the proxy's `hyperblock` endpoint relies on the fact that its observers have this setting enabled. Other examples -are `/network/esdt/supply/:tokenID` or `/transaction/:txhash?withResults=true`. +- `45e7131ba....294812f004` is the BLS key of the first node, represented as a 192-character-long hexadecimal string; +- `ecf6fdbf5....70f1d251f7` is the BLS key of the second node, represented as a 192-character-long hexadecimal string; +- no reserved placeholder is needed, as opposed to the staking transaction (see above) -### Historical balances +## **Unbonding** -Usage: -``` -./node --operation-mode historical-balances -``` +A node operator may reclaim the stake which was previously locked for their validator nodes using an _unbonding transaction_ to the Staking smart contract. Before unbonding, the node operator must have already sent an unstaking transaction for some of their validators, and a predetermined amount of rounds must have passed after the unstaking transaction was processed. -The `historical-balances` operation mode will change the node's configuration in order to support historical balances queries. -By setting this mode, the node won't perform the usual trie pruning, resulting in a more disk usage, but also in -the ability to query the balance or the nonce of an address at blocks that were proposed long time ago. +The unbonding transaction is almost identical to the unstaking transaction, and contains the following: +- An amount of 0 EGLD +- The concatenated list of the BLS keys belonging to the individual nodes for which the stake is claimed back +- A gas limit of 6 000 000 gas units × the number of nodes -### Snapshotless observers +Following the example in the previous sections, an unbonding transaction for the two nodes contains the following information: -Usage: -``` -./node --operation-mode snapshotless-observer +```rust +UnbondingTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l + Value: 0 EGLD + GasLimit: 12000000 + Data: "unBond" + + "@45e7131ba....294812f004" + + "@ecf6fdbf5....70f1d251f7" +} ``` -The `snapshotless-observer` operation mode will change the node's configuration in order to make it efficient for real-time requests -by disabling the trie snapshotting mechanism and making sure that older data is removed. - -A use-case for such an observer would be serving live balances requests, or broadcasting transactions, eliminating the costly operations -of the trie snapshotting. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ ---- +Note that: -### Node redundancy +- 45e7131ba....294812f004 is the BLS key of the first node, represented as a 192-character-long hexadecimal string; +- `ecf6fdbf5....70f1d251f7` is the BLS key of the second node, represented as a 192-character-long hexadecimal string; +- no reserved placeholder is needed, as opposed to the staking transaction (see above) -MultiversX Validator Nodes can be configured to have one or more hot-standby nodes. -This means additional nodes will run on different servers, in sync with the Main Validator node. -Their role is to stand in for the Main Validator node in case it fails, to ensure high availability. +## **Unjailing** -This is a redundancy mechanism which allows the Main Validator operator to start additional 'n' hot-standby nodes, -each of them running the same 'validatorKey.pem' file. The difference between -configurations consists on an option inside the `prefs.toml` file. +If a node operator notices that some of their validator nodes have been jailed due to low rating, they can restore the nodes back to being active validators by paying a small fine. This is done using an _unjailing transaction_, sent to the Staking smart contract, which contains the following: -Hot standby nodes are configured using the 'RedundancyLevel' option in the 'prefs.toml' configuration file: +- An amount of 2.5 EGLD (the fine) for each jailed node - this value must be correctly calculated; any other amount will result in a rejected unjail transaction +- The concatenated list of the BLS keys belonging to the individual nodes that are to be unjailed +- A gas limit of 6 000 000 gas units × the number of nodes -- a 0 value will represent that the node is the Main Validator. - The value 0 will be the default, therefore if the option is missing it will still make that node the Main Validator by default. With consideration to backwards compatibility, the already-running Validators are not affected by the addition of this option. Moreover, we never overwrite the `prefs.toml` files during the node's upgrade. +Continuing the example in the previous section, if the nodes `45e7131ba....294812f004` and `ecf6fdbf5....70f1d251f7` were placed in jail due to low rating, they can be unjailed with the following transaction: -The values of `RedundancyLevel` are interpreted as follows: +```rust +UnjailTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l + Value: 5 EGLD + GasLimit: 12000000 + Data: "unJail" + + "@45e7131ba....294812f004" + + "@ecf6fdbf5....70f1d251f7" +} +``` -- a positive value will represent the "order of the hot-standby node" in the automatic fail-over sequence. - Example: suppose we have 3 nodes running with the same BLS key. One has the redundancy level set to 0, - another has 1 and another with 3. The node with level 0 will propose and sign blocks. The other 2 will - sync data with the same shard as the Main Validator (and shuffle in and out of the same shards) but will - not sign anything. If the Main Validator fails, the hot-standby node - with level 1 will start producing/signing blocks after `level*5` missed rounds. So, after 5 - missed rounds by the Main Validator, the hot-standby node with level 1 will take the turn. - If hot-standby node 1 is down as well, hot-standby node 2 will step in - after `3*5 = 15 rounds` after the Main Validator failed and 10 rounds after the failed hot-standby node 1 - should have been produced a block. -- a large value for this level option (say 1 million), or a negative value (say -1) will mean that the - hot-standby nodes won't get the chance to produce/sign blocks but will sync with the network and - shuffle between shards just as the Main Validator will. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -:::tip -The hot-standby nodes will advertise on the network a different public key (autogenerated at start-up) and thus, concealing the real public key that will be used when signing the header blocks. -::: +Note that: -:::tip -If the Main Validator (RedundancyLevel 0) gets back online, the hot-standby node(s) revert to standby mode. -::: +- `45e7131ba....294812f004` is the BLS key of the first node, represented as a 192-character-long hexadecimal string; +- `ecf6fdbf5....70f1d251f7` is the BLS key of the second node, represented as a 192-character-long hexadecimal string; +- no reserved placeholder is needed, as opposed to the staking transaction (see above) -:::caution -Do not use the same redundancy level on more than one node. Otherwise, the nodes with the same `RedundancyLevel` value will start signing blocks in parallel in the same time. Although the protocol is not negatively affected by double signing, in the near future the BLS key that will perform double signing will have its stake slashed. -::: +## **Claiming unused tokens from Staking** -The random BLS key on hot-standby nodes has the following purposes: +If a node operator has sent a staking transaction containing an amount of EGLD higher than the requirement for the nodes listed in the transaction, they can claim back the remainder of the sum with a simple _claim transaction_, containing: -- the hot-standby node(s) will not cause BLS signature re-verification when idle. -- it slightly prevents DDoS attacks as an attacker can not find all IPs behind a targeted BLS public key: - when an attacker takes down the Main Validator, the hot-standby nodes will advertise the public key when they - will need to sign blocks, but not sooner. +- An amount of 0 EGLD +- A gas limit of 6 000 000 gas units ---- +An example of a claim transaction is: -### Protecting your keys +```rust +ClaimTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l + Value: 0 EGLD + Data: "claim" + GasLimit: 6000000 +} +``` -This page contains information about how to protect your validator and wallet keys. +_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +After this transaction is processed, the Staking smart contract will produce a transaction _back_ to the sender account, but only if the sender account has previously staked for nodes, using a staking transaction. -## How sensitive are your keys +--- -Validator Keys are very sensitive: +### Unjailing -- if you lose them and your node crashes irreparably (i.e. you delete the virtual machine, your VPS provider deletes/loses it), you lose access to that node, you won't be able to bring it back up online and will thus stop earning money with it -- if someone steals them and maliciously uses them in the MultiversX network, they can engage in bad behavior such as double-signing, produce bad blocks, inject fake transactions, mint new coins, etc. - all of those actions are slashable, meaning you can lose your EGLD stake - all 2500! +This page will guide you through the process of unjailing a validator node. -Wallet Keys are extremely sensitive because: -- if you lose the keys, you can't recover your stake or claim your rewards -> you lose all the money -- if someone steals your keys, they can send an unstake transaction from it and claim the EGLD -> the bad guys steal your money +## **Introduction** +In the unfortunate situation of losing too much **rating score**, a validator will be **jailed**, which means that they will be taken out of the shards, they will not participate in consensus, and thus they will not earn any more rewards. Currently, the rating limit at which a node will be jailed is `10`. Read more on the [Ratings](/validators/rating) page. -## How to protect your keys +You can reinstate one of your jailed validators using an **unjailing transaction**. This transaction effectively represents the payment of a fine. After the transaction is successfully executed, your validator will return to the network in the next epoch, and treated as if the validator is brand new, with the rating reset to `50`. -How to protect them: +It is easy to submit an unjailing transaction. You have the option of unjailing your validators either through the online Wallet at [https://wallet.multiversx.com](https://wallet.multiversx.com/), or by using `mxpy` in the command-line. -- make multiple safe backups of the private keys & files - - paper - - hardware - - encrypted physical storage - - distributed cloud storage, etc - - [some hints](https://coinsutra.com/bitcoin-private-key/) +You'll see some BLS public keys in the examples on this page. Make sure you don't copy-paste them into your staking transaction. These BLS keys have been randomly generated and do not belong to any real node. -:::tip -Wallet Keys are not required on host running the Node. Store them on a different location. -::: +Each unjailing process requires a transaction to be sent to the Staking Smart Contract. These transactions must contain all the required information, encoded properly, and must provide a high enough gas limit to allow for successful execution. These details are described in the following pages. +There are currently 2 supported methods of constructing and submitting these transactions to the Staking SmartContract: -## How to secure your node +- Manually constructing the transaction, then submitting it to [wallet.multiversx.com](https://wallet.multiversx.com/); +- Automatically constructing the transaction and submitting it using the `mxpy` command-line tool. -Secure your MultiversX node +The following pages will describe both approaches in each specific case. -- no ports should open in the firewall except for the ones used by the node's normal operation -(the port range can be checked [here](/validators/system-requirements).) -- don't run the node as `root` -- use encryption, all other measures -- [some hints ](https://www.liquidweb.com/kb/security-for-your-linux-server/) ---- +## **Prerequisites** -### Rating +In order to submit an unjailing transaction, you require the following: -This page exposes the rating system used for MultiversX validators. +- A wallet with at least 2.5 EGLD (the cost of unjailing a _single validator_). If you want to unjail multiple validators at once, you need to multiply that minimum amount with the number of validators. For example, unjailing 3 validators at once will require 7.5 EGLD. Make sure you have enough in your wallet. +- The **BLS public keys** of the validators you want to unjail. You absolutely **do not require the secret key** of the validators. The BLS public keys of the validators are found in the `validatorKey.pem` files. Please read [Validator Keys](/validators/key-management/validator-keys) to find out how to extract the public key only. Remember that the BLS public key consists of exactly 192 hexadecimal characters (that is, `0` to `9` and `a` to `f` only). -## **Introduction** +## **Unjailing through the Wallet** -Each individual validator has a **rating score**, which expresses its overall reliability, performance and responsiveness. It is an important value, and node operators should be always mindful of the rating of their validators. +Open your wallet on [https://wallet.multiversx.com](https://wallet.multiversx.com/) and click the "Send" button. Carefully fill the form with the following information. Make sure it is clear to you what this information is, and where to adjust it with your own information. -Rating influences the probability of a validator to be selected for consensus in each round. A performant validator will be preferred in consensus, as opposed to a validator which sometimes fails to contribute or which is not always online. +In the "To" field, paste the address of the Staking SmartContract, which also handles unjailing: `erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l` -:::tip -Observer nodes do not have a rating score. Only validator nodes do. -::: +For the "Amount" field, you first need to calculate the amount of EGLD required for unjailing. This is done by multiplying 2.5 EGLD by the _number of nodes_ you want to unjail. For example, if you want to unjail a single node, you need to enter `2.5`. For two nodes, it's `5` and for three nodes it is `7.5`. -When validators join the network immediately after staking, they start with an initial score of `50` points. +Next, expand the "Fee limit" section of the form. You'll see the "Gas limit" field appear. The value that needs to be entered here also depends on the _number of nodes_ you want to unjail. To calculate the "Gas limit" value, multiply `6000000` (six million gas units) by the number of nodes. For example, if you want to unjail a single node, enter `6000000`. For two nodes, enter `12000000`, for three nodes enter `18000000` and so on. Observe how the "Fee limit" field automatically calculates the cost of this transaction. -Validators gain or lose rating points in a round depending on their role in that round (consensus proposer vs. consensus validator) and on their behavior within that role. Rating penalties are currently set to be `4` times as large as the corresponding gains. This means that a validator has to perform an action correctly 4 times in order to compensate for performing it once incorrectly. Moreover, consecutive losses are _compounding_, which means that the rating penalty increases with each transgression. See [Rating shard validators](/validators/rating#rating-shard-validators) and [Rating metashard validators](/validators/rating#rating-metashard-validators) for details on the calculations. -:::tip -Rating gains and losses on the metashard are different from the gains and losses on the normal shards. -::: +## **The "Data" field** -The past and current rating of an individual validator can be found in the MultiversX Network Explorer at https://explorer.multiversx.com/nodes. Use the "Search" box to find a validator and click on its entry in the list. The "Node Details" page opens, which contains status information about the validator. +Next, you must fill the "Data" field. The text you will write here will be read by the Staking SmartContract to find out what nodes you want to unjail. Remember, you can unjail any number of nodes at once. -The "Node Details" page displays a plot of the validator rating during the past epochs: +When writing in the "Data" field, you must adhere to a strict format, described in the following subsections. -![img](https://gblobscdn.gitbook.com/assets%2F-LhHlNldCYgbyqXEGXUS%2F-MA1wJCHfE7ffob9gOjE%2F-MA1we9u12mvMRF1PU9y%2Fplot-rating.png?alt=media&token=6a1f0071-66d0-4aec-8192-2a8f716e67bb) -The X-axis represent the epochs, and the Y-axis represents the rating. +### **Unjailing a single node** +If you want to unjail a single node, the format of the "Data" field is simple: -## **The jail** +```python +unJail@ +``` -For the overall health of the network, if the rating of a validator drops below `10` points, it will be **jailed**. Being jailed means that the validator will be taken out of the shards, it will not participate in consensus, and thus it will not earn any rewards. +Do not copy-paste the above format as-is into the "Data". Instead, you must **replace** `` with the **BLS public key** of the node you want to stake for. You can find the BLS public key in the `validatorKey.pem` file of that node. Read the page [Validator Keys](/validators/key-management/validator-keys) to help you interpret the contents of the file and locate the BLS public key. -There is an exception to jailing, though. If the network finds itself in a situation where jailing a validator would reduce the size of a shard below an allowed limit, the validator will not be jailed. +Make sure you do not remove the `@` character. They are used to separate the pieces of information in the "Data" field. Only replace``. The angle-brackets `<` and `>` must be removed as well. -:::important -Jailing only occurs at the end of an epoch. This means that a validator with low rating still has until the end of the epoch to recover. If the validator fails to recover, and its rating remains below `10` at the end of the epoch, then it will start the new epoch in jail. -::: +As an example, the "Data" field of an unjailing transaction for a single node looks like this: -To reinstate a jailed validator, its operator must submit an **unjail** transaction to the Staking SmartContract. This causes the validator to be taken out of the "jail" and added to the network as if it were a new validator. +unJail@b617d8bc442bda59510f77e04a1680e8b2d3293c8c4083d94260db96a4d732deaaf9855fa0cef2273f5a67b4f442c725efc06a5d366b9f15a66da9eb8208a09c9ab4066b6b3d38c3cf1ea7fab6489a90713b3b56d87de68c6558c80d7533bf27 -A reinstated validator will be passive during the epoch of its unjailing. In the immediately following epoch, the validator will be assigned to a shard, where it must wait the entire epoch and spend it to synchronize with its new shard. +![img](https://gblobscdn.gitbook.com/assets%2F-LhHlNldCYgbyqXEGXUS%2F-MA1YB7F53LJCTlFj8qn%2F-MA1_N1up06vncGVTyfp%2Funjailing-single-node.png?alt=media&token=fe0ca638-6433-4c07-b7ac-ef3fcf199835) -:::note rating reset -Rating is **not** reset to 50 due to shard shuffling. The rating of a validator is retained when changing shards due to shuffling. -::: -:::tip -The only way to increase the rating of a validator is to keep it up-to-date, keep it well-connected and make sure it is running on hardware that conforms to the [System requirements](/validators/system-requirements). -::: +### **Unjailing multiple nodes at once** -:::note multiple validators on the same machine -Running **multiple validators on a single machine** will impact your rating and consequently _your rewards,_ if the machine doesn't have the as many times the minimum requirements as there are validators running on it. -::: +Unjailing more than one node at a time isn't very different from unjailing a single node. You only need to append the BLS public keys of the remaining nodes, separated by `@`, to the "Data" field constructed for a single node. Please read the previous section "Unjailing a single node" before continuing, if you haven't already. Also, _do not forget_ to update the "Amount" and "Gas Limit" fields according to the number of nodes you are unjailing. +For a _single_ node, as explained in the previous subsection, the format is this one: -## **Consensus probabilities** +```python +unJail@ +``` -Rating affects the probability of a validator to be selected in the consensus group of a round. This is done by applying **rating modifiers** on the probability of selection for each validator. +For _two_ nodes, the format is as follows: -Without rating, all validators of a shard would have the same probability of being chosen for consensus. But rating modifiers will alter the probability of individual validators based on their rating score, in order to give performant validators an edge over the average validators, and also to diminish the probability of selecting weak validators. +```python +unJail@@ +``` -The following table shows how the rating of a validator influences its probability of being chosen for consensus: +And for _three_ nodes, the format is: -| Rating interval | Modifier | -|-----------------|----------| -| [0-10] | -100% | -| (10-20] | -20% | -| (20-30] | -15% | -| (30-40] | -10% | -| (40-50] | -5% | -| (50-60] | 0% | -| (60-70] | +5% | -| (70-80] | +10% | -| (80-90] | +15% | -| (90-100] | +20% | +```python +unJail@@@ +``` -:::important -The algorithm that selects validators for consensus treats these modified selection probabilities as being relative to each other. -::: +Notice how each extra node adds the part `@` to the previous format. You need to replace with `` with the actual **BLS public keys** of your nodes, which you can find inside their individual `validatorKey.pem` files. Make sure you **do not write the BLS secret keys**! Read the page [Validator Keys](/validators/key-management/validator-keys) to see how to interpret the `validatorKey.pem` files. +For example, the "Data" field for an unjailing transaction for two nodes looks like this: -## **Calibration** +unJail@b617d8bc442bda59510f77e04a1680e8b2d3293c8c4083d94260db96a4d732deaaf9855fa0cef2273f5a67b4f442c725efc06a5d366b9f15a66da9eb8208a09c9ab4066b6b3d38c3cf1ea7fab6489a90713b3b56d87de68c6558c80d7533bf27@f921a0f76ed70e8a806c6f9119f87b12700f96f732e6070b675e0aec10cb0723803202a4c40194847c38195db07b1001f6d50c81a82b949e438cd6dd945c2eb99b32c79465aefb9144c8668af67e2d01f71b81842d9b94e4543a12616cb5897d -Assuming a **24-hour-long epoch**, the rating mechanism has been calibrated with the following intentions: +![img](https://gblobscdn.gitbook.com/assets%2F-LhHlNldCYgbyqXEGXUS%2F-MA1mbsWLwDtxs1LX3w-%2F-MA1nGcSQTZqmnGoxtRA%2Funjailing-two-nodes.png?alt=media&token=991f11c8-fe7c-46f5-93fb-566ab0590279) -- A new validator requires **approx. 72 hours** to reach maximum rating, assuming it remains in the same shard and won't be shuffled out (therefore it will be productive all the time, without any waiting time). -- The amount of rating gains earned as a block validator should be in balance with the amount of rating gains earned as a block proposer. This balance must take into account the fact that being selected as proposer is considerably less likely than being selected in consensus as block validator. +### **The general format** -## **Rating shard validators** +You can write the text for the "Data" field for _any_ number of nodes. The general format looks like this: +```python +unJail@@@…@ +``` -### **Rating the shard block proposer** -The node chosen to propose the block for a specific round will: +## **Unjailing through mxpy** -- Gain `0.23148` points for a successful proposal: (1) the block is built correctly, (2) it is accepted by the consensus validators and (3) the proposer applies the final signature and propagates the block throughout the network; -- Lose `0.92592` points for an unsuccessful proposal. +Submitting the unjailing transaction using `mxpy` avoids having to write the "Data" field manually. Instead, the transaction is constructed automatically by `mxpy` and submitted to the network directly, in a single command. -Observe that the loss is 4 times larger than the gain, which means that a proposer must succeed 4 times to gain the points lost for a single missed block. +Make sure `mxpy` is installed and has the latest version before continuing. If `mxpy` is not installed, please follow [these instructions](/sdk-and-tools/mxpy/installing-mxpy). -Rating for proposers is even stricter: there is a compounding penalty rule, which makes the rating of a node drop even faster when it proposes unsuccessfuly. -The amount of `0.92592` points is deducted from the rating of the proposer on the first unsuccessful proposal, but the second unsuccessful proposal will be penalized by `0.92592 × 1.1`. The third, by `0.92592 × 1.1 × 1.1`. The general formula is: +## **Your Wallet PEM file** -`0.92592 × 1.1^{cfp-1}0.92592×1.1^cfp^−1` +To send transactions on your behalf _without_ using the online MultiversX Wallet, `mxpy` must be able to sign for you. For this reason, you have to generate a PEM file using your Wallet mnemonic. -where `cfp` is the number of consecutive failed proposals. +Please follow the guide [Deriving the Wallet PEM file](/sdk-and-tools/mxpy/mxpy-cli). Make sure you know exactly where the PEM file was generated, because you'll need to reference its path in the `mxpy` commands. -This compounding penalty has the effect of quickly jailing repeatedly unsuccessful proposers. +After the PEM file was generated, you can issue transactions from `mxpy`directly. -### **Rating the shard block validator** +## **The unjailing transaction** -The nodes that take part in the consensus of a round (other than the proposer) will: +The following commands assume that the PEM file for your Wallet was saved with the name `walletKey.pem` in the current folder, where you are issuing the commands from. -- Gain `0.00367` points for a successful validation: (1) the proposer has built and proposed a block, (2) the validator appears as a "signer" on that block; being a "signer" of a block means that the validator has approved of the block and was fast enough to be among the first ⅔ + 1 validators to have its signature received by the block proposer; -- Lose `0.01469` points for an unsuccessful proposal. +The command to submit an unjailing transaction with `mxpy` is this: -Observe that the first bulled mentions "the proposer has built and proposed a block". This sentence implies that _all validators will lose rating_ if the proposer fails to propose in the respective round. +```sh +mxpy --verbose validator unjail --pem=walletKey.pem --value="" --nodes-public-keys=",,...," --proxy=https://gateway.multiversx.com +``` -Moreover, the validator must have been a "signer" in at least 1% of the previous blocks, otherwise it will not gain rating. In other words: if the validator has been performing poorly in the past, it will have to perform well for a while until it can start receiving any gains. +Notice that we are using the `walletKey.pem` file. Moreover, before executing this command, you need to replace the following: +- Replace `` with the amount of EGLD required for unjailing your validators. You need to calculate this value with respect to the number of nodes you are unjailing. See the [beginning of the Unjailing through the Wallet](/validators/staking/unjailing#unjailing-through-the-wallet) section for info on how to do it. +- Replace all the `` with the actual **BLS public keys** of your nodes, which you can find inside their individual `validatorKey.pem` files. Make sure you **do not write the BLS secret keys**! Read the page [Validator Keys](/validators/key-management/validator-keys) to see how to interpret the `validatorKey.pem` files. -## **Rating metashard validators** +Here's an example for an unjailing command for one validator: -The rating mechanism for the metashard is identical with the rating mechanism of the normal shards, but the gain / loss values themselves are configured differently. +```sh +mxpy --verbose validator unjail --pem=walletKey.pem --value="2500000000000000000000" --nodes-public-keys="b617d8bc442bda59510f77e04a1680e8b2d3293c8c4083d94260db96a4d732deaaf9855fa0cef2273f5a67b4f442c725efc06a5d366b9f15a66da9eb8208a09c9ab4066b6b3d38c3cf1ea7fab6489a90713b3b56d87de68c6558c80d7533bf27" --proxy=https://gateway.multiversx.com +``` +:::note important +You must take **denomination** into account when specifying the `value` parameter in **mxpy**. +::: -### **Rating the metashard block proposer** +For two validators, the command becomes this one: -The metachain proposer will: +```sh +mxpy --verbose validator unjail --pem=walletKey.pem --value="5000000000000000000000" --nodes-public-keys="b617d8bc442bda59510f77e04a1680e8b2d3293c8c4083d94260db96a4d732deaaf9855fa0cef2273f5a67b4f442c725efc06a5d366b9f15a66da9eb8208a09c9ab4066b6b3d38c3cf1ea7fab6489a90713b3b56d87de68c6558c80d7533bf27,f921a0f76ed70e8a806c6f9119f87b12700f96f732e6070b675e0aec10cb0723803202a4c40194847c38195db07b1001f6d50c81a82b949e438cd6dd945c2eb99b32c79465aefb9144c8668af67e2d01f71b81842d9b94e4543a12616cb5897d" --proxy=https://gateway.multiversx.com +``` -- Gain `0.23148` points for a successful proposal; -- Lose `0.92592` points for an unsuccessful proposal. +Notice that the two BLS public keys are separated by a comma, with no extra space between them. -The compounding penalty rule also applies to block proposers of the metachain. See [Rating the shard block proposer](#rating-the-shard-block-proposer) for details. +--- +### Useful Links & Tools -### **Rating the metashard block validator** +This page offer useful links and resources that can be used by validators and nodes operators. -A validator taking part in consensus on the metachain will: -- Gain `0.00057` points for a successful validation; -- Lose `0.00231` points for an unsuccessful validation. +## Resources -The rules from [Rating the shard block validator](#rating-the-shard-block-validator) apply for the metashard validators as well. +Official resources: ---- +- Blockchain Explorer: [https://explorer.multiversx.com/](https://explorer.multiversx.com/) +- Wallet: [https://wallet.multiversx.com](https://wallet.multiversx.com/) +- GitHub: https://github.com/multiversx +- Dockerhub: https://hub.docker.com/u/multiversx +- Telegram Validators Chat: https://t.me/MultiversXValidators +- Telegram Bot for Staking & Validators: https://t.me/ElrondNetwork_Bot -### Scripts & User config -MultiversX provides scripts designed to streamline the process of installing a MultiversX node. This validator script is a general script for accessing the Mainnet, Devnet and Testnet networks. +Community resources: -To get started, you will begin by getting a copy of the latest version of the scripts from Github and configure it to match your local setup. +- Zabbix monitoring guide: https://thepalmtree.network/zabbix-elrond-guide +- Zabbix plugin: https://github.com/arcsoft-ro/zabbix-elrond-plugin +- Configure Validator API & NetData https access: https://gist.github.com/hiddentao/e6283952b9fffe3f6b42dfeec87c684e -:::caution -Nodes scripts should not be run as a root user. Such usage is not supported and may result in unexpected behavior. -::: +--- +### Validator Keys -## **Download the MultiversX Scripts** +Each validator required a private key to be used for signing blocks. This key is called the **Validator Key**. +The Validator Key is also used to sign the consensus messages that the validator sends to the other validators. -```bash -cd ~ -git clone https://github.com/multiversx/mx-chain-scripts -``` +## Validator key format -## **Configure the scripts correctly** +A file containing the keys for your node. -The scripts require a few configurations to be set in order to work correctly. +The **Validator Keys** are located in the `validatorKey.pem` file, which is generated in the node setup process. By default, each node stores its own .pem file in the `$HOME/elrond-nodes/node-0` folder. A copy also archived as a zip file in the `$HOME/VALIDATOR_KEYS` folder, for restore purposes. -First and foremost, you need your exact username on your local machine. You can find out your current username by running the `whoami` command, which will print it out: +Below you can find their anatomy and how to extract the information from them -```bash -whoami -``` +Example: -Next, in the `variables.cfg` file, edit and add your username in the following variables: +-----BEGIN PRIVATE KEY for _45e7131ba37e05c5de3f8862b4d8294812f004a5b660abb793e89b65816dbff2b02f54c25f139359c9c98be0fa657d0bf1ae4115dcf6fdbf5f3a470f1d251f769610b48fe34eeab59e82ac1cc0336d1d9109a14b768b97ccb4db4c2431629688_----- -- `ENVIRONMENT`: The MultiversX network to be used: mainnet, testnet or devnet. -- `CUSTOM_HOME`: This refers to the folder on the computer in which you will install your node. -- `CUSTOM_USER`: which is the username on the computer under which you will run the installation, upgrade, and other processes +**YmRiNmViOGYzMmQ3OWY0YjE4ODJjMzE1ODA4YjQyZmZjODhiZDQxNzMwNmE5MTRiZjQ4OTAyNjM0MTcyNjMzMw==** -Open `variables.cfg` in the `nano` editor: +-----END PRIVATE KEY for _45e7131ba37e05c5de3f8862b4d8294812f004a5b660abb793e89b65816dbff2b02f54c25f139359c9c98be0fa657d0bf1ae4115dcf6fdbf5f3a470f1d251f769610b48fe34eeab59e82ac1cc0336d1d9109a14b768b97ccb4db4c2431629688_----- -```bash -cd ~/mx-chain-scripts/config -nano variables.cfg +In plain English: + +``` +-----The private key for this``*PUBLIC KEY*``starts below----- +**PRIVATE KEY** +-----The private key for this``*PUBLIC KEY*``was listed above----- ``` -Change the variables `ENVIRONMENT`, `CUSTOM_HOME` and `CUSTOM_USER` as highlighted in the image below: +The string in _italics_ from the example is the _PUBLIC KEY_. The string in **bold** from the example is the **PRIVATE KEY**. -![img](/validators/scripts/variables.png) +More clearly: -For `CUSTOM_USER` variable, use the output of the `whoami` command that was run earlier. +`*PUBLIC KEY:* `_45e7131ba37e05c5de3f8862b4d8294812f004a5b660abb793e89b65816dbff2b02f54c25f139359c9c98be0fa657d0bf1ae4115dcf6fdbf5f3a470f1d251f769610b48fe34eeab59e82ac1cc0336d1d9109a14b768b97ccb4db4c2431629688_ -Save the file and exit: +`**PRIVATE KEY:**`**YmRiNmViOGYzMmQ3OWY0YjE4ODJjMzE1ODA4YjQyZmZjODhiZDQxNzMwNmE5MTRiZjQ4OTAyNjM0MTcyNjMzMw==** -- If you’re editing with **nano**, press `Ctrl+X`, then `y`, and `Enter` -- If you’re editing with **vi** or **vim**, hold down `Shift` and press `z` twice. +Always save and protect **private keys**, they are like your username + password + 2FA at your bank, all combined. +_Public keys_ are like your phone number - no harm in others knowing it, it actually is needed for some scenarios. Still, only share it on a need to basis, like you would do with your own phone number. -## **Ensure user privileges** -Ensure your user has `sudo` enabled and accessible so that it doesn't ask for a password every time it executes something. +## How to generate a new key -If you are certain this is already done, feel free to skip forward. Otherwise, you will need to add your username to a special list. +The easiest way to generate a new validator key is by using the `keygenerator` tool that resides near the node. -So let's add it to the overrides: +- [https://github.com/multiversx/mx-chain-go/tree/master/cmd/keygenerator](https://github.com/multiversx/mx-chain-go/tree/master/cmd/keygenerator) -```bash -sudo visudo -f /etc/sudoers.d/myOverrides +How to generate a new validator key if golang is already set on the host: + +```shell +$ git clone https://github.com/multiversx/mx-chain-go.git +$ cd mx-chain-go/cmd/keygenerator +$ go build +$ ./keygenerator --key-type validator ``` -Now, navigate to the end of the file by pressing `Shift + G`. Next, press `o` to add a new line, and type the following, replacing `username` with the output of the `whoami` command that was run earlier. +Alternatively, if you've already installed a node on the host, you can issue the following command: -```bash -yourusername ALL=(ALL) NOPASSWD:ALL +```shell +$ cd ~/elrond-utils/ +$ ./keygenerator --key-type validator ``` -Conclude by pressing `Esc`, then save and close the file by holding down `Shift` while pressing `z` twice. +--- -Your user should now be able to execute `sudo` commands. +### Validators - Overview ---- +This page provides an overview of the Validator Nodes and the associated Tools. -### Staking & Unstaking -This page will guide you through the process of staking and unstaking nodes. +## Table of contents -## **Introduction** +### Install and maintain a node -Before staking, a node is a mere observer. After staking, the node becomes a validator, which means that it will be eligible for consensus and will earn rewards. Validators play a central role in the operation of the network. +| Name | Description | +| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| [System requirements](/validators/system-requirements) | System requirements for running a MultiversX node. | +| [Install a Mainnet/Testnet/Devnet Node](/validators/nodes-scripts/config-scripts) | Instructions about how to get a Testnet or a Devnet node up and running. | -**Staking** is the process by which the operator of the node sends a sum of 2500 EGLD to be locked in a system SmartContract. Multiple nodes can be staked at once, and their operator must lock 2500 EGLD for each of the nodes. This sum acts as a collateral, and it will be released back to the node operator through the process of **unstaking**, with a final step called **unbonding**. -A validator node produces rewards, which are transferred to the node operator at their **reward address** of choice, decided upon during the staking process. The reward address may be changed after staking as well. +### Keys Management -Each staking or unstaking process requires a transaction to be sent to the Staking Smart Contract. These transactions must contain all the required information, encoded properly, and must provide a high enough gas limit to allow for successful execution. These details are described in the following pages. +| Name | Description | +|-----------------------------------------------------------------|--------------------------------------------------------------| +| [Validator keys](/validators/key-management/validator-keys) | Learn about a validator key. | +| [Wallet keys](/validators/key-management/wallet-keys) | Learn about a wallet key. | +| [Protecting your keys](/validators/key-management/protect-keys) | Learn how you can secure your keys. | +| [Multikey nodes](/validators/key-management/multikey-nodes) | Learn how to manage a set of keys by using a group of nodes. | -There are currently 2 supported methods of constructing and submitting these transactions to the Staking SmartContract: -- Manually constructing the transaction, then submitting it to [wallet.multiversx.com](https://wallet.multiversx.com/); -- Automatically constructing the transaction and submitting it using the `mxpy` command-line tool. +### Staking -The following pages will describe both approaches in each specific case. +| Name | Description | +| ----------------------------------------------------------------------------------- | ------------------------------------------------------------- | +| [Staking, unstaking and unjailing](/validators/staking/staking-unstaking-unjailing) | Learn about how to stake, unstake or unjail a Node. | +| [How to Stake a Node](/validators/staking) | Learn how to stake a node via a step-by-step tutorial. | +| [How to unJail a Node](/validators/staking/unjailing) | Learn how to unJail a node. | +| [The Staking Smart Contract](/validators/staking/staking-smart-contract) | How to interact with the Smart Contract that manages Staking. | -## **Prerequisites** +### Delegation Manager -In order to submit a staking transaction, you must have the following: +| Name | Description | +| ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| [Delegation Manager](/validators/delegation-manager) | Learn how to create a new Staking Provider, how to configure it and how to interact with it. | +| [How to convert an existing Validator into a Staking Pool](/validators/staking/convert-existing-validator-into-staking-provider) | Learn how to create a new Staking Provider, starting from an existing Validator. | +| [Merge an existing Validator into a Staking Pool](/validators/staking/merge-validator-delegation-sc) | Learn how to merge a validator into a Staking Provider. | -- 2500 EGLD for each node and 0.006 EGLD per node as transaction fee -- A unique `validatorKey.pem` file of each node -You have the option of staking through the online Wallet at [https://wallet.multiversx.com](https://wallet.multiversx.com/) or by using `mxpy`. +### Useful +| Name | Description | +| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| [Node operation modes](/validators/node-operation-modes) | Learn about the nodes' operation modes and how to use them for setting up node for various use-cases. | +| [Node Configuration](/validators/node-configuration) | Learn about the nodes' configuration files: how to use them, how to override values, and so on. | +| [Rating](/validators/rating) | Learn about the nodes' rating, how it increases and decreases and how it impacts the earnings. | +| [Node Upgrade](/validators/node-upgrades) | Learn about the periodical or emergency upgrades that nodes' owner have to do. | +| [Node Redundancy / Back-up](/validators/redundancy) | How to set up a back-up node for your main machine. | +| [Import DB](/validators/import-db) | Learn how to start the node in import-db mode, that allows reprocessing of old data, without syncing the network. | +| [Node CLI](/validators/node-cli) | How to use the Command Line Interface of the Node. | +| [Node Databases](/validators/node-databases) | How to use the nodes' databases and how to copy them from one node to another. | +| [Useful link & tools](/validators/useful-links) | Useful links about the explorer, wallet and some guides. | +| [FAQ](/validators/faq) | Frequently Asked Questions about nodes. | -## **Staking through the Wallet** -1. Go to https://wallet.multiversx.com and log into your wallet -2. Go to the Validate section -3. Press "Stake now" +## Overview -![staking1](/validators/staking1.png) +The MultiversX network is made up of nodes and their interconnectivity - balanced by virtue of its design, secured through its size and fast, _very_ fast, because efficiency is what motivated its development. Every time a node joins the network, it adds more security and efficiency. The network, in turn, rewards the nodes for their contribution, generating a virtuous cycle. -4. Navigate to the location of the .pem file or drag & drop it -5. Press "Continue" +We will call a _node_ any running instance of the software application developed by the MultiversX team, [publicly available as open source](https://github.com/multiversx/mx-chain-go). Anyone can run a node on their machine - great care was taken to make the node consume as little computing resources as possible. Mid-level recent hardware can effortlessly run multiple individual nodes at the same time, earning more rewards for the same physical machine. -![staking2](/validators/staking2.png) +We will call a _node operator_ any person or entity who manages one or more nodes. These pages are for them. -6. The staking transaction data is automatically populated using the public key in the .pem certificate you provided. The private key is not touched and the data does not leave your browser. Only the transaction with this public information will be sent to the network once you press Confirm -7. Press "Confirm" -![staking3](/validators/staking3.png) +## Background -8. The status of the transaction will be displayed on screen, together with a success message. Click "Done" once you see the Success message. +MultiversX is a decentralized blockchain network. This means that its nodes collaborate to create sequential **blocks** with strict regularity - blocks which contain the results of operations that were requested by the users of the network. Such operations may be simple transfers of tokens, or may be calls to SmartContracts. Either way, _all_ operations take the form of **transactions**. -![staking4](/validators/staking4.png) +Any user who submits a transaction to the network must pay a fee, in EGLD tokens. These fees are what produces **rewards** for the nodes. -9. You can review the transaction in your history. Based on the current staking capacity of the network, you will get an OK message indicating that your node has become a validator, or a response indicating that the network staking is at capacity and your node has been put in the Queue. +Note that not all nodes earn rewards from these fees. Only **validator nodes** qualify, because they are the nodes which are allowed to take part in [consensus](/learn/consensus), to produce and validate blocks and to earn rewards. -![staking5](/validators/staking5.png) +Because of the influence they have in the network, validator nodes are required to have a **stake**, which is a significant amount of EGLD locked as collateral for the good behavior of the validator. Currently, the stake amount is set to 2500 EGLD. Nodes without a stake are called **observer nodes** - they don't participate in consensus and do not earn rewards, but they support the network in different ways. -10. The information about the staked nodes from the current wallet will be updated -11. You can further interact with your node(s) by clicking on the three vertical dots next to the public key, which brings up a menu for performing actions such as Unjail, Unstake and Unbond. +If the validator consistently misbehaves or performs malicious actions, it will be fined accordingly and lose EGLD, an action known as _stake slashing,_ and by also having its validator status removed. This form of punishment is reserved for serious offences. -![staking6](/validators/staking6.png) +Validator nodes each have an individual **rating score**, which expresses their overall reliability and responsiveness. Rating will increase for well-behaved nodes: every time a validator takes part in a successful consensus, its rating is increased. +The opposite is also true: a validator which is either offline during consensus or fails to contribute to the block being produced will be considered unreliable. And a consistently unreliable validator will see its rating drop. -## **Staking through mxpy** +**Consensus selection probability** is strongly influenced by a validators rating. The consensus process _favors validators with high rating_ and will avoid selecting validators with low rating. -Submitting the staking transaction using `mxpy` avoids having to write the "Data" field manually. Instead, the staking transaction is constructed automatically by `mxpy` and submitted to the network directly, in a single command. +This implies that a node with high rating produces far more rewards than a node with low rating, so it is essential that operators maintain their validators online, up-to-date and responsive. -Make sure `mxpy` is installed by issuing this command on a terminal: +Moreover, if the rating of a validator becomes too low, it will be **jailed**. A jailed validator will not be selected for consensus - thus earning no rewards. To restore the validator, it must be **unjailed**, which requires a fine to be paid, currently set to 2.5 EGLD. -```bash -mxpy --version -``` +--- -If `mxpy` is not installed (`command not found`), please follow [these instructions](/sdk-and-tools/mxpy/installing-mxpy). +### Wallet Keys -Make sure `mxpy` is installed and has the latest version before continuing. +This page describes the wallet keys, that are used for staking and managing nodes. -## **Your Wallet PEM file** +## Wallet keys description -To send transactions on your behalf _without_ using the online MultiversX Wallet, `mxpy` must be able to sign for you. For this reason, you have to generate a PEM file using your Wallet mnemonic. +As a Validator you use the Wallet Keys to access the address from which you send the staking transaction. Your EGLD holdings leave this address and are deposited into a staking smart contract. Rewards are sent to this address. You can change it later on by using a `changeRewards` transaction. -Please follow the guide [Deriving the Wallet PEM file](/sdk-and-tools/mxpy/mxpy-cli#converting-a-wallet). Make sure you know exactly where the PEM file was generated, because you'll need to reference its path in the `mxpy` commands. +This wallet is the only one that can be used to send an un-stake transaction, meaning to recover your 2500 EGLD from the staking smart contract. -After the PEM file was generated, you can issue transactions from `mxpy` directly. +A Wallet Key can be created via multiple ways that are described on the [Wallets section](/wallet/overview/). +The wallets use the bip44 standard with the mention that because MultiversX uses Ed25519 only hardened paths are used. Our coin_type is 508, making the path for the first address:m/44'/508'/0'/0'/0’ -## **The staking transaction** +--- -The following commands assume that the PEM file for your Wallet was saved with the name `walletKey.pem` in the current folder, where you are issuing the commands from. +## Integrators +### Accounts Management -The command to submit a staking transaction with `mxpy` is this: +Managing Wallets and Addresses -```bash -mxpy --verbose validator stake --pem=walletKey.pem --value="" --validators-file= --proxy=https://gateway.multiversx.com -``` +This page summarizes the recommended approach for managing accounts in an application that integrates with the Network. -Notice that we are using the `walletKey.pem` file. Moreover, before executing this command, you need to replace the following: +:::tip +If integrating a **system** with the **Network** involves transfers between different users (accounts) - a good example for this case is the integration between an **exchange system** and the **Network** - the recommended approach is to have **a MultiversX Account (Address) for each user of the system**. +::: -- Replace `` with the amount you are staking. You need to calculate this value with respect to the number of nodes you are staking for. See the [beginning of the "Staking through the Wallet"](/validators/staking#staking-through-the-wallet) section for info on how to do it. -- Replace `` with the JSON file that lists the nodes you are staking for. This JSON file should look like this: +Accounts creation can be achieved through different approaches: -```json -{ - "validators": [ - { - "pemFile": "valPem1.pem" - }, - { - "pemFile": "valPem2.pem" - }, - { - "pemFile": "valPem3.pem" - } - ] -} -``` +- using the [MultiversX Web Wallet](https://wallet.multiversx.com/) +- programmatically, using the [sdk-js - JavaScript SDK](/sdk-and-tools/sdk-js) +- programmatically, using the [mxpy - Python SDK](/sdk-and-tools/sdk-py/) +- programmatically, using the [sdk-go - Golang SDK](/sdk-and-tools/sdk-go) +- programmatically, using the [sdk-java - Java SDK](/sdk-and-tools/mxjava) +- using the [lightweight CLI](https://www.npmjs.com/package/@multiversx/sdk-wallet-cli) +- using our [lightweight HTTP utility](https://github.com/multiversx/mx-sdk-js-wallet-http) +- programmatically, using the [TrustWalletCore extension](https://github.com/trustwallet/wallet-core/tree/master/src/MultiversX) for MultiversX -The `pemFile` field should point to valid Validator PEM file. **Note that paths must be relative to the JSON file itself.** +--- -Here's an example for a staking command for one node: +### Advanced Observer Settings -``` -mxpy --verbose validator stake --pem=walletKey.pem --value="2500000000000000000000" --validators-file=my-validators.json --proxy=https://gateway.multiversx.com -``` +This page describes some of the settings an integrator might want to apply on the observers in order to better make use of the nodes. -:::note important -You must take **denomination** into account when specifying the `value` parameter in **mxpy**. -::: +For the settings from the `config.toml` file that are needed to be altered, we recommend using the `OverridableConfigTomlValues` section found in the `prefs.toml` file. More info can be found [here](/validators/node-configuration#overriding-configtoml-values) -For two nodes, it becomes this: -``` -mxpy --verbose validator stake --pem=walletKey.pem --value="5000000000000000000000" --validators-file=my-validators.json --proxy=https://gateway.multiversx.com +## Web antiflood settings + +Each node, either observer or validator, will be configured automatically with the web antiflood turned on and set to relatively low limits. This was chosen as a protection measure during the initial setup of the node but can be easily changed through the configuration files. + +The original section found in the `config.toml` file looks like this: + +```toml +[WebServerAntiflood] + WebServerAntifloodEnabled = true + # SimultaneousRequests represents the number of concurrent requests accepted by the web server + # this is a global throttler that acts on all http connections regardless of the originating source + SimultaneousRequests = 100 + # SameSourceRequests defines how many requests are allowed from the same source in the specified + # time frame (SameSourceResetIntervalInSec) + SameSourceRequests = 10000 + # SameSourceResetIntervalInSec time frame between counter reset, in seconds + SameSourceResetIntervalInSec = 1 + # TrieOperationsDeadlineMilliseconds represents the maximum duration that an API call targeting a trie operation + # can take. + TrieOperationsDeadlineMilliseconds = 10000 + # GetAddressesBulkMaxSize represents the maximum number of addresses to be fetched in a bulk per API request. 0 means unlimited + GetAddressesBulkMaxSize = 100 + # VmQueryDelayAfterStartInSec represents the number of seconds to wait when starting node before accepting vm query requests + VmQueryDelayAfterStartInSec = 120 + # EndpointsThrottlers represents a map for maximum simultaneous go routines for an endpoint + EndpointsThrottlers = [{ Endpoint = "/transaction/:hash", MaxNumGoRoutines = 10 }, + { Endpoint = "/transaction/send", MaxNumGoRoutines = 2 }, + { Endpoint = "/transaction/simulate", MaxNumGoRoutines = 1 }, + { Endpoint = "/transaction/send-multiple", MaxNumGoRoutines = 2 }] ``` +The general off/on switch is done through the `WebServerAntifloodEnabled` config value. As an integrator you might want to turn off the web antiflooder, **in case the node is not directly reachable over the Internet** as a mean to provide your gateway instance (proxy) to use as many node resources it wants. +This is the easiest way to tell the node to not bother with that type of antiflooding. However, this does not imply the integrator to remove all the web antiflooding protection, especially before his/her owned gateway instance (proxy). -## **The --reward-address parameter** +The following list explains what the parameters do: +- `SimultaneousRequests` tells the node how many REST API calls can simultaneous serve; +- `SameSourceRequests` tells how many REST API requests originating from the same source the node can serve per time unit. The time unit is specified in the `SameSourceResetIntervalInSec` parameter and is expressed in seconds; +- `TrieOperationsDeadlineMilliseconds` tells the node when it should time out the REST API requests that involves data trie traversing. This should be increased in case the node is trying to fetch data from large smart contracts or user accounts; +- `GetAddressesBulkMaxSize` tells the node what is the maximum number of addresses that can be fetched in a bulk-get operation; +- `VmQueryDelayAfterStartInSec` is a parameter that should is taken into account only when the node is started. The vm-query requests are not executed in this initial startup phase; +- `EndpointsThrottlers` is a list that defines some REST API endpoints and their maximum simultaneous requests that the node can handle. -When you submit a staking transaction, the Staking SmartContract remembers the wallet you sent it from, and the rewards from your staked validators will go to that wallet. This is the _default_ behavior. In this case, it will be the wallet which you used to generate the `walletKey.pem` file in the earlier subsection ["Your Wallet PEM file"](/validators/staking#your-wallet-pem-file). -Alternatively, you can tell `mxpy` to specify another wallet to which your rewards should be transferred. You will need the **address of your reward wallet** (it looks like `erd1xxxxx…`) for this, which you will pass to `mxpy` using the `--reward-address` parameter. +## VM settings for query operations -For example, a staking command for a single node, with a reward address specified, looks like this: +The node can handle one or more query-services able to simultaneous execute vm queries. -``` -mxpy --verbose validator stake --reward-address="erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" --pem=walletKey.pem --value="2500000000000000000000" --validators-file=my-validators.json --proxy=https://gateway.multiversx.com +The original section found in the `config.toml` file looks like this: + +```toml +[VirtualMachine.Querying] + NumConcurrentVMs = 1 ``` -The above command will submit a staking command and will also inform the Staking SmartContract that the rewards should be transferred to the wallet `erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th` . +The `NumConcurrentVMs` value specifies the number of VMs instances allocated for vm-query operations. The higher this value is, the more concurrent vm-query operation the node can handle. ---- +:::important +The increased number of VMs allocated for the vm-query engine will put pressure on the RAM allocation for the node process at around 500-600 MB / extra instance. Disk & CPU utilization will also get higher in a proportional manner. +::: -### Staking Providers -```mdx-code-block -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; +## UserAccounts cache parameters + +As found during heavy testing, spending more RAM on caches might help in vm-query execution. The reason behind this statement is that, usually, smart contract execution need smart contract data to be read from storage and then processed. +The data fetch mechanism costs in terms of CPU cycles and disk utilization, so they affect the execution time of the vm-query. +If the node has more than the [minimum required RAM](/validators/system-requirements) then the following section from the `config.toml` file can be altered: + +```toml +[AccountsTrieStorage] + [AccountsTrieStorage.Cache] + Name = "AccountsTrieStorage" + Capacity = 500000 + Type = "SizeLRU" + SizeInBytes = 314572800 #300MB ``` +- `Capacity` can be increased to 10000000 or to a larger value; +- `SizeInBytes` can be increased to `1073741824` (1GB) or even pushed to `4294967296` (4GB). -## Introducing staking providers +--- -A **staking provider** is defined as a custom delegation smart contract, the associated nodes and the funds staked in the pool by participants. **Node operators** may wish to set up a staking provider for their nodes, which can then be funded by anyone in exchange for a proportion of the validator rewards. This form of funding the stake for validators is called **delegation**. +### Creating Transactions -Staking providers bridge the gap between node operators, who need funds to stake for their nodes, and fund holders who wish to earn rewards by staking their funds, but are not interested in managing validator nodes. +This page describes how to create, sign and broadcast transactions to the MultiversX Network. -Node operators can set up a staking provider to manage one or more validator nodes. For this purpose, they may use the **delegation manager** built into the MultiversX Protocol to create their own **delegation contract**. A delegation contract automates certain tasks required for the management of a staking provider, such as keeping track of every account that has funded the staking provider, keeping track of the nodes themselves, as well as providing information to the delegators. -:::important -A staking provider requires 1250 EGLD deposited by the node operator at the moment of its creation. However, 2500 EGLD is required to stake a single validator node and start earning rewards. -::: +## **Transaction structure** -This page describes how to request a new delegation contract from the delegation manager and how to use it. It will focus on the delegation _contract_ more than the delegation _manager_, but the two concepts are intimately linked. However, it is important to remember that it is the delegation _contract_ which handles the staking provider and the nodes associated with it. +As described in section [Signing Transactions](/developers/signing-transactions), a ready-to-broadcast transaction is structured as follows: -Note that the delegation manager is not required to set up a staking provider. For example, it is also possible to set up delegation using a regular smart contract, although that is a more complex process and is not discussed here. +```json +{ + "nonce": 42, + "value": "100000000000000000", + "receiver": "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", + "sender": "erd1ylzm22ngxl2tspgvwm0yth2myr6dx9avtx83zpxpu7rhxw4qltzs9tmjm9", + "gasPrice": 1000000000, + "gasLimit": 70000, + "data": "Zm9vZCBmb3IgY2F0cw==", + "chainID": "1", + "version": 1, + "signature": "5845301de8ca3a8576166fb3b7dd25124868ce54b07eec7022ae3ffd8d4629540dbb7d0ceed9455a259695e2665db614828728d0f9b0fb1cc46c07dd669d2f0e" +} +``` -Node operators may also choose to set up a delegation dashboard, although they may use any user interface or none whatsoever. As an example, the boilerplate for such a delegation dashboard can be found here: https://github.com/multiversx/mx-delegation-dapp. Alternatively, the old boilerplate is located here: https://github.com/multiversx/mx-deprecated-starter-dapp/tree/master/react-delegationdashboard. -A detailed description of the delegation process can be consulted at https://github.com/multiversx/mx-specs/blob/main/sc-delegation-specs.md. +## **SDK and tools support for creating and signing transactions** +There are SDKs or tools with support for interacting with the MultiversX blockchain, so one can use one of the following SDKs to perform +transactions creation and signing: -## Creating a new delegation contract +- [sdk-js - JavaScript SDK](/sdk-and-tools/sdk-js) +- [sdk-py - Python SDK](/sdk-and-tools/sdk-py) +- [sdk-go - Golang SDK](/sdk-and-tools/sdk-go) +- [sdk-java - Java SDK](/sdk-and-tools/mxjava) +- [lightweight JS CLI](https://www.npmjs.com/package/@multiversx/sdk-wallet-cli) +- [lightweight HTTP utility](https://github.com/multiversx/mx-sdk-js-wallet-http) -The delegation contract for a new staking provider can be created by issuing a request to the delegation manager. This is done by submitting a transaction of the following form: -```rust -NewDelegationContractTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6 - Value: 1250000000000000000000 (1250 EGLD) - GasLimit: 60000000 - Data: "createNewDelegationContract" + - "@" + + - "@" + +## **General network parameters** + +General network parameters, such as the **chain ID**, **the minimum gas price**, **the minimum gas limit** and the **oldest acceptable transaction version** are available at the API endpoint [Get Network Configuration](/sdk-and-tools/rest-api/network#get-network-configuration). + +```json +{ + "config": { + "erd_chain_id": "1", + "erd_gas_per_data_byte": 1500, + "erd_min_gas_limit": 50000, + "erd_min_gas_price": 1000000000, + "erd_min_transaction_version": 1, + ... + } } ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - -The `Receiver` address is set to `erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6`, which is the fixed address of the delegation manager, located on the Metachain. -The `Value` is set to 1250 EGLD, which will be automatically added into the funds of the newly created delegation contract, i.e. this is the initial amount of EGLD in the staking provider. This amount of EGLD always belongs to the owner of the delegation contract. +## **Nonce management** -:::important -The initial 1250 EGLD count towards the total delegation cap, like all the funds in the staking provider. +Each transaction broadcasted to the Network must have the **nonce** field set consistently with the **account nonce**. In the Network, transactions of a given sender address are processed in order, with respect to the transaction nonce. -The initial amount of 1250 EGLD added to the pool makes the owner the first delegator of the staking provider. This means that the owner is also entitled to a proportion of the rewards, which can be claimed like any other delegator. -::: +The account nonce can be fetched from the API: [Get Address Nonce](/sdk-and-tools/rest-api/addresses#get-address-nonce). -In the `Data field`, the first argument passed to `createNewDelegationContract` is the total delegation cap (the maximum possible size of the staking provider). It is expressed as a fully denominated amount of EGLD, meaning that it is the number of $10^{-18}$ subdivisions of the EGLD, and not the actual number of EGLD tokens. The fully denominated total delegation cap must then be encoded hexadecimally. Make sure not to encode the ASCII string representing the total delegation cap. +**The nonce must be a strictly increasing number, scoped to a given sender.** The sections below describe common issues and possible solutions when managing the nonce for transaction construction. -:::tip -For example, to obtain the fully denominated form of 7231.941 EGLD, the amount must be multiplied by $10^{18}$, resulting in 7231941000000000000000. Do not encode the ASCII string `"7231941000000000000000"`, but encode the integer 7231941000000000000000 itself. This would result in `"01880b57b708cf408000"`. -::: -Setting the total delegation cap to 0 ("00" in hexadecimal) specifies an unlimited total delegation amount. It can always be modified later (see [Delegation cap](/validators/delegation-manager#delegation-cap)). +### **Issue: competing transactions** -The second argument passed to `createNewDelegationContract` is the service fee that will be reserved for the owner of the delegation contract. It is computed as a proportion of the total rewards earned by the validator nodes. The remaining rewards apart from this proportion will be available to delegators to either claim or redelegate. The service fee is expressed as hundredths of a percent. +Broadcasted transactions that reach the _mempool_ having the same sender address and the same nonce are _competing transactions_, and only one of them will be processed (the one providing a higher gas price or, if they have the same gas price, the one that arrived the second - but keep in mind that arrival time is less manageable). :::tip -For example, a service fee of 37.45% is expressed by the integer 3745. This integer must then be encoded hexadecimally (3745 becomes `"0ea1"`). +Avoid competing transactions by maintaining a strictly increasing nonce sequence when broadcasting transactions of the same sender address. ::: -Setting the service fee to 0 (`"00"` in hexadecimal) specifies that no rewards are reserved for the owner of the delegation contract - all rewards will be available to the delegators. The service fee can always be modified later (see [Service fee](/validators/delegation-manager#service-fee)). - -The following is a complete example of a transaction requesting the creation of a new delegation contract: +Although an explicit _transaction cancellation trigger_ is not yet available in the Network, cancellation of a transaction T1 with nonce 42 could be _possible_ if one broadcasts a second transaction T2 with same nonce 42, with higher gas price (and without a value to transfer) **immediately** (e.g. 1 second) after broadcasting T1. -```rust -NewDelegationContractTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6 - Value: 1250000000000000000000 - GasLimit: 60000000 - Data: "createNewDelegationContract" + - "@01880b57b708cf408000" + - "@0ea1" -} -``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +### **Issue: nonce gaps** -The above transaction creates a new delegation contract owned by the sender, with total delegation cap of 7231.941 EGLD and service fee of 37.45% from the rewards. Moreover, the newly created delegation contract will start with a staking provider of 1250 EGLD. +If broadcasted transactions have their nonces higher than the current account nonce of the sender, this is considered a _nonce gap_, and the transactions will remain in the mempool unprocessed, until new transactions from the same sender arrive _to resolve the nonce gap -_ or until the transactions are swept from the mempool (sweeping takes place regularly). +:::tip +**Avoid nonce gaps** by regularly fetching the current account nonce, in order to populate the nonce field correctly before broadcasting the transactions. This technique is also known as **periodically recalling the nonce**. +::: -## Configuring the delegation contract -The owner of the delegation contract has a number of operations at their disposal. +### **Issue: too many transactions from the same account** +Starting with the [Sirius Mainnet Upgrade](https://github.com/multiversx/mx-specs/blob/main/releases/protocol/release-specs-v1.6.0-Sirius.md), the transaction pool allows a maximum of **100** transactions from the same sender to exist, at a given moment. -### Metadata +For example, if an address broadcasts `120` transactions with nonces from `1` to `120`, then the transactions with nonces `1 - 100` will be accepted for processing, while the remaining `20` transactions will be dropped. -The delegation contract can store information that identifies the staking provider: its human-readable name, its website and its associated GitHub identity. +The solution is to use chunks holding a **maximum of `100` transactions** and a place a generous **delay between sending the chunks**. Let's suppose an account has the nonce `1000` and it wants to send `120` transactions. It should send the first chunk, that is, the transactions with nonces `1000 - 1099`, wait until all of them are processed (the account nonce increments on each processed transaction), then send the second chunk, the transactions with nonces `1100 - 1019`. -```rust -SetMetadataTransaction { - Sender: - Receiver:
- Value: 0 - GasLimit: 2000000 - Data: "setMetaData" - "@" + + - "@" + + - "@" + -} -``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +### **Issue: fetching a stale account nonce** -An example for the `Data` field that sets the name to `"Test Mx Provider"`, the website to `"testmx.provider"` and the GitHub identifier to `"testmxprovider"` is: +You should take care when fetching the current account nonce from the API immediately after broadcasting transactions. -```rust - "setMetaData" + - "@54657374204d782050726f7669646572" // Test Mx Provider - "@746573746d782e70726f7669646572" // testmx.provider - "@746573746d7870726f7669646572" // testmxprovider -``` +Example: -:::important -Setting the identity of the staking provider in the metadata is the **first step** in connecting the delegation contract and a GitHub identity. The second step is explained in the next section [Display information](/validators/delegation-manager#display-information) where the inverse connection is made: from the GitHub identity to the delegation contract address. -::: +1. Time 12:00:01 - the sender's nonce is recalled, and its value is 42 +2. Time 12:00:02 - the sender broadcasts the transaction T1 with nonce 42 +3. Time 12:00:03 - the sender's nonce is recalled again, in order to broadcast a new transaction. **The nonce is still 42. It is stale, not yet incremented on the Network (since T1 is still pending or being processed at this very moment).** +4. Time 12:00:04 - the sender broadcasts T2 with nonce 42, which will compete with T1, as they have the same nonce. +:::tip +Avoid fetching stale account nonces by **periodically recalling the nonce.** -### Display information +Avoid recalling the nonce in between **rapidly sequenced transactions from the same sender** . For rapidly sequenced transactions, you have to programmatically manage, keep track of the account nonce using a **local mirror (copy) of the account nonce** and increment it appropriately. +::: -:::caution -As of January 2024, the only accepted way to customize the information of a delegation contract is via MultiversX Assets. -Provisioning of information from `keybase` or your `GitHub` repository is no longer accepted. +### **Issue: sending large batches of transactions cause nonce gaps** -The only accepted changes are on `mx-assets` identities. +Whenever sending a large batch of transactions, even if the node/gateway returned transaction hashes for each transaction in the batch and no error, there is no strict guarantee that those transactions will end up being executed. +The reason is that the node will not immediately send each transaction or transaction batch but rather accumulate them in packages to be efficiently send through the p2p network. +At this moment, the node might decide to drop one or more packet because it detected a possible p2p flooding condition. This can happen independent of the transaction sender, the number of transactions sent and so on. -You can safely delete the `keybase` profile and also the `multiversx` repository inside your organization's `GitHub`. -::: +To handle this behavior, special care should be carried by the integrators. One possible way to handle this efficiently is to temporarily store all transactions that need to be sent on the network and continuously monitor the senders accounts involved if their nonces increased. +If not, a resend of the required transaction is needed, otherwise the transaction might be discarded from the temporary storage as it was executed. -To customize the information for your delegation contract, which will be available inside the MultiversX ecosystem (such as Explorer, Web Wallet, xPortal, and so on), -some additional information has to be added to the MultiversX assets repository. +We have implemented several components written in GO language that solve the transaction send issues along with the correct nonce management. +The source code can be found [here](https://github.com/multiversx/mx-sdk-go/tree/main/interactors/nonceHandlerV2) +The main component is the `nonceTransactionsHandlerV2` that will create an address-nonce handler for each involved address. This address nonce handler will be specialized in the nonce and transactions sending mechanism for a single address and will be independent of the other addresses involved. +The main component has a few exported functionalities: +- `ApplyNonceAndGasPrice` method that is able to apply the current handled nonce of the sender and the network's gas price on a provided transaction instance +- `SendTransaction` method that will forward the provided transaction towards the proxy but also stores it internally in case it will need to be resent. +- `DropTransactions` method that will clean all the stored transactions for a provided address. +- `Close` cleanup method for the component. -In order to do so, the owner of the nodes/staking provider must open a Pull Request against https://github.com/multiversx/mx-assets (`master` branch). -**Step 1: choose the environment** +## **Gas limit computation** -- for mainnet go to `mx-assets/identities` -- for testnet go to `mx-assets/testnet/identities` -- for devnet go to `mx-assets/devnet/identities` +Please follow [Gas and Fees](/developers/gas-and-fees/overview/). -**Step 2: create a directory with an unique name** -Must be lowercase, alphanumeric, with no spaces. For example, `my-new-identity` +## **Signing transactions** -**Step 3: add a `info.json` file and a logo.png** +Please follow [Signing Transactions](/developers/signing-transactions). -Create a `info.json` file where you specify the owned address(es), the name, the description and social links. -Example: -```json -{ - "description": "This is my new identity", - "name": "New identity", - "website": "http://newidentity.com", - "twitter": "https://twitter.com/newidentity", - "location": ", ", - "owners": [ - "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqxgeryqxzqjkh", // staking provider address - "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" // the address of regular validator nodes owner - ] -} -``` -Inside the owners array: -- if you want to define an identity of a Staking Provider, add its SC address -- if you want to define an identity of simple nodes, add the owner address (all the keys owned by that address will have that identity) -- if you want to define the same identity for both a Staking Provider and simple nodes, set both the address of the Staking Provider and of the owner of the nodes +## **Simulate transaction execution** -Also, upload a `logo.png` that will be displayed near the identity. +:::important +Documentation about transaction simulation is preliminary and subject to change. +::: -**Step 4: Open PR and validate the ownership** +--- -After opening the PR, you also have to validate the ownership. Please refer to the [this](#assets-ownership-validation) section. +### Deep History Squad -**Done** +This page describes the Deep History Squad, which holds the entire trie data, so it can be used to query the state of an account at any point in time. -After the PR is merged, the specified information together with the **service fee, percentage filled** and **APR** (for Staking Providers) will be displayed across the ecosystem. -If this information cannot be found a generic logo and the address is displayed. -An example of how the delegation contract will be displayed based on the information provided in the GitHub is provided below. +## General information -![stakingpool](/img/stakingpool.png) +A variant of the standard [**observing squad**](/integrators/observing-squad) is one that retains a non-pruned history of the blockchain and allows one to query the state of an account at an arbitrary block in the past. Such a setup is called a [**deep-history observing squad**](https://github.com/multiversx/mx-chain-deep-history). -:::note -Before MultiversX Assets, nodes owners needed to update the `Identity` field inside the `prefs.toml` file of each node. This is not required anymore now and you can safely leave the identity empty on your node configuration. +:::tip +The standard observing squad is sufficient for most use-cases. It is able to resolve past blocks, miniblocks, transactions, transaction events etc. Most often, you do not need a deep-history squad, but a [**regular observing squad**](/integrators/observing-squad), instead. ::: -### Assets ownership validation - -This applies for both adding of updating an identity over the MultiversX assets. - -In order to validate the ownership of a staking provider or validator nodes, one will need to sign a message by using -the owner wallet and then submit a comment on the PR with the branding of a validator node or a staking provider. -In case of a staking provider, its owner must perform this message signing. - -Message signing can be performed either via [Web Wallet](https://wallet.multiversx.com), either via [MultiversX Utils](https://utils.multiversx.com/sign-message). -The message to be signed is the commit hash of the latest commit in the PR. Inside the PR, go to the `Commits` tab and copy the latest commit hash (the bottom one is the latest commit). +A deep-history setup is able to resolve historical account (state) queries, that is, to answer questions such as: -![commit-hash](/img/commit-hash.png) +> What was Alice's balance on [May the 4th](https://explorer.multiversx.com/blocks/5f6a02d6a5d2a851fd6dc1fb53435083830c2a13121e003958d97c2389711f06)? -Then, by using a message signer (Web Wallet of MultiversX Utils), sign that commit hash (make sure you don't have additional spaces or other characters) by using the owner wallet. +```bash +GET http://squad:8080/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th?blockNonce=9250000 +``` -![commit-hash-sign](/img/commit-hash-sign.png) +:::tip +The API client has to perform the conversion from _desired timestamp_ to _block nonce_. Timestamp-based queries aren't directly supported yet. +::: -After that, leave a comment on that PR with the resulted signature. +> How much UTK were in the [`UTK / WEGLD` Liquidity Pool](https://explorer.multiversx.com/accounts/erd1qqqqqqqqqqqqqpgq0lzzvt2faev4upyf586tg38s84d7zsaj2jpsglugga) on [1st of October](https://explorer.multiversx.com/blocks/cefd41e1e9bbe3ba023a695f412b99cecb15ef789475648ee7c31e7d9fef31d1)? -![commit-hash-sig-comm](/img/commit-hash-sig-comm.png) +```markup +GET http://squad:8080/address/erd1qqqqqqqqqqqqqpgq0lzzvt2faev4upyf586tg38s84d7zsaj2jpsglugga/key/726573657276650000000a55544b2d326638306539?blockNonce=11410000 +``` -That's it. A GitHub workflow will validate the signature and if everything is ok, the PR will merged by a MultiversX responsible and the identity will be live anytime soon. +In the example above, the key `726573657276650000000a55544b2d326638306539` is decoded as `reserve\x00\x00\x00\nUTK-2f80e9`. -### Service fee +### Historical VM queries -The service fee is a percentage of the validator rewards that will be reserved for the owner of the delegation contract. The rest of the rewards will be available to delegators to either claim or redelegate. +Starting with the [Sirius Patch 5](https://github.com/multiversx/mx-chain-mainnet-config/releases/tag/v1.6.18.0), deep-history observers can resolve historical VM queries. Such a query specifies the `blockNonce` parameter: -The service fee can be changed at any time using a transaction of the form: +``` +POST http://localhost:8080/vm-values/query?blockNonce={...} HTTP/1.1 +Content-Type: application/json -```rust -ChangeServiceFeeTransaction { - Sender: - Receiver:
- Value: 0 - GasLimit: 2000000 - Data: "changeServiceFee" + - "@" + +{ + "scAddress": "...", + "funcName": "...", + "args": [ ... ] } ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -In the `Data` field, the only argument passed to `changeServiceFee` is the new value of the service fee, expressed as hundredths of a percent. +### MultiversX squad -Setting the service fee to 0 (`"00"` in hexadecimal) specifies that no rewards are reserved for the owner of the delegation contract - all rewards will be available to the delegators. The service fee can always be modified later. +The observing squads backing the public Gateways, in addition to being full history squads (serving past blocks, transactions and events up until the Genesis), also act as 3-epochs deep-history squads. That is, for **mainnet**, one can use https://gateway.multiversx.com to resolve historical account (state) queries, for the last 3 days. This interval is driven by the configuration parameter `[StoragePruning.NumEpochsToKeep]`, which is set to `4`, by default. -:::tip -For example, a service fee of 37.45% is expressed by the integer 3745. This integer must then be encoded hexadecimally (3745 becomes `"0ea1"`). +In general: -Finally, a `Data` field containing `changeServiceFee@0ea1` will change the service fee to 37.45%. -::: +``` +... deep history not available +CurrentEpoch - NumEpochsToKeep - 1: deep history not available +CurrentEpoch - NumEpochsToKeep: deep history not available +CurrentEpoch - NumEpochsToKeep + 1: deep history partially available +CurrentEpoch - NumEpochsToKeep + 2: deep history available +CurrentEpoch - NumEpochsToKeep + 3: deep history available +... deep history available +CurrentEpoch: deep history available +``` +In particular, for the public Gateway: -### Automatic activation +``` +... deep history not available +CurrentEpoch - 5: deep history not available +CurrentEpoch - 4 deep history not available +CurrentEpoch - 3: deep history partially available +CurrentEpoch - 2: deep history available +CurrentEpoch - 1: deep history available +CurrentEpoch: deep history available +``` -When automatic activation is enabled, the delegation contract will activate (stake) inactive nodes as soon as funds have become available in sufficient amount. Consequently, any [delegation transaction](/validators/delegation-manager#delegating-funds) can potentially trigger the activation of inactive nodes, assuming the transaction has sufficient gas. -Automatic activation can be enabled or disabled using a transaction of the form: +### On-premises squad -```rust -SetAutomaticActivationTransaction { - Sender: - Receiver:
- Value: 0 - GasLimit: 2000000 - Data: "setAutomaticActivation" + - "@" + <"true" or "false" in hexadecimal encoding> -} +Deep-history squads can be set up on-premises, just as regular observing squads. However, the storage requirements is significantly higher. For example, a deep-history squad for **mainnet**, configured for the interval July 2020 (Genesis) - January 2024 (Sirius), requires about 7.5 TB of storage: + +``` +307G ./node-metachain +1.4T ./node-0 +3.9T ./node-1 +2.0T ./node-2 ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +Since each observer of a deep-history squad must have a non-pruned history, their (non-ordinary) databases have to be either **downloaded** or **reconstructed**, in advance (covered later, in detail). -The only argument passed to `setAutomaticActivation` is either `true` or `false`, as an ASCII string encoded hexadecimally. For reference, `true` is `"74727565"` and `false` is `"66616c7365"`. +## Observer installation and configuration + +The installation of a deep history squad is almost the same as that of a [regular observing squad](/integrators/observing-squad). +The difference is that the deep history observers are set up to retain the whole, non-pruned history of the blockchain. Therefore, the following flag must be added in the `config/variables.cfg` file before running the `observing_squad` command of the installation script: +```bash +NODE_EXTRA_FLAGS="--operation-mode=historical-balances" +``` :::tip -For example, a `Data` field containing `"setAutomaticActivation@74727565"` enables automatic activation. +Apart from the flag mentioned above, the setup of a deep-history observer is identical to a regular full-history observer. ::: +:::warning +Never attach a non-pruned database to a regular observer (i.e. that does not have the above **operation-mode**) - unless you are not interested into the deep-history features. The regular observer irremediably removes, truncates and prunes the data (as configured, for storage efficiency). +::: -### Delegation cap - -The total delegation cap is the maximum possible size amount of EGLD which can be held by the delegation contract. After reaching the total delegation cap, the contract will reject any subsequent funds. +Now that we have finished with the installation part, we can proceed to populate the non-pruned database. There are two options here: +- Reconstruct non-pruned database (recommended). +- Download non-pruned database (we can provide archives for the required epochs, on request). -The total delegation cap can be modified at any time using a transaction of the form: -```rust -ModifyTotalDelegationCapTransaction { - Sender: - Receiver:
- Value: 0 - GasLimit: 2000000 - Data: "modifyTotalDelegationCap" + - "@" + -} -``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +## Reconstructing non-pruned databases -In the `Data` field, the only argument passed to `modifyTotalDelegationCap` is the new value for the delegation cap. It is expressed as a fully denominated amount of EGLD, meaning that it is the number of $10^{-18}$ subdivisions of the EGLD, and not the actual number of EGLD tokens. Take sure not to encode the ASCII string representing the total delegation cap. +The recommended method for populating a non-pruned database is to reconstruct it locally (on your own infrastructure). -:::tip -For example, to obtain the fully denominated form of 7231.941 EGLD, the amount must be multiplied by the denomination factor $10^{18}$, resulting in 7231941000000000000000. Do not encode the ASCII string `"7231941000000000000000"`, but encode the integer 7231941000000000000000 itself. This would result in "01880b57b708cf408000". +There are also two options for reconstructing a non-pruned database: +- Based on the **[import-db](/validators/import-db/)** feature, which re-processes past blocks - and, while doing so, retains the whole, non-pruned accounts history. +- By performing a regular sync from the network (e.g. from Genesis), using a properly configured deep-history observer. -Finally, a `Data` field containing `"modifyTotalDelegationCap@01880b57b708cf408000"` will change the total delegation cap to 7231.941 EGLD. +:::note +The reconstruction flow has to be performed **for each shard, separately**. ::: -Setting the total delegation cap to 0 (`"00"` in hexadecimal) specifies an unlimited total delegation amount. It can always be modified later. +### Reconstructing using import-db -:::important -The total delegation cap cannot be set to a value lower than the amount staked for currently active nodes. It must be either higher than that amount or set to 0 (infinite cap). -::: +First, you need to decide whether to reconstruct a **complete** or a **partial** history. A _complete_ history provides the deep-history squad the ability to resolve historical account (state) queries **up until the Genesis**. A _partial_ history, instead, allows it to resolve state queries up until **a chosen past epoch** or **between two chosen epochs**. -## Managing nodes +#### Reconstruct a complete history +:::note +Below, the reconstruction is exemplified for **mainnet, shard 0**. The same applies to other networks (devnet, testnet) and shards (including the metachain). +::: -### Adding nodes -When a delegation contract is first created, it contains no information about nodes. The owner of the contract must then register nodes into the contract, so that they can be later activated. Any newly added node is "inactive" by default. +Next, we need to obtain (download) and extract **a recent daily archive (snapshot)** for the shard in question (e.g. shard 0). The daily archives are available to download **on request** ([Discord](https://discord.gg/multiversxbuilders) or [Telegram](https://t.me/MultiversXValidators)), from a cloud-based, _S3-compatible storage_ (Digital Ocean Spaces) - or you could fetch them from an existing regular full-history observer that you own. -Adding nodes requires the BLS key pairs belonging to each of them, which the owner of the contract uses to prove that they have access to the nodes. This proof consists of signing the address of the delegation contract itself with the secret BLS key of each node, individually. This results in as many signed messages as there are nodes. +Upon extracting the downloaded archive, you'll have a new folder in your workspace: `db`, which contains the blockchain data for the shard in question. This data should be moved to a new folder, named `import-db`. All in all, the steps are as follows: -Adding `N` nodes to the delegation contract is done by submitting a transaction with the values set as follows: +``` +# Ask for the full download link on Discord or Telegram: +wget https://.../23-Jan-2024/Full-History-DB-Shard-0.tar.gz +# This produces a new folder: "db" +tar -xf Full-History-DB-Shard-0.tar.gz -```rust -AddNodesTransaction { - Sender: - Receiver:
- Value: 0 - GasLimit: 1000000 + N·6000000 - Data: "addNodes" + - "@" + + - "@" +
+ - "@" + + - "@" +
+ - <...> + - "@" + + - "@" +
-} +# "import-db" should contain the whole blockchain to be re-processed +mkdir -p ./import-db && mv db ./import-db +# "db" will contain the reconstructed database (empty at first) +mkdir -p ./db ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +:::note +Downloading the archives and extracting them might take a while. +::: -As shown above, the `Data` field contains an enumeration of `N` pairs. Such a pair consists of the public BLS key of a node along with the message produced by signing the address of the delegation contract with the secret BLS key of the respective node. There are as many pairs as there are nodes to add. +When reconstructing the whole history, the workspace should look as follows (irrelevant files omitted): +``` +. +├── config # network configuration files +│ ├── api.toml +│ ├── config.toml +│ ... +├── db # empty +├── import-db +│ └── db # blockchain data +│ └── 1 +│ │ ... +│ ├── Epoch_1270 +│ │ └── Shard_0 +│ ├── Epoch_1271 +│ │ └── Shard_0 +│ ├── Epoch_1272 +│ │ └── Shard_0 +│ └── Static +│ └── Shard_0 +├── node # binary file, the Node itself +``` -### Staking nodes +We are ready to start the reconstruction process :rocket: -When the staking provider held by the delegation contract contains a sufficient amount of EGLD, the inactive (non-staked) nodes can be staked (activated). This promotes the nodes to the status of **validator**, which means they participate in consensus and earn rewards. +``` +./node --import-db=./import-db --operation-mode=historical-balances --import-db-no-sig-check --log-level=*:INFO --log-save --destination-shard-as-observer 0 +``` -This subsection describes the _manual_ staking (activation) of nodes. To automatically stake (activate) nodes when funds become available, [automatic activation](/validators/delegation-manager#automatic-activation) can be enabled. +:::note +The reconstruction (which uses _import-db_ under the hood, as previously stated) takes a long time (e.g. days) - depending on the machine's resources (CPU, storage capabilities and memory). +::: -To stake specific nodes manually, a transaction of the following form can be submitted: +Once the **import-db** is over, the `db` folder contains the reconstructed database for the shard in question, ready to support historical account (state) queries. -```rust -StakeNodesTransaction { - Sender: - Receiver:
- Value: 0 - GasLimit: 1000000 + N·6000000 - Data: "stakeNodes" + - "@" + + - "@" + + - <...> + - "@" + + -} -``` +Now, do the same for the other shards, as needed. -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +:::tip +You can smoke test the data by launching an (in-place) ephemeral observer: -The `Data` field contains an enumeration of `N` public BLS keys corresponding to the nodes to be staked. +``` +./node --operation-mode=historical-balances --log-save --destination-shard-as-observer 0 +``` +Then, in another terminal, do a historical (state) query against the ephemeral observer: -### Unstaking nodes +``` +curl http://localhost:8080/address/erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx?blockNonce=42 | jq +``` +::: -Validator nodes that are already staked (active) can be manually unstaked. -:::important -Validators are demoted to observer status at the beginning of the next epoch _after unstaking_. This means that they stop receiving rewards. +#### Reconstruct a partial history -Unstaking _does not_ mean that the staked amount returns to the staking provider (see [undelegating](/validators/delegation-manager#undelegating-funds) and [withdrawing](/validators/delegation-manager#withdrawing)). +:::tip +Make sure to read the section [**Reconstruct a complete history**](#reconstruct-a-complete-history) first, to understand the basics. ::: -To cancel the deactivation before the unstaking is complete, the nodes can be [restaked](/validators/delegation-manager#restaking-nodes). +Instead of reconstructing the whole history since Genesis, you may want to reconstruct a partial history, starting from a chosen epoch up until the latest epoch, or a history between two chosen epochs. -To begin the deactivation process for a selection of validator nodes, a transaction of the following form is used: +Below, we'll take the example of reconstructing the history for epochs `1255 - 1260` (inclusive), for **mainnet, shard 0**. -```rust -UnstakeNodesTransaction { - Sender: - Receiver:
- Value: 0 - GasLimit: 1000000 + N·6000000 - Data: "unStakeNodes" + - "@" + + - "@" + + - <...> + - "@" + + -} -``` +Since we'd like to stop upon reconstruction of epoch `1260` (our example), we enable need to enable the `BlockProcessingCutoff` feature in `config/prefs.toml`: -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +``` +[BlockProcessingCutoff] + Enabled = true + Mode = "pause" + CutoffTrigger = "epoch" + # The chosen end epoch, plus 1. + Value = 1261 +``` -The `Data` field contains an enumeration of `N` public BLS keys corresponding to the nodes to be unstaked. +Now, get (download) and extract **two daily archives (snapshots)** for the shard in question (e.g. shard 0): one archive that was created on the epoch preceding the **chosen start epoch** (e.g. the archive created during epoch `1255 - 1 = 1254`); and one archive that was created on the epoch following the **chosen end epoch** (e.g. the archive created during epoch `1260 + 1 = 1261`). You can compute the correct URLs for the two archives manually, given the [**mainnet** Genesis time](https://gateway.multiversx.com/network/config) and the chosen epochs, or you can use the following Python snippet: +``` +import datetime -### Restaking nodes +# Available on request (Discord or Telegram) +url_base = "https://..." +shard = 0 +chosen_start_epoch = 1255 +chosen_end_epoch = 1260 +genesis_timestamp = 1596117600 -Validator nodes that have been unstaked can be restaked (reactivated) before their deactivation is complete. To cancel their deactivation, a transaction of the following form is used: +genesis_datetime = datetime.datetime.fromtimestamp(genesis_timestamp, tz=datetime.timezone.utc) +first_archive_day = (genesis_datetime + datetime.timedelta(days=chosen_start_epoch - 1)).strftime("%d-%b-%Y") +second_archive_day = (genesis_datetime + datetime.timedelta(days=chosen_end_epoch + 1)).strftime("%d-%b-%Y") -```rust -RestakeNodesTransaction { - Sender: - Receiver:
- Value: 0 - GasLimit: 1000000 + N·6000000 - Data: "reStakeUnStakedNodes" + - "@" + + - "@" + + - <...> + - "@" + + -} +print("First daily archive:", f"{url_base}/{first_archive_day}/Full-History-DB-Shard-{shard}.tar.gz") +print("Second daily archive:", f"{url_base}/{second_archive_day}/Full-History-DB-Shard-{shard}.tar.gz") ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - -The `Data` field contains an enumeration of `N` public BLS keys corresponding to the nodes to be restaked. +The first archive should be used as `db` and the second archive should be used as `import-db/db`. +``` +# Download & extract first archive to "db": +wget https://.../05-Jan-2024/Full-History-DB-Shard-0.tar.gz +tar -xf Full-History-DB-Shard-0.tar.gz -### Unbonding nodes +# Download & extract second archive to "import-db/db": +wget https://.../12-Jan-2024/Full-History-DB-Shard-0.tar.gz +mkdir -p ./import-db +tar -xf Full-History-DB-Shard-0.tar.gz --directory ./import-db +``` -Nodes that have been [unstaked](/validators/delegation-manager#unstaking-nodes) can be completely deactivated, a process called **unbonding**. +When reconstructing a partial history, the workspace should look as follows (irrelevant files omitted): -:::important -Validators are demoted to observer status at the beginning of the next epoch _after unstaking_, not unbonding. See [unstaking](/validators/delegation-manager#unstaking-nodes) above. -::: +``` +. +├── config # network configuration files +│ ├── api.toml +│ ├── config.toml +│ ... +├── db # blockchain data (second archive) +│ └── 1 +│ │ ... +│ ├── Epoch_1252 +│ │ └── Shard_0 +│ ├── Epoch_1253 +│ │ └── Shard_0 +│ ├── Epoch_1254 +│ │ └── Shard_0 +│ └── Static +│ └── Shard_0 +├── import-db +│ └── db # blockchain data (first archive) +│ └── 1 +│ │ ... +│ ├── Epoch_1259 +│ │ └── Shard_0 +│ ├── Epoch_1260 +│ │ └── Shard_0 +│ ├── Epoch_1261 +│ │ └── Shard_0 +│ └── Static +│ └── Shard_0 +├── node # binary file, the Node itself +``` -Validator nodes that have been unbonded cannot be restaked (reactivated). They must be staked anew. +We are now ready to start the reconstruction process :rocket: -```rust -UnbondNodesTransaction { - Sender: - Receiver:
- Value: 0 - GasLimit: 1000000 + N·6000000 - Data: "unBondNodes" + - "@" + + - "@" + + - <...> + - "@" + + -} +``` +./node --import-db=./import-db --operation-mode=historical-balances --import-db-no-sig-check --log-level=*:INFO --log-save --destination-shard-as-observer 0 ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +Once the **import-db** is over, the `db` folder will contain the archive for the deep-history observer to support historical account (state) queries for the epochs `1255 - 1260`. -The `Data` field contains an enumeration of `N` public BLS keys corresponding to the nodes to be unbonded. +:::warning +Make sure to set the **BlockProcessingCutoff** back to `false` before starting an observer intended to continue processing blocks past the cutoff. +``` +[BlockProcessingCutoff] + Enabled = false +``` +::: +### Reconstructing by performing a regular sync -### Removing nodes +This is the simpler approach (even if it takes a bit more time, depending on the availability of peers, plus the network traffic). -Inactive (not staked, unbonded) nodes can be removed from the delegation contract by the owner at any time. Neither active (staked) nor unstaked nodes cannot be removed. +Basically, if the required [node flag configuration](#observer-installation-and-configuration) was set at installation, all that's left to be done is to start the node and it will begin building the database by synchronizing from the network since Genesis. -Unlike [adding nodes](/validators/delegation-manager#adding-nodes), this step does not require the BLS key pairs of the nodes. +On the other hand, if the observer only needs to support historical account (state) queries starting from a specific past epoch, we again need to download a daily archive (snapshot) for the start epoch in question and extract it in the **db** folder, then proceed to start the node and it will begin building the database by synchronizing from the network starting with the last block available in the **db** folder. -Removing `N` nodes from the delegation contract is done by submitting a transaction with the values set as follows: -```rust -RemoveNodesTransaction { - Sender: - Receiver:
- Value: 0 - GasLimit: 1000000 + N·6000000 - Data: "removeNodes" + - "@" + + - "@" + + - <...> + - "@" + + -} -``` +## Starting a squad -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +Suppose you have prepared the data for a deep-history squad beforehand, whether by downloading it or by reconstructing it locally. Then, the deep-history data root folder should look as follows: -The `Data` field contains an enumeration of `N` public BLS keys corresponding to the nodes to be removed. +``` +. +├── node-0 +│ └── db +├── node-1 +│ └── db +├── node-2 +│ └── db +└── node-metachain + └── db +``` -### Unjailing nodes +The squad and the proxy can be started using the command: -When active validator nodes perform poorly or to the detriment of the network, they are penalized by having their rating reduced. Rating is essential to earning rewards, because it directly determines the likelihood of a validator to be [selected for consensus](/learn/consensus). +```bash +~/mx-chain-scripts/script.sh start +``` -However, it can happen that rating of a validator might drop under the acceptable threshold. As a consequence, the validator will begin its next epoch **jailed**, which prevents it from participating in consensus. +Alternatively, you can set up a squad using any other known approach, **but make sure to apply the proper `operation-mode`** described in the section [**Observer installation and configuration**](#observer-installation-and-configuration). -:::important -A jailed validator does not lose its stake nor its status. It remains active, but it cannot earn rewards while in jail. -::: +**Congratulations!** You've set up a deep-history observing squad; the gateway should be ready to resolve historical account (state) queries :rocket: -Recovering a validator from jail and restoring it is called **unjailing**, for which a fine of 2.5 EGLD must be paid. Multiple validators can be recovered from jail at the same time by paying 2.5 EGLD for each validator. The format of the unjailing transaction is as follows: +--- -```rust -UnjailNodesTransaction { - Sender: - Receiver:
- Value: 2.5 EGLD × - GasLimit: 1000000 + N·6000000 - Data: "unJailNodes" + - "@" + + - "@" + + - <...> + - "@" + + -} -``` +### EGLD integration guide -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +This section provides high-level technical requirements of integrating the MultiversX's native coin, EGLD in a platform that handles EGLD transactions for their users. -Note that the `Value` field depends on `N`, the number of validators to unjail. -The `Data` field contains an enumeration of `N` public BLS keys corresponding to the nodes to be unjailed. +## Overview +In order to make possible for a platform to integrate EGLD transactions for its users, these are the minimum requirements: -## Delegating and managing delegated funds +- [setting up an observing squad](/integrators/observing-squad) +- [setting up a mechanism for accounts management](/integrators/accounts-management) +- [setting up a mechanism for creating and signing transactions](/integrators/creating-transactions) +- [setting up a mechanism that queries the blockchain for new transactions to process](/integrators/querying-the-blockchain/#querying-hyperblocks-and-fully-executed-transactions) -Accounts that delegate their own funds to the staking provider are called **delegators**. The delegation contract offers them a set of actions as well. This means that these actions are available to the owner of the delegation contract as well. +## Integration workflow -### Delegating funds +An integration could mean an automatic system that parses all the transactions on the chain and performs different +actions when an integrator's address is the sender or receiver of the transaction. Based on that, it should be able +to sign transactions or update the user's balance internally. Also, different things such as hot wallets can be +integrated as well for a better tokens management and less EGLD spent on gas. -Accounts become delegators by funding the staking provider, i.e. they delegate their funds. The delegators are rewarded for their contribution with a proportion of the rewards earned by the validator nodes. By default, the owner of the delegation contract is the first delegator, having already contributed 1250 EGLD to the staking provider at its creation. +In order to summarize the information and bring all the pieces together, this section will provide an example of how an integration can look: -:::important -Extra funds received by the delegation contract from delegators will be immediately used to top-up the stake of the existing active validators, consequently increasing their rewards. -::: -Submitting a delegation transaction takes into account the status of [automatic activation](/validators/delegation-manager#automatic-activation): if the delegated funds cause the amount in the staking provider to become sufficient for the staking of extra nodes, it can trigger their activation automatically. This happens only if the transaction contains enough gas. +### 1. Observing squad running -But if gas is insufficient, or if automatic activation is disabled, the amount received through the delegation transaction simply becomes top-up for the stake of already active validators. Subsequent [manual staking](/validators/delegation-manager#staking-nodes) will be necessary to use the funds for staking, assuming they are sufficient. +The integrator has an observing squad (an observer on each shard + proxy) running and synced. -Funds can be delegated by any fund holder by submitting a transaction of the following form: -```rust -DelegateTransaction { - Sender: - Receiver:
- Value: minimum 1 EGLD - GasLimit: 12000000 - Data: "delegate" -} -``` +### 2. Getting hyperblock by nonce -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +The system should always memorize the last processed nonce. After processing a hyperblock at a given nonce, it should +move on to the hyperblock that corresponds to the next nonce (when available, if not already existing). -If the transaction is successful, the funds' holder has become a delegator and the funds either become a top-up amount for the stake of active validators, or may trigger the staking of inactive nodes, as described above. +In order to fetch the hyperblock for a given nonce, the system should perform an API call to `/hyperblock/by-nonce/`. +If the response contains an error, it probably means that the nonce isn't yet processed on the chain and a retry should be done after a small waiting period. -### Claiming rewards +:::tip +A round in the blockchain is set to 6 seconds, so the nonce should change after a minimum of 6 seconds. +A good refresh interval for nonce-changing detection could be 2 seconds. +::: -A portion of the rewards earned by validator nodes is reserved for each delegator. To claim the rewards, a delegator may issue a transaction of the following form: -```rust -ClaimRewardsTransaction { - Sender: - Receiver:
- Value: 0 - Gas: 6000000 - Data: "claimRewards" -} -``` +#### 2.1. Fallback mechanism -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +If, for example, a server issue occurs and the observing squad gets stuck, the latest processed nonce must be saved +somewhere so when the observing squad is back online, the system should continue processing from the next nonce after the saved one. -If the transaction is successful, the delegator receives the proportion of rewards they are entitled to. +#### 2.2. Example -### Redelegating rewards +For example, when the system is up, it should start processing from a nonce in the same epoch. Let's say the chain is in epoch +5 and the first hyperblock nonce in that epoch is 900 -Current delegation rewards can also be immediately delegated instead of [claimed](/validators/delegation-manager#claiming-rewards). This makes it an operation very similar to [delegation](/validators/delegation-manager#delegating-funds). +``` +... +-> fetched hyperblock with nonce 900 +-> processed hyperblock with nonce 900 +-> saved last processed nonce = 900 +-> waiting 2 seconds +-> fetching hyperblock with nonce 901: API error (nonce not yet processed on chain side), skip +-> waiting 2 seconds +-> fetching hyperblock with nonce 901: API error (nonce not yet processed on chain side), skip +-> waiting 2 seconds +-> fetched the hyperblock with nonce 901 +-> processed hyperblock with nonce 901 +-> saved last processed nonce = 901 +-> waiting 2 seconds +... +``` -:::important -Just like delegation, redelegation of rewards takes into account the status of [automatic activation](/validators/delegation-manager#automatic-activation): if the redelegated rewards cause the amount in the staking provider to become sufficient for the staking of extra nodes, it can trigger their activation automatically (requires sufficient gas in the redelegation transaction). +:::caution +Keep in mind that a hyperblock shouldn't be processed twice as this might cause issues. +Make sure the block processing and the saving of the last processed nonce should be atomic. ::: -Rewards are redelegated using a transaction of the form: - -```rust -RedelegateRewardsTransaction { - Sender: - Receiver:
- Value: 0 - Gas: 12000000 - Data: "reDelegateRewards" -} -``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +#### 2.3. Querying the transactions -If the transaction is successful, the delegator does not receive any EGLD at the moment, but the rewards they were entitled to will be added to their delegated amount. +The system fetches the response and iterates over each successful transaction and determine if any address from the integrator is involved. -### Undelegating funds +### 3. Transaction handling -Delegators may express the intent to withdraw a specific amount of EGLD from the staking provider. However, this process cannot happen at once and may take a few epochs before the amount is actually available for withdrawal, because the funds may already be used to stake for active validators and this means that unstaking of nodes may be necessary. +After identifying a relevant transaction in step 2.3 (the sender or the receiver is an integrator's address) actions could be taken on integrator's side. -:::important -If the amount to undelegate requested by the delegator will cause the staking provider to drop below the sufficient amount required to keep all the current validators active, some validators will inevitably end up unstaked. The owner of the delegation contract may intervene and add extra funds to prevent such situations. -::: +It is recommended that the integrator performs some balances checks before triggering internal transfers. -Funds that have been previously used as stake for validators have been transferred into a separate system smart contract at the moment of staking, therefore the delegation contract itself does not hold these funds. But submitting an undelegation request will cause the delegation contract to attempt their retrieval. +For example, if the receiver is an integrator's address, the integrator can update its balance on internal storage systems. -The delegation contract may receive the funds immediately if they're not currently used as stake; this makes them available for subsequent [withdrawal](/validators/delegation-manager#withdrawing). This is the case where previously delegated funds acted as top-up to the stake of existing validators. -On the other hand, if the requested funds are currently in use as stake, the delegation contract cannot receive them yet. +### Mentions -:::important -Funds used as stake can only be retrieved after 144000 blocks have been built on the Metachain (a little over 10 chronological epochs). It doesn't matter whether the validator was already demoted to observer, or whether it has been decommissioned entirely - the funds may not return until the aforementioned time has passed. +- steps 2 and 3 should be executed in a continuous manner while always keeping record of the last processed nonce, in order to ensure + that no transaction is skipped. +- other usual actions such as transferring (from time to time) all addresses funds to a hot wallet could also be implemented. -After 144000 blocks, the funds can be [withdrawn](/validators/delegation-manager#withdrawing) normally by their rightful owner. -::: -To express the intention of future withdrawal of funds from the staking provider, a delegator may submit the following transaction: +## Finality of the transactions / number of confirmations -```rust -UndelegateTransaction { - Sender: - Receiver:
- Value: 0 - Gas: 12000000 - Data: "unDelegate" - "@" + -} -``` +The hyperblock includes only finalized transactions so only one confirmation is needed. The integrator however has the flexibility to wait for any number of additional confirmations. -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -In the `Data` field, the only argument passed to `unDelegate` is the desired amount of EGLD to undelegate and later withdraw. It is expressed as a fully denominated amount of EGLD, meaning that it is the number of $10^{-18}$ subdivisions of the EGLD, and not the actual number of EGLD tokens. The fully denominated amount must then be encoded hexadecimally. Make sure not to encode the ASCII string representing the amount. +## Balances check +From time to time, or for safety reasons before performing a transaction, an integrator would want to check the balance of some +addresses. This can be performed via [Get address balance endpoint](/sdk-and-tools/rest-api/addresses#get-address-balance). -### Withdrawing -After submitting an [undelegation transaction](/validators/delegation-manager#undelegating-funds), a delegator may finally withdraw funds from the staking provider. +## Useful tools and examples -:::important -Funds must always be [undelegated](/validators/delegation-manager#undelegating-funds) first. They cannot be directly withdrawn. -::: +MultiversX SDKs or tools can be used for signing transactions and performing accounts management. -This action withdraws _all the currently undelegated funds_ belonging to the specific delegator. +A complete list and more detailed information can be found on the [accounts management](/integrators/accounts-management) and +[signing transaction](/integrators/creating-transactions) sections. -Withdrawing funds is done using a transaction of the following form: +There is also an example that matches the above-presented workflow and can be found on the Go SDK for MultiversX, [sdk-go](https://github.com/multiversx/mx-sdk-go/tree/main/examples/examplesFlowWalletTracker). -```rust -WithdrawTransaction { - Sender: - Receiver:
- Value: 0 - GasLimit: 12000000 - Data: "withdraw" -} -``` +However, other SDKs can be used as well for handling accounts management or transaction signing. -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +--- -If the transaction is successful, the delegator receives all the EGLD they have previously requested to undelegate. The amount is removed from the staking provider. +### ESDT tokens integration guide +## **Introduction** +Integrating ESDT tokens support can be done alongside native EGLD integration, so one should refer to the [egld-integration-guide](/integrators/egld-integration-guide). -## Delegation contract view functions +The only differences are internal ways to store ESDT tokens alongside with their token identifier and number of decimals and different approaches +for identifying and parsing ESDT transactions. -The delegation contract can be queried using the following view functions. These queries should be done on a local proxy on the `/vm-values/query` endpoint. -The following documentation sections only show the value of the relevant `returnData` field and omit the other fields for simplicity. +## **ESDT transactions parsing** +Considering that the platform which wants to support ESDT tokens already supports EGLD transfers, this section will +provide the additional steps that are to be integrated over the existing system. -```json -{ - "data": { - "data": { - "returnData": [], - "returnCode": "ok", - "returnMessage": "", - "gasRemaining": 0, - "gasRefund": 0, - "outputAccounts": null, - "deletedAccounts": null, - "touchedAccounts": null, - "logs": [] - } - }, - "error": "", - "code": "successful" -} -``` +If so, all the transactions on the network are being parsed so the integrator will check if the sender or receiver of the transaction +is an address that is interested in. +In addition to this, one can check if the transaction is a successful ESDT transfer. If so, then the transferred token, the amount and the +receiver can be further parsed. +One has to follow these steps: +- check if the transaction is an ESDT transfer (data field format is `ESDTTransfer@@`. More details [here](/tokens/fungible-tokens#transfers)) +- parse the tokens transfer details from Logs&Events. More details [here](/tokens/fungible-tokens#parse-fungible-tokens-transfer-logs) -### POST Contract config {#contract-config} -The response contains an array of the properties in a fixed order (base64 encoded): owner address, service fee, maximum delegation cap, initial owner funds, automatic activation, with delegation cap, can change service fee, check cap on redelegate, nonce on creation and unbond period. +### Example +Let's suppose we are watching these addresses: +- `erd1zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3z92425g3zygs3mthts` +- `erd1venxvenxvenxvenxvenxvenxvenxvenxvenxvenxvenxvenxvenq5ezmpv` - - +And we support the following tokens: +- `TKN-99hh44` (hex encoded: `544b4e2d393968683434`) +- `TKN2-77hh33` (hex encoded: `544b4e322d373768683333`) -```bash -https://proxy:port/vm-values/query +Therefore, we will look for transactions that have the following structure: ``` - -```json -{ - "scAddress": "
", - "funcName": "getContractConfig" +TokenTransferTransaction { + Sender: erd1.... + Receiver: erd1zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspavcaj OR erd1venxvenxvenxvenxvenxvenxvenxvenxvenxvenxvenxvenxvenq5ezmpv + Value: 0 + GasLimit: 60000000 + Data: "ESDTTransfer" + + "@" + // 544b4e2d393968683434 OR 544b4e322d373768683333 + "@" } ``` +*For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format).* - - +Any other transaction that does not follow this structure has to be omitted. -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response +When we find such a transaction, we will fetch the logs of the transaction, as described [here](/tokens/fungible-tokens#parse-fungible-tokens-transfer-logs). +The logs will provide us information about the receiver, the token and the amount to be transferred. If the log is there, we are sure that +the transfer is successful, and we can start processing with the extracted data. -```json -{ - "returnData": [ - "", - "", - "", - "", - "", - "" - ] -} -``` - +## **Sending ESDT tokens** +Sending ESDT tokens to a given recipient can be done via preparing and broadcasting to the network a transaction that +follows the format described [here](/tokens/fungible-tokens#transfers). -Request +Also, there is support for building tokens transfer transaction on many SDKs. A few examples are: +- [sdk-js - ESDTTransferPayloadBuilder](https://github.com/multiversx/mx-sdk-js-core/blob/main/src/tokenTransferBuilders.ts) +- [erdjava - ESDTTransferBuilder](https://github.com/multiversx/mx-sdk-erdjava/blob/main/src/main/java/multiversx/esdt/builders/ESDTTransferBuilder.java) -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", - "funcName": "getContractConfig" -} -``` -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +## **Balances check** +From time to time, or for safety reasons before performing a transaction, an integrator would want to check the tokens balance of some +addresses. This can be performed via [Get address token balance endpoint](/tokens/fungible-tokens#get-balance-for-an-address-and-an-esdt-token). -```json -{ - "returnData": [ - "gKzHUD288mzScNX6nEmkGm4CHneMdrrPhJyPET9iGA8=", - null, - "", - "Q8M8GTdWSAAA", - "dHJ1ZQ==", - "ZmFsc2U=", - "dHJ1ZQ==", - "AuU=", - "+g==" - ] -} -``` - - +## **Getting tokens properties** +Each token has some properties such as the name, the ticker, the token identifier or the number of decimals. +These properties can be fetched via an API call described [here](/tokens/fungible-tokens#get-esdt-token-properties). -### POST Contract metadata {#contract-metadata} +## **Useful tools** +- ESDT documentation can be found [here](/tokens/fungible-tokens). +- ESDT API docs can be found [here](/tokens/fungible-tokens#rest-api). +- sdk-js helper functions can be found [here](https://github.com/multiversx/mx-sdk-js-core/blob/release/v9/src/esdtHelpers.ts). +- sdk-js token transfer transactions builder can be found [here](https://github.com/multiversx/mx-sdk-js-core/blob/main/src/tokenTransferBuilders.ts). +- erdjava token transfer transactions builder can be found [here](https://github.com/multiversx/mx-sdk-erdjava/blob/main/src/main/java/multiversx/esdt/builders/ESDTNFTTransferBuilder.java). +- [@multiversx/sdk-transaction-decoder](https://www.npmjs.com/package/@multiversx/sdk-transaction-decoder). -The response contains an array of the properties in a fixed order (base64 encoded): staking provider name, website and identifier. +--- - - +### Frequently Asked Questions -```bash -https://proxy:port/vm-values/query -``` +[comment]: # "mx-abstract" -```json -{ - "scAddress": "
", - "funcName": "getMetaData" -} -``` +This page contains answers to frequently asked questions about connecting an application to MultiversX, be it an exchange, wallet, dApp, Web3 indexer or data provider. - - +## General information -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response +### What is the native token of MultiversX? + +**EGLD** on Mainnet, **XeGLD** on Devnet and Testnet. The atomic unit of the native token (think of `wei` for Ethereum) is not named. -```json -{ - "returnData": [ - "", - "", - "" - ] -} +``` +1 EGLD = 10^18 atomic units = 1000000000000000000 atomic units ``` - +See [constants](/developers/constants). -Request +### What kind of consensus does MultiversX use? -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", - "funcName": "getMetaData" -} -``` +See more at [secure proof of stake](/learn/consensus). -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +### What is the block time (round duration)? -```json -{ - "returnData": [ - "U3Rha2luZyBwcm92aWRlciB0ZXN0", - "d3d3LmVscm9uZHN0YWtpbmcuY29t", - "dGVzdEtleWJhc2VJZGVudGlmaWVy" - ] -} -``` +The block time (round duration) is [6 seconds](/developers/constants). Also see [the welcome page](/welcome/welcome-to-multiversx). - - +### Does MultiversX employ sharding? +Currently, the MultiversX network has 3 regular shards, plus a special one, called the _metachain_ - this arrangement holds not only on _mainnet_, but also on _devnet_ and _testnet_. -### POST Number of delegators {#number-of-delegators} +Transactions between accounts assigned to the same shard are called _intra-shard transactions_. Transactions between accounts located in distinct shards are called _cross-shard transactions_. -The response contains a value representing the number of delegators in base64 encoding of the hex encoding. +More details about the sharded architecture of MultiversX can be found [here](/learn/sharding). +Integrators may choose to have a unified view of the network, leveraging the [hyperblock](/integrators/egld-integration-guide) abstraction. - - +## Wallet -```bash -https://proxy:port/vm-values/query -``` +### What signature scheme does MultiversX use? -```json -{ - "scAddress": "
", - "funcName": "getNumUsers" -} -``` +For transactions, [ed25519](/developers/signing-transactions) is used. - - +### What BIP-0044 coin type is being used? -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response +CoinType is **508**, according to: [SLIP-0044](https://github.com/satoshilabs/slips/blob/master/slip-0044.md). -```json -{ - "returnData": [ - "" - ] -} -``` +### What is the derivation path for wallets? - +The derivation path is `m/44'/508'/0'/0'/{address_index}'`. That is, the _account index_ stays fixed at `0`, while the _address index_ is allowed to vary. -Request +## Transactions -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", - "funcName": "getNumUsers" -} -``` +### What is the schema of a transaction? -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +See [transactions](/learn/transactions). -```json -{ - "returnData": ["BQ=="] -} -``` +### How to determine the status of a transaction? - - +See [querying the blockchain](/integrators/querying-the-blockchain). +### What can be said about transactions finality? -### POST Number of nodes {#number-of-nodes} +A transaction is final when the block or blocks (for cross-shard transactions) that notarize it have been declared **final**. +Generally speaking, a transaction can be considered final as soon as it presents the _hyperblock coordinates_ (hyperblock nonce and hyperblock hash) when queried from the network, and these coordinates are under (older than) the [latest final hyperblock](/integrators/querying-the-blockchain#querying-finality-information). -The response contains the number of nodes in base64 encoding of the hex encoding. +For more details, see [integration guide](/integrators/egld-integration-guide) and [querying the blockchain](/integrators/querying-the-blockchain). - - +## Accounts -```bash -https://proxy:port/vm-values/query -``` +### How does an address look like? -```json -{ - "scAddress": "
", - "funcName": "getNumNodes" -} -``` +An **account** is identified by an **address**, which is the **bech32-encoded** public key of the account. +For the **bech32** encoding, the human-readable part (HRP) is `erd`. - - +### Types of accounts -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response +Both _regular user accounts_ and _smart contract accounts_ can hold tokens: EGLD and ESDT tokens (fungible, semi-fungible or non-fungible ones). -```json -{ - "returnData": [ - "" - ] -} -``` +Regular user accounts can sign transactions using their private key. Smart contracts cannot create actual transactions, as they cannot sign them. Instead, they interact with other accounts by crafting so-called _unsigned transactions_ (or smart contract results). - +### How are accounts created? -Request +Regular user accounts get created on the blockchain when the corresponding address receives tokens for the first time. On the other hand, (user-defined) smart contract accounts are created when a smart contract is deployed. The address of a smart contract is a function of `[address of the deployer, nonce of deployment transaction]`. For a smart contract account, there is no (known) private key. -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", - "funcName": "getNumNodes" -} -``` +### How to distinguish between a normal account and a smart contract? -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +Examples of addresses: -```json -{ - "returnData": ["Dg=="] -} -``` +- **regular user account:** `erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th` +- **smart contract:** `erd1qqqqqqqqqqqqqpgqq66xk9gfr4esuhem3jru86wg5hvp33a62jps2fy57p` - - +If the address (decoded as bytes) has a prefix of 8 bytes of `0x00`, then it refers to a smart contract. +## Smart Contracts -### POST Nodes states {#nodes-states} +There are two types of smart contracts on MultiversX: **system smart contracts** and **user-defined smart contracts**. System smart contracts are coded into the Protocol itself, while user-defined smart contracts are developed and deployed by users. The latter are written primarily in **Rust**. The Virtual Machine uses the **WASM** format, by leveraging the [Wasmer](https://wasmer.io/) engine. -The response contains an enumeration of alternating status codes and BLS keys. Each status code is followed by the BLS key of the node it describes. Both status codes and BLS keys are encoded in base64. +### How to detect contract deployment events? - - +Look for events of type [`SCDeploy`](/developers/event-logs/contract-deploy-events). -```bash -https://proxy:port/vm-values/query -``` +### Is it possible to upgrade a smart contract? -```json -{ - "scAddress": "
", - "funcName": "getAllNodeStates" -} -``` +Yes, if the `upgradeable` flag is set in the contract's [metadata](/developers/data/code-metadata). Also see [upgrading smart contracts](/developers/developer-reference/upgrading-smart-contracts). - - +--- -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response +### Integrators - Overview -```json -{ - "returnData": [ - "", - "" - ] -} -``` +## Introduction - +If you want to integrate the MultiversX Network in your app, even if we are talking about an exchange, wallet, or a dApp that +uses its own infrastructure, please choose a direction from the following table -Request -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", - "funcName": "getAllNodeStates" -} -``` +## Table of contents -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +| Name | Description | +| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| [EGLD integration guide](/integrators/egld-integration-guide) | How to integrate MultiversX's native token EGLD. | +| [ESDT tokens integration guide](/integrators/esdt-tokens-integration-guide) | How to integrate MultiversX's ESDT tokens. | +| [Observing squad](/integrators/observing-squad) | How to run an infrastructure with a general view over all the shards. | +| [Snapshotless Observing squad](/integrators/snapshotless-observing-squad) | How to set up a light Observing squad with access to real time state only. | +| [Deep-history squad](/integrators/deep-history-squad) | How to set up an Observing squad able to resolve historical state queries. | +| [Accounts management](/integrators/accounts-management) | How to create and manage EGLD accounts. | +| [Creating transactions](/integrators/creating-transactions) | How to create and sign transactions. | +| [Querying the blockchain](/integrators/querying-the-blockchain) | How to query the MultiversX Blockchain in order to watch desired addresses or events. | +| [WalletConnect JSON-RPC Methods](/integrators/walletconnect-json-rpc-methods) | How to ensure the proper communication using WalletConnect between a wallet and a dapp. | -```json -{ - "returnData": [ - "c3Rha2Vk", - "KJ6auG3rKQydktc9soWvyBOa5UPA7DYezttTqlS6JIIvsvOaH8ghs2Qruc4aXLUXNJ1if7Ot9gbt5dNUrmNfkLtZl1hpLvPllrGmFP4bKCzZ25UNiTratwOMcXhhCmSD", - "bm90U3Rha2Vk", - "7gJzQ3GQ4htSx6CYvOkXPDdwGfzdahuDY4agZkGhIAMfB44K08FP6z3wLQEnn2IULfZ8/Hds38LEu3Xq+mJZ4FktF0vm8C1T34b5uAEpZWtDZLICAEFCuQZrqS5Qb1CR", - "vTyNQ/vDxg0L8LmoGuKP+4/wsbyWv8RaqeQ+WH+xrMvk1m7Q3wjheOpjYtQPz80YZ1CrwKj6ObsCUejP4uuvi3MQ1oMEGKg5yh3kRgybRb4TXAWEpAPszYMLIQhrIn2P", - "9TbGQCcrbyXH9HBAhzIWOuH/cdSNO1dwxO5foM2L28tWU0p9Kos6DKsPMtKMx4sAeRal08K3Dk0gQxeTSAvC2fb3DAQt01rmPSAqCSXZetSX12BVcTi+pYGUHaXKJ/OW" - ] -} -``` +--- - - +### Observing Squad +The N+1 setup for connecting to the MultiversX Network -### POST Total active stake {#total-active-stake} +In order to integrate with the MultiversX Network and be able to [broadcast transactions](/integrators/creating-transactions) and [query blockchain data](/integrators/querying-the-blockchain) in an _optimized_ approach, one needs to set up an **on-premises Observing Squad**. -The response contains a value representing the total active stake in base64 encoding of the hex encoding. +An Observing Squad is defined as a set of `N` **Observer Nodes** (one for each Shard, including the Metachain) plus an [**MultiversX Proxy**](/sdk-and-tools/proxy) instance which will connect to these Observers and provide an HTTP API (by delegating requests to the Observers). - - +:::tip +Currently the MultiversX Mainnet has 3 Shards, plus the Metachain. Therefore, the Observing Squad is composed of 4 Observers and one Proxy instance. +::: -```bash -https://proxy:port/vm-values/query -``` +By setting up an Observing Squad and querying the blockchain data through the Proxy, the particularities of MultiversX's sharded architecture are abstracted away. **This means that the client interacting with the Proxy does not have to be concerned about sharding at all.** -```json -{ - "scAddress": "
", - "funcName": "getTotalActiveStake" -} -``` - - +## **System requirements** -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response +The Observing Squad can be installed on multiple machines or on a single, but more powerful machine. -```json -{ - "returnData": [""] -} -``` +In case of a single machine, our recommendation is as follows: - +- 16 x CPU +- 32 GB RAM +- Disk space that can grow up to 5 TB +- 100 Mbit/s always-on Internet connection +- Linux OS (Ubuntu 20.04 recommended) -Request +The recommended number of CPUs has been updated from `8` to `16` in April 2021, considering the increasing load over the network. -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", - "funcName": "getTotalActiveStake" -} -``` +:::tip +These specs are only a recommendation. Depending on the load over the API or the observers, one should upgrade the machine as to keep the squad synced and with good performance. +::: -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) -```json -{ - "returnData": ["ArXjrxaxiAAA"] -} -``` +## **Setup via the Mainnet scripts** - - +:::caution +`elrond-go-scripts-mainnet` are deprecated as of November 2022. Please use `mx-chain-scripts`, explained below. +::: -### POST Total unstaked stake {#total-unstaked-stake} +## **Setup via mx-chain-scripts** -The response contains a value representing the total unstaked stake in base64 encoding of the hex encoding. - - +## **Installation and Configuration** + +The Observing Squad can be set up using the [installation scripts](/validators/nodes-scripts/config-scripts/). Within the installation process, the `DBLookupExtension` feature (required by the Hyperblock API) will be enabled by default. + +Clone the installer repository: ```bash -https://proxy:port/vm-values/query +git clone https://github.com/multiversx/mx-chain-scripts ``` -```json -{ - "scAddress": "
", - "funcName": "getTotalUnStaked" -} +Edit `config/variables.cfg` accordingly. For example: + +```bash +ENVIRONMENT="mainnet" +... +CUSTOM_HOME="/home/ubuntu" +CUSTOM_USER="ubuntu" ``` - - +Additionally, you might want to set the following option, so that the logs are saved within the `logs` folder of the node: -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response +```bash +NODE_EXTRA_FLAGS="-log-save" +``` -```json -{ - "returnData": [ - "" - ] -} +Please check that the `CUSTOM_HOME` directory exists. Run the installation script as follows: + +```bash +./script.sh observing_squad ``` - +After installation, 5 new `systemd` units will be available (and enabled). -Request +Start the nodes and the Proxy using the command: -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", - "funcName": "getTotalUnStaked" -} +```bash +./script.sh start ``` -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +In order to check the status of the Observing Squad, please see the section **Monitoring and trivial checks** below. -```json -{ - "returnData": ["ArXjrxaxiAAA"] -} -``` - - +## **Upgrading the Observing Squad** +The Observing Squad can be updated using the installation scripts. -### POST Total cumulated rewards {#total-cumulated-rewards} -The response contains a value representing the sum of all accumulated rewards in base64 encoding of the hex encoding. +### **General upgrade procedure** - - +:::important +`elrond-go-scripts-mainnet` are deprecated as of November 2022. Users of these scripts have to migrate to [mx-chain-scripts](/validators/nodes-scripts/config-scripts/). +The migration guide can he found [here](/validators/nodes-scripts/install-update/#migration-from-old-scripts). +::: + +In order to upgrade the Observing Squad - that is, both the Observers and the Proxy, one should issue the following commands: ```bash -https://proxy:port/vm-values/query +$ cd ~/mx-chain-scripts +$ ./script.sh github_pull +$ ./script.sh stop +$ ./script.sh upgrade_squad +$ ./script.sh upgrade_proxy +$ ./script.sh start ``` -```json -{ - "scAddress": "
", - "funcName": "getTotalCumulatedRewards", - "caller": "erd1qqqqqqqqqqqqqqqpqqqqqqqqlllllllllllllllllllllllllllsr9gav8" -} -``` +After running the commands above, the upgraded Observing Squad will start again. The expected downtime is about 2-3 minutes. - - -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response +## **February 2023 upgrade** -```json -{ - "returnData": [ - "" - ] -} -``` +:::note +For observing squad users that still use the old `elrond-go-scripts`: since the rebranding to `MultiversX`, the scripts have been rebranded as well to `mx-chain-scripts`. +::: - +In order to upgrade the squad, you first need to migrate to the new scripts, while still running the squad via the old scripts. After that, +we'll use the new scripts to upgrade the squad. -Request -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", - "funcName": "getTotalCumulatedRewards", - "caller": "erd1qqqqqqqqqqqqqqqpqqqqqqqqlllllllllllllllllllllllllllsr9gav8" -} +### **How to migrate to the new scripts** + +If you already migrated from `elrond-go-scripts` to `mx-chain-scripts`, you can skip this section. + +Make sure you are on the same directory as the old scripts. + +```bash +$ cd ~ +$ git clone https://github.com/multiversx/mx-chain-scripts +$ cd mx-chain-scripts +$ ./script.sh migrate ``` -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +The above commands should clone the new scripts and migrate the old configuration files to the new ones. You may now proceed to the next section. -```json -{ - "returnData": ["czSCSSYZr8E="] -} + +### **How to upgrade to the newest version via the new scripts** + +In order to upgrade the squad, we first need to stop the squad, then upgrade the squad and finally start the squad again. These steps are done by: + +```bash +$ cd ~/mx-chain-scripts +$ ./script.sh github_pull +$ ./script.sh stop +$ ./script.sh upgrade_squad +$ ./script.sh upgrade_proxy +$ ./script.sh start ``` - - +After successfully migrating to the new scripts and upgrading the squad, you can now remove the old scripts. (example: `rm -rf ~/elrond-go-scripts`) -### POST Delegator claimable rewards {#delegator-claimable-rewards} +## **Monitoring and trivial checks** -The response contains a value representing the total claimable rewards for the delegator in base64 encoding of the hex encoding. +One can monitor the running Observers using the **termui** utility (installed during the setup process itself in the `CUSTOM_HOME="/home/ubuntu" +` folder), as follows: - - +```bash +~/elrond-utils/termui --address localhost:8080 # Shard 0 +~/elrond-utils/termui --address localhost:8081 # Shard 1 +~/elrond-utils/termui --address localhost:8082 # Shard 2 +~/elrond-utils/termui --address localhost:8083 # Metachain +``` + +Alternatively, one can query the status of the Observers by performing GET requests using **curl**: ```bash -https://proxy:port/vm-values/query +curl http://localhost:8080/node/status | jq # Shard 0 +curl http://localhost:8081/node/status | jq # Shard 1 +curl http://localhost:8082/node/status | jq # Shard 2 +curl http://localhost:8083/node/status | jq # Metachain ``` -```json -{ - "scAddress": "
", - "funcName": "getClaimableRewards", - "args": [ - "" - ] -} +The Proxy does not offer a **termui** monitor, but its activity can be inspected using **journalctl**: + +```bash +journalctl -f -u elrond-proxy.service ``` - - +Optionally, one can perform the following smoke test in order to fetch the latest synchronized hyperblock: -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response +```bash +export NONCE=$(curl http://localhost:8079/network/status/4294967295 | jq '.data["status"]["erd_highest_final_nonce"]') +curl http://localhost:8079/hyperblock/by-nonce/$NONCE | jq -```json -{ - "returnData": [ - "" - ] -} ``` - -Request +## **Setup via Docker** -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", - "funcName": "getClaimableRewards", - "args": ["ebfd923cd251f857ed7639e87143ac83f12f423827abc4a0cdde0119c3e37915"] -} +The Observing Squad can be also set up using Docker. + +Clone the Observing Squad repository: + +```bash +git clone https://github.com/multiversx/mx-chain-observing-squad.git ``` -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +Install docker-compose if not already installed: -```json -{ - "returnData": ["Ft9RZzF7Dyc"] -} +```bash +apt install docker-compose ``` - - +Install and run the whole Observing Squad using the `./start_stack.sh` script from the mainnet folder: +```bash +cd mainnet +./start_stack.sh +``` -### POST Delegator total accumulated rewards {#delegator-total-accumulated-rewards} +In order to check if the Observing Squad is running, you can list the running containers: -The response contains a value representing the total accumulated rewards for the delegator in base64 encoding of the hex encoding. +```bash +docker ps +``` - - +In order to check the status inside a container, you can check the logs on the machine for the last synchronized block nonce: ```bash -https://proxy:port/vm-values/query +docker exec -it 'CONTAINER ID' /bin/bash +cat logs/mx-chain-.......log ``` -```json -{ - "scAddress": "
", - "funcName": "getTotalCumulatedRewardsForUser", - "args": [ - "" - ] -} -``` +More detailed commands for installing, building and running an Observing Squad using Docker are described [here](https://github.com/multiversx/mx-chain-observing-squad.git). The images (for the Proxy and for the Observers) are published on [Docker Hub](https://hub.docker.com/u/multiversx). - - -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response +## One click deploy in Tencent Cloud +Tencent Cloud nodes for Full Observing Squads can be easily deployed from the [Tencent Cloud Market](https://www.tencentcloud.com/market/product/P20240326183001630223531). -```json -{ - "returnData": [ - "" - ] -} -``` - +## One click deploy in Digital Ocean +Digital Ocean droplets for Full Observing Squads can be easily deployed via our droplets available in the [Digital Ocean Marketplace](https://marketplace.digitalocean.com/apps/multiversx-full-observing-squad). -Request +--- -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", - "funcName": "getTotalCumulatedRewardsForUser", - "args": ["ebfd923cd251f857ed7639e87143ac83f12f423827abc4a0cdde0119c3e37915"] -} -``` +### Querying the Blockchain -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +This page describes how to query the Network in order to fetch data such as transactions and blocks (hyperblocks). -```json -{ - "returnData": ["Ft9RZzF7Dyc"] -} -``` +:::note +On this page, we refer to the [Gateway (Proxy) REST API](/sdk-and-tools/rest-api/gateway-overview) - i.e. the one backed by an [observing squad](/integrators/observing-squad). +::: - - +## **Querying broadcasted transactions** -### POST Delegator active stake {#delegator-active-stake} +In order to fetch a previously-broadcasted transaction, use: -The response contains a value representing the active stake for the delegator in base64 encoding of the hex encoding. +- [get transaction by hash](/sdk-and-tools/rest-api/transactions#get-transaction) - - +:::note +Fetching a _recently_ broadcasted transaction may not return the _hyperblock coordinates_ (hyperblock nonce and hyperblock hash) in the response. However, once the transaction is notarized on both shards (with acknowledgement from the metachain), the hyperblock coordinates will be set and present in the response. +::: -```bash -https://proxy:port/vm-values/query -``` +In order to inspect the **status** of a transaction, use: -```json -{ - "scAddress": "
", - "funcName": "getUserActiveStake", - "args": [ - "" - ] -} -``` +- [get transaction **shallow status** by hash](/sdk-and-tools/rest-api/transactions#get-transaction-status) +- [get transaction **process status** by hash](/sdk-and-tools/rest-api/transactions#get-transaction-process-status) - - +For the difference between the _shallow status_ and the _process status_, see the next section. -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -```json -{ - "returnData": [""] -} -``` +## **Transaction Status** - +### Shallow status -Request +The **shallow status** of a transaction indicates whether a transaction has been **handled and executed** by the network. +However, the _shallow_ status does not provide information about the transaction's **processing outcome**, and does not capture processing errors. +That is, transactions processed with errors (e.g. _user error_ or _out of gas_) have the status `success` (somehow counterintuitively). -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", - "funcName": "getUserActiveStake", - "args": ["ebfd923cd251f857ed7639e87143ac83f12f423827abc4a0cdde0119c3e37915"] -} -``` +:::note +The _shallow_ status is, generally speaking, sufficient for integrators that are only interested into simple transfers (of EGLD or custom tokens). +::: -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +The **shallow status** of a transaction can be one of the following: + - `success` - the transaction has been fully executed - with respect to the network's sharded architecture, it has been executed in both source and destination shards. + - `invalid` - the transaction has been marked as invalid for execution at sender's side (e.g., not enough balance at sender's side, sending value to non-payable contracts etc.). + - `pending` - the transaction has been accepted in the _mempool_ or accepted and partially executed (in the source shard). -```json -{ - "returnData": ["slsrv1so8QAA"] -} -``` +### Process status - - +The **process status** of a transaction indicates whether a transaction has been processed successfully or not. +:::note +The _process_ status is, generally speaking, useful for integrators that are interested in smart contract interactions. +::: -### POST Delegator unstaked stake {#delegator-unstaked-stake} +:::note +Fetching the _process status_ of a transaction is less efficient than fetching the _shallow status_. +::: -The response contains a value representing the unstaked stake for the delegator in base64 encoding of the hex encoding. +The **process status** of a transaction can be one of the following: + - `success` - the transaction has been fully executed - with respect to the network's sharded architecture, it has been executed in both source and destination shards. + - `fail` - the transaction has been processed, but with errors (e.g., _user error_ or _out of gas_), or it has been marked as invalid (see _shallow_ status). + - `pending` - the transaction has been accepted in the _mempool_ or accepted and partially executed (in the source shard). + - `unknown` - the processing status cannot be precisely determined yet. - - -```bash -https://proxy:port/vm-values/query -``` +## **Querying hyperblocks and fully executed transactions** -```json -{ - "scAddress": "
", - "funcName": "getUserUnStakedValue", - "args": [ - "" - ] -} -``` +In order to query executed transactions, please follow: - - +- [get hyperblock by nonce](/sdk-and-tools/rest-api/blocks#get-hyperblock-by-nonce) +- [get hyperblock by hash](/sdk-and-tools/rest-api/blocks#get-hyperblock-by-hash) -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response -```json -{ - "returnData": [ - "" - ] -} +## **Querying finality information** + +In order to fetch the nonce (the height) of **the latest final (hyper) block**, one would perform the following request against the on-premises Proxy instance: + +``` +curl http://myProxy:8079/network/status/4294967295 ``` - +Above, `4294967295` is a special number - the ID of the Metachain. -Request +From the response, one should be interested into the field `erd_highest_final_nonce`, which will point to the latest final hyperblock. -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", - "funcName": "getUserUnStakedValue", - "args": ["ebfd923cd251f857ed7639e87143ac83f12f423827abc4a0cdde0119c3e37915"] +``` + "data": { + "status": { + "erd_highest_final_nonce": 54321 + ... + } + }, + ... } + ``` -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +--- -```json -{ - "returnData": ["ARWORgkT0AAA"] -} -``` +### Snapshotless Observing Squad - - +This page describes the Snapshotless Observing Squad, a type of Observing Squad optimized for real-time requests such as accounts data fetching and vm-query operations. +More details related to exposed endpoints are available [here](/sdk-and-tools/proxy#snapshotless-observers-support). -### POST Delegator unbondable stake {#delegator-unbondable-stake} +## Overview -The response contains a value representing the unbondable stake in base64 encoding of the hex encoding. +Whenever a node is executing the trie snapshotting process, the accounts data fetching & vm-query operations can be greatly affected. +This is caused by the fact that the snapshotting operation has a high CPU and disk I/O utilization. +The nodes started with the flag `--operation-mode snapshotless-observer` will not create trie snapshots on every epoch and will also prune the trie storage in order to save space. - - -```bash -https://proxy:port/vm-values/query -``` +## Setup -```json -{ - "scAddress": "
", - "funcName": "getUserUnBondable", - "args": [ - "" - ] -} -``` - - +### Creating a Snapshotless Observing Squad from scratch -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response +If you choose to install a snapshotless Observing Squad from scratch, you should follow the instruction from the [observing squad section](/integrators/observing-squad) and remember to add in the `variables.cfg` file the operation mode in the node's extra flags definition: +``` +NODE_EXTRA_FLAGS="-log-save -operation-mode snapshotless-observer" +``` +After that, you can resume the normal Observing Squad installation steps. -```json -{ - "returnData": [ - "" - ] -} +Then, based on the needs there are multiple options concerning the proxy: +* if only a snapshotless squad is needed, nothing else should be done +* if both regular and snapshotless squads are needed: + * with two different proxies: one started with regular observers and one started with snapshotless observers, nothing else should be done + * with only one proxy (being served by all 8 observers), `IsSnapshotless = true` should be added to each observer started with this flag, in the proxy config (found at `$CUSTOM_HOME/elrond-proxy/config/config.toml`), as follows. Please note that this step is optional, although it would help the proxy to forward the requests in an efficient manner. +```toml +[[Observers]] + ShardId = 0 + Address = "http://127.0.0.1:8080" + IsSnapshotless = true ``` - -Request +### Converting a normal Observing Squad to a Snapshotless Observing Squad -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", - "funcName": "getUserUnBondable", - "args": ["ebfd923cd251f857ed7639e87143ac83f12f423827abc4a0cdde0119c3e37915"] -} +If you already have an Observing Squad, and you want to transform it into a Snapshotless Observing Squad, the easiest way is to manually edit the service file `/etc/systemd/system/elrond-node-x.service` (with `sudo`) and append the `-operation-mode snapshotless-observer` flag at the end of the `ExecStart=` line. +In the end, the file should look like: ``` +[Unit] + Description=MultiversX Node-0 + After=network-online.target -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) + [Service] + User=jls + WorkingDirectory=/home/ubuntu/elrond-nodes/node-0 + ExecStart=/home/ubuntu/elrond-nodes/node-0/node -use-log-view -log-logger-name -log-correlation -log-level *:DEBUG -rest-api-interface :8080 -log-save -profile-mode -operation-mode snapshotless-observer + StandardOutput=journal + StandardError=journal + Restart=always + RestartSec=3 + LimitNOFILE=4096 -```json -{ - "returnData": ["ARWORgkT0AAA"] -} + [Install] + WantedBy=multi-user.target ``` - - +Save the file, and force a reload units with the command +```bash +sudo systemctl daemon-reload +``` +After units reload, you can restart the nodes. -### POST Delegator undelegated stake {#delegator-undelegated-stake} +:::caution +Even if the nodes are synced, after changing the operation mode, they will start to re-sync their state in +"snapshotless" format. The nodes should be temporarily started with the extra node flag `--force-start-from-network` that will force the node to start from network. +Let the node sync completely and then remove this extra flag and restart the node. +Failure to do so will make the node error with a message like `consensusComponentsFactory create failed: epoch nodes configuration does not exist epoch=0`. +::: -The response contains an enumeration representing the different undelegated stake values in base64 encoding of the hex encoding. - - +## One click deploy in AWS +AWS instances for Snapshotless Observing Squads can be easily deployed via our Amazon Machine Image available in the [AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-pbwpmtdtwmkgs). -```bash -https://proxy:port/vm-values/query -``` -```json -{ - "scAddress": "
", - "funcName": "getUserUnDelegatedList", - "args": [ - "" - ] -} -``` +## One click deploy in Google Cloud +Google Cloud instances for Snapshotless Observing Squads can be easily deployed via our virtual machine image available in the [Google Cloud Marketplace](https://console.cloud.google.com/marketplace/product/multiversx-gcp-markeplace/multiversx-snapshotless-observing-squad). - - -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response +## One click deploy in Tencent Cloud +Tencent Cloud nodes for Snapshotless Observing Squads can be easily deployed from the [Tencent Cloud Market](https://www.tencentcloud.com/market/product/P20240326183001630223531). -```json -{ - "returnData": [""] -} -``` - +## One click deploy in Digital Ocean +Digital Ocean droplets for Snapshotless Observing Squads can be easily deployed via our droplets available in the [Digital Ocean Marketplace](https://marketplace.digitalocean.com/apps/multiversx-observing-squad). -Request +--- -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", - "funcName": "getUserUnDelegatedList", - "args": ["ebfd923cd251f857ed7639e87143ac83f12f423827abc4a0cdde0119c3e37915"] -} -``` +### WalletConnect JSON-RPC Methods -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) +The WalletConnect [Sign API](https://specs.walletconnect.com/2.0/specs/clients/sign) establishes a session between a dapp and a wallet in order to expose a set of blockchain accounts that can sign transactions and/or messages using a secure remote JSON-RPC transport with methods and events. +The following methods are already implemented in [xPortal](/wallet/xportal/) and in WalletConnect's [Web Examples](https://github.com/WalletConnect/web-examples). -```json -{ - "returnData": ["Q8M8GTdWSAAA", "iscjBInoAAA="] -} -``` +- [React Wallet (Sign v2)](https://github.com/WalletConnect/web-examples/tree/main/wallets/react-wallet-v2) ([Demo](https://react-wallet.walletconnect.com/)) +- [React dApp (with standalone client) - v2](https://github.com/WalletConnect/web-examples/tree/main/dapps/react-dapp-v2) ([Demo](https://react-app.walletconnect.com/)) - - +The Transaction Object is the same for both the `mvx_signTransaction` and the `mvx_signTransactions` methods. +To get the transaction object into a ready-to-serialize, plain JavaScript object, one can use `.toPlainObject()` from `@multiversx/sdk-core` or [any other available SDKs](/sdk-and-tools/overview). -### POST Delegator funds data {#delegator-funds-data} +## mvx_signTransactions -The response contains an enumeration for the delegator encoded base64 of the hexadecimal encoding of the following: active stake, unclaimed rewards, unstaked stake and unbondable stake. +Sign a list of transactions. +This method returns a signature and any additional properties ( e.g. Guardian info ) that must be applied to each of the provided Transactions before broadcasting them on the network. - - -```bash -https://proxy:port/vm-values/query -``` +### Parameters -```json -{ - "scAddress": "
", - "funcName": "getDelegatorFundsData", - "args": [ - "" - ] -} +```text +1. `Object` - Signing parameters: + 1.1. `transactions` : `Array` - Array of Transactions + 1.1.1 `Object` - Transaction Object + 1.1.1.1 `nonce` : `String` - The Nonce of the Sender. + 1.1.1.2 `value` : `String` - The Value to transfer, as a string representation of a Big Integer (can be "0"). + 1.1.1.3 `receiver` : `String` - The Address (bech32) of the Receiver. + 1.1.1.4 `sender` : `String` - The Address (bech32) of the Sender. + 1.1.1.5 `gasPrice` : `Number` - The desired Gas Price (per Gas Unit). + 1.1.1.6 `gasLimit` : `Number` - The maximum amount of Gas Units to consume. + 1.1.1.7 `data` : `String | undefined` - The base64 string representation of the Transaction's message (data). + 1.1.1.8 `chainID` : `String` - The Chain identifier. ( `1` for Mainnet, `T` for Testnet, `D` for Devnet ) + 1.1.1.9 `version` : `String | undefined` - The Version of the Transaction (e.g. 1). + 1.1.1.10 `options` : `String | undefined` - The Options of the Transaction (e.g. 1). + 1.1.1.11 `guardian` : `String | undefined` - The Address (bech32) of the Guardian. + 1.1.1.12 `receiverUsername` : `String | undefined` - The base64 string representation of the Sender's username. + 1.1.1.10 `senderUsername` : `String | undefined` - The base64 string representation of the Receiver's username. ``` - - -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response +### Returns -```json -{ - "returnData": [ - "", - "", - "", - "" - ] -} +```text +1. `Object` + 1.1. `signatures` : `Array` + 1.1.1 `Object` - corresponding signature and optional properties response for the provided transaction + 1.1.1.1 `signature` : The Signature (hex-encoded) of the Transaction. + 1.1.1.2 `guardian` : `String | undefined` - The Address (bech32) of the Guardian. + 1.1.1.3 `guardianSignature` : `String | undefined` - The Guardian's Signature (hex-encoded) of the Transaction. + 1.1.1.4 `options` : `Number | undefined` - The Version of the Transaction (e.g. 1). + 1.1.1.5 `version` : `Number | undefined` - The Options of the Transaction (e.g. 1). ``` - -Request +### Example -```json +```javascript +// Request { - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhllllsajxzat", - "funcName": "getUserUnDelegatedList", - "args": ["ebfd923cd251f857ed7639e87143ac83f12f423827abc4a0cdde0119c3e37915"] + "id": 1, + "jsonrpc": "2.0", + "method": "mvx_signTransactions", + "params": { + "transactions": [ + { + "nonce": 42, + "value": "100000000000000000", + "receiver": "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", + "sender": "erd1uapegx64zk6yxa9kxd2ujskkykdnvzlla47uawh7sh0rhwx6y60sv68me9", + "gasPrice": 1000000000, + "gasLimit": 70000, + "data": "Zm9vZCBmb3IgY2F0cw==", // base64 representation of "food for cats" + "chainID": "1", + "version": 1 + }, + { + "nonce": 43, + "value": "300000000000000000", + "receiver": "erd1ylzm22ngxl2tspgvwm0yth2myr6dx9avtx83zpxpu7rhxw4qltzs9tmjm9", + "sender": "erd1uapegx64zk6yxa9kxd2ujskkykdnvzlla47uawh7sh0rhwx6y60sv68me9", + "gasPrice": 1000000000, + "gasLimit": 70000, + "data": "Zm9vZCBmb3IgZG9ncw==", // base64 representation of "food for dogs" + "chainID": "1", + "version": 1 + } + ] + } } -``` -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) - -```json +// Result { - "returnData": ["REAihYg4zAAA", "Q8M8GTdWSAAA", "REAihYg4zAAA", "Q8M8GTdWSAAA"] + "id": 1, + "jsonrpc": "2.0", + "result": { + "signatures": [ + { + "signature": "1aa6cdd9f614e2a1cedcc207e6e7c574674c9b05e98f31035cac89fcca2673ca9273c48823418cf44696f64a2c535ab3784f680a0c6d6e84b960c33e586cb30b" + }, + { + "signature": "43127c0ac3d5b124ced9c15e884940fb3c1256c463a74db33c1842fa323971e1f43725eea62225c6b2f9b2634edf68ad2e315241df734d60c41b920dec85b60a" + } + ] + } } ``` - - - -### POST Get reward data for epoch {#get-reward-data-for-epoch} +## mvx_signTransaction -The response contains an enumeration for the specified epoch representing the base64 encoding of the hexadecimal encoding for the rewards to distribute, total active stake and service fee. +This method returns a signature and any additional properties ( e.g. Guardian info ) that must be applied to the transaction before broadcasting it on the network. +Similar to `mvx_signTransactions`, but only one Transaction can be signed at a time instead of a list of transactions. +The same logic applies to the Transaction Object here too. - - -```bash -https://proxy:port/vm-values/query -``` +### Parameters {#mvx_signTransaction-parameters} -```json -{ - "scAddress": "
", - "funcName": "getRewardData", - "args": [""] -} +```text +1. `Object` - Signing parameters: + 1.1. `transaction` : `Object` - Transaction Object + 1.1.1 `nonce` : `String` - The Nonce of the Sender. + 1.1.2 `value` : `String` - The Value to transfer, as a string representation of a Big Integer (can be "0"). + 1.1.3 `receiver` : `String` - The Address (bech32) of the Receiver. + 1.1.4 `sender` : `String` - The Address (bech32) of the Sender. + 1.1.5 `gasPrice` : `Number` - The desired Gas Price (per Gas Unit). + 1.1.6 `gasLimit` : `Number` - The maximum amount of Gas Units to consume. + 1.1.7 `data` : `String | undefined` - The base64 string representation of the Transaction's message (data). + 1.1.8 `chainID` : `String` - The Chain identifier. ( `1` for Mainnet, `T` for Testnet, `D` for Devnet ) + 1.1.9 `version` : `String | undefined` - The Version of the Transaction (e.g. 1). + 1.1.10 `options` : `String | undefined` - The Options of the Transaction (e.g. 1). + 1.1.11 `guardian` : `String | undefined` - The Address (bech32) of the Guardian. + 1.1.12 `receiverUsername` : `String | undefined` - The base64 string representation of the Sender's username. + 1.1.10 `senderUsername` : `String | undefined` - The base64 string representation of the Receiver's username. ``` - - -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response +### Returns {#mvx_signTransaction-returns} -```json -{ - "returnData": [ - "", - "", - "" - ] -} +```text +1. `Object` - corresponding signature and optional properties response for the provided transaction + 1.1 `signature` : The Signature (hex-encoded) of the Transaction. + 1.2 `guardian` : `String | undefined` - The Address (bech32) of the Guardian. + 1.3 `guardianSignature` : `String | undefined` - The Guardian's Signature (hex-encoded) of the Transaction. + 1.4 `options` : `Number | undefined` - The Version of the Transaction (e.g. 1). + 1.5 `version` : `Number | undefined` - The Options of the Transaction (e.g. 1). ``` - -```json +### Example {#mvx_signTransaction-example} + +```javascript +// Request { - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqp0llllswfeycs", - "funcName": "getRewardData", - "args": ["fc2b"] + "id": 1, + "jsonrpc": "2.0", + "method": "mvx_signTransaction", + "params": { + "transaction": { + "nonce": 42, + "value": "100000000000000000", + "receiver": "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", + "sender": "erd1ylzm22ngxl2tspgvwm0yth2myr6dx9avtx83zpxpu7rhxw4qltzs9tmjm9", + "gasPrice": 1000000000, + "gasLimit": 70000, + "data": "Zm9vZCBmb3IgY2F0cw==", // base64 representation of "food for cats" + "chainID": "1", + "version": 1 + } + } } -``` -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) - -```json +// Result { - "returnData": ["REAihYg4zAAA", "Q8M8GTdWSAAA", "REAihYg4zAAA"] + "id": 1, + "jsonrpc": "2.0", + "result": { + "signature": "5845301de8ca3a8576166fb3b7dd25124868ce54b07eec7022ae3ffd8d4629540dbb7d0ceed9455a259695e2665db614828728d0f9b0fb1cc46c07dd669d2f0e" + } } ``` - - - - -## Delegation manager view functions - -### POST All contract addresses {#all-contract-addresses} +## mvx_signMessage -The response contains an enumeration of bech32 keys bytes in base64 encoding. +This method returns a signature for the provided message from the requested signer address. - - -```bash -https://proxy:port/vm-values/query -``` +### Parameters {#mvx_signMessage-parameters} -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", - "funcName": "getAllContractAddresses" -} +```text +1. `Object` - Signing parameters: + 1.1. `message` : `String` - the message to be signed + 1.2. `address` : `String` - bech32 formatted MultiversX address ( erd1... ) ``` - - -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response +### Returns {#mvx_signMessage-returns} -```json -{ - "returnData": [ - "
" - ] -} +```text +1. `Object` + 1.1. `signature` : `String` - corresponding signature for the signed message ``` - -Request +### Example {#mvx_signMessage-example} -```json +```javascript +// Request { - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", - "funcName": "getAllContractAddresses" + "id": 1, + "jsonrpc": "2.0", + "method": "mvx_signMessage", + "params": { + "message": "food for cats", + "address": "erd1uapegx64zk6yxa9kxd2ujskkykdnvzlla47uawh7sh0rhwx6y60sv68me9" + } } -``` - -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) -```json +// Result { - "returnData": [ - "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAL///8=", - "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAP///8=", - "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAT///8=", - "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAX///8=", - "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAb///8=", - "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAf///8=", - "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAj///8=", - "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAn///8=", - "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAr///8=" - ] + "id": 1, + "jsonrpc": "2.0", + "result": { + "signature": "513fb2fa5ac39282ffc3aa90a89024b77057ac4542199673b05601302668bdda36c1076952f4c7445f4c6487a4263d51f72dff325012ab3f236594546ef54408" + } } ``` - - +## mvx_signNativeAuthToken -### POST Contract config {#contract-config-1} +A dApp (and its backend) might want to reliably assign an off-chain user identity to a MultiversX address. On this purpose, the signing providers allow a login token to be used within the login flow - this token is signed using the wallet of the user. Afterwards, a backend application would normally [verify the signature](/sdk-and-tools/sdk-js/sdk-js-signing-providers/#verifying-the-signature-of-a-login-token) of the token. -The response contains an enumeration of the properties in a fixed order (base64 encoded): current number of contracts, last created contract address, minimum and maximum service fee, minimum deposit and delegation. +The functionality is mostly the same as `mvx_signMessage`, only in this case instead of signing the provided message, the wallet will sign a special format including the requested signer address and the provided login token in the form of `${address}${token}`. - - -```bash -https://proxy:port/vm-values/query -``` +### Parameters {#mvx_signNativeAuthToken-parameters} -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", - "funcName": "getContractConfig" -} +```text +1. `Object` - Signing parameters: + 1.1. `token` : `String` - the loginToken to be signed + 1.2. `address` : `String` - bech32 formatted MultiversX address ( erd1... ) ``` - - -Only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response +### Returns {#mvx_signNativeAuthToken-returns} -```json -{ - "returnData": [ - "", - "", - "", - "", - "", - "" - ] -} +```text +1. `Object` + 1.1. `signature` : `String` - corresponding signature for the signed token ``` - -Request +### Example {#mvx_signNativeAuthToken-example} -```json +```javascript +// Request { - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", - "funcName": "getContractConfig" + "id": 1, + "jsonrpc": "2.0", + "method": "mvx_signNativeAuthToken", + "params": { + "token": "aHR0cHM6Ly9kZXZuZXQueGV4Y2hhbmdlLmNvbQ.c6191feb77da75e1acb3c5c3e8d4053be370d925fe7a78c7958ff5edc63d0c8c.86400.eyJ0aW1lc3RhbXAiOjE2OTM3NjQ1ODh9", + "address": "erd1uapegx64zk6yxa9kxd2ujskkykdnvzlla47uawh7sh0rhwx6y60sv68me9" + } } -``` - -Response (only `returnData` shown below; see [view functions](/validators/delegation-manager#delegation-contract-view-functions) for complete response) -```json +// Result { - "returnData": [ - "Gw==", - "AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAABz///8=", - "", - "JxA=", - "Q8M8GTdWSAAA", - "iscjBInoAAA=" - ] + "id": 1, + "jsonrpc": "2.0", + "result": { + "signature": "2789172fd8e0f3b81767392b4f3450807a5894e5c704073d18a0d5e4d0819cd8fac53ef8ba3e3b0430481d6e396f67ae484ae1f295befa766e49a3abfdf76e0a" + } } ``` - - ---- +## mvx_signLoginToken -### Staking Providers APR +Exactly the same functionality as `mvx_signNativeAuthToken`, only the login token format differs. The Wallet can display a different UI based on the login token method request. -Staking Providers are entities that allow their delegators, plus the owner's funds to deploy validators on the Network. The Staking Provider owner -is responsible for the nodes' up-time, as well as for configuring the contract's parameters. According to the contract's configuration and operations, the delegation cap, -the contract fee, the number of tokens kept as top-up or the number of nodes to be deployed are all variables in computing the final APR (Annual Percentage Return) -of the provider. +## mvx_cancelAction -## Introduction +Wallets can implement this method to improve the UX. It is used to transmit that the user wishes to renounce on a triggered action. Close a sign transaction modal or a sign message modal, etc. -By using the [Delegation Manager](/validators/delegation-manager/) system smart contract, a new staking provider can be -set up. According to the initial deposits (half of the minimum node stake) + delegations from other users (or even the owner itself) -the staking contract can spawn new nodes. Currently, the minimum node cost is 2500 EGLD, so, for example, if a staking contract -gathered 7500 EGLD it can spawn 3 new nodes. +### Parameters {#mvx_cancelAction-parameters} -### Base stake and top-up +```text +1. `Object` - Action parameters + 1.1. `action` : `String | undefined` - Current action to be cancelled ( for ex. `cancelSignTx` ) +``` -As stated, a validator node requires at least 2500 EGLD. So multiple nodes would mean at least _2500 multiplied by the number of nodes_ EGLD in the contract. -The difference is considered top-up. Also, the staking provider owner can choose to keep the tokens as top-up, even -if the top-up is enough to spawn a new validator node. -Let's take some examples: +### Returns {#mvx_cancelAction-parameters} -a). A Staking Contract has 2550 EGLD. This would mean a base stake of 2500 EGLD + 50 EGLD top-up +`void` -b). A Staking Contract has 5200 EGLD. This could mean: -- a base stake of 2500 EGLD (1 node) + 2700 EGLD top-up -- a base stake of 5000 EGLD (2 nodes) + 200 EGLD top-up +### Example {#mvx_cancelAction-parameters} -Network-wise, the base stake is currently limited to 8,000,000 EGLD (3200 nodes \* 2500 EGLD / node). However, current staking -metrics indicate that the total EGLD staked is around 13,000,000 EGLD, resulting in a base stake of 8 millions EGLD + ~5 millions EGLD top-up. +```javascript +// Request +{ + "id": 1, + "jsonrpc": "2.0", + "method": "mvx_cancelAction", + "params": { + "action": "cancelSignTx" + } +} +``` +--- -### Service Fee +## Advanced +### Architecture -At each epoch change, the delegation contract receives the rewards in accordance to its stake. Over those rewards, -a service fee applies so the owner can cover the hosting and nodes management costs. +import useBaseUrl from '@docusaurus/useBaseUrl'; +import ThemedImage from '@theme/ThemedImage'; -So for example, if the rewards are 10 EGLD in an epoch, and the service fee is set to 10%, the owner of the staking -contract will be eligible for 1 EGLD, while the difference (9 EGLD) will be allocated to the delegators. +# Architecture -### Inflation Rate +Ad-Astra Bridge is a system that allows the transfer of ERC20 tokens between EVM-compatible chains and the MultiversX network. +Currently, there are 2 bridges available: between the Ethereum and MultiversX networks and between the BSC and MultiversX networks. +The system is composed of several contracts and relayers that work together to facilitate the transfer of tokens. -MultiversX's economics model is based on an inflation rate that decreases each year. More about this can be read on the -[blog](https://multiversx.com/blog/the-wealth-of-crypto-networks-elrond-economics-paper). +Without providing many details regarding the smart contracts interactions, this is a simplified view of the entire bridge architecture. + + -This means that for each year the estimated rewards for a validator will change. This has to be taken into account -when computing the APR. -The configuration for the inflation rate can be found [here](https://github.com/multiversx/mx-chain-mainnet-config/blob/master/economics.toml) (`YearSettings`). +## EVM-compatible chains contracts +The repository for the Solidity contracts used on the EVM-compatible side can be found here: https://github.com/multiversx/mx-bridge-eth-sc-sol +The main contracts are described below: +1. **Safe**: A contract that allows users to deposit ERC20 tokens that they want to transfer to the MultiversX network; +2. **Bridge**: A contract that facilitates the transfer of tokens from MultiversX to an EVM-compatible chain. Only the relayers are allowed to use this contract. -The protocol doesn't take leap years into consideration, but rather approximate each year at 365 days. -The approximated inflation rate is as follows: +## MultiversX contracts +The repository for the Rust contracts used on the MultiversX side can be found here: https://github.com/multiversx/mx-bridge-eth-sc-rs +1. **Safe**: A contract that allows users to deposit ESDT tokens that they want to transfer to EVM-compatible networks; +2. **Bridge**: A contract that facilitates the transfer of tokens from the EVM-compatible chain to MultiversX. As the Bridge contract on the EVM-compatible chain side, this + the contract is allowed to be operated by the registered relayers; +3. **MultiTransfer**: A helper contract that is used to perform multiple token transfers at once; +4. **BridgedTokensWrapper**: A helper contract that is used to support wrapping the same token from multiple chains into a single ESDT token; +5. **BridgeProxy**: A helper contract that is used to store and handle the smart contract execution and the possible refund operation after the swap is done. -| Year | Start Date | Inflation rate | -|------|-------------|----------------| -| 1 | 2020.07.30 | 10.84% | -| 2 | 2021.07.30 | 9.7% | -| 3 | 2022.07.30 | 8.56% | -| 4 | 2023.07.30 | 7.42% | -| 5 | 2024.07.29 | 6.27% | -| 6 | 2025.07.29 | 5.13% | -| 7 | 2026.07.29 | 3.99% | -| 8 | 2027.07.29 | 2.85% | -| 9 | 2028.07.28 | 1.71% | -| 10 | 2029.07.28 | 0.57% | -| 11 | 2030.07.28 | 0% | +## Relayers +The repository for the code that the relayers use can be found here: https://github.com/multiversx/mx-bridge-eth-go -### Protocol Sustainability +For each existing bridge, the following list applies: +- **5 Relayers** are managed by the MultiversX Foundation; +- **5 Relayers** are distributed to the MultiversX validators community, with each validator having one relayer. -In accordance to the Mainnet's [configuration](https://github.com/multiversx/mx-chain-mainnet-config/blob/master/economics.toml#L35) (`ProtocolSustainabilityPercentage`). -at each epoch change, when new tokens are distributed among the validators, 10% of the value goes to Protocol Sustainability Address. +--- -This also has to be taken into account when calculating the APR. +### Axelar Amplifier Setup +# Axelar Amplifier setup for MultiversX -## Rewards calculation +## Prerequisites -When wanting to calculate the APR (Annual Percentage Return) of a Staking Provider, there are multiple factors that have -to be taken into account, such as total value locked at Network-level, the inflation based on the current year, the -staking provider base stake and top-up stake, and so on. +- have an [Axelar Validator](https://docs.axelar.dev/validator/setup/overview/) running (node, tofnd & vald) +## Become an Amplifier Verifier -### Network Top-Up rewards +For more detailed information check out the [Become a Verifier](https://docs.axelar.dev/validator/amplifier/verifier-onboarding/) Axelar docs. -The formula for determining the rewards received by the validators for the top-up in a given epoch is: +You can skip this if already having an Amplifier Verifier up and running. -$$ -topUpRewards(e) = \frac{2 * topUpRewardLimit(e)}{\pi} * atan(\frac{eligibleCumulatedTopUp(e)}{p}) -$$ +### Set up `tofnd` -Where: +If running on the same machine as the Axelar Validator, the existing `tofnd` can be used. -- `e` represents the given epoch -- `topUpRewardLimit(e)` represents the maximum top-up rewards that can be distributed in the given epoch. This can be viewed - as the maximum value out of the epoch rewards that can be distributed as rewards for the top-up stake, and depends - on the total rewards to be distributed in the epoch and a configured network parameter that defines the proportion out of the total rewards. -- `eligibleCumulatedTopUp(e)` represents the rewards distributed in the epoch for signing and proposing blocks. - This does not include the protocol sustainability rewards, developer fees or the penalty for missed blocks. -- `p` represents a chosen parameter to control the gradient of top-up rewards. It can be viewed as the cumulated top-up stake - where the given top-up rewards reach ½ of the top-up rewards set limit. It is currently set to 2M EGLD. +If you want to setup on a new machine, then you can setup `tofnd` using Docker: +``` +docker pull axelarnet/tofnd:v1.0.1 +docker run -p 50051:50051 --env MNEMONIC_CMD=auto --env NOPASSWORD=true --env ADDRESS=0.0.0.0 -v tofnd:/.tofnd axelarnet/tofnd:v1.0.1 +``` -### Network base rewards +### Set up `ampd` -$$ -baseRewards(e) = blocksRewards(e) - topUpReward(e) -$$ +Setup the `ampd` process using Docker: -Where: +``` +docker pull axelarnet/axelar-ampd:v1.3.1 +``` -- `blocksRewards(e)` represents the rewards received by the validators by either signing or proposing blocks during the epoch that are now - part of the canonical chain -- `topUpReward(e)` is computed above +Make sure that the `ampd` process can communicate with `tofnd`. +To view your Verifier address you can run: `docker run axelarnet/axelar-ampd:v1.3.1 verifier-address` -## APR calculation +### Configure the verifier -After determining the base and the top-up rewards for an epoch, the APR can be calculated for a Staking Provider. +You need to create a configuration file at `~/.ampd/config.toml` and add the required configuration depending on your environment. -First, we have to determine the maximum rewards that can be reached in ideal situations (no missed block in an epoch). +For complete configuration files for different environments, check out the [Configure the verifier](https://docs.axelar.dev/validator/amplifier/verifier-onboarding/#configure-the-verifier) section in the Axelar Amplifier docs. +Example basic `config.toml` for mainnet: -### Staking Provider base stake rewards +``` +# replace with your Axelar mainnet node +tm_jsonrpc="http://127.0.0.1:26657" +tm_grpc="tcp://127.0.0.1:9090" +event_buffer_cap=100000 -We have to calculate the estimated rewards received by a Staking Provider in one epoch for the base stake. +[service_registry] +cosmwasm_contract="axelar1rpj2jjrv3vpugx9ake9kgk3s2kgwt0y60wtkmcgfml5m3et0mrls6nct9m" -This is done by calculating the share of the total rewards in accordance to the provider's number of nodes. +[broadcast] +batch_gas_limit="20000000" +broadcast_interval="1s" +chain_id="axelar-dojo-1" +gas_adjustment="2" +gas_price="0.007uaxl" +queue_cap="1000" +tx_fetch_interval="1000ms" +tx_fetch_max_retries="15" -Therefore, if a Staking Provider has 320 nodes out of 3200 nodes, it will receive 10% of the base rewards. Note that the base rewards -are calculated after decreasing the protocol sustainability rewards, as well as the computed top-up rewards. +[tofnd_config] +batch_gas_limit="10000000" +key_uid="axelar" +party_uid="ampd" +url="http://127.0.0.1:50051" -$$ -stakingProviderBaseStakeRewards(e) = \frac{stakingProviderNumberOfNodes}{totalNumberOfNodesInNetwork} * baseRewards(e) -$$ +[[handlers]] +cosmwasm_contract="axelar14a4ar5jh7ue4wg28jwsspf23r8k68j7g5d6d3fsttrhp42ajn4xq6zayy5" +type="MultisigSigner" +``` +You need to configure additional `handlers` for each chain you want to support. Check out the [ampd README file](https://github.com/axelarnetwork/axelar-amplifier/blob/main/ampd/README.md) for more information. +Find below an example for configuring handlers for **MultiversX**. -### Top-Up rewards +### Activate and run the verifier -Similar to base stake rewards, the rewards for top-up are estimated by computing the share of the provider's top-up in -accordance to the network's total top-up. +For more information check out the [Axelar docs](https://docs.axelar.dev/validator/amplifier/verifier-onboarding/#activate-and-run-the-verifier). -$$ -stakingProviderTopUpRewards(e) = \frac{stakingProviderTopUpAmount}{networkTotalTopUp} * topUpRewards(e) -$$ +Find below basic instructions for mainnet: +1. Bond your verifier: `ampd bond-verifier amplifier 50000000000 uaxl` -### APR calculation +2. Register public key: -In order to obtain the estimation of the APR, we first need to calculate the share of the provider's earnings in an epoch as compared -to it's total stake locked. Then we will multiply by 365 (the number of days in a year) and get the result. +`ampd register-public-key ecdsa` -$$ -aprWithoutFee = \frac{stakingProviderBaseStakeRewards(e) + stakingProviderTopUpRewards(e)}{providerTotalStake} * 365 -$$ +`ampd register-public-key ed25519` -The last step is to decrease the fee deducted by the staking provider owner: +3. Register support for chains for which you have configured handlers: `ampd register-chain-support amplifier flow ethereum multiversx [MORE_CHAINS]` -$$ -apr = \frac{100 - fee}{100} * aprWithoutFee -$$ +Run the `ampd` process with `docker run axelarnet/axelar-ampd:v1.3.1` +## Add support for MultiversX to Verifier -## Example +### Running a MultiversX Observing Squad -The formulas and all the mathematics involved might be quite complicated, so let's take an example. +For security reasons, you will need to run your own MultiversX Observing Squad, which is a collection of nodes, one node for each MultiversX shard + the Proxy API service. This API will be used by the Verifier to get transactions from the MultiversX network in order to be able to verify them. -Let's say we have the following parameters: +You can find detailed steps in the [MultiversX Observing Squad docs](https://docs.multiversx.com/integrators/observing-squad). There exist installation scripts that making setting up an Observing Squad easy. -Network parameters +Below you can find basic information on how to setup a squad for mainnet: -``` -genesisTotalSupply = 20M EGLD -inflationRate = 9.7% (year 2) -p = 2M EGLD -totalNodes = 3200 -eligibleCumulatedTopUp = 2.6M -totalCumulatedTopUp = 5.2M -protocolSustainabilityRewards = 10% -numDaysInAYear = 365 -topUpFactor = 0.5 -``` +1. Clone the `mx-chain-scripts` repo: `git clone https://github.com/multiversx/mx-chain-scripts` -Staking provider parameters: +2. Edit the `config/variables.cfg` according, for example: ``` -stakingProviderNumberOfNodes = 10 -stakingProviderBaseStake = 25,000 EGLD -stakingProviderTopUpAmount = 6,472 EGLD -stakingProviderTotalStake = 31,472 EGLD -fee = 2% +ENVIRONMENT="mainnet" +... +CUSTOM_HOME="/home/ubuntu" +CUSTOM_USER="ubuntu" ``` -For a random day, the maximum rewards that can be distributed is: +3. Setup the Observing Squad: `./script.sh observing_squad` + +4. Start the nodes & the Proxy: `./script.sh start` + +### Updating Verifier `config.toml` file + +In order to support MultiversX, first you need to add the two required handlers at the end of your `~/.ampd/config.toml` file: + +#### Devnet ``` -maximumRewardsInADay(e) = inflationRate * genesisTotalSupply / numDaysInAYear = 9.7% * 20M / 365 = 5315 EGLD +[[handlers]] +type = 'MvxMsgVerifier' +cosmwasm_contract = 'axelar1sejw0v7gmw3fv56wqr2gy00v3t23l0hwa4p084ft66e8leap9cqq9qlw4t' +# replace with your MultiversX Proxy URL +proxy_url = 'http://127.0.0.1:8079' + +[[handlers]] +type = 'MvxVerifierSetVerifier' +cosmwasm_contract = 'axelar1sejw0v7gmw3fv56wqr2gy00v3t23l0hwa4p084ft66e8leap9cqq9qlw4t' +# replace with your MultiversX Proxy URL +proxy_url = 'http://127.0.0.1:8079' ``` -We have to decrease the protocol sustainability rewards, resulting in: +#### Testnet ``` -maximumRewardsInADay(e) = 4783 EGLD +[[handlers]] +type = 'MvxMsgVerifier' +cosmwasm_contract = 'TBD' +# replace with your MultiversX Proxy URL +proxy_url = 'http://127.0.0.1:8079' + +[[handlers]] +type = 'MvxVerifierSetVerifier' +cosmwasm_contract = 'TBD' +# replace with your MultiversX Proxy URL +proxy_url = 'http://127.0.0.1:8079' ``` -The maximum top-up reward for the epoch is: +#### Mainnet ``` -topUpRewardLimit(e) = topUpFactor * maximumRewardsInADay(e) = 0.5 * 4783 =~ 2391 EGLD +[[handlers]] +type = 'MvxMsgVerifier' +cosmwasm_contract = 'TBD' +# replace with your MultiversX Proxy URL +proxy_url = 'http://127.0.0.1:8079' + +[[handlers]] +type = 'MvxVerifierSetVerifier' +cosmwasm_contract = 'TBD' +# replace with your MultiversX Proxy URL +proxy_url = 'http://127.0.0.1:8079' ``` -Therefore, the network top-up would be: +### Register MultiversX chain -``` -topUpRewards(e) = (2 * topUpRewardLimit(e) / pi) * atan(eligibleCumulatedTopUp(e) / p) = (2 * 2391 / pi) * atan(2.6M / 2M) =~ 1522 * 0.91 =~ 1385 EGLD -``` +1. (optional) If you have not done so already, first register the `ed25519` public key: `ampd register-public-key ed25519` -The base rewards would be: +2. Then register support for the `multiversx` chain: `ampd register-chain-support amplifier multiversx` -``` -baseRewards(e) = blocksRewards(e) - topUpReward(e) = maximumRewardsInADay(e) - topUpReward(e) = 4783 - 1385 = 3398 EGLD -``` +At this point you can restart the `ampd` process and you should be able to validate MultiversX messages. -Moving to the staking provider: +--- -``` -stakingProviderBaseStakeRewards(e) = stakingProviderNumberOfNodes / totalNodes * baseRewards(e) = 10 / 3200 * 3398 = 10.61 EGLD -stakingProviderTopUpRewards(e) = stakingProviderTopUpAmount / totalCumulatedTopUp * topUpRewards(e) = 6472 / 5.2M * 1385 = 1.72 EGLD -``` +### Bitcoin L2 -And, finally, calculating the APR: +# Bitcoin L2 -``` -aprWithoutFee = (stakingProviderBaseStakeRewards(e) + stakingProviderTopUpRewards(e)) / providerTotalStake * 365 = (10.61 + 1.72) / 31472 * 365 = 14.29 % -``` +:::note -After deducting the fee: +This documentation is not complete. More content will be added once it is accepted and discussed on Agora or once it is implemented and available for production. -``` -apr = (100 - fee) / 100 * aprWithoutFee = (100 - 2) / 100 * 14.29 = 14.00 % -``` +::: --- -### Staking v4 +### Concept -# **Introduction** +# Concept -Staking and delegation are processes that evolve over time. No system has to remain static. Our assumptions about how -the market works and reacts can change, just as user behavior and market dynamics may evolve. Currently, we have -approximately 400 validators, with some acting as staking providers and others as individual validator operators. While -most nodes have a comfortable top-up on the base stake of 2.500 eGLD, some do not contribute to the network's security by -adding more top-up. +:::note +This documentation is not complete. More content will be added once it is accepted and discussed on Agora or once it is implemented and available for production. -# **Limitations of the Current Implementation** +The content created here is derived from: +- [Sovereign Shards - MIP7 - part - 1](https://agora.multiversx.com/t/sovereign-shards-mip-7-part-1/185) -- Limiting the number of nodes to 3200, creating an additional queue. New validators can join the network only if - someone leaves the system. -- Concentration of power among large providers, hindering decentralization. Top 11 staking agencies control 33%. +::: -One of our primary objectives is to eliminate the additional queue and leverage the top-up value per node to determine -the best nodes. This ensures that we do not restrict the entry of new validators, as the current system requires an old -validator to leave for a new one to enter. The market will determine the actual node price, operating as a soft auction -where anyone paying the node price (2.500 eGLD) can register, but the node becomes a validator only if it has sufficient -top-up. The shuffling from the waiting list to the eligible list remains unaffected, focusing solely on selecting nodes -from the auction list to the waiting list. +The MultiversX blockchain utilizes a fully sharded architecture designed for scalability, processing tens of thousands of transactions per second, and allowing developers to deploy decentralized applications using smart contracts. The in-protocol token standard, ESDT, facilitates simple, inexpensive, secure and scalable token transfers. As the network expands, new shards are created, enabling horizontal scaling. Additionally, parallelization within the same shard supports vertical scaling. The system is decentralized, public, open, and uses EGLD as its base token. -This approach, known as a _Soft Auction_, democratizes the validator system, transforming it into a free market. +However, certain systems and applications require more customization than what a general-purpose network like MultiversX can offer. App-specific blockchains are tailored to run specific applications, providing several benefits: -The selection process for the best X nodes from the auction into the "to be shuffled into waiting" list can be executed -in two ways. +- **Increased Performance**: Dedicating blockspace and computational resources to one concept can improve transaction throughput, reduce latency, and lower fees. This is important for applications needing high performance, such as gaming, real world use cases or DeFi. -**The first** and easy version is a strict selection where all nodes from the auction list are sorted based on their -topUp, and only the first ones are selected. +- **Flexibility and Sovereignty**: Developers can customize various aspects of their blockchain, including the security model, fee token and model, governance mechanism, and VM. This customization can attract and retain users by offering unique incentive schemes, tokenomics models, and user experiences. -This approach is strict, requiring validators to manage their number of nodes if their topUp is close to the selection -threshold. The topUp is computed as TotalStake/NumberOfNodes - 2.500 eGLD. If a validator registers more nodes, their -topUp per node decreases. For example: +Sovereign Chains aim to provide an SDK that allows developers to create highly efficient chains connected to the global MultiversX Network. This enables composability, interoperability, and simplified usability across all chains, leveraging existing infrastructure and enhancing user experience. -- 10 nodes, 50.000 eGLD = 2.500 eGLD topUp per node -- 20 nodes, 50.000 eGLD = 0 eGLD topUp per node +## Historical Context -In this scenario, if a staking provider registers all 20 nodes, they will be at the end of the list in case of an -auction selection. If only 320 nodes out of 340 can be selected, and every other node has at least 1 eGLD topUp on each -of them, none of the 20 nodes from this staking provider will be selected. +### Issues with Layer2 Solutions -Since this selection occurs at the end of every epoch, staking providers near the topUp limit must continually monitor -and adjust their nodes, unstaking or staking nodes based on the topUp per node of other providers. Sorting nodes based -on topUp does not provide adequate protection for staking providers, requiring constant supervision and action. +The concept of Sovereign Chains is introduced to address some of the most encountered issues: -**The second version**, currently implemented and explained in the following chapters, addresses the shortcomings of the -first version. +- **Unified Liquidity and Application Ecosystem:** Sovereign Chains eliminate the need for fragmented multi-signature wallets by providing a unified ecosystem. This ensures that liquidity is not dispersed and applications can operate seamlessly without silos. +- **Simplified User Experience:** With Sovereign Chains, users interact within a single ecosystem that supports all necessary operations, eliminating the need for multiple gas tokens. This streamlines the user experience, making interactions more intuitive and user-friendly. -# **Current Implementation** +- **Built-In Interoperability and Composability:** Sovereign Chains are designed with interoperability and composability as core features. Applications deployed on Sovereign Chains can interact seamlessly, leveraging built-in protocols that ensure secure and efficient cross-chain operations. -:::note -Please note that the numbers below are indicative and only used to better exemplify the model. -::: +- **Seamless Fund Movement and Deployment Adaptation**: Users can move funds effortlessly across different shards and deployments within the Sovereign Chain ecosystem. This flexibility simplifies adaptation to various applications and enhances overall adoption. -In the current implementation (staking 3.5), we have: +## Concept of Sovereign Chains -1. A capped number of 3200 **nodes in the network**, including: - - 1600 active/eligible validators globally, split into 400 nodes per shard - - 1600 waiting validators globally, split into 400 nodes per shard -2. An uncapped `FIFO` queue where newly staked nodes are placed and await participation in the network. +**High-Level Features** -:::important +- **High Performance**: Capable of >4000 DeFi/Gaming transactions per second, achieved by a new consensus model that dedicates 90-95% of time to processing. Nevertheless this model will also be launched on the mainchain. +- **Configurable Features**: Options for private vs. public chains, staking setups, smart contracts, ESDTs, and customizable gas and transaction fees. -Currently, a queued node can participate in the network only if an existing node is either unstaked or jailed. +**Virtual Machine (VM) Configurations** -::: +- **Default VMs**: SystemVM (GO) and WasmVM. +- **Custom VMs**: + - Ethereum Compatible VM; + - Bitcoin Compatible VM; + - Solana Compatible VM; -![Current Staking](/validators/stakingV4/current-staking.png) +**Built-in Cross-Chain Mechanism** -Nodes are distributed in the following steps: +A built-in cross-chain mechanism facilitates token transfers between the sovereign chain and the MultiversX network without relayers. The process ensures seamless interoperability and security through validator verification and proof systems. -1. Randomly shuffle out 80 eligible validators for each shard, resulting in 320 (80 validators per 4 shards) - shuffled-out validators. -2. Select these 320 shuffled-out validators to be randomly but evenly distributed at the end of each shard's waiting - list. -3. For each shard, replace the previously shuffled-out validators with 80 waiting validators from the top of each - shard's waiting list. +## Cross-Chain Mechanism Concept -In the current implementation, each node, regardless of its top-up, has equal chances of participating in the consensus. -Starting with staking phase 4, the probability of validators entering the validation process will be significantly -influenced by the amount of their staked top-up. Validators with a higher staked top-up will have considerably greater -chances of participation, while those with little or no top-up will find their chances of entering into validation -markedly reduced. +### Mainchain to Sovereign Chain Concept +#### 1. Sovereign Notifier Service and Header Inclusion -# **Staking V4** +Sovereign Notifier Service -Staking phase 4 will unfold in three consecutive steps, each corresponding to a specific epoch. +Validators on the Sovereign Chain should run a notifier service to monitor cross-chain transactions from the MultiversX mainchain. This service can connect to the validators' own nodes or public notifier services to receive notification events. +Header Inclusion Process -## Staking v4. Step 1. +- *Detection*: When the leader validator detects a new header from the MultiversX mainchain, it attempts to include this header in the current block it is building. +- *Adding* `ShardHeaderHash`: The leader specifically adds the MultiversX `ShardHeaderHash` to the `SovereignShardHeader`. +- *Transaction Processing*: If there are cross-chain transactions from the mainchain, the protocol mandates the leader and validators to process all these transactions before adding the new `ShardHeaderHash`. This mimics the cross-shard transaction processing in MultiversX. -In the first step, we will completely **remove the staking queue** and **unstake all nodes from the staking queue**. -This process will occur automatically at the end of the epoch and requires no interaction from validators. -Nodes' distribution remains unchanged. +ShardHeader Inclusion -For owners which had **unstaked** nodes, these can be **restaked** using 'RestakeUnstakedNodes' complete details [here](https://docs.multiversx.com/validators/delegation-manager/#restaking-nodes) +1. A `SovereignShardHeader` can contain multiple ShardHeaderHashes. +2. The leader adds only finalized ShardHeaders from the mainchain. +3. New MultiversX HeaderHash cannot be added if previous transactions are not executed. -![Staking V4 Step 1](/validators/stakingV4/stakingV4-step1.png) +#### 2. Execution of Incoming Transactions +Creating Miniblocks -:::important Important notes +- *Transaction Collection*: The leader collects all incoming cross-chain transactions and creates a miniblock named `“INCOMING TXS”`. +- *Data Preparation*: The notifier service prepares relevant data, including proof of execution, data availability, correctness, and finality. +- *Data Sending*: This prepared data is pushed to the Sovereign Chain client. -Starting with this epoch: +Validator Verification -- Every **newly staked** node will be placed in the **auction list**. -- Every **unjailed** node will be placed in the **auction list**. -- The global **number of new nodes** that can take part in the system is **uncapped**. -- Owners with **insufficient base staked EGLD** for their nodes will have them **removed** at the end of the epoch in - the following order: `auction` -> `waiting` -> `eligible`. +1. Validators on the Sovereign Chain verify the leader's miniblock creation. +2. Validators perform header verification, incoming transaction creation, proof verification, and finality checks. +3. If verification fails, validators do not sign the block, preventing its creation. -::: +Gas-Free Execution -For example, if an owner has insufficient base stake for their nodes, the nodes will be removed from the network at the -end of the epoch based on the order: `auction` -> `waiting` -> `eligible`. This ensures that nodes contributing to the -ecosystem with a healthy top-up will not be adversely affected. +1. Incoming cross-chain transactions are executed without gas on the Sovereign Chain. + - these transactions are treated as DestME ESDT transfers, ensuring fast execution and complete integration with the MultiversX chain. -Below is an example of how nodes are unstaked based on insufficient base stake. Suppose an owner has four nodes: +#### 3. UI and Smart Contract on MultiversX Chain -- 1 eligible: `node1` -- 2 waiting: `node2`, `node3` -- 1 auction: `node4` +The intention is that the user should not feel the complexity of transferring a token from one chain to the other. That's why in 3 easy steps a cross-chain transaction should be felt as successful: -Assuming a minimum price of 2500 EGLD per staked node, the owner should have a minimum base stake of 10,000 EGLD (4 * -2500 EGLD). If, during the epoch, the owner unstakes 4000 EGLD, resulting in a base stake of 6000 EGLD, only two staked -nodes can be covered. At the end of the epoch, the nodes `node4` and `node3` will be unstaked in the specified order. +1. Users execute a transaction on the MultiversX chain directed towards the cross-chain contract. +2. Upon successful transaction on the mainchain, the user receives tokens on the Sovereign Chain. +3. The execution and transfer are done as the initial user, maintaining seamless cross-chain interaction. +But how do we achieve that? Even though the main components are going to be described at several stages in the documentation, from conceptual point of view we have the: -## Staking v4. Step 2. +ESDTSafe Contract -In the second step, all **shuffled-out** nodes from the **eligible list** will be sent to the **auction list**. Waiting -lists will not be filled by any shuffled-out nodes. +- Functionality: Users can deposit tokens and specify the destination address and execution call. +- Endpoint: `deposit@address@gasLimit@functionToCall@arguments` receiving a `MultiESDTNFTTransfer`. +- Verification: The contract verifies the validity of the address and generates a specific `logEvent`. -Using the example above, this will resize each waiting list per shard from 400 nodes to 320 nodes. +`logEvent` Structure +```json +Identifier: deposit +Address: scAddress +Topics: address, LIST +Data: localNonce (increasing), originalSender, gasLimit, functionToCall, arguments +``` -![Staking V4 Step 2](/validators/stakingV4/stakingV4-step2.png) +Customizable Features +- General Fee Model: Configurable fee and fee token. +- Whitelist/Blacklist: Tokens can be whitelisted or blacklisted for transfer. -## Staking v4. Step 3. +#### 4. Sovereign Chain Cross-Chain Execution +Preparation and Miniblock Creation -Starting with this epoch: +1. The notifier service generates an Extended Shard Header structure and an Incoming Txs miniblock based on logEvents. +2. Validators finalize blocks by including mainchain shard headers and all incoming transactions. -- Maximum number of nodes in the network will be changed from 3200 to 2880 (3200 - 320), consisting of: - - a global number of 1600 active/eligible validators, split into 400 nodes/shard - - a global number of 1280 waiting validators to join the active list, split into 320 nodes/shard -- All **shuffled out** nodes from the eligible list will be sent to the auction list to take part in the auction - selection. The more topUp an owner has, the higher the chances of having their auction nodes selected will be. -- Based on the _soft auction selection_ (see the next section), all **qualified** nodes from the **auction** will be - distributed to the waiting list (depending on the `available slots`). The other **unqualified** nodes will remain in - the auction and wait to be selected in the next epoch if possible. The number of available slots is based on the - number of shuffled-out nodes and other nodes leaving the network (e.g.: `unstake/jail`). It guarantees that the - waiting list is filled, and the nodes' configuration is maintained. -- Distribution from the `waiting` to the `eligible` list will remain unchanged. +Token ID Collision Avoidance -![](/validators/stakingV4/stakingV4-step3.png) +To avoid token ID collisions, each deployed Sovereign Chain adds a unique prefix or increasing nonce when registering token IDs on the ESDT SC. This ensures clear distinction of token origins on the Sovereign Chain. +#### 5. Mainchain to Sovereign Shard Process -## Staking v4. Soft Auction Selection Mechanism + Notifier Service: Validators receive notifications of mainchain bridge transactions. + Header Inclusion: Leaders include finalized MultiversX ShardHeaders in the SovereignShardHeader. + Transaction Execution: Leaders collect and execute incoming transactions, forming an INCOMING TXS miniblock. + Validator Verification: Validators verify and sign the block, ensuring all transactions are processed. + Gas-Free Execution: Transactions are executed without gas on the Sovereign Shard, treated as DestME ESDT transfers. -Nodes from the auction list will be selected to be distributed in the waiting list based on the **soft auction** -mechanism. For each owner, based on their topUp, we compute how many validators they would be able to run by -distributing their total topUp per fewer nodes (considering we would not select all of their auction nodes, but only a -part of them). This mechanism ensures that for each owner, we select as many nodes as possible, based on the **minimum -required topUp** to fill the`available slots`. This is a global selection, not per shard. We preselect the best global -nodes at the end of the epoch. +### Sovereign Chain to MultiversX Mainchain -Suppose we have the following auction list, and 3 available slots: +The Sovereign Chain to MultiversX Mainchain cross-chain communication process involves synchronizing the mainchain with the Sovereign Chain, handling cross-chain transactions, and ensuring smooth token transfers between the two. This description provides a conceptual explanation of the roles, processes, and smart contract interactions involved. -![](/validators/stakingV4/soft-auction1.png) +#### Responsibilities of Sovereign Validators -``` -+--------+------------------+------------------+-------------------+--------------+-----------------+-------------------------+ -| Owner | Num staked nodes | Num active nodes | Num auction nodes | Total top up | Top up per node | Auction list nodes | -+--------+------------------+------------------+-------------------+--------------+-----------------+-------------------------+ -| owner1 | 3 | 2 | 1 | 3669 | 1223 | pubKey1 | -| owner2 | 3 | 1 | 2 | 2555 | 851 | pubKey2, pubKey3 | -| owner3 | 2 | 1 | 1 | 2446 | 1223 | pubKey4 | -| owner4 | 4 | 1 | 3 | 2668 | 667 | pubKey5, pubKe6, pubKe7 | -+--------+------------------+------------------+-------------------+--------------+-----------------+-------------------------+ -``` +Sovereign Validators have two primary responsibilities: -For the configuration above: +1. Syncing with the mainchain and pushing bridge transactions. +2. Managing transfers from Sovereign to Main and vice versa. -- Minimum possible `topUp per node` = 667, considering `owner4` will have all of his **3 auction nodes selected** - - owner4's total top up/(1 active node + 3 auction nodes) = 2668 / 4 = 667 -- Maximum possible `topUp per node` = 1334, considering `owner4` will only have **one of his auction nodes selected** - - owner4's total top up/(1 active node + 1 auction node) = 2668 / 2 = 1334 +Types of Tokens -Based on the above interval: `[667, 1334]`, we compute the `minimum required topUp per node` to be qualified from the -auction list. We gradually increase from min to max possible topUp per node with a step, such that we can fill -the `available slots`. At each step, we compute for each owner what's the maximum number of nodes that they could run by -distributing their total topUp per fewer auction nodes, leaving their other nodes as unqualified in the auction list. -This is a soft auction selection mechanism, since it is dynamic at each step and does not require owners to "manually -unstake" their nodes so that their topUp per node would be redistributed (and higher). This threshold ensures that we -maximize the number of owners that will be selected, as well as their number of auction nodes. +There are two types of tokens involved in the bridging process: -In this example, if we use a step of 10 EGLD in the `[667, 1334]` interval, the `minimum required topUp per node` would -be 1216, such that: +- **Token Type A**: Tokens initially originating from Main to Sovereign, with liquidity held in the safe contract on Main. +- **Token Type B**: New tokens first issued on the Sovereign Chain, requiring creation and specific roles (`localMint`, `nftCreate`) for the `esdt-safe` contract. -![](/validators/stakingV4/soft-auction2.png) +**Cross-Chain tx Process** -``` -+--------+------------------+----------------+--------------+-------------------+-----------------------------+------------------+---------------------------+-----------------------------+ -| Owner | Num staked nodes | TopUp per node | Total top up | Num auction nodes | Num qualified auction nodes | Num active nodes | Qualified top up per node | Selected auction list nodes | -+--------+------------------+----------------+--------------+-------------------+-----------------------------+------------------+---------------------------+-----------------------------+ -| owner1 | 3 | 1223 | 3669 | 1 | 1 | 2 | 1223 | pubKey1 | -| owner2 | 3 | 851 | 2555 | 2 | 1 | 1 | 1277 | pubKey2 | -| owner3 | 2 | 1223 | 2446 | 1 | 1 | 1 | 1223 | pubKey4 | -| owner4 | 4 | 667 | 2668 | 3 | 1 | 1 | 1334 | pubKey5 | -+--------+------------------+----------------+--------------+-------------------+-----------------------------+------------------+---------------------------+-----------------------------+ +Token Deposit + +**1. Users Deposit Tokens:** + Users on Sovereign deposit tokens into the `esdt-safe` contract on the Sovereign Chain. Now tokens are either burned (for Token Type A) or kept in the safe (for Token Type B). + +**2. Block Creation and Finalization:** + The Sovereign chain creates the next block and finalizes it. Outgoing transactions (Crossing to MultiversX) are compiled into compressed operations data. + +**3. Header Hash Addition:** + The leader adds the hash of the outgoing transaction data to the header of the Sovereign Chain block. Validators reach consensus, signing the header, which the leader combines using BLS multi-signature. + +**4. Posting to Mainchain:** + The signed header and list of outgoing transactions are posted to the MultiversX chain. The transaction data on MultiversX contains the signed header of the Sovereign chain, the outgoing operations data hash, and the full arguments of the outgoing operations. + +**5. Contract Validation and Execution:** + The contract on MultiversX validates the signed header and BLS multi-signature. It verifies if + +```rust +hash(outgoing operations data) = header.outgoingOpsHash ``` -- `owner1` possesses one auction node, `pubKey1`, with a qualified topUp per node of 1223, surpassing the threshold of - 1216. -- `owner2` holds two auction nodes, `pubKey2` and `pubKey3`, with a topUp per node of 851. By leaving one - node (`pubKey3`) in the auction while selecting only one (`pubKey2`), the topUp per node is rebalanced to 1277 ( - 2555/2), exceeding the minimum threshold of 1216. -- `owner3` has one auction node, `pubKey4`, with a qualified topUp per node of 1223, surpassing the threshold of 1216. -- `owner4` possesses three auction nodes, `pubKey5`, `pubKey6`, and `pubKey7`, with a topUp per node of 667. By leaving - two nodes (`pubKey6`, `pubKey7`) in the auction while selecting only one (`pubKey5`), the topUp per node is rebalanced - to 1334 (2668/2), exceeding the minimum threshold of 1216. +The bridge contract executes the operations, performing `TransferESDT/MultiTransferESDTNFT` for *Token Type A* and `minting/creating NFTs` for *Token Type B*. -If the threshold were increased by one more step from `1216` to `1226`, only two nodes, `pubKey2` and `pubKey5`, would -qualify, which is insufficient to fill all slots. +**Finality and Consensus** -:::note +To ensure the integrity and synchronization between the Sovereign Chain and the mainchain, the following steps are taken: -If an owner has multiple nodes in the auction, but only a portion is selected for distribution in the waiting list, the -selection will be based on sorting the BLS keys. +- The Sovereign chain header includes an outgoing operations hash, which encapsulates the details of the operations to be executed on the mainchain. +- Once the outgoing operations are executed on the mainchain in subsequent blocks, the Sovereign chain remains synchronized by validating every MultiversX header hash. +- The finality gadget plays an important role by ensuring that a header with *Nonce X* on the Sovereign chain is only finalized after confirming that the mainchain has executed the outgoing operations from the previous header. This interconnected process maintains consistency and order of operations. -::: +**Incentives and Validation** -:::note +To encourage validators to ensure the smooth operation of bridge transactions and maintain network integrity, the following incentive and validation mechanisms are implemented: -The minimum required topUp per node, along with the real-time auction list, is accessible in the explorer at all -times. This allows owners to determine the optimal strategy for maximizing the number of selected auction nodes. +- **Incentive Structure Proposal:** Validators are incentivized to push outgoing transactions to the mainchain by distributing collected fees among them. Specifically, 10% of the fees are allocated to the leader, while the remaining 90% is distributed among the other validators. This incentivization ensures active participation and prompt processing of transactions. -::: +- **Validation Process:** Finality is crucial to maintaining the order and integrity of transactions. The system ensures that outgoing operations are executed in sequence without any gaps. This is achieved by generating `logEvents` on the mainchain, which are then pushed to the Sovereign Chain. These `logEvents` serve as proof of execution, ensuring validators can verify the completion and correctness of transactions, thereby maintaining a reliable and secure cross-chain operation. -Finally, validators are sorted based on the qualified topUp per node, and the selection is made considering available -slots. In instances where two or more validators share the same topUp (e.g., `pubKey1` and `pubKey4`), the selection -process is random but deterministic. The selection involves an XOR operation between the validators' public keys and the -current block's randomness. This mechanism prevents validators from "minting" their BLS keys to gain an advantage in -selection, as the randomness is only revealed at the time of selection. +#### Smart Contracts for Cross Chain Tx -``` - +--------+----------------+--------------------------+ - | Owner | Registered key | Qualified TopUp per node | - +--------+----------------+--------------------------+ - | owner4 | pubKey5 | 1334 | - | owner2 | pubKey2 | 1277 | - | owner1 | pubKey1 | 1223 | - +--------+----------------+--------------------------+ - | owner3 | pubKey4 | 1223 | - +--------+----------------+--------------------------+ -``` +Process: -Following the example above, there are two nodes with a qualified top-up of 1223 per node: +1. `SovereignMultiSigContract` is created and is the parent of the *ESDTSafe* contract, and only the `sovereignMultiSigContract` is allowed to transfer out funds from the `ESDTSafe` contract. -- `owner1` with 1 BLS key = `pubKey1` -- `owner3` with 1 BLS key = `pubKey4` +2. The OutGoingTXData BLS MultiSigned by the validators will be put inside a mainchain transaction and sent by the leader of the Sovereign Chain for the current block. This transaction contains a set of token operations for a set of addresses. +``` +txData = bridgeOps@LIST, gasLimit,functionToCall, Arguments>>@Nonce@BLSMultiSig +``` -Assuming the result of the XOR operation between their BLS keys and randomness is: +The `sovereignMultiSigContract` first verifies if the BLSMultiSig is valid and whether it is signed by 67% of the BLSPubkeys registered in the sovereignMultiSigContract. If yes, the contract calls with the ESDTSafe contract to transfer the set of tokens. The ESDTSafe contract will iterate on the given list and make a multiESDTNFTTransfer to the given addresses with the given tokens. -- `XOR1` = `pubKey1` XOR `randomness` = `[143...]` -- `XOR2` = `pubKey4` XOR `randomness` = `[131...]` +The contract emits a logEvent the same way as mainchain to sovereign ESDTSafe SC does, in order to keep track of the processed outgoing transactions. This logEvent will be pushed towards the sovereign shard, in order to notarize the finalization of processing of the OutGoingTxData. This will close the loop of processing and offer utmost security for all funds. -Since `XOR1` > `XOR2`, `pubKey1` will be selected, while `pubKey4` remains in the auction list. +``` +Identifier = bridgeOps +Address = scAddress +Topics = sender, hash(outGoingTxData) +Data = nonce - increasing from internal storage +``` +The created event is an attestation that the `outGoingTx` was executed on the mainchain, and using this attestation on the sovereign chain, the rewards can be distributed from the accumulated fees. +--- -## Introducing Node Limitations for Enhanced Decentralization +### Cross Chain Execution -In tandem with the upcoming staking v4 feature, we are implementing a crucial change aimed at fostering -decentralization, increasing the Nakamoto coefficient, and reinforcing the principles of a decentralized network. +# Introduction +When we take a look at the blockchain industry, we observe a segregated ecosystem lacking cohesion, interoperability and teamwork. The vision lead to the Blockchain Revolution, knows as “Web3” — a new era of the internet that is user-centered, emphasizing data ownership and decentralized trust. -### Dynamic Node Limitation +Sovereign Chains will dismantle the barriers between isolated blockchain networks by allowing smart contracts to seamlessly interact across different Sovereign Chains and the main MultiversX chain. +This cross-chain interoperability is crucial for fostering an environment where decentralized apps (dApps) can utilize functionalities or assets from across the ecosystem. -To achieve our decentralization goals, a cap on the number of nodes an owner can have will be introduced. This -limitation is dynamic, recalculated at each epoch, ensuring adaptability to the evolving network conditions. +## What is Cross-Chain Execution? +Cross-Chain execution is the ability of a smart contracts or a decentralized applications on one blockchain to invoke actions on another blockchain. This feature allows for smart contract execution or transfer of funds from one chain to another, enabling developers to build applications that are chain agnostic. -### Impact and Considerations +## Cross-Chain Execution within Sovereign Chains -This restriction primarily affects scenarios where users wish to stake new nodes. If an individual already possesses -more nodes than the specified threshold, their existing nodes will not be affected. However, they won't be able to stake -additional nodes beyond the limit; only unstaking will be allowed. +Since a Sovereign Chain is a separate blockchain with a different rule-set from the MultiversX blockchain, there has to be a way of communication between them. The interaction is being done by Smart Contracts, the Sovereign Bridge Service and Nodes. +> The MultiversX blockchain will be referred as the _MultiversX Mainchain_ in the further sections. -### Decentralization in Action +![To Sovereign](../../static/sovereign/to-sovereign.png) -This initiative encourages staking providers to critically evaluate their node count. For larger providers, having an -excessive number of nodes may lead to a decrease in overall APR. Achieving enough top-up to select numerous nodes from -the auction could become challenging. +When a transaction starts from the MultiversX Mainchain, either from a wallet or a smart contract, it goes through the `Mvx-ESDT-Safe` smart contract. The Observer nodes monitor the events that the deposit transaction emits and then the Sovereign Nodes notarize the state changes inside the Sovereign Chain. This notarization means the end of a Cross-Chain transfer. +![From Sovereign](../../static/sovereign/from-sovereign.png) -### Proactive Measures -Staking providers are encouraged to strategize accordingly. For instance, they might choose to unstake some nodes -themselves or explore collaboration with other small providers. Merging resources can enhance their chances of being -selected in the auction, especially for those with limited top-up. -No immediate action is required from users; however, thoughtful consideration of their node portfolio and strategic -decisions will play a pivotal role in navigating this shift toward a more decentralized network. +When the transaction is generated from the Sovereign Chain, the `Sov-ESDT-Safe` smart contract must be used. This smart contract will generate events and from that point, the sovereign nodes, at the end of the round, will read all the outgoing operations (plus all the unconfirmed operations), then these are signed by all the validators, then send to the bridge service, which will make the transaction on main chain +This is a high-level description of the whole process, the smart contracts that take place in it are far more detailed and have a lot of specific scenarios and behaviours. The current Sovereign Chain suite consists of four main contracts, here is the high-level description for some of the cross chain smart contracts: -# **FAQ** +## Mvx-ESDT-Safe & Sov-ESDT-Safe +The two contracts have the same role: to facilitate a cross-chain execution depending on what side the process starts. The reason for the prefix of `Sov` and `Mvx` is to show where the smart contract is deployed, `Sov` means that the contract is deployed on a Sovereign Chain and `Mvx` that is deployed on the MultiversX Mainchain. There will be an in-depth description of each smart contract in the upcoming modules ([`Mvx-ESDT-Safe`](mvx-esdt-safe.md) and [`Sov-ESDT-Safe`](sov-esdt-safe.md)). The description will consist of flows for the cross-chain interactions, important modules and endpoints. +Cross-chain transfers imply sending funds through the smart contracts mentioned above. There are two types of bridging mechanism available: *Lock&Send* and *Burn&Mint*. -## How much topUp should I have as a validator? +#### Lock and Send +Lock & Send: a custodial bridging mechanism in which the source-chain tokens are locked inside the bridge contract, while an equal amount is minted on the destination chain. The locked balance on the source chain backs the circulating supply on the sovereign chain 1-for-1 until the tokens are returned and unlocked. -The required topUp for validators depends on various factors, including the number of nodes in the auction and the soft -auction selection mechanism. The soft auction selection dynamically computes the minimum required topUp per node to -qualify for distribution from the auction to the waiting list. To maximize the chances of having auction nodes selected, -validators are encouraged to maintain a competitive topUp. Real-time auction list information and the minimum required -topUp per node is available in the explorer, allowing validators to strategize effectively. +#### Burn and Mint +Mint & Burn: a non-custodial bridging mechanism where the source-chain tokens are burned (permanently removed from supply) and an identical amount is minted on the destination chain, so the total supply moves between chains without any tokens being held in escrow. +## Fee-Market +Since every Sovereign Chain will have a customizable fee logic, it was paramount that this configuration had to be separated into a different contract. The rules set inside this contract are: +* fee per transferred token +* fee per gas unit +* users whitelist to bypass the fee -## What happens if there are fewer nodes in the auction than available slots? +This contract is also present in the MultiversX Mainchain and in any Sovereign Chain. -In this case, all nodes will be selected, regardless of their topUp. +## Header-Verifier +Any cross-chain transaction that happens inside a Sovereign Chain is called and *operation*. The main role of this contract is to verify operations. They have to be signed by the validators of the Sovereign Chain. If the operation is successfully verified it will be registered and then can be executed by the `Mvx-ESDT-Safe` smart contract. All the *BLS keys* of the validators will be stored inside this contract. The in-depth description of how those _operations_ are registered can be found in the [`Header-Verifier`](header-verifier.md) module. +:::note +The source for the smart contracts can be found at the official [MultiversX Sovereign Chain SCs repository](https://github.com/multiversx/mx-sovereign-sc). +::: -## One of my nodes was sent to auction during stakingV4 step 2. Will I lose rewards? +## Sovereign Bridge Service +This feature facilitates the execution of outgoing operations. This service is an application that receives Sovereign operations. After that, it will call the `execute_operation` endpoint from the `Mvx-ESDT-Safe` smart contract. The registration and execution of operations looks like this: -If one of your nodes is shuffled out into the auction list during step2, it will enter into competition with the other -existing nodes. If you have enough topUp, nothing changes, and no rewards will be lost. For owners contributing to the -ecosystem and maintaining a sufficient topUp, this change will not have any negative impact. However, if you have low -topUp or close to zero, your nodes might be unqualified and remain in the auction list. +- For N operations there is only one [register transaction](mvx-esdt-safe.md#registering-a-set-of-operations) inside the Header-Verifier smart contract. +- N transactions for the [execution](mvx-esdt-safe.md#executing-an-operation) of N operations inside the ESDT-Safe smart contract, one execution transaction per operation. +> There can be one or more services deployed in the network at the same time. -## Why downsize the waiting list? +--- -Short answer: _to keep the APR unchanged_. +### Custom Configurations -Before stakingV4, if a node was shuffled out and moved to the waiting list, it was guaranteed to be "idle" (not -participating in consensus) for 5 epochs. During this time, the node would not gain any rewards. +# Custom Configurations -During stakingV4 step2, no node from the waiting list is moved to active. If we were to keep the same configuration, a -shuffled out node from this step would have to wait 6 epochs until eligible (if selected from the auction) and -therefore decreasing the overall APR: +## Sovereign network customisations +The Sovereign Chain SDK is built with flexibility in mind, allowing you to tailor it to your specific needs. This page highlights various customizations you can apply to make your network unique. -## How does the dynamic node limitation work? +### config.toml -The dynamic node limitation is determined by the `NodeLimitPercentage`, which defines a percentage of -the `TotalNumOfEligibleNodes` from the current epoch. For example, if `NodeLimitPercentage` is set to 0.005 (0.5%) and -the `TotalNumOfEligibleNodes` for a given epoch is 1600 nodes, this means owners cannot exceed having more than 8 nodes. -The specific parameters, including the initial limit and `NodeLimitPercentage`, can be decided through a governance -vote. This ensures community involvement in determining the rules governing node ownership. +- `GeneralSettings.ChainID` - defines your unique chain identifier +- `EpochStartConfig.RoundsPerEpoch` - defines how many round are in each epoch -The actual limit is 50 nodes per provider with the calculation details from the 'systemSmartContractsConfig.toml' [here](https://github.com/multiversx/mx-chain-mainnet-config/blob/2ca2da07427c5a802202d1ed364a923f0e366f13/systemSmartContractsConfig.toml#L15) +### economics.toml ---- +- `GlobalSettings.GenesisTotalSupply` - total native ESDT supply at genesis +- `GlobalSettings.YearSettings` - adjust the inflation rate each year +- `FeeSettings` - adjust the fee settings as needed -### System Requirements +### ratings.toml -This page provides the system requirements for running a MultiversX node. +- `General` - adjust the rating parameters as needed +### systemSmartContractsConfig.toml -## **MultiversX Nodes explained** +- `ESDTSystemSCConfig.ESDTPrefix` - the prefix for all issued tokens +- `ESDTSystemSCConfig.BaseIssuingCost` - base cost for issuing a token +- `StakingSystemSCConfig.NodeLimitPercentage` [[docs](https://docs.multiversx.com/validators/staking-v4/#how-does-the-dynamic-node-limitation-work)] -Nodes are computers running the MultiversX software, so they contribute to the MultiversX network by relaying information and validating it. Each node needs to stake 2500 EGLD to become a **Validator** and is rewarded for its service. Nodes without a stake are called **Observers** - they are connected to the network and relay information, but they have no role in processing transactions and thus do not earn rewards. +### sovereignConfig.toml +- `GenesisConfig.NativeESDT` - Native ESDT identifier for the Sovereign Chain -## **Minimum System Requirements for running 1 MultiversX Node** +### prefs.toml -- 4 x dedicated/physical CPUs, either Intel or AMD, **with `SSE4.1` and `SSE4.2` flags** (use [lscpu](https://manpages.ubuntu.com/manpages/trusty/man1/lscpu.1.html) to verify) -- 8 GB RAM -- 200 GB SSD -- 100 Mbit/s always-on internet connection, at least 4 TB/month data plan -- Linux OS (Ubuntu 22.04/Debian 12 minimum) / MacOS +The `OverridableConfigTomlValues` will overwrite the parameters in the config files. Make sure that your new config parameters are not overwritten by this file. -:::caution -1. The CPUs must be `SSE4.1` and `SSE4.2` capable, otherwise the node won't be able to use the Wasmer 2 VM available through the VM 1.5 (and above) and the node will not be able to sync blocks from the network. -2. If the system chosen to host the node is a VPS, the host must have dedicated CPUs. Using shared CPUs can hinder your node's performance that will result in a decrease of node's rating and eventually the node might get jailed. -3. If you run multiple MultiversX Nodes on the same machine, the host running those nodes should have the specs at least equal to the minimum system requirements multiplied by the number of nodes running on that host. +:::note +These are just a few examples that you can adjust to make the Sovereign Chain unique. All the files you could adjust when creating a Sovereign Chain can be found in the [deployment guide](/sovereign/distributed-setup#step-4-edit-the-sovereign-configuration). ::: -:::tip -We are promoting using processors that support the `fma` or `fma3` instruction set since it is widely used by our VM. Displaying the available CPU instruction set can be done using the Linux shell command `sudo lshw` or `lscpu` +:::note +We will continue to add configurations for features such as token-less chains, gas-less chains, and other customizations at a later stage, following their implementation. ::: +--- -## **ARM Architecture Support** +### Disclaimer -Processors with ARM architecture are now supported, starting from mainnet epoch 1265 (related to [this](https://github.com/multiversx/mx-chain-mainnet-config/releases/tag/v1.6.7.0) release). -Synchronization from genesis to epoch 1265 is not possible on ARM processors. +# Disclaimer -:::caution -This update comes after extensive testing to ensure compatibility and functionality. However, we advise caution with its use in production environments. +:::note + This is a living document. More content will be added as it is accepted and discussed on Agora, or once it is implemented and available for production. As this documentation evolves, some sections may be updated or modified to reflect the latest developments and best practices. Community feedback and contributions are encouraged to help improve and refine this guide. Please note that the information provided is subject to change and may not always reflect the latest updates in the technology or procedures. All information and changes will be communicated and discussed with the community. ::: -Usage recommendations: -- Testnet/Devnet Validators: ARM processors can be utilized effectively as validator nodes on Testnet or Devnet. -- Mainnet Observers: ARM processors can be utilized effectively as observer nodes that can provide API support to non-critical services. -- Mainnet Validators: Despite successful testing, **it is NOT recommended to use ARM processors as mainnet validators** at this time due to potential performance and reliability concerns. +## Support and Scope -We will continue to monitor and improve support for ARM architecture, and we encourage the community to provide [feedback](https://t.me/MultiversXValidators) on their experiences. +While MultiversX provides the **chain sdk** core code and scripts necessary to start a sovereign chain, it is important to note that the infrastructure for all aspects of sovereign setup is not provided. MultiversX focus is on delivering the tools and resources needed to launch and maintain a sovereign chains. However, users may need to describe the need for additional support or resources or for more specific or advanced requirements by using Agora forum or other support channels that are available for them to use. +### What MultiversX Provides -### **Networking** +- **Core Code**: The fundamental codebase necessary to run a sovereign chain. +- **Scripts**: Automated scripts to assist with the initial setup and deployment processes. +- **Documentation**: Guides and manuals (like this one) to help you understand and implement the core functionalities. +- **Technical Support**: Technical support especially for potential issues or configurations that are caused by the above mentioned materials. -In order for a node to be reachable by other nodes several conditions have to be met: +### What will be supported in the limit of capacity -1. The port opened by the node on the interfaces must not be blocked by a firewall that denies inbound connections on it -2. If behind a NAT device, the node must be able to use the UPnP protocol to successfully negotiate a port that the NAT device will forward the incoming connections to (in other words, the router should be UPnP compatible) -3. There must be maximum 1 NAT device between the node and the Internet at large. Otherwise, the node will not be reachable by other nodes, even if it can connect itself to them. +- **Comprehensive Technical Support**: In-depth, personalized support for all potential issues or configurations. +- **Custom Solutions**: Any specific customizations or bespoke solutions required by individual users. +- **Operational Management**: Day-to-day management and operational support for running a sovereign chain. -To make sure the required ports are open, use the following command before continuing: +### Living Document -``` -sudo ufw allow 37373:38383/tcp -``` +As blockchain technology and the concept of sovereign chains continue to evolve, this documentation will also undergo changes. MultiversX may add new sections, update existing content, or deprecate information that becomes outdated. These changes aim to ensure that the documentation remains relevant and useful for all users. -:::note -The above ports need to be open in order to allow the node to communicate with other nodes via p2p. The configuration for the port range is set [here](https://github.com/multiversx/mx-chain-go/blob/master/cmd/node/config/p2p.toml#L7). -::: +MultiversX commits to transparency and collaboration in this process. All significant updates and changes will be communicated to the community through appropriate channels, and feedback will be actively sought to ensure the documentation meets the needs of its users. A weekly technical call will be scheduled where all issues, concerns and problems can be directly addressed to the development team. -:::caution -In case a firewall for outgoing traffic is used please make sure traffic to ports 10000 (p2p seeder) as well as 123 (NTP) is explicitly allowed. -::: +### Community Involvement ---- +Your feedback is important in helping MultiversX improve the documentation. We encourage community members to participate in discussions on Agora and other platforms, sharing their experiences, challenges, and suggestions. Together, we can create a more robust and comprehensive resource for everyone involved in the MultiversX ecosystem. -### The Staking Smart Contract +--- -This page will guide you through the the operations of the Staking System Smart Contract. +For any questions or further assistance, please refer to the [Engage the developer community section](https://multiversx.com/builders-hub) or the [official agora forum](https://agora.multiversx.com/). +--- -## **Staking** +### Distributed Setup -Nodes are _promoted_ to the role of **validator** when their operator sends a _staking transaction_ to the Staking smart contract. Through this transaction, the operator locks ("stakes") an amount of their own EGLD for each node that becomes a validator. A single staking transaction contains the EGLD and the information needed to stake for one or more nodes. Such a transaction contains the following: +# Distributed Setup -- The number of nodes that the operator is staking for -- The concatenated list of BLS keys belonging to the individual nodes -- The stake amount for each individual node, namely the number of nodes × 2500 EGLD -- A gas limit of 6 000 000 gas units × the number of nodes -- Optionally, a separate address may be specified, to which the rewards should be transferred, instead of the address from which the transaction itself originates. The reward address must be first decoded to bytes from the Bech32 representation, then re-encoded to base16 (hexadecimal). +## Create distributed Sovereign Chain configuration -For example, if an operator manages two individual nodes with the 96-byte-long BLS keys `45e7131ba....294812f004` and `ecf6fdbf5....70f1d251f7`, then the staking transaction would be built as follows: +This guide will help you deploy a public Sovereign Chain with real validators, enabling a truly decentralized setup. At its core, blockchain technology—and Sovereign Chains in particular—are designed to operate in a decentralized manner, powered by multiple independent validators. This ensures transparency, security, and resilience, as no single entity has control over the entire system. Unlike other guides we’ve provided, which focus on local setups, this solution emphasizes decentralization by involving multiple stakeholders in the validation process. By following the steps below, the owner can create the Sovereign Chain configuration for the network: -```rust -StakingTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l - Value: 5000 EGLD - GasLimit: 12000000 - Data: "stake" + - "@0002" + - "@45e7131ba....294812f004" + - "@67656e65736973" - "@ecf6fdbf5....70f1d251f7" + - "@67656e65736973" - "@optional_reward_address_HEX_ENCODED" -} -``` +### Step 1: Get the `mx-chain-sovereign-go` Repository -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +Before proceeding, ensure that a **SSH key** for GitHub is configured on your machine. -Because this transaction is a call to the Staking smart contract, it passes information via the `Data` field: +1. Clone the GitHub repository: + ```bash + git clone git@github.com:multiversx/mx-chain-sovereign-go.git + ``` -- `stake` is the name of the smart contract function to be called; -- `0002` is the number of nodes (unsigned integer, hex-encoded); -- 45e7131ba....294812f004 is the BLS key of the first node, represented as a 192-character-long hexadecimal string; -- `67656e65736973` is a reserved placeholder, required after each BLS key; -- `ecf6fdbf5....70f1d251f7` is the BLS key of the second node, represented as a 192-character-long hexadecimal string; -- `67656e65736973` is the aforementioned reserved placeholder, repeated; -- `optional_reward_address_HEX_ENCODED` is the address of the account which will receive the rewards for the staked nodes (decoded from its usual Bech32 representation into binary, then re-encoded to a hexadecimal string). +2. Navigate to project directory: + ```bash + cd mx-chain-sovereign-go + ``` +### Step 2: Seeder Build -## **Changing the reward address** +Build and run the seed node +```bash +cd cmd/seednode +go build +./seednode -rest-api-interface 127.0.0.1:9091 -log-level *:DEBUG -log-save +``` -Validator nodes produce rewards, which are then transferred to an account. By default, this account is the same one from which the staking transaction was submitted (see the section above). In the staking transaction, the node operator has the option to select a different reward address. +You should have an output similar to the one displayed below. The highlighted part is important and will be used later. -The reward address can also be changed at a later time, with a special transaction to the Staking smart contract. It is essential to know exactly how many nodes were specified in the original staking transaction, in order to properly compute the gas limit for changing the reward address. +| Seednode addresses: | +|---------------------------------------------------------------------------------------------| +| `/ip4/`127.0.0.1`/tcp/10000/p2p/16Uiu2HAmSY5NpuqC8UuFHunJensFbBc632zWnMPCYfM2wNLuvAvL` | +| `/ip4/`192.168.10.100`/tcp/10000/p2p/16Uiu2HAmSY5NpuqC8UuFHunJensFbBc632zWnMPCYfM2wNLuvAvL` | -- An amount of 0 EGLD -- A gas limit of 6 000 000 gas units × the nodes for which the reward address is changed (as specified by the original staking transaction). -- The new reward address. The reward address must be first decoded into binary from its normal Bech32 representation, then re-encoded to base16 (hexadecimal). +:::info +All the validator nodes will have to connect to this seed node. +::: -For example, changing the reward address for two nodes requires the following transaction: +### Step 3: Sovereign node build -```rust -ChangeRewardAddressTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l - Value: 0 EGLD - Data: "changeRewardAddress@reward_address_HEX_ENCODED" - GasLimit: 12000000 -} +Build the sovereign node +```bash +cd .. +cd cmd/sovereignnode/ +go build -v -ldflags="-X main.appVersion=v0.0.1" ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - - -## **Unstaking** - -A node operator may _demote_ their validator nodes back to **observer** status by sending an _unstaking transaction_ to the Staking smart contract, containing the following: - -- An amount of 0 EGLD -- The concatenated list of the BLS keys belonging to the individual nodes which are to be demoted from validator status -- A gas limit of 6 000 000 gas units × the number of nodes +:::info +Use your own custom version instead of `v0.0.1`. +::: -Note that the demotion does not happen instantaneously: the unstaked nodes will remain validators until the network releases them, a process which is subject to various influences. +### Step 4: Edit the sovereign configuration -Moreover, the amount of EGLD which was previously locked as stake will not return instantaneously. It will only be available after a predetermined number of rounds, after which the node operator may claim back the amount with a third special transaction (see the following section). +Node configs can be found in `cmd/node/config`. Below are the files and folders: +``` +gasSchedules folder +genesisContracts folder +genesis.json* +genesisSmartContracts.json +nodesSetup.json* +api.toml +config.toml +economics.toml +enableEpochs.toml +enableRounds.toml +external.toml +fullArchiveP2P.toml +p2p.toml +prefs.toml +ratings.toml +systemSmartContractsConfig.toml +``` -Continuing the example in the previous section, an unstaking transaction for the two nodes contains the following: +_Note: Files marked with * will be discussed later in the document._ -```rust -UnstakingTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l - Value: 0 EGLD - GasLimit: 12000000 - Data: "unStake" + - "@45e7131ba....294812f004" + - "@ecf6fdbf5....70f1d251f7" -} +Sovereign configs can be found in `cmd/sovereignnode/config` +``` +enableEpochs.toml +prefs.toml +sovereignConfig.toml ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +#### Minimum recommended changes -Note that: +1. Move the config files from `/node/config` into `/sovereignnode/config`, except _economics.toml_, _enableEpochs.toml_, _prefs.toml_. +2. Config changes: + 1. **config.toml** + 1. GeneralSettings.ChainID + 2. EpochStartConfig.RoundsPerEpoch + 2. **p2p.toml** + 1. KadDhtPeerDiscovery:InitialPeerList = `[/ip4/PUBLIC_IP/tcp/10000/p2p/16Uiu2HAmSY5NpuqC8UuFHunJensFbBc632zWnMPCYfM2wNLuvAvL]` + - PUBLIC_IP is the IP of the machine where seed node is running, the other part is seed node address + 3. **systemSmartContractsConfig.toml** + 1. ESDTSystemSCConfig.ESDTPrefix + 2. StakingSystemSCConfig.NodeLimitPercentage [[docs](https://docs.multiversx.com/validators/staking-v4/#how-does-the-dynamic-node-limitation-work)] + 4. **sovereignConfig.toml** + 1. GenesisConfig.NativeESDT +3. Other changes: + - Use the [custom configuration](/sovereign/custom-configurations) page to see more configs we recommend to be changed -- `45e7131ba....294812f004` is the BLS key of the first node, represented as a 192-character-long hexadecimal string; -- `ecf6fdbf5....70f1d251f7` is the BLS key of the second node, represented as a 192-character-long hexadecimal string; -- no reserved placeholder is needed, as opposed to the staking transaction (see above) +### Step 5: Genesis configuration +#### `genesis.json` -## **Unbonding** +This file should contain all the genesis addresses that will be funded and will be validators. Adjust as needed. -A node operator may reclaim the stake which was previously locked for their validator nodes using an _unbonding transaction_ to the Staking smart contract. Before unbonding, the node operator must have already sent an unstaking transaction for some of their validators, and a predetermined amount of rounds must have passed after the unstaking transaction was processed. +:::note +The sum of `supply` should be equal to `GenesisTotalSupply` from economics.toml +::: -The unbonding transaction is almost identical to the unstaking transaction, and contains the following: +Example with 2 validators: +``` +[ + { + "address": "erd1a2jq3rrqa0heta0fmlkrymky7yj247mrs54g6fyyx8dm45menkrsmu3dez", + "supply": "10000000000000000000000000", + "balance": "9997500000000000000000000", + "stakingvalue": "2500000000000000000000", + "delegation": { + "address": "", + "value": "0" + } + }, + { + "address": "erd1pn564xpwk4anq9z50th3ae99vplsf7d2p55cnugf00eu0gcq6gdqcg7ytx", + "supply": "10000000000000000000000000", + "balance": "9997500000000000000000000", + "stakingvalue": "2500000000000000000000", + "delegation": { + "address": "", + "value": "0" + } + } +] +``` -- An amount of 0 EGLD -- The concatenated list of the BLS keys belonging to the individual nodes for which the stake is claimed back -- A gas limit of 6 000 000 gas units × the number of nodes +#### `nodesSetup.json` -Following the example in the previous sections, an unbonding transaction for the two nodes contains the following information: +This file contains all the initial nodes. Adjust as needed. -```rust -UnbondingTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l - Value: 0 EGLD - GasLimit: 12000000 - Data: "unBond" + - "@45e7131ba....294812f004" + - "@ecf6fdbf5....70f1d251f7" +:::note +- `consensusGroupSize` should be equal to `minNodesPerShard` +- each node pair contains one genesis address associated with a validator public key +- `startTime` should be a timestamp from the future, the time when the network will start +- `roundDuration` is the duration in milliseconds per round +- `metaChainConsensusGroupSize` and `metaChainMinNodes` should always be 0 +::: + +Example: +``` +{ + "startTime": 1733138599, + "roundDuration": 6000, + "consensusGroupSize": 2, + "minNodesPerShard": 2, + "metaChainConsensusGroupSize": 0, + "metaChainMinNodes": 0, + "hysteresis": 0, + "adaptivity": false, + "initialNodes": [ + { + "pubkey": "6a1ee46baa8da9279f53addbfbc61a525604eb42d964bd3a25bf7f34097c3b3a31706728718ccdbe3d43386c37ec3011df6ceb4188e14025ab149bd568cafaba18a78b51e71c24046c5276a187a6c1d6da83e30590a6025875b8f6df8984ec05", + "address": "erd1a2jq3rrqa0heta0fmlkrymky7yj247mrs54g6fyyx8dm45menkrsmu3dez", + "initialRating": 0 + }, + { + "pubkey": "40f3857218333f0b2ba8592fc053cbaebec8e1335f95957c89f6c601ce0758372ba31c30700f10f25202d8856bb948055f9f0ef53dea57b62f013ee01c9dc0346a2b3543f2b4d423166ee1981b310f2549fb879d4cd89de6c392d902a823d116", + "address": "erd1pn564xpwk4anq9z50th3ae99vplsf7d2p55cnugf00eu0gcq6gdqcg7ytx", + "initialRating": 0 + } + ] } ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ - -Note that: +___ -- 45e7131ba....294812f004 is the BLS key of the first node, represented as a 192-character-long hexadecimal string; -- `ecf6fdbf5....70f1d251f7` is the BLS key of the second node, represented as a 192-character-long hexadecimal string; -- no reserved placeholder is needed, as opposed to the staking transaction (see above) +:::note +At this point, a `config` folder should be created that will contain all the .toml files and genesis configuration. This folder should be shared with the other validators so they will be able to join the network. +::: +## Join a Sovereign Chain as validator/observer -## **Unjailing** +### Sovereign validator setup -If a node operator notices that some of their validator nodes have been jailed due to low rating, they can restore the nodes back to being active validators by paying a small fine. This is done using an _unjailing transaction_, sent to the Staking smart contract, which contains the following: +Each validator should have: +- **walletKey.pem** - wallet that will be funded at genesis [[docs](/validators/key-management/wallet-keys)] +- **validatorKey.pem** (or **allValidatorsKey.pem** if multi key node) - validator key [[docs](/validators/key-management/validator-keys/#how-to-generate-a-new-key)] +- **config** folder - received from Sovereign Chain creator -- An amount of 2.5 EGLD (the fine) for each jailed node - this value must be correctly calculated; any other amount will result in a rejected unjail transaction -- The concatenated list of the BLS keys belonging to the individual nodes that are to be unjailed -- A gas limit of 6 000 000 gas units × the number of nodes +### Sovereign validator/observer node start -Continuing the example in the previous section, if the nodes `45e7131ba....294812f004` and `ecf6fdbf5....70f1d251f7` were placed in jail due to low rating, they can be unjailed with the following transaction: +The following commands will start the sovereign validator node with the configuration from **config** folder and with the **validatorKey** (or multi key from **allValidatorsKey**). +Adjust the flags as needed. You can find all the available flags in `/mx-chain-sovereign-go/cmd/sovereignnode/flags.go` -```rust -UnjailTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l - Value: 5 EGLD - GasLimit: 12000000 - Data: "unJail" + - "@45e7131ba....294812f004" + - "@ecf6fdbf5....70f1d251f7" -} +#### # single key +``` +./sovereignnode --validator-key-pem-file ./config/validatorKey.pem --profile-mode --log-save --log-level *:INFO --log-logger-name --log-correlation --use-health-service --rest-api-interface :8080 --working-directory ~/my_validator_node ``` -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ +#### # multi key +``` +./sovereignnode --all-validator-keys-pem-file ./config/allValidatorsKey.pem --profile-mode --log-save --log-level *:INFO --log-logger-name --log-correlation --use-health-service --rest-api-interface :8080 --working-directory ~/my_validator_node +``` -Note that: +#### # observer +``` +./sovereignnode --profile-mode --log-save --log-level *:INFO --log-logger-name --log-correlation --use-health-service --destination-shard-as-observer 0 --rest-api-interface :8080 --operation-mode db-lookup-extension --working-directory ~/my_observer_node +``` -- `45e7131ba....294812f004` is the BLS key of the first node, represented as a 192-character-long hexadecimal string; -- `ecf6fdbf5....70f1d251f7` is the BLS key of the second node, represented as a 192-character-long hexadecimal string; -- no reserved placeholder is needed, as opposed to the staking transaction (see above) +### Staking transaction +Before staking, a node is a mere observer. After staking, the node becomes a validator, which means that it will be eligible for consensus and will earn rewards. You can find the documentation how to make the staking transaction with mxpy [here](/validators/staking#staking-through-mxpy). -## **Claiming unused tokens from Staking** +## Deploy services -If a node operator has sent a staking transaction containing an amount of EGLD higher than the requirement for the nodes listed in the transaction, they can claim back the remainder of the sum with a simple _claim transaction_, containing: +You can find the documentation on how to deploy services [here](/sovereign/services). -- An amount of 0 EGLD -- A gas limit of 6 000 000 gas units +--- -An example of a claim transaction is: +### Dual Staking -```rust -ClaimTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l - Value: 0 EGLD - Data: "claim" - GasLimit: 6000000 -} -``` +# Dual Staking -_For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format)._ -After this transaction is processed, the Staking smart contract will produce a transaction _back_ to the sender account, but only if the sender account has previously staked for nodes, using a staking transaction. +:::note +This version of the documentation focuses solely on the essential steps required to set up and deploy a sovereign chain on either a local or remote computer. More content will be added as it is accepted and discussed on [Agora](https://agora.multiversx.com/), or once it is implemented and available for production. +::: --- -### Unjailing +### Ethereum L2 -This page will guide you through the process of unjailing a validator node. +# Ethereum L2 +:::note -## **Introduction** +This documentation is not complete. More content will be added once it is accepted and discussed on Agora or once it is implemented and available for production. -In the unfortunate situation of losing too much **rating score**, a validator will be **jailed**, which means that they will be taken out of the shards, they will not participate in consensus, and thus they will not earn any more rewards. Currently, the rating limit at which a node will be jailed is `10`. Read more on the [Ratings](/validators/rating) page. +::: -You can reinstate one of your jailed validators using an **unjailing transaction**. This transaction effectively represents the payment of a fine. After the transaction is successfully executed, your validator will return to the network in the next epoch, and treated as if the validator is brand new, with the rating reset to `50`. +--- -It is easy to submit an unjailing transaction. You have the option of unjailing your validators either through the online Wallet at [https://wallet.multiversx.com](https://wallet.multiversx.com/), or by using `mxpy` in the command-line. +### Governance -You'll see some BLS public keys in the examples on this page. Make sure you don't copy-paste them into your staking transaction. These BLS keys have been randomly generated and do not belong to any real node. +# Governance -Each unjailing process requires a transaction to be sent to the Staking Smart Contract. These transactions must contain all the required information, encoded properly, and must provide a high enough gas limit to allow for successful execution. These details are described in the following pages. -There are currently 2 supported methods of constructing and submitting these transactions to the Staking SmartContract: +:::note +This version of the documentation focuses solely on the essential steps required to set up and deploy a sovereign chain on either a local or remote computer. More content will be added as it is accepted and discussed on [Agora](https://agora.multiversx.com/), or once it is implemented and available for production. +::: -- Manually constructing the transaction, then submitting it to [wallet.multiversx.com](https://wallet.multiversx.com/); -- Automatically constructing the transaction and submitting it using the `mxpy` command-line tool. +--- -The following pages will describe both approaches in each specific case. +### Governance - Overview +This page provides an overview of the On-chain Governance module that will be available on the `v1.6.x` release. -## **Prerequisites** -In order to submit an unjailing transaction, you require the following: +## Table of contents -- A wallet with at least 2.5 EGLD (the cost of unjailing a _single validator_). If you want to unjail multiple validators at once, you need to multiply that minimum amount with the number of validators. For example, unjailing 3 validators at once will require 7.5 EGLD. Make sure you have enough in your wallet. -- The **BLS public keys** of the validators you want to unjail. You absolutely **do not require the secret key** of the validators. The BLS public keys of the validators are found in the `validatorKey.pem` files. Please read [Validator Keys](/validators/key-management/validator-keys) to find out how to extract the public key only. Remember that the BLS public key consists of exactly 192 hexadecimal characters (that is, `0` to `9` and `a` to `f` only). +| Name | Description | +|-----------------------------------------------------------------------------------|--------------------------------------------------------------------------| +| [Interacting with the on-chain governance SC](/governance/governance-interaction) | Interacting with the governance system smart contract. | -## **Unjailing through the Wallet** +## Overview -Open your wallet on [https://wallet.multiversx.com](https://wallet.multiversx.com/) and click the "Send" button. Carefully fill the form with the following information. Make sure it is clear to you what this information is, and where to adjust it with your own information. +The MultiversX network enables on-chain governance by issuing special types of transactions. This mechanism increases decentralization by allowing community members to directly propose and vote on changes, such as protocol upgrades or configuration adjustments. -In the "To" field, paste the address of the Staking SmartContract, which also handles unjailing: `erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l` +- **Anyone** can create a proposal by paying the proposal fee and specifying: + - a **commit hash** (unique identifier, usually a GitHub commit or spec reference) + - a **start epoch** and an **end epoch** (the voting window). +- **Any staked user** (direct or delegated) can vote during the active period. +- Voting power is proportional to stake: + `voting_power = staked_value` (linear voting). -For the "Amount" field, you first need to calculate the amount of EGLD required for unjailing. This is done by multiplying 2.5 EGLD by the _number of nodes_ you want to unjail. For example, if you want to unjail a single node, you need to enter `2.5`. For two nodes, it's `5` and for three nodes it is `7.5`. -Next, expand the "Fee limit" section of the form. You'll see the "Gas limit" field appear. The value that needs to be entered here also depends on the _number of nodes_ you want to unjail. To calculate the "Gas limit" value, multiply `6000000` (six million gas units) by the number of nodes. For example, if you want to unjail a single node, enter `6000000`. For two nodes, enter `12000000`, for three nodes enter `18000000` and so on. Observe how the "Fee limit" field automatically calculates the cost of this transaction. +## Implementation details +### Proposals -## **The "Data" field** +In order for a proposal to be submitted, the following requirements need to be met: +- For a period of at least 15 days, the proposal needs to be published on Agora for public debate and analysis; +- Each proposal requires paying a `ProposalFee` (currently **500 EGLD**); +- Each proposal costs around 51 million of gas units to be submitted. +- The starting epoch of the proposal's voting period needs to be set inside an interval of 30 epochs from the epoch in which the proposal was submitted; -Next, you must fill the "Data" field. The text you will write here will be read by the Staking SmartContract to find out what nodes you want to unjail. Remember, you can unjail any number of nodes at once. +After a proposal is created, the following rules will apply: +- If the proposal passes, the fee is refunded to the issuer. +- If the proposal fails, a penalty fee (called `LostProposalFee`) is deducted from the initial proposal fee, and the remaining amount is returned to the proposer. +- If the proposal is vetoed, the fee is slashed (transferred to the **Community Governance Pool**) or reduced by the configured `LostProposalFee`. +- Proposals cannot be duplicated: the same commit hash cannot be submitted twice. +- Proposals can be **cancelled** by the issuer **before the voting period starts**. (introduced in v1.10.0 Barnard release) +- Lost proposal fees may either be accumulated in the governance contract (retrievable by the contract owner) or redirected to a **Community Governance Pool**, depending on configuration. -When writing in the "Data" field, you must adhere to a strict format, described in the following subsections. +### Voting +- The voting starts from the provided starting epoch and lasts at most *10 days*. +- There are four vote types: **Yes**, **No**, **Abstain**, and **Veto**. +- Voting consumes gas (approx. 6 million units). +- Voting power is derived from staked and delegated EGLD. +- Delegation and liquid staking contracts can forward user votes. +- Voting power is **linear** with stake: the more EGLD staked or delegated, the higher the voting power. +- The contract tracks both **used voting power/stake** (applied in a proposal) and **total available voting power/stake** for each address. -### **Unjailing a single node** +### Quorum and thresholds +A proposal can pass only if all conditions are met: -If you want to unjail a single node, the format of the "Data" field is simple: +- **Quorum**: at least `MinQuorum%` of the total voting power must participate. (currently at least **20%** of total voting power) +- **Acceptance threshold**: YES / (YES + NO + VETO) ≥ **66.67%** (`MinPassThreshold`) +- **Veto threshold**: if VETO votes exceed **33%** of total participating voting power, the proposal is rejected and the proposal fee is slashed. -```python -unJail@ -``` +### Closing and cleanup +**Proposal closing permissions:** +- **Before voting starts:** Only the proposal issuer can close the proposal. +- **After voting ends:** Any user can close the proposal. -Do not copy-paste the above format as-is into the "Data". Instead, you must **replace** `` with the **BLS public key** of the node you want to stake for. You can find the BLS public key in the `validatorKey.pem` file of that node. Read the page [Validator Keys](/validators/key-management/validator-keys) to help you interpret the contents of the file and locate the BLS public key. +**Effects of closing:** +1. Finalizes the proposal outcome (passed/failed/vetoed). +2. Unlocks any locked funds. +3. Updates the proposal status in the governance system. -Make sure you do not remove the `@` character. They are used to separate the pieces of information in the "Data" field. Only replace``. The angle-brackets `<` and `>` must be removed as well. +**Cleanup options:** +- The `clearEndedProposals` function can be called to clean up expired but unclosed proposals. +- This helps maintain system efficiency by removing inactive proposals. -As an example, the "Data" field of an unjailing transaction for a single node looks like this: +### Governance configuration +All thresholds and fees are defined in `GovernanceConfigV2`: +- `MinQuorum` +- `MinPassThreshold` +- `MinVetoThreshold` +- `ProposalFee` +- `LostProposalFee` +- `LastProposalNonce` + +These values are set by the system and can be updated by contract owner calls. + + +### Example +Let's suppose we have the following addresses that cast the following votes: +- `alice`: staked value **2000 EGLD** that vote **Yes** +- `bob`: staked value **3000 EGLD** that vote **Yes** +- `charlie`: staked value **4000 EGLD** that vote **Yes** +- `delta`: staked value **1500 EGLD** that vote **No** + +The totals are: +- Quorum = `(2000+3000+4000+1500) = 10,500 EGLD` +- **Yes** = `9000 EGLD` +- **No** = `1500 EGLD` +- **Abstain** = `0` +- **Veto** = `0` -unJail@b617d8bc442bda59510f77e04a1680e8b2d3293c8c4083d94260db96a4d732deaaf9855fa0cef2273f5a67b4f442c725efc06a5d366b9f15a66da9eb8208a09c9ab4066b6b3d38c3cf1ea7fab6489a90713b3b56d87de68c6558c80d7533bf27 +Assume: +- total staked in the system = `20,000 EGLD` +- `MinQuorum` = 20% +- `MinPassThreshold` = 50% +- `MinVetoThreshold` = 33% -![img](https://gblobscdn.gitbook.com/assets%2F-LhHlNldCYgbyqXEGXUS%2F-MA1YB7F53LJCTlFj8qn%2F-MA1_N1up06vncGVTyfp%2Funjailing-single-node.png?alt=media&token=fe0ca638-6433-4c07-b7ac-ef3fcf199835) +Evaluation: +- Quorum is satisfied: `10,500 > 4000` +- Yes > No: `9000 > 1500` +- Yes is ≥ 50% of votes: `9000 / 10,500 = 85.7%` +- Veto is below 33%: `0 < 3465` +To sum it all, the proposal **passes**. -### **Unjailing multiple nodes at once** +--- -Unjailing more than one node at a time isn't very different from unjailing a single node. You only need to append the BLS public keys of the remaining nodes, separated by `@`, to the "Data" field constructed for a single node. Please read the previous section "Unjailing a single node" before continuing, if you haven't already. Also, _do not forget_ to update the "Amount" and "Gas Limit" fields according to the number of nodes you are unjailing. +### Governance interaction -For a _single_ node, as explained in the previous subsection, the format is this one: +### Introduction -```python -unJail@ -``` +The interaction with the governance system smart contract is done through correctly formatted transactions to submit actions and through the usage of the vm-query REST API calls for reading the proposal(s) status. -For _two_ nodes, the format is as follows: -```python -unJail@@ -``` +### Creating a proposal -And for _three_ nodes, the format is: +The proposal creation transaction has the following parameters: -```python -unJail@@@ +```rust +GovernanceProposalTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla + Value: 500 EGLD + GasLimit: 51000000 + Data: "proposal" + + "@" + + "@" + + "@" +} ``` -Notice how each extra node adds the part `@` to the previous format. You need to replace with `` with the actual **BLS public keys** of your nodes, which you can find inside their individual `validatorKey.pem` files. Make sure you **do not write the BLS secret keys**! Read the page [Validator Keys](/validators/key-management/validator-keys) to see how to interpret the `validatorKey.pem` files. - -For example, the "Data" field for an unjailing transaction for two nodes looks like this: +The value parameter represents the mandatory proposal submission deposit of 500 EGLD. -unJail@b617d8bc442bda59510f77e04a1680e8b2d3293c8c4083d94260db96a4d732deaaf9855fa0cef2273f5a67b4f442c725efc06a5d366b9f15a66da9eb8208a09c9ab4066b6b3d38c3cf1ea7fab6489a90713b3b56d87de68c6558c80d7533bf27@f921a0f76ed70e8a806c6f9119f87b12700f96f732e6070b675e0aec10cb0723803202a4c40194847c38195db07b1001f6d50c81a82b949e438cd6dd945c2eb99b32c79465aefb9144c8668af67e2d01f71b81842d9b94e4543a12616cb5897d +The proposal identifier is a hex string containing exactly 40 characters. This is the git commit hash on which the proposal is made. -![img](https://gblobscdn.gitbook.com/assets%2F-LhHlNldCYgbyqXEGXUS%2F-MA1mbsWLwDtxs1LX3w-%2F-MA1nGcSQTZqmnGoxtRA%2Funjailing-two-nodes.png?alt=media&token=991f11c8-fe7c-46f5-93fb-566ab0590279) +The starting & ending epochs should be an even-length hex string containing the starting epoch and the ending epoch. During this time frame, lasting at most 10 days, the votes can be cast. +>**Note:** When providing the starting epoch, it should be taken into consideration that the governance contract enforcing a configured maximum gap of 30 epochs between the current epoch and the proposal’s starting epoch. -### **The general format** +After issuing the proposal, there is a log event generated having the `proposal` identifier that will contain the following encoded topics: -You can write the text for the "Data" field for _any_ number of nodes. The general format looks like this: +- `nonce` as encoded integer which uniquely identifies the proposals +- `identifier` is the provided 40 hex characters long identifier +- `start epoch` as encoded integer for the starting epoch +- `end epoch` as encoded integer for the ending epoch -```python -unJail@@@…@ -``` +### Voting a proposal using the direct staked or delegation-system amount -## **Unjailing through mxpy** +Any wallet that has staked EGLD (either direct staked or through the delegation sub-system) can cast a vote for a proposal. -Submitting the unjailing transaction using `mxpy` avoids having to write the "Data" field manually. Instead, the transaction is constructed automatically by `mxpy` and submitted to the network directly, in a single command. +```rust +GovernanceVoteTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla + Value: 0 EGLD + GasLimit: 6000000 + Data: "vote" + + "@" + + "@" +} +``` -Make sure `mxpy` is installed and has the latest version before continuing. If `mxpy` is not installed, please follow [these instructions](/sdk-and-tools/mxpy/installing-mxpy). +The value parameter for the voting transaction must be set to 0, since the function is non-payable. +The `nonce` is the hex encoded value of the proposal's unique nonce and the `vote_type` can be one of the following values: +- for **Yes**: `796573`; +- for **No**: `6e6f`; +- for **Abstain**: `6162737461696e`; +- for **Veto**: `7665746f`. -## **Your Wallet PEM file** +The vote value for the account that will vote a proposal is the sum of all staked values along with the sum of all delegated values in the delegation sub-system. -To send transactions on your behalf _without_ using the online MultiversX Wallet, `mxpy` must be able to sign for you. For this reason, you have to generate a PEM file using your Wallet mnemonic. +After issuing the vote, a log event is generated containing the `proposal` identifier and the following encoded topics: -Please follow the guide [Deriving the Wallet PEM file](/sdk-and-tools/mxpy/mxpy-cli). Make sure you know exactly where the PEM file was generated, because you'll need to reference its path in the `mxpy` commands. +- `nonce` as encoded integer which uniquely identifies the proposals +- `vote_type` as encoded string representing the vote +- `total_stake` total staked EGLD for the sender address +- `total_voting_power` total available voting power for the sender address -After the PEM file was generated, you can issue transactions from `mxpy`directly. +Example response: +```json +{ + "returnData": [ + "WQ==", (nonce: hex = 59 -> decimal = 89) + "eWVz", (vote_type: hex = "79657" -> "yes") + "BxRA1dqcrTxyQw==", (total_stake: hex = "071440d5da9cad3c7243" -> 33430172142116630458947 -> ~33430 EGLD) + "BxRA1dqcrTxyQw==", (total_voting_power: hex = "071440d5da9cad3c7243" -> 33430172142116630458947 -> ~33430 EGLD) + ] +} +``` -## **The unjailing transaction** -The following commands assume that the PEM file for your Wallet was saved with the name `walletKey.pem` in the current folder, where you are issuing the commands from. +### Voting a proposal through smart contracts (delegation voting) -The command to submit an unjailing transaction with `mxpy` is this: +Whenever we deal with a smart contract that delegated through the delegation sub-system or owns staked nodes it is the out of scope of the metachain's governance contract to track each address that sent EGLD how much is really staked (if any EGLD is staked). -```sh -mxpy --verbose validator unjail --pem=walletKey.pem --value="" --nodes-public-keys=",,...," --proxy=https://gateway.multiversx.com -``` +That is why we offered an endpoint to the governance smart contact that can be called **only by a shard smart contract** and the governance contract will record the address provided, the vote type and vote value. -Notice that we are using the `walletKey.pem` file. Moreover, before executing this command, you need to replace the following: +This is very useful whenever implementing liquid-staking-like smart contracts. The liquid-staking contract knows the balance for each user, so it will delegate the call to the governance contract providing the corresponding voting power. -- Replace `` with the amount of EGLD required for unjailing your validators. You need to calculate this value with respect to the number of nodes you are unjailing. See the [beginning of the Unjailing through the Wallet](/validators/staking/unjailing#unjailing-through-the-wallet) section for info on how to do it. -- Replace all the `` with the actual **BLS public keys** of your nodes, which you can find inside their individual `validatorKey.pem` files. Make sure you **do not write the BLS secret keys**! Read the page [Validator Keys](/validators/key-management/validator-keys) to see how to interpret the `validatorKey.pem` files. +:::important +The maximum value for the staked value & the voting power for the liquid-staking contract is known by the governance contract, so it can not counterfeit the voting. If, due to a bug the liquid-staking contract allows, for example, for a user to vote with a larger quota, the liquid-staking contract will deplete its maximum allowance making other voting transactions (on his behalf) to fail. +::: -Here's an example for an unjailing command for one validator: +The user that delegated through a liquid-staking-like contract in order to vote on a proposal, sends a transaction to the liquid-staking-like contract. The smart contract then creates the GovernanceVoteThroughDelegationTransaction based on the user’s transaction inputs, forwarding the vote and the computed voting power to the governance contract. -```sh -mxpy --verbose validator unjail --pem=walletKey.pem --value="2500000000000000000000" --nodes-public-keys="b617d8bc442bda59510f77e04a1680e8b2d3293c8c4083d94260db96a4d732deaaf9855fa0cef2273f5a67b4f442c725efc06a5d366b9f15a66da9eb8208a09c9ab4066b6b3d38c3cf1ea7fab6489a90713b3b56d87de68c6558c80d7533bf27" --proxy=https://gateway.multiversx.com +```rust +GovernanceVoteThroughDelegationTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla + Value: 0 EGLD + GasLimit: 51000000 + Data: "delegateVote" + + "@" + + "@" + + "@" + + "@" +} ``` -:::note important -You must take **denomination** into account when specifying the `value` parameter in **mxpy**. -::: +The value parameter for the voting transaction must be set to 0, since the function is non-payable. -For two validators, the command becomes this one: +The `nonce` is the hex encoded value of the proposal's unique nonce and the `vote_type` can be one of the following values: +- for **Yes**: `796573`; +- for **No**: `6e6f`; +- for **Abstain**: `6162737461696e`; +- for **Veto**: `7665746f`. -```sh -mxpy --verbose validator unjail --pem=walletKey.pem --value="5000000000000000000000" --nodes-public-keys="b617d8bc442bda59510f77e04a1680e8b2d3293c8c4083d94260db96a4d732deaaf9855fa0cef2273f5a67b4f442c725efc06a5d366b9f15a66da9eb8208a09c9ab4066b6b3d38c3cf1ea7fab6489a90713b3b56d87de68c6558c80d7533bf27,f921a0f76ed70e8a806c6f9119f87b12700f96f732e6070b675e0aec10cb0723803202a4c40194847c38195db07b1001f6d50c81a82b949e438cd6dd945c2eb99b32c79465aefb9144c8668af67e2d01f71b81842d9b94e4543a12616cb5897d" --proxy=https://gateway.multiversx.com -``` +The `account address handled by the smart contract` is the address handled by the smart contract that will delegate the vote towards the governance smart contract. This address will be recorded for casting the vote. -Notice that the two BLS public keys are separated by a comma, with no extra space between them. +The `vote_balance` is the amount of stake the address has in the smart contract. The governance contract will "believe" that this is the right amount as it impossible to verify the information. The balance will diminish the total voting power the smart contract has. ---- +After issuing the vote, a log event is generated containing the `proposal` identifier and the following encoded topics: -### Useful Links & Tools +- `nonce` as encoded integer which uniquely identifies the proposals +- `vote_type` as encoded string representing the vote +- `voter` account address handled by the smart contract +- `total_stake` total staked EGLD for the sender address +- `total_voting_power` total available voting power for the sender address -This page offer useful links and resources that can be used by validators and nodes operators. +Example response: +```json +{ + "returnData": [ + "WQ==", (nonce: hex = 59 -> decimal = 89) + "eWVz", (vote_type: hex = "79657" -> "yes") + "ATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeE=", (voter: hex = "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1" -> erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th) + "BxRA1dqcrTxyQw==", (total_stake: hex = "071440d5da9cad3c7243" -> 33430172142116630458947 -> ~33430 EGLD) + "BxRA1dqcrTxyQw==", (total_voting_power: hex = "071440d5da9cad3c7243" -> 33430172142116630458947 -> ~33430 EGLD) + ] +} +``` -## Resources -Official resources: +### Closing a proposal -- Blockchain Explorer: [https://explorer.multiversx.com/](https://explorer.multiversx.com/) -- Wallet: [https://wallet.multiversx.com](https://wallet.multiversx.com/) -- GitHub: https://github.com/multiversx -- Dockerhub: https://hub.docker.com/u/multiversx -- Telegram Validators Chat: https://t.me/MultiversXValidators -- Telegram Bot for Staking & Validators: https://t.me/ElrondNetwork_Bot +A proposal can be closed by the issuer before the voting period starts but with an early closing fee equal to `LostProposalFee`. A proposal can also be closed by anyone in an epoch that is strictly higher than the end epoch value provided when the proposal was opened. +```rust +CloseProposalTransaction { + Sender: + Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla + Value: 0 EGLD + GasLimit: 51000000 + Data: "closeProposal" + + "@" +} +``` -Community resources: +#### Rules for closing +- The proposal can be closed by the issuer before the voting starts or by anyone after voting ended. +- If the proposal **passes** → the full proposal fee is refunded. +- If the proposal **fails** or is **vetoed** → the refund is reduced by the `LostProposalFee`. +- Once a proposal is closed, it cannot be reopened. +- Closing also finalizes the vote tally (the proposal is marked as `Passed` or not, based on the results). -- Zabbix monitoring guide: https://thepalmtree.network/zabbix-elrond-guide -- Zabbix plugin: https://github.com/arcsoft-ro/zabbix-elrond-plugin -- Configure Validator API & NetData https access: https://gist.github.com/hiddentao/e6283952b9fffe3f6b42dfeec87c684e ---- +### Querying the status of a proposal -### Validator Keys +The status of any proposal can be queried at any time through `vm-values/query` REST API endpoints provided by the gateway/API. -Each validator required a private key to be used for signing blocks. This key is called the **Validator Key**. -The Validator Key is also used to sign the consensus messages that the validator sends to the other validators. +```bash +https://.multiversx.com/vm-values/query +``` +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla", + "funcName": "viewProposal", + "args": [""] +} +``` -## Validator key format +- The argument `nonce` is the proposal nonce in hex format. +- The response will contain the following json definition where all fields are base64-encoded: -A file containing the keys for your node. +```json +{ + "returnData": [ + "", (amount locked by proposer) + "", (unique identifier of the proposal) + "", (proposal number) + "", (address of the proposer) + "", (epoch when voting starts) + "", (epoch when voting ends) + "", (current quorum stake: sum of stake that participated) + "", (total stake voting YES) + "", (total stake voting NO) + "", (total stake vetoing the proposal) + "", (total stake abstaining) + "", + "" + ] +} +``` -The **Validator Keys** are located in the `validatorKey.pem` file, which is generated in the node setup process. By default, each node stores its own .pem file in the `$HOME/elrond-nodes/node-0` folder. A copy also archived as a zip file in the `$HOME/VALIDATOR_KEYS` folder, for restore purposes. +Example response: +```json +{ + "returnData": [ + "Gxrk1uLvUAAA", (proposal locked amount: 500 EGLD denominated = 500 * 10^18) + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABg==", (commit hash: 0x0000...006) + "AQ==", (nonce: 1) + "aj88GtqHy9ibm5ePPQlG4aqLhpgqsQWygoTppckLa4M=", (proposer address) + "bQ==", (starting epoch: 109) + "bg==", (ending epoch: 110) + "ntGU2xmyOMAAAA==", (quorum: 750000 EGLD denominated) + "", (yes votes: 0) + "", (no votes: 0) + "ntGU2xmyOMAAAA==", (veto votes: 750000 EGLD denominated) + "", (abstain votes: 0) + "dHJ1ZQ==", (proposal closed: true) + "ZmFsc2U=" (proposal passed: false) + ] +} +``` -Below you can find their anatomy and how to extract the information from them -Example: ------BEGIN PRIVATE KEY for _45e7131ba37e05c5de3f8862b4d8294812f004a5b660abb793e89b65816dbff2b02f54c25f139359c9c98be0fa657d0bf1ae4115dcf6fdbf5f3a470f1d251f769610b48fe34eeab59e82ac1cc0336d1d9109a14b768b97ccb4db4c2431629688_----- +### Querying an address voting status (direct staking or system delegation) -**YmRiNmViOGYzMmQ3OWY0YjE4ODJjMzE1ODA4YjQyZmZjODhiZDQxNzMwNmE5MTRiZjQ4OTAyNjM0MTcyNjMzMw==** +The voting status of a certain address can be queried at any time by using the `vm-values/query` REST API endpoints provided by the gateway/API. ------END PRIVATE KEY for _45e7131ba37e05c5de3f8862b4d8294812f004a5b660abb793e89b65816dbff2b02f54c25f139359c9c98be0fa657d0bf1ae4115dcf6fdbf5f3a470f1d251f769610b48fe34eeab59e82ac1cc0336d1d9109a14b768b97ccb4db4c2431629688_----- +```bash +https://gateway.multiversx.com/vm-values/query +``` -In plain English: +With the following payload: +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla", + "funcName": "viewDelegatedVoteInfo", + "args": ["", "
"] +} ``` ------The private key for this``*PUBLIC KEY*``starts below----- -**PRIVATE KEY** ------The private key for this``*PUBLIC KEY*``was listed above----- -``` - -The string in _italics_ from the example is the _PUBLIC KEY_. The string in **bold** from the example is the **PRIVATE KEY**. -More clearly: +- `proposal_nonce` → the proposal identifier (nonce, hex-encoded). +- `address` → the bech32 address of the account to check. -`*PUBLIC KEY:* `_45e7131ba37e05c5de3f8862b4d8294812f004a5b660abb793e89b65816dbff2b02f54c25f139359c9c98be0fa657d0bf1ae4115dcf6fdbf5f3a470f1d251f769610b48fe34eeab59e82ac1cc0336d1d9109a14b768b97ccb4db4c2431629688_ +> **Note:** The older function `viewUserVoteHistory` (which returned lists of proposal nonces) is now considered legacy. +> Use `viewDelegatedVoteInfo` for detailed voting power and stake information. -`**PRIVATE KEY:**`**YmRiNmViOGYzMmQ3OWY0YjE4ODJjMzE1ODA4YjQyZmZjODhiZDQxNzMwNmE5MTRiZjQ4OTAyNjM0MTcyNjMzMw==** +The response will contain the following json definition where all fields are base64-encoded: -Always save and protect **private keys**, they are like your username + password + 2FA at your bank, all combined. +```json +{ + "returnData": [ + "", (voting power used by this address on the proposal) + "", (stake associated with the used power) + "", (total available voting power for the address) + "" (total stake considered in governance for the address) + ] +} +``` -_Public keys_ are like your phone number - no harm in others knowing it, it actually is needed for some scenarios. Still, only share it on a need to basis, like you would do with your own phone number. +Example for an address that voted: +```json +{ + "returnData": [ + "Cg==", (used power: 100) + "ZAA=", (used stake: 100) + "A+g=", (total power: 1000) + "Gg4M=", (total stake: 1000) + ] +} +``` +In this example, the queried address voted on this proposal with **100 stake**, which translated into **100 voting power**. The proposal overall had **1000 total stake** and **1000 total voting power** recorded. -## How to generate a new key +Example for an address that did not vote: -The easiest way to generate a new validator key is by using the `keygenerator` tool that resides near the node. +```json +{ + "returnData": [ + "AA==", (used power: 0) + "AA==", (used stake: 0) + "A+g=", (total power: 1000) + "A+g=", (total stake: 1000) + ] +} +``` -- [https://github.com/multiversx/mx-chain-go/tree/master/cmd/keygenerator](https://github.com/multiversx/mx-chain-go/tree/master/cmd/keygenerator) +### Querying the voting power of an address -How to generate a new validator key if golang is already set on the host: +The voting power of a certain address can be queried at any time by using the `vm-values/query` REST API endpoints provided by the gateway/API. -```shell -$ git clone https://github.com/multiversx/mx-chain-go.git -$ cd mx-chain-go/cmd/keygenerator -$ go build -$ ./keygenerator --key-type validator +```bash +https://gateway.multiversx.com/vm-values/query ``` -Alternatively, if you've already installed a node on the host, you can issue the following command: +With the following payload: -```shell -$ cd ~/elrond-utils/ -$ ./keygenerator --key-type validator +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla", + "funcName": "viewVotingPower", + "args": [""] +} ``` ---- +Here's an example of the response: -### Validators - Overview +```json +{ + "returnData": [ + "Q8M8GTdWSAAA" (1250000000000000000000 -> 1250 EGLD) + ] +} +``` -This page provides an overview of the Validator Nodes and the associated Tools. +### Querying the configuration of the Governance contract +The configuration of the Governance System Smart Contract can be queried at any time by using the `vm-values/query` REST API endpoints provided by the gateway/API. -## Table of contents +```bash +https://gateway.multiversx.com/vm-values/query +``` +With the following payload: -### Install and maintain a node +```json +{ + "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla", + "funcName": "viewConfig", + "args": [] +} +``` -| Name | Description | -| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [System requirements](/validators/system-requirements) | System requirements for running a MultiversX node. | -| [Install a Mainnet/Testnet/Devnet Node](/validators/nodes-scripts/config-scripts) | Instructions about how to get a Testnet or a Devnet node up and running. | +The response will contain the following json definition where all fields are base64-encoded: +```json +{ + "returnData": [ + "", (the amount of EGLD required to issue a new proposal) + "", (the amount of EGLD that the issuer loses if the proposal fails) + "", (the minimum percentage of total voting power required for a proposal to be considered valid) + "", (the minimum percentage of veto votes needed to reject a proposal and slash its fee) + "", (the minimum percentage of YES votes required for a proposal to pass) + "" (the nonce of the last created proposal) + ] +} +``` -### Keys Management +Here's how a response might look like: +```json +{ + "returnData": [ + "NTAwMDAwMDAwMDAwMDAwMDAwMDAw", ("500000000000000000000" -> 500 EGLD) + "MTAwMDAwMDAwMDAwMDAwMDAwMDA=", ("10000000000000000000" -> 10 EGLD) + "MC4yMDAw", ("0.2000" -> 20%) + "MC4zMzAw", ("0.3300" -> 33%) + "MC42NjY3", ("0.6667" -> 66.67%) + "MA==", ("0" -> 0) + ], +} +``` -| Name | Description | -|-----------------------------------------------------------------|--------------------------------------------------------------| -| [Validator keys](/validators/key-management/validator-keys) | Learn about a validator key. | -| [Wallet keys](/validators/key-management/wallet-keys) | Learn about a wallet key. | -| [Protecting your keys](/validators/key-management/protect-keys) | Learn how you can secure your keys. | -| [Multikey nodes](/validators/key-management/multikey-nodes) | Learn how to manage a set of keys by using a group of nodes. | +--- +### Header Verifier -### Staking +# Header-Verifier -| Name | Description | -| ----------------------------------------------------------------------------------- | ------------------------------------------------------------- | -| [Staking, unstaking and unjailing](/validators/staking/staking-unstaking-unjailing) | Learn about how to stake, unstake or unjail a Node. | -| [How to Stake a Node](/validators/staking) | Learn how to stake a node via a step-by-step tutorial. | -| [How to unJail a Node](/validators/staking/unjailing) | Learn how to unJail a node. | -| [The Staking Smart Contract](/validators/staking/staking-smart-contract) | How to interact with the Smart Contract that manages Staking. | +As mentioned in the [Introduction](cross-chain-execution.md) the `Header-Verifier` smart contract is responsible to verify signatures, store the *BLS Keys* of the validators and register incoming *Operations*. +## Registering a set of *Operations* +```rust + #[endpoint(registerBridgeOps)] + fn register_bridge_operations( + &self, + signature: BlsSignature, + bridge_operations_hash: ManagedBuffer, + operations_hashes: MultiValueEncoded, + ) +``` -### Delegation Manager +Any *Operation* before being executed has to be registered in this smart contract. The reason behind this is that the hash will be verified and it will be locked until the operation is executed by the `Mvx-ESDT-Safe` contract. -| Name | Description | -| ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -| [Delegation Manager](/validators/delegation-manager) | Learn how to create a new Staking Provider, how to configure it and how to interact with it. | -| [How to convert an existing Validator into a Staking Pool](/validators/staking/convert-existing-validator-into-staking-provider) | Learn how to create a new Staking Provider, starting from an existing Validator. | -| [Merge an existing Validator into a Staking Pool](/validators/staking/merge-validator-delegation-sc) | Learn how to merge a validator into a Staking Provider. | +The registering endpoint operates as follows: +1. Verifies that `bridge_operations_hash` is not found in the `hash_of_hashes_history` storage mapper, otherwise it will return an error. +2. Verifies that the hash of all `operations_hashes` matches the `bridge_operations_hash`, otherwise, the endpoint will return an error. +3. All `operations_hashes` are stored in the smart contract's storage with the status `OperationsHashStatus::NotLocked`. +4. The `bridge_operations_hash` is added to the `hash_of_hashes_history` storage mapper. +## Locking *Operations* +```rust + #[endpoint(lockOperationHash)] + fn lock_operation_hash(&self, hash_of_hashes: ManagedBuffer, operation_hash: ManagedBuffer) +``` -### Useful +The Header-Verifier has a system in place for locking *Operation* hashes. Locking those registered hashes prevents any unwanted behaviour when executing or removing an *Operation* hash. Remember that the execution of *Operations* can only be done by the `Mvx-ESDT-Safe` smart contract. This endpoint when called will follow this flow: -| Name | Description | -| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| [Node operation modes](/validators/node-operation-modes) | Learn about the nodes' operation modes and how to use them for setting up node for various use-cases. | -| [Node Configuration](/validators/node-configuration) | Learn about the nodes' configuration files: how to use them, how to override values, and so on. | -| [Rating](/validators/rating) | Learn about the nodes' rating, how it increases and decreases and how it impacts the earnings. | -| [Node Upgrade](/validators/node-upgrades) | Learn about the periodical or emergency upgrades that nodes' owner have to do. | -| [Node Redundancy / Back-up](/validators/redundancy) | How to set up a back-up node for your main machine. | -| [Import DB](/validators/import-db) | Learn how to start the node in import-db mode, that allows reprocessing of old data, without syncing the network. | -| [Node CLI](/validators/node-cli) | How to use the Command Line Interface of the Node. | -| [Node Databases](/validators/node-databases) | How to use the nodes' databases and how to copy them from one node to another. | -| [Useful link & tools](/validators/useful-links) | Useful links about the explorer, wallet and some guides. | -| [FAQ](/validators/faq) | Frequently Asked Questions about nodes. | +1. Check if the caller is the `Mvx-ESDT-Safe` smart contract. +2. Check if the *Operation* is registered. +3. If the hash is not locked set the status in the storage as locked or else return panic. +:::note +The hash can be in two different states: `OperationHashStatus::NotLocked` or `OperationHashStatus::Locked` +::: -## Overview +## Removing *Operations* +```rust + #[endpoint(removeExecutedHash)] + fn remove_executed_hash(&self, hash_of_hashes: &ManagedBuffer, operation_hash: &ManagedBuffer) +``` -The MultiversX network is made up of nodes and their interconnectivity - balanced by virtue of its design, secured through its size and fast, _very_ fast, because efficiency is what motivated its development. Every time a node joins the network, it adds more security and efficiency. The network, in turn, rewards the nodes for their contribution, generating a virtuous cycle. +After registering and executing an *Operation* the status of the hash associated to it must be removed from the Header-Verifier's internal storage. This endpoint will be called by the `Mvx-ESDT-Safe` smart contract after the execution of the *Operation* is successful. The steps are pretty clear: -We will call a _node_ any running instance of the software application developed by the MultiversX team, [publicly available as open source](https://github.com/multiversx/mx-chain-go). Anyone can run a node on their machine - great care was taken to make the node consume as little computing resources as possible. Mid-level recent hardware can effortlessly run multiple individual nodes at the same time, earning more rewards for the same physical machine. +1. Check if the caller is the `Mvx-ESDT-Safe` smart contract. +2. Remove the status of the hash from storage. -We will call a _node operator_ any person or entity who manages one or more nodes. These pages are for them. +:::note +The source code for this contract can be found [here](https://github.com/multiversx/mx-sovereign-sc/blob/main/header-verifier/src/lib.rs). +::: +--- -## Background +### Interoperability -MultiversX is a decentralized blockchain network. This means that its nodes collaborate to create sequential **blocks** with strict regularity - blocks which contain the results of operations that were requested by the users of the network. Such operations may be simple transfers of tokens, or may be calls to SmartContracts. Either way, _all_ operations take the form of **transactions**. +# Interoperability and Modular Design -Any user who submits a transaction to the network must pay a fee, in EGLD tokens. These fees are what produces **rewards** for the nodes. +Sovereign Chains are designed to serve as an interoperability layer between different ecosystems. Ecosystem partners and builders have the opportunity to create a set of components that can be activated or deactivated based on the specific needs of a particular Sovereign Chain. But how will sovereign chains achieve that? -Note that not all nodes earn rewards from these fees. Only **validator nodes** qualify, because they are the nodes which are allowed to take part in [consensus](/learn/consensus), to produce and validate blocks and to earn rewards. +## Virtual Machine Structure -Because of the influence they have in the network, validator nodes are required to have a **stake**, which is a significant amount of EGLD locked as collateral for the good behavior of the validator. Currently, the stake amount is set to 2500 EGLD. Nodes without a stake are called **observer nodes** - they don't participate in consensus and do not earn rewards, but they support the network in different ways. +SpaceVM has two parts, developed in GO and Rust: -If the validator consistently misbehaves or performs malicious actions, it will be fined accordingly and lose EGLD, an action known as _stake slashing,_ and by also having its validator status removed. This form of punishment is reserved for serious offences. +**1. One part handles**: +- OPCODES +- State management +- Transfers + +**2. The other part functions as the executor.** + +Starting from the address model, multiple VMs are enabled to run within the system. There is a clear implementation of how these VMs can call each other independently, facilitated by the blockchainHook. TODO: add information about the implementation. -Validator nodes each have an individual **rating score**, which expresses their overall reliability and responsiveness. Rating will increase for well-behaved nodes: every time a validator takes part in a successful consensus, its rating is increased. +## Modular Design and Multi-VM Capabilities -The opposite is also true: a validator which is either offline during consensus or fails to contribute to the block being produced will be considered unreliable. And a consistently unreliable validator will see its rating drop. +The basic idea is to utilize the modular design and multi-VM capabilities to create direct connections to existing technologies from the market. -**Consensus selection probability** is strongly influenced by a validators rating. The consensus process _favors validators with high rating_ and will avoid selecting validators with low rating. +### Ethereum L2 -This implies that a node with high rating produces far more rewards than a node with low rating, so it is essential that operators maintain their validators online, up-to-date and responsive. +### Bitcoin L2 -Moreover, if the rating of a validator becomes too low, it will be **jailed**. A jailed validator will not be selected for consensus - thus earning no rewards. To restore the validator, it must be **unjailed**, which requires a fine to be paid, currently set to 2.5 EGLD. +### Solana L2 --- -### Wallet Keys +### Key Components -This page describes the wallet keys, that are used for staking and managing nodes. +# Key Components +:::note -## Wallet keys description +This documentation is not complete. More content will be added once it is accepted and discussed on Agora or once it is implemented and available for production. -As a Validator you use the Wallet Keys to access the address from which you send the staking transaction. Your EGLD holdings leave this address and are deposited into a staking smart contract. Rewards are sent to this address. You can change it later on by using a `changeRewards` transaction. +::: -This wallet is the only one that can be used to send an un-stake transaction, meaning to recover your 2500 EGLD from the staking smart contract. +Sovereign Chains mark significant progress for MultiversX in the field of appchains. While the concept is straightforward to discuss in general terms, developers, builders, or other entities looking to launch a sovereign chain need to consider several critical components. For clarity, we will divide these components into four major categories and describe the importance and role of each one. -A Wallet Key can be created via multiple ways that are described on the [Wallets section](/wallet/overview/). +![img](/sovereign/sovereign-1.png) -The wallets use the bip44 standard with the mention that because MultiversX uses Ed25519 only hardened paths are used. Our coin_type is 508, making the path for the first address:m/44'/508'/0'/0'/0’ ---- +### **1. Sovereign Cross-Chain Smart Contracts** -## Integrators -### Accounts Management +In order to have a native *connection* with the MultiversX MainChain three smart contracts have to be deployed: -Managing Wallets and Addresses +#### **```esdt-safe``` Contract** -This page summarizes the recommended approach for managing accounts in an application that integrates with the Network. +The ```esdt-safe``` smart contract, which operates on both the mainchain and the sovereign chain, is the primary contract responsible for token transfers. -:::tip -If integrating a **system** with the **Network** involves transfers between different users (accounts) - a good example for this case is the integration between an **exchange system** and the **Network** - the recommended approach is to have **a MultiversX Account (Address) for each user of the system**. -::: +Mainchain Endpoints: -Accounts creation can be achieved through different approaches: +- ```deposit```: This endpoint performs a ```MultiESDTNFTTransfer``` to the contract, locking the tokens on the mainchain and initiating their transfer to the sovereign chain (tokens on the mainchain remain locked in the smart contract, while those on the sovereign chain are burned). +- ```executeBridgeOps```: Called by the bridge service wallet, this endpoint transfers tokens on the mainchain. It first verifies that these operations have been previously stored in the multisig contract before executing. +- ```registerToken```: Issues a token and maps it to a sovereign token ID, granting the mainchain contract the rights to mint/burn and requiring a payment of 0.05 EGLD for the token issuance on the mainchain. -- using the [MultiversX Web Wallet](https://wallet.multiversx.com/) -- programmatically, using the [sdk-js - JavaScript SDK](/sdk-and-tools/sdk-js) -- programmatically, using the [mxpy - Python SDK](/sdk-and-tools/sdk-py/) -- programmatically, using the [sdk-go - Golang SDK](/sdk-and-tools/sdk-go) -- programmatically, using the [sdk-java - Java SDK](/sdk-and-tools/mxjava) -- using the [lightweight CLI](https://www.npmjs.com/package/@multiversx/sdk-wallet-cli) -- using our [lightweight HTTP utility](https://github.com/multiversx/mx-sdk-js-wallet-http) -- programmatically, using the [TrustWalletCore extension](https://github.com/trustwallet/wallet-core/tree/master/src/MultiversX) for MultiversX +Sovereign Chain Endpoint: ---- +- ```deposit```: Burns the token being sent to the mainchain. `registerToken` must be called beforehand to enable bridging, and the contract must have the *burn right* set. The default `ESDTRoleBurnForAll` role is assigned to any issued token. If this role is deactivated, a transaction is required to assign the *burn role*, facilitated by the script `setLocalBurnRoleSovereign`. -### Advanced Observer Settings +#### **Fee Market Contract** -This page describes some of the settings an integrator might want to apply on the observers in order to better make use of the nodes. +This contract allows you to specify the fee for deposit transactions. The fee structure can vary and includes: +- No fee; +- Fixed fee with a specific token; +- Fee with any token (currently, this requires a check through an xexchange contract, which is not yet operational); -For the settings from the `config.toml` file that are needed to be altered, we recommend using the `OverridableConfigTomlValues` section found in the `prefs.toml` file. More info can be found [here](/validators/node-configuration#overriding-configtoml-values) +The fee is applicable either per token in the `MultiESDTNFTTransfer` action or based on the gas used in the deposit transaction. +#### **MultiSig Verifier** -## Web antiflood settings +The `multisig` smart contract, operates solely on the mainchain, and is responsible for managing and validating the multi-signature operations for bridge transactions. It registers the public keys of the validators from the sovereign chain. It also validates the multi-signature received on this endpoint which is called by the bridge service: +- `registerBridgeOps` - here, the multisig is validated, the hashes of the bridge operations are verified, and the hashes are recorded (these hashes are checked during the execution of executeBridgeOps). -Each node, either observer or validator, will be configured automatically with the web antiflood turned on and set to relatively low limits. This was chosen as a protection measure during the initial setup of the node but can be easily changed through the configuration files. +### **2. Sovereign Notifier** -The original section found in the `config.toml` file looks like this: +:::note +The Sovereign Notifier exports outport blocks to Sovereign Chain. It needs to be a server outport host driver. +::: -```toml -[WebServerAntiflood] - WebServerAntifloodEnabled = true - # SimultaneousRequests represents the number of concurrent requests accepted by the web server - # this is a global throttler that acts on all http connections regardless of the originating source - SimultaneousRequests = 100 - # SameSourceRequests defines how many requests are allowed from the same source in the specified - # time frame (SameSourceResetIntervalInSec) - SameSourceRequests = 10000 - # SameSourceResetIntervalInSec time frame between counter reset, in seconds - SameSourceResetIntervalInSec = 1 - # TrieOperationsDeadlineMilliseconds represents the maximum duration that an API call targeting a trie operation - # can take. - TrieOperationsDeadlineMilliseconds = 10000 - # GetAddressesBulkMaxSize represents the maximum number of addresses to be fetched in a bulk per API request. 0 means unlimited - GetAddressesBulkMaxSize = 100 - # VmQueryDelayAfterStartInSec represents the number of seconds to wait when starting node before accepting vm query requests - VmQueryDelayAfterStartInSec = 120 - # EndpointsThrottlers represents a map for maximum simultaneous go routines for an endpoint - EndpointsThrottlers = [{ Endpoint = "/transaction/:hash", MaxNumGoRoutines = 10 }, - { Endpoint = "/transaction/send", MaxNumGoRoutines = 2 }, - { Endpoint = "/transaction/simulate", MaxNumGoRoutines = 1 }, - { Endpoint = "/transaction/send-multiple", MaxNumGoRoutines = 2 }] -``` +There is only one Sovereign Notifier per Sovereign Chain, started specifically on the shard where the Sovereign's `esdt-safe` contract is deployed. It is subscribed to the address of the `esdt-safe` contract (automatically set during genesis by scripts). It listens for events with the IDs `deposit` and `execute`. When it detects an event, it sends the event to the sovereign chain as an incoming operation. The nodes then recognize the operation, process it, and the funds are credited to the account. -The general off/on switch is done through the `WebServerAntifloodEnabled` config value. As an integrator you might want to turn off the web antiflooder, **in case the node is not directly reachable over the Internet** as a mean to provide your gateway instance (proxy) to use as many node resources it wants. -This is the easiest way to tell the node to not bother with that type of antiflooding. However, this does not imply the integrator to remove all the web antiflooding protection, especially before his/her owned gateway instance (proxy). +### **3. Sovereign Cross-Chain TX Service** -The following list explains what the parameters do: -- `SimultaneousRequests` tells the node how many REST API calls can simultaneous serve; -- `SameSourceRequests` tells how many REST API requests originating from the same source the node can serve per time unit. The time unit is specified in the `SameSourceResetIntervalInSec` parameter and is expressed in seconds; -- `TrieOperationsDeadlineMilliseconds` tells the node when it should time out the REST API requests that involves data trie traversing. This should be increased in case the node is trying to fetch data from large smart contracts or user accounts; -- `GetAddressesBulkMaxSize` tells the node what is the maximum number of addresses that can be fetched in a bulk-get operation; -- `VmQueryDelayAfterStartInSec` is a parameter that should is taken into account only when the node is started. The vm-query requests are not executed in this initial startup phase; -- `EndpointsThrottlers` is a list that defines some REST API endpoints and their maximum simultaneous requests that the node can handle. +The Sovereign Cross-Chain TX service, connected via gRPC, manages the interaction between the mainchain and the sovereign chain for token transfers and other operations. +For the setup described in this documentation, there is a single cross-chain tx service in operation (recommended it is for every validator from Sovereign Chain to have a running cross-chain tx service for each instance). It has a wallet attached from the mainchain and contains addresses from the cross-chain contracts (`multisig` for registration and `esdt-safe` for execution), along with various security certificates. -## VM settings for query operations +#### Functionality: +- **Outgoing Operations**: The service listens for outgoing operations created from logs of deposit events in esdt-safe on the sovereign chain. +- **Validation and Signing**: These operations are validated, signed, hashed, and processed by all validators on the sovereign chain. +- **Register and Execute**: Once the service receives a validated operation, it sends the operation to the registerBridgeOps endpoint in the multisig contract and then to the executeBridgeOps endpoint in the esdt-safe contract for execution. -The node can handle one or more query-services able to simultaneous execute vm queries. +### **4. Sovereign Chain** -The original section found in the `config.toml` file looks like this: +--- -```toml -[VirtualMachine.Querying] - NumConcurrentVMs = 1 -``` +### Keystore files -The `NumConcurrentVMs` value specifies the number of VMs instances allocated for vm-query operations. The higher this value is, the more concurrent vm-query operation the node can handle. +The MultiversX keystore is a JSON file that holds a mnemonic (seed phrase), encrypted with a password (as chosen by the user). Thus, the keystore provides users with a reliable and convenient method for managing their hot wallets and protecting their assets. -:::important -The increased number of VMs allocated for the vm-query engine will put pressure on the RAM allocation for the node process at around 500-600 MB / extra instance. Disk & CPU utilization will also get higher in a proportional manner. -::: + +## MultiversX Keystore -## UserAccounts cache parameters +### Loading a keystore into the web wallet -As found during heavy testing, spending more RAM on caches might help in vm-query execution. The reason behind this statement is that, usually, smart contract execution need smart contract data to be read from storage and then processed. -The data fetch mechanism costs in terms of CPU cycles and disk utilization, so they affect the execution time of the vm-query. -If the node has more than the [minimum required RAM](/validators/system-requirements) then the following section from the `config.toml` file can be altered: +Choose connecting with keystore: -```toml -[AccountsTrieStorage] - [AccountsTrieStorage.Cache] - Name = "AccountsTrieStorage" - Capacity = 500000 - Type = "SizeLRU" - SizeInBytes = 314572800 #300MB -``` +![img](/developers/keystore/connect_with_keystore.png) -- `Capacity` can be increased to 10000000 or to a larger value; -- `SizeInBytes` can be increased to `1073741824` (1GB) or even pushed to `4294967296` (4GB). +Add your keystore and introduce the password: ---- +![img](/developers/keystore/load_json_introd_pass.png) -### Creating Transactions +Upon loading the keystore, the user is presented with a choice of addresses, as derived from the mnemonic held by the keystore (if the keystore has `"kind":"mnemonic"`). -This page describes how to create, sign and broadcast transactions to the MultiversX Network. +![img](/developers/keystore/bech32.png) -## **Transaction structure** +### **How does a MultiversX Keystore look like?** -As described in section [Signing Transactions](/developers/signing-transactions), a ready-to-broadcast transaction is structured as follows: +Here is an example: ```json { - "nonce": 42, - "value": "100000000000000000", - "receiver": "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", - "sender": "erd1ylzm22ngxl2tspgvwm0yth2myr6dx9avtx83zpxpu7rhxw4qltzs9tmjm9", - "gasPrice": 1000000000, - "gasLimit": 70000, - "data": "Zm9vZCBmb3IgY2F0cw==", - "chainID": "1", - "version": 1, - "signature": "5845301de8ca3a8576166fb3b7dd25124868ce54b07eec7022ae3ffd8d4629540dbb7d0ceed9455a259695e2665db614828728d0f9b0fb1cc46c07dd669d2f0e" + "version": 4, + "id": "5b448dbc-5c72-4d83-8038-938b1f8dff19", + "kind": "mnemonic", + "crypto": { + "ciphertext": "6d70fbdceba874f56f15af4b1d060223799288cfc5d276d9ebb91732f5a38c3c59f83896fa7e7eb6a04c05475a6fe4d154de9b9441864c507abd0eb6987dac521b64c0c82783a3cd1e09270cd6cb5ae493f9af694b891253ac1f1ffded68b5ef39c972307e3c33a8354337540908acc795d4df72298dda1ca28ac920983e6a39a01e2bc988bd0b21f864c6de8b5356d11e4b77bc6f75ef", + "cipherparams": { + "iv": "2da5620906634972d9a623bc249d63d4" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "aa9e0ba6b188703071a582c10e5331f2756279feb0e2768f1ba0fd38ec77f035", + "n": 4096, + "r": 8, + "p": 1 + }, + "mac": "5bc1b20b6d903b8ef3273eedf028112d65eaf85a5ef4215917c1209ec2df715a" + } } ``` +At first, you will see an unappealing JSON file, which appears to contain magic parameters used for numerous complex cryptographic operations with unclear and vague purpose. But if you dig a little deeper you will see that it contains: -## **SDK and tools support for creating and signing transactions** +- **kind** - Can be `secretKey` or `mnemonic` and represents the input to be encrypted using the `cipher`; +- **ciphertext** - Your MultiversX mnemonic or secret key encrypted using the `cipher` algorithm below; +- **cipher** - The name of a symmetric AES algorithm; +- **cipherparams** - The parameters required for the `cipher` algorithm above; +- **kdf** - A key derivation function used to let you encrypt your keystore file with a password; +- **kdfparams** - The parameters required for the `kdf` algorithm above; +- **mac** - A code used to verify your password. -There are SDKs or tools with support for interacting with the MultiversX blockchain, so one can use one of the following SDKs to perform -transactions creation and signing: +Keystore files created with the first major version of the web wallet (available prior February 14th, 2023) hold the encrypted secret key, instead of the encrypted mnemonic (as the new keystore files do). Though the older files are still compatible with the new web wallet - compatibility is achieved through the aforementioned "kind" field. -- [sdk-js - JavaScript SDK](/sdk-and-tools/sdk-js) -- [sdk-py - Python SDK](/sdk-and-tools/sdk-py) -- [sdk-go - Golang SDK](/sdk-and-tools/sdk-go) -- [sdk-java - Java SDK](/sdk-and-tools/mxjava) -- [lightweight JS CLI](https://www.npmjs.com/package/@multiversx/sdk-wallet-cli) -- [lightweight HTTP utility](https://github.com/multiversx/mx-sdk-js-wallet-http) +When **kind** is set (or not set at all) to `secretKey`, the `ciphertext` field will contain the encrypted secret key, as it did before. However, when **kind** is set to `mnemonic`, the `ciphertext` field will contain the encrypted mnemonic instead. +Auxiliary reference: [ERC-2335: BLS12-381 Keystore](https://eips.ethereum.org/EIPS/eip-2335). -## **General network parameters** +--- -General network parameters, such as the **chain ID**, **the minimum gas price**, **the minimum gas limit** and the **oldest acceptable transaction version** are available at the API endpoint [Get Network Configuration](/sdk-and-tools/rest-api/network#get-network-configuration). +### Ledger -```json -{ - "config": { - "erd_chain_id": "1", - "erd_gas_per_data_byte": 1500, - "erd_min_gas_limit": 50000, - "erd_min_gas_price": 1000000000, - "erd_min_transaction_version": 1, - ... - } -} -``` +You can safely store your EGLD by installing the MultiversX EGLD app on your Ledger Nano S or Ledger Nano X device. We recommend using a hardware wallet for managing large amounts of EGLD. -## **Nonce management** +## Before you begin -Each transaction broadcasted to the Network must have the **nonce** field set consistently with the **account nonce**. In the Network, transactions of a given sender address are processed in order, with respect to the transaction nonce. +1. [Set up](https://support.ledger.com/hc/en-us/articles/360000613793) your Ledger device +2. [Download](https://www.ledger.com/ledger-live/download) the Ledger Live application +3. [Upgrade](https://support.ledger.com/hc/en-us/articles/360002731113) your device with the latest firmware‌ -The account nonce can be fetched from the API: [Get Address Nonce](/sdk-and-tools/rest-api/addresses#get-address-nonce). -**The nonce must be a strictly increasing number, scoped to a given sender.** The sections below describe common issues and possible solutions when managing the nonce for transaction construction. +## **Install the MultiversX EGLD app on your Ledger** +Teach your Ledger device to work with EGLD by installing the MultiversX app on it.‌ -### **Issue: competing transactions** +- Open the Ledger Live application +- Click on the “Manager” section in the app +- Connect and unlock your Ledger device +- If prompted, allow the Manager on your device by pressing the two buttons +- In the App catalog search for “MultiversX” +- Click the “Install” button next to the MultiversX app +- Your Ledger device will display “Processing” as the app installs -Broadcasted transactions that reach the _mempool_ having the same sender address and the same nonce are _competing transactions_, and only one of them will be processed (the one providing a higher gas price or, if they have the same gas price, the one that arrived the second - but keep in mind that arrival time is less manageable). +![img](/wallet/ledger/ledger_live.png) -:::tip -Avoid competing transactions by maintaining a strictly increasing nonce sequence when broadcasting transactions of the same sender address. -::: +Awesome, you did everything perfectly! -Although an explicit _transaction cancellation trigger_ is not yet available in the Network, cancellation of a transaction T1 with nonce 42 could be _possible_ if one broadcasts a second transaction T2 with same nonce 42, with higher gas price (and without a value to transfer) **immediately** (e.g. 1 second) after broadcasting T1. +## Log into your MultiversX wallet using the Ledger device -### **Issue: nonce gaps** +- Make sure your Ledger device is connected to your computer +- Log into your Ledger device +- Navigate to the MultiversX app & open it by pushing both buttons on your device +- In your web browser navigate to https://wallet.multiversx.com +- Select "Ledger" from the Login options -If broadcasted transactions have their nonces higher than the current account nonce of the sender, this is considered a _nonce gap_, and the transactions will remain in the mempool unprocessed, until new transactions from the same sender arrive _to resolve the nonce gap -_ or until the transactions are swept from the mempool (sweeping takes place regularly). +![img](/wallet/ledger/connect_ledger.png) -:::tip -**Avoid nonce gaps** by regularly fetching the current account nonce, in order to populate the nonce field correctly before broadcasting the transactions. This technique is also known as **periodically recalling the nonce**. -::: +- A pop-up will notify you that your Ledger wants to connect. Allow it by clicking "Connect" +![img](/wallet/ledger/paired.png) -### **Issue: too many transactions from the same account** +After connecting the Ledger you will be prompted to choose the address you want to connect with. Select the address then click on "Access Wallet". -Starting with the [Sirius Mainnet Upgrade](https://github.com/multiversx/mx-specs/blob/main/releases/protocol/release-specs-v1.6.0-Sirius.md), the transaction pool allows a maximum of **100** transactions from the same sender to exist, at a given moment. +![img](/wallet/ledger/ledger_address.png) -For example, if an address broadcasts `120` transactions with nonces from `1` to `120`, then the transactions with nonces `1 - 100` will be accepted for processing, while the remaining `20` transactions will be dropped. +Your MultiversX Wallet will now open. -The solution is to use chunks holding a **maximum of `100` transactions** and a place a generous **delay between sending the chunks**. Let's suppose an account has the nonce `1000` and it wants to send `120` transactions. It should send the first chunk, that is, the transactions with nonces `1000 - 1099`, wait until all of them are processed (the account nonce increments on each processed transaction), then send the second chunk, the transactions with nonces `1100 - 1019`. +## Overview of your EGLD balance -### **Issue: fetching a stale account nonce** +After logging into your wallet, your EGLD balances are immediately visible and displayed in easy to follow boxes. -You should take care when fetching the current account nonce from the API immediately after broadcasting transactions. +![img](/wallet/web-wallet/wallet_balance_overview.png) -Example: +- **Available:** Freely transferable EGLD balance +- **Stake Delegation:** Amount of EGLD delegated towards a Staking Services provider +- **Legacy Delegation:** Amount of EGLD delegated towards MultiversX Community Delegation +- **Staked Nodes:** Amount of EGLD locked for your staked nodes -1. Time 12:00:01 - the sender's nonce is recalled, and its value is 42 -2. Time 12:00:02 - the sender broadcasts the transaction T1 with nonce 42 -3. Time 12:00:03 - the sender's nonce is recalled again, in order to broadcast a new transaction. **The nonce is still 42. It is stale, not yet incremented on the Network (since T1 is still pending or being processed at this very moment).** -4. Time 12:00:04 - the sender broadcasts T2 with nonce 42, which will compete with T1, as they have the same nonce. -:::tip -Avoid fetching stale account nonces by **periodically recalling the nonce.** +## Send a transaction using the Ledger -Avoid recalling the nonce in between **rapidly sequenced transactions from the same sender** . For rapidly sequenced transactions, you have to programmatically manage, keep track of the account nonce using a **local mirror (copy) of the account nonce** and increment it appropriately. -::: +In the MultiversX web wallet: +- Click "Send" +- Input the destination address & amount +- Click Send +- In the next screen click "Confirm on Ledger"! -### **Issue: sending large batches of transactions cause nonce gaps** +![img](/wallet/ledger/confirm.png) -Whenever sending a large batch of transactions, even if the node/gateway returned transaction hashes for each transaction in the batch and no error, there is no strict guarantee that those transactions will end up being executed. -The reason is that the node will not immediately send each transaction or transaction batch but rather accumulate them in packages to be efficiently send through the p2p network. -At this moment, the node might decide to drop one or more packet because it detected a possible p2p flooding condition. This can happen independent of the transaction sender, the number of transactions sent and so on. +On your Ledger device: -To handle this behavior, special care should be carried by the integrators. One possible way to handle this efficiently is to temporarily store all transactions that need to be sent on the network and continuously monitor the senders accounts involved if their nonces increased. -If not, a resend of the required transaction is needed, otherwise the transaction might be discarded from the temporary storage as it was executed. +- Review the receiver address, amount & fee on your Ledger device +- Sign the transaction by pressing both buttons -We have implemented several components written in GO language that solve the transaction send issues along with the correct nonce management. -The source code can be found [here](https://github.com/multiversx/mx-sdk-go/tree/main/interactors/nonceHandlerV2) -The main component is the `nonceTransactionsHandlerV2` that will create an address-nonce handler for each involved address. This address nonce handler will be specialized in the nonce and transactions sending mechanism for a single address and will be independent of the other addresses involved. -The main component has a few exported functionalities: -- `ApplyNonceAndGasPrice` method that is able to apply the current handled nonce of the sender and the network's gas price on a provided transaction instance -- `SendTransaction` method that will forward the provided transaction towards the proxy but also stores it internally in case it will need to be resent. -- `DropTransactions` method that will clean all the stored transactions for a provided address. -- `Close` cleanup method for the component. +Awesome, your transaction is now sent using your Ledger device. +![img](/wallet/ledger/tx_completed.png) -## **Gas limit computation** -Please follow [Gas and Fees](/developers/gas-and-fees/overview/). +## Receiving EGLD in your wallet +After logging into your wallet, as described above, you will be able to see your wallet address and share it with others, so they can send you EGLD.‌ -## **Signing transactions** +Your address is immediately visible on the top part of the wallet, as "erd1... ". You can copy the address by pressing the copy button (two overlapping squares). ‌ -Please follow [Signing Transactions](/developers/signing-transactions). +You can also click "Receive" on the right-hand side to see a QR code for the address, which can be scanned to reveal the public address. +![img](/wallet/ledger/receive.png) -## **Simulate transaction execution** -:::important -Documentation about transaction simulation is preliminary and subject to change. -::: +## Guardians ---- +To set up a guardian for a Ledger account, follow [these steps](/wallet/web-wallet.md#guardian). -### Deep History Squad +## **Need help?** -This page describes the Deep History Squad, which holds the entire trie data, so it can be used to query the state of an account at any point in time. +- Reach out to MultiversX on your [preferred channel](https://linktr.ee/MultiversX) +--- -## General information +### Local Setup -A variant of the standard [**observing squad**](/integrators/observing-squad) is one that retains a non-pruned history of the blockchain and allows one to query the state of an account at an arbitrary block in the past. Such a setup is called a [**deep-history observing squad**](https://github.com/multiversx/mx-chain-deep-history). +# Full Local Setup -:::tip -The standard observing squad is sufficient for most use-cases. It is able to resolve past blocks, miniblocks, transactions, transaction events etc. Most often, you do not need a deep-history squad, but a [**regular observing squad**](/integrators/observing-squad), instead. -::: +## Deploy local Sovereign Chain -A deep-history setup is able to resolve historical account (state) queries, that is, to answer questions such as: +This guide will help you deploy a full sovereign local network connected to MultiversX network. This includes all the smart contracts and dependent services needed. Follow these steps carefully to ensure a successful deployment. -> What was Alice's balance on [May the 4th](https://explorer.multiversx.com/blocks/5f6a02d6a5d2a851fd6dc1fb53435083830c2a13121e003958d97c2389711f06)? +### Step 1: Get the `mx-chain-sovereign-go` Repository -```bash -GET http://squad:8080/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th?blockNonce=9250000 -``` +Before proceeding, ensure that a **SSH key** for GitHub is configured on your machine. -:::tip -The API client has to perform the conversion from _desired timestamp_ to _block nonce_. Timestamp-based queries aren't directly supported yet. -::: +1. Clone the GitHub repository: + ```bash + git clone git@github.com:multiversx/mx-chain-sovereign-go.git + ``` -> How much UTK were in the [`UTK / WEGLD` Liquidity Pool](https://explorer.multiversx.com/accounts/erd1qqqqqqqqqqqqqpgq0lzzvt2faev4upyf586tg38s84d7zsaj2jpsglugga) on [1st of October](https://explorer.multiversx.com/blocks/cefd41e1e9bbe3ba023a695f412b99cecb15ef789475648ee7c31e7d9fef31d1)? +2. Navigate to testnet directory: + ```bash + cd mx-chain-sovereign-go/scripts/testnet + ``` -```markup -GET http://squad:8080/address/erd1qqqqqqqqqqqqqpgq0lzzvt2faev4upyf586tg38s84d7zsaj2jpsglugga/key/726573657276650000000a55544b2d326638306539?blockNonce=11410000 -``` +3. Run the prerequisites script: + ```bash + ./prerequisites.sh + ``` -In the example above, the key `726573657276650000000a55544b2d326638306539` is decoded as `reserve\x00\x00\x00\nUTK-2f80e9`. + :::info + The prerequisites script verifies and downloads the necessary packages to run the nodes and clones the required repositories: + - **mx-chain-deploy-sovereign-go**: Initializes the configuration for the chain and deployment parameters. + - **mx-chain-proxy-sovereign-go**: Repository for the sovereign proxy. + - **mx-chain-sovereign-bridge-go**: Repository for the cross-chain service. + - **mx-chain-tools-go**: Repository for updating elastic indices. + ::: -### Historical VM queries +### Step 2: Deploy Sovereign setup -Starting with the [Sirius Patch 5](https://github.com/multiversx/mx-chain-mainnet-config/releases/tag/v1.6.18.0), deep-history observers can resolve historical VM queries. Such a query specifies the `blockNonce` parameter: +#### Sovereign chain with no main chain connection -``` -POST http://localhost:8080/vm-values/query?blockNonce={...} HTTP/1.1 -Content-Type: application/json +1. Update chain parameters in `variables.sh` file -{ - "scAddress": "...", - "funcName": "...", - "args": [ ... ] -} +2. Start the chain with local scripts: +```bash +./config.sh && ./sovereignStart.sh ``` +#### Sovereign chain with main chain connection -### MultiversX squad - -The observing squads backing the public Gateways, in addition to being full history squads (serving past blocks, transactions and events up until the Genesis), also act as 3-epochs deep-history squads. That is, for **mainnet**, one can use https://gateway.multiversx.com to resolve historical account (state) queries, for the last 3 days. This interval is driven by the configuration parameter `[StoragePruning.NumEpochsToKeep]`, which is set to `4`, by default. - -In general: - -``` -... deep history not available -CurrentEpoch - NumEpochsToKeep - 1: deep history not available -CurrentEpoch - NumEpochsToKeep: deep history not available -CurrentEpoch - NumEpochsToKeep + 1: deep history partially available -CurrentEpoch - NumEpochsToKeep + 2: deep history available -CurrentEpoch - NumEpochsToKeep + 3: deep history available -... deep history available -CurrentEpoch: deep history available +Navigate to the `sovereignBridge` folder: +```bash +cd sovereignBridge ``` -In particular, for the public Gateway: +1. Install the [software dependencies](/sovereign/software-dependencies) and download the cross-chain contracts by running the sovereign bridge prerequisites script: + ```bash + ./prerequisites.sh + ``` -``` -... deep history not available -CurrentEpoch - 5: deep history not available -CurrentEpoch - 4 deep history not available -CurrentEpoch - 3: deep history partially available -CurrentEpoch - 2: deep history available -CurrentEpoch - 1: deep history available -CurrentEpoch: deep history available -``` +2. Create a new wallet: + ```bash + mxpy wallet new --format pem --outfile ~/wallet.pem + ``` + :::info + This wallet is the owner for cross chain smart contracts and is used by the sovereign bridge service. + ::: + + :::note + You can use any wallet of your choice, but for the purpose of this guide we are generating a new wallet. + ::: -### On-premises squad +3. Get funds in this wallet on the chain (testnet/devnet/mainnet) you want the sovereign to be connected to. -Deep-history squads can be set up on-premises, just as regular observing squads. However, the storage requirements is significantly higher. For example, a deep-history squad for **mainnet**, configured for the interval July 2020 (Genesis) - January 2024 (Sirius), requires about 7.5 TB of storage: +4. Update the configuration file `config/configs.cfg` with paths you want to use, wallet location and main chain constants. The following example shows the paths you have to use in order to connect to public MultiversX testnet: + ``` + # Sovereign Paths + SOVEREIGN_DIRECTORY="~/sovereign" + TXS_OUTFILE_DIRECTORY="${SOVEREIGN_DIRECTORY}/txsOutput" + CONTRACTS_DIRECTORY="${SOVEREIGN_DIRECTORY}/contracts" -``` -307G ./node-metachain -1.4T ./node-0 -3.9T ./node-1 -2.0T ./node-2 -``` + # Owner Configuration + WALLET="~/wallet.pem" -Since each observer of a deep-history squad must have a non-pruned history, their (non-ordinary) databases have to be either **downloaded** or **reconstructed**, in advance (covered later, in detail). + # Main chain network config + MAIN_CHAIN_ELASTIC=https://testnet-index.multiversx.com + PROXY = https://testnet-gateway.multiversx.com + CHAIN_ID = T + # Native token + NATIVE_ESDT_TICKER=SOV + NATIVE_ESDT_NAME=SovToken + ``` -## Observer installation and configuration + :::note + - **SOVEREIGN_DIRECTORY, OUTFILE_PATH, CONTRACTS_DIRECTORY** - represent the paths to the location where the deployment scripts will generate the outputs. + - **WALLET** - should represent the wallet generated at Step 2.2. + - **MAIN_CHAIN_ELASTIC** - represents the elasticsearch db from the main chain + - **PROXY** - in this case, for the purpose of the test, the used proxy is the testnet one. Of course that the proper proxy should be used when deploying your own set of contracts depending on the development phase of your project. + - **CHAIN_ID** - should represent the chain ID of the chain where the contracts are to be deployed. + - **"1"** for Mainnet; + - **"D"** for Devnet; + - **"T"** for Testnet; + - or use you own local network ID + - **NATIVE_ESDT_TICKER** - represents the ticker from which the native esdt will be generated, and updated in configs + - **NATIVE_ESDT_NAME** - represents the native esdt name + ::: -The installation of a deep history squad is almost the same as that of a [regular observing squad](/integrators/observing-squad). -The difference is that the deep history observers are set up to retain the whole, non-pruned history of the blockchain. Therefore, the following flag must be added in the `config/variables.cfg` file before running the `observing_squad` command of the installation script: -```bash -NODE_EXTRA_FLAGS="--operation-mode=historical-balances" -``` -:::tip -Apart from the flag mentioned above, the setup of a deep-history observer is identical to a regular full-history observer. -::: +5. Source the script: + ```bash + source script.sh + ``` -:::warning -Never attach a non-pruned database to a regular observer (i.e. that does not have the above **operation-mode**) - unless you are not interested into the deep-history features. The regular observer irremediably removes, truncates and prunes the data (as configured, for storage efficiency). -::: +6. Create and deploy main chain observer: + ```bash + createAndDeployMainChainObserver + ``` + + :::info + After running this command, one should wait until the observer is fully synchronized. + ::: -Now that we have finished with the installation part, we can proceed to populate the non-pruned database. There are two options here: -- Reconstruct non-pruned database (recommended). -- Download non-pruned database (we can provide archives for the required epochs, on request). +6. Deploy all cross-chain contracts on main chain and deploy Sovereign Chain with all required services: + ```bash + deploySovereignWithCrossChainContracts sov + ``` + :::info + `deploySovereignWithCrossChainContracts` command will: + - deploy all smart contracts on the main chain and update the sovereign chain configuration + - set up sovereign nodes and deploy the main chain observer + - use `sov` as the default prefix for ESDT tokens on the sovereign chain + (can be changed to any 1–4 character lowercase alphanumeric string and **must be unique across all deployed sovereign chains**) + - if no prefix is provided, a random one will be generated + ::: +### Step 3: Deploy services -## Reconstructing non-pruned databases +You can find the documentation on how to deploy services [here](/sovereign/services). -The recommended method for populating a non-pruned database is to reconstruct it locally (on your own infrastructure). +___ -There are also two options for reconstructing a non-pruned database: -- Based on the **[import-db](/validators/import-db/)** feature, which re-processes past blocks - and, while doing so, retains the whole, non-pruned accounts history. -- By performing a regular sync from the network (e.g. from Genesis), using a properly configured deep-history observer. +## Stop and clean local Sovereign Chain -:::note -The reconstruction flow has to be performed **for each shard, separately**. -::: +1. Navigate to `mx-chain-sovereign-go/scripts/testnet/sovereignBridge`. + Source the script: + ```bash + source script.sh + ``` -### Reconstructing using import-db +2. Stop and clean the chain, and all sovereign services: + ```bash + stopAndCleanSovereign + ``` -First, you need to decide whether to reconstruct a **complete** or a **partial** history. A _complete_ history provides the deep-history squad the ability to resolve historical account (state) queries **up until the Genesis**. A _partial_ history, instead, allows it to resolve state queries up until **a chosen past epoch** or **between two chosen epochs**. +--- +### Managing Sovereign -#### Reconstruct a complete history +# Managing Sovereign Chains :::note -Below, the reconstruction is exemplified for **mainnet, shard 0**. The same applies to other networks (devnet, testnet) and shards (including the metachain). +This version of the documentation focuses solely on the essential steps required to set up and deploy a sovereign chain on either a local or remote computer. More content will be added as it is accepted and discussed on [Agora](https://agora.multiversx.com/), or once it is implemented and available for production. ::: +--- -Next, we need to obtain (download) and extract **a recent daily archive (snapshot)** for the shard in question (e.g. shard 0). The daily archives are available to download **on request** ([Discord](https://discord.gg/multiversxbuilders) or [Telegram](https://t.me/MultiversXValidators)), from a cloud-based, _S3-compatible storage_ (Digital Ocean Spaces) - or you could fetch them from an existing regular full-history observer that you own. - -Upon extracting the downloaded archive, you'll have a new folder in your workspace: `db`, which contains the blockchain data for the shard in question. This data should be moved to a new folder, named `import-db`. All in all, the steps are as follows: +### MultiversX DeFi Wallet -``` -# Ask for the full download link on Discord or Telegram: -wget https://.../23-Jan-2024/Full-History-DB-Shard-0.tar.gz -# This produces a new folder: "db" -tar -xf Full-History-DB-Shard-0.tar.gz +Popularly referred to as the "future of money," MultiversX currently has a robust web wallet extension known as the **MultiversX DeFi Wallet**. It is a powerful browser extension for the MultiversX Wallet that effectively automates and reduces the steps and time required for users to interact with MultiversX Decentralized apps. -# "import-db" should contain the whole blockchain to be re-processed -mkdir -p ./import-db && mv db ./import-db -# "db" will contain the reconstructed database (empty at first) -mkdir -p ./db -``` +The MultiversX DeFi Wallet can be installed on Firefox, Chrome, Brave, and other chromium-based browsers. This extension is free and secure, with compelling features that allow you to create a new wallet or import existing wallets, manage multiple wallets on the MultiversX mainnet, and store MultiversX tokens such as EGLD, ESDT, or NFTs on the MultiversX Network with easy accessibility. -:::note -Downloading the archives and extracting them might take a while. -::: +Let's walk through the steps on how to install and set up the MultiversX DeFi Wallet extension: -When reconstructing the whole history, the workspace should look as follows (irrelevant files omitted): -``` -. -├── config # network configuration files -│ ├── api.toml -│ ├── config.toml -│ ... -├── db # empty -├── import-db -│ └── db # blockchain data -│ └── 1 -│ │ ... -│ ├── Epoch_1270 -│ │ └── Shard_0 -│ ├── Epoch_1271 -│ │ └── Shard_0 -│ ├── Epoch_1272 -│ │ └── Shard_0 -│ └── Static -│ └── Shard_0 -├── node # binary file, the Node itself -``` +## Prerequisites +Wallets don't have custody of your funds, you do. Before utilizing the MultiversX DeFi Wallet Extension, certain preliminary steps must be taken to ensure its proper use. -We are ready to start the reconstruction process :rocket: -``` -./node --import-db=./import-db --operation-mode=historical-balances --import-db-no-sig-check --log-level=*:INFO --log-save --destination-shard-as-observer 0 -``` +### Add MultiversX DeFi Wallet to your browser -:::note -The reconstruction (which uses _import-db_ under the hood, as previously stated) takes a long time (e.g. days) - depending on the machine's resources (CPU, storage capabilities and memory). -::: +- On the [Chrome Web Store Extension](https://chrome.google.com/webstore/category/extensions), search for **MultiversX DeFi Wallet.** and add it to your browser. -Once the **import-db** is over, the `db` folder contains the reconstructed database for the shard in question, ready to support historical account (state) queries. + ![MultiversX-defi-chrome](/wallet/wallet-extension/add_extension.png) -Now, do the same for the other shards, as needed. +- Confirm the action in the pop-up. -:::tip -You can smoke test the data by launching an (in-place) ephemeral observer: + ![wallet_extension_step2](/wallet/wallet-extension/confirm_extension.png) -``` -./node --operation-mode=historical-balances --log-save --destination-shard-as-observer 0 -``` +- You should receive a notification that the extension has been added successfully. -Then, in another terminal, do a historical (state) query against the ephemeral observer: + ![wallet_extension_step3](/wallet/wallet-extension/extension_installed.png) -``` -curl http://localhost:8080/address/erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx?blockNonce=42 | jq -``` -::: +### Set up MultiversX DeFi wallet -#### Reconstruct a partial history +- Once it has been successfully installed, click on the extension to get started. -:::tip -Make sure to read the section [**Reconstruct a complete history**](#reconstruct-a-complete-history) first, to understand the basics. -::: +- You will be presented with two options: you can either _Create new wallet_ or _Import existing wallet._ -Instead of reconstructing the whole history since Genesis, you may want to reconstruct a partial history, starting from a chosen epoch up until the latest epoch, or a history between two chosen epochs. + ![MultiversX-defi-example-welcome-page](/wallet/wallet-extension/extension.png) -Below, we'll take the example of reconstructing the history for epochs `1255 - 1260` (inclusive), for **mainnet, shard 0**. -Since we'd like to stop upon reconstruction of epoch `1260` (our example), we enable need to enable the `BlockProcessingCutoff` feature in `config/prefs.toml`: +### Create a new wallet -``` -[BlockProcessingCutoff] - Enabled = true - Mode = "pause" - CutoffTrigger = "epoch" - # The chosen end epoch, plus 1. - Value = 1261 -``` +**Step 1:** To get started, first install the [MultiversX DeFi wallet extension](https://chrome.google.com/webstore/detail/multiversx-defi-wallet/dngmlblcodfobpdpecaadgfbcggfjfnm). -Now, get (download) and extract **two daily archives (snapshots)** for the shard in question (e.g. shard 0): one archive that was created on the epoch preceding the **chosen start epoch** (e.g. the archive created during epoch `1255 - 1 = 1254`); and one archive that was created on the epoch following the **chosen end epoch** (e.g. the archive created during epoch `1260 + 1 = 1261`). You can compute the correct URLs for the two archives manually, given the [**mainnet** Genesis time](https://gateway.multiversx.com/network/config) and the chosen epochs, or you can use the following Python snippet: +**Step 2:** Open up the extension and click on _‘’Create new wallet”_. -``` -import datetime +**Step 3:** Next, a secret phrase consisting of a set of 24 secret words will be displayed. Safely backup these secret words. We strongly recommend you write them down or copy and store them in a safe place like a password manager. These secret words are the key to your wallet account and cannot be recovered if lost. -# Available on request (Discord or Telegram) -url_base = "https://..." -shard = 0 -chosen_start_epoch = 1255 -chosen_end_epoch = 1260 -genesis_timestamp = 1596117600 +![MultiversX-defi-example-secret-phrase](/wallet/wallet-extension/mnemonic_extension.png) -genesis_datetime = datetime.datetime.fromtimestamp(genesis_timestamp, tz=datetime.timezone.utc) -first_archive_day = (genesis_datetime + datetime.timedelta(days=chosen_start_epoch - 1)).strftime("%d-%b-%Y") -second_archive_day = (genesis_datetime + datetime.timedelta(days=chosen_end_epoch + 1)).strftime("%d-%b-%Y") +**Step 4:** Before proceeding to the next step, confirm that you have safely stored your secret phrase. -print("First daily archive:", f"{url_base}/{first_archive_day}/Full-History-DB-Shard-{shard}.tar.gz") -print("Second daily archive:", f"{url_base}/{second_archive_day}/Full-History-DB-Shard-{shard}.tar.gz") -``` +**Step 5:** For further verification, you will be prompted to input some of the secret words. -The first archive should be used as `db` and the second archive should be used as `import-db/db`. +![MultiversX-defi-example-authentication](/wallet/wallet-extension/confirm_secret_phrase.png) -``` -# Download & extract first archive to "db": -wget https://.../05-Jan-2024/Full-History-DB-Shard-0.tar.gz -tar -xf Full-History-DB-Shard-0.tar.gz +**Step 6:** Create a password that will be used to access the wallets stored in the MultiversX DeFi wallet extension. Ensure you keep this password safe as it will be needed to access your wallets regularly. Please note that this password cannot be recovered if lost. -# Download & extract second archive to "import-db/db": -wget https://.../12-Jan-2024/Full-History-DB-Shard-0.tar.gz -mkdir -p ./import-db -tar -xf Full-History-DB-Shard-0.tar.gz --directory ./import-db -``` +![MultiversX-defi-example-password](/wallet/wallet-extension/extension_password.png) -When reconstructing a partial history, the workspace should look as follows (irrelevant files omitted): +**Step 7:** Completed! Your MultiversX DeFi Wallet has been successfully created and set to be used. -``` -. -├── config # network configuration files -│ ├── api.toml -│ ├── config.toml -│ ... -├── db # blockchain data (second archive) -│ └── 1 -│ │ ... -│ ├── Epoch_1252 -│ │ └── Shard_0 -│ ├── Epoch_1253 -│ │ └── Shard_0 -│ ├── Epoch_1254 -│ │ └── Shard_0 -│ └── Static -│ └── Shard_0 -├── import-db -│ └── db # blockchain data (first archive) -│ └── 1 -│ │ ... -│ ├── Epoch_1259 -│ │ └── Shard_0 -│ ├── Epoch_1260 -│ │ └── Shard_0 -│ ├── Epoch_1261 -│ │ └── Shard_0 -│ └── Static -│ └── Shard_0 -├── node # binary file, the Node itself -``` +![MultiversX-defi-example-successful](/wallet/wallet-extension/wallet_extension_created.png) -We are now ready to start the reconstruction process :rocket: -``` -./node --import-db=./import-db --operation-mode=historical-balances --import-db-no-sig-check --log-level=*:INFO --log-save --destination-shard-as-observer 0 -``` +### Import existing wallet -Once the **import-db** is over, the `db` folder will contain the archive for the deep-history observer to support historical account (state) queries for the epochs `1255 - 1260`. +Do you already have a wallet? -:::warning -Make sure to set the **BlockProcessingCutoff** back to `false` before starting an observer intended to continue processing blocks past the cutoff. -``` -[BlockProcessingCutoff] - Enabled = false -``` -::: +Then there is no need to create a new one. The MultiversX Wallet Extension provides an option to import your existing wallet. However, to import an existing wallet you must have access to its **secret (recovery) phrase**. -### Reconstructing by performing a regular sync +The MultiversX wallet has a set of 24-words, which serve as your wallet’s secret phrase. Using a secret phrase to import an existing wallet does not affect your wallet in any way. -This is the simpler approach (even if it takes a bit more time, depending on the availability of peers, plus the network traffic). +**To get started:** -Basically, if the required [node flag configuration](#observer-installation-and-configuration) was set at installation, all that's left to be done is to start the node and it will begin building the database by synchronizing from the network since Genesis. +**Step 1:** With the [MultiversX DeFi wallet extension](https://chrome.google.com/webstore/detail/multiversx-defi-wallet/dngmlblcodfobpdpecaadgfbcggfjfnm) installed. Click on ‘’Import existing wallet”. -On the other hand, if the observer only needs to support historical account (state) queries starting from a specific past epoch, we again need to download a daily archive (snapshot) for the start epoch in question and extract it in the **db** folder, then proceed to start the node and it will begin building the database by synchronizing from the network starting with the last block available in the **db** folder. +![MultiversX-defi-example-welcome-page](/wallet/wallet-extension/extension.png) +**Step 2:** Next, enter your 24-word secret phrase. You can either enter these words one at a time or you can simply paste in the words using the "_paste_" icon. -## Starting a squad +![MultiversX-defi-example-import-wallet](/wallet/wallet-extension/write_secret_phase.png) -Suppose you have prepared the data for a deep-history squad beforehand, whether by downloading it or by reconstructing it locally. Then, the deep-history data root folder should look as follows: +**Step 3:** Enter in your wallet password and confirm this password. -``` -. -├── node-0 -│ └── db -├── node-1 -│ └── db -├── node-2 -│ └── db -└── node-metachain - └── db +![MultiversX-defi-example-password](/wallet/wallet-extension/extension_password.png) -``` +**Step 4:** Completed! Your MultiversX DeFi Wallet has been successfully imported and set to be used. -The squad and the proxy can be started using the command: +![MultiversX-defi-example-successful](/wallet/wallet-extension/wallet.png) -```bash -~/mx-chain-scripts/script.sh start -``` -Alternatively, you can set up a squad using any other known approach, **but make sure to apply the proper `operation-mode`** described in the section [**Observer installation and configuration**](#observer-installation-and-configuration). +## Key features -**Congratulations!** You've set up a deep-history observing squad; the gateway should be ready to resolve historical account (state) queries :rocket: +Now you have a wallet registered in the MultiversX DeFi Wallet Extension and it's ready to use. Great! Here's what you can do with this wallet: ---- -### EGLD integration guide +### Send to a wallet -This section provides high-level technical requirements of integrating the MultiversX's native coin, EGLD in a platform that handles EGLD transactions for their users. +One of the key features of this extension is that it allows you to send funds from your wallet to another wallet. To use this feature, you will need to have some funds in your wallet before proceeding. +**To get started** -## Overview +**Step 1:** Go to the MultiversX Wallet extension, enter your password and click on _“**Send**”_. -In order to make possible for a platform to integrate EGLD transactions for its users, these are the minimum requirements: +![MultiversX-defi-example-step9](/wallet/wallet-extension/wallet.png) -- [setting up an observing squad](/integrators/observing-squad) -- [setting up a mechanism for accounts management](/integrators/accounts-management) -- [setting up a mechanism for creating and signing transactions](/integrators/creating-transactions) -- [setting up a mechanism that queries the blockchain for new transactions to process](/integrators/querying-the-blockchain/#querying-hyperblocks-and-fully-executed-transactions) +**Step 2:** Enter the address of the wallet you intend to send to and the amount. +![MultiversX-defi-example-send](/wallet/wallet-extension/send.png) -## Integration workflow +_(Optional)_ **Step 3**: Enter the data. This is a description of the transaction or any information you wish to pass through. -An integration could mean an automatic system that parses all the transactions on the chain and performs different -actions when an integrator's address is the sender or receiver of the transaction. Based on that, it should be able -to sign transactions or update the user's balance internally. Also, different things such as hot wallets can be -integrated as well for a better tokens management and less EGLD spent on gas. +**Step 4:** Click on the _“Continue”_ button to complete the transaction. -In order to summarize the information and bring all the pieces together, this section will provide an example of how an integration can look: +### Lock/unlock -### 1. Observing squad running +After 60 minutes of being inactive, the extension automatically locks itself. You can unlock it at any time using your password. In addition, you can lock the extension manually, by clicking the **“lock”** icon in the header. -The integrator has an observing squad (an observer on each shard + proxy) running and synced. +### Deposit to a wallet -### 2. Getting hyperblock by nonce +A deposit can be made to your wallet using the wallet extension. This feature allows you to share your QR code or wallet address to receive a token deposit. To get started: -The system should always memorize the last processed nonce. After processing a hyperblock at a given nonce, it should -move on to the hyperblock that corresponds to the next nonce (when available, if not already existing). +- Open up your MultiversX Wallet extension. -In order to fetch the hyperblock for a given nonce, the system should perform an API call to `/hyperblock/by-nonce/`. +- Next, click on the _"*deposit*"_ and share your QR code or wallet address. -If the response contains an error, it probably means that the nonce isn't yet processed on the chain and a retry should be done after a small waiting period. + ![MultiversX-defi-example-deposit](/wallet/wallet-extension/receive.png) -:::tip -A round in the blockchain is set to 6 seconds, so the nonce should change after a minimum of 6 seconds. -A good refresh interval for nonce-changing detection could be 2 seconds. -::: +### Transactions history -#### 2.1. Fallback mechanism +On the wallet extension dashboard, the wallet records all transactions sent and received in your wallet. If you are a new user, it says "_No transactions found for this wallet_" until you make your first transaction. -If, for example, a server issue occurs and the observing squad gets stuck, the latest processed nonce must be saved -somewhere so when the observing squad is back online, the system should continue processing from the next nonce after the saved one. +![MultiversX-defi-example-step9](/wallet/wallet-extension/wallet.png) -#### 2.2. Example +### Networks -For example, when the system is up, it should start processing from a nonce in the same epoch. Let's say the chain is in epoch -5 and the first hyperblock nonce in that epoch is 900 +In the settings section on your extension dashboard, you can connect to the different networks provided by MultiversX, such as the mainnet, testnet, and devnet. -``` -... --> fetched hyperblock with nonce 900 --> processed hyperblock with nonce 900 --> saved last processed nonce = 900 --> waiting 2 seconds --> fetching hyperblock with nonce 901: API error (nonce not yet processed on chain side), skip --> waiting 2 seconds --> fetching hyperblock with nonce 901: API error (nonce not yet processed on chain side), skip --> waiting 2 seconds --> fetched the hyperblock with nonce 901 --> processed hyperblock with nonce 901 --> saved last processed nonce = 901 --> waiting 2 seconds -... -``` +Choose either of these networks. -:::caution -Keep in mind that a hyperblock shouldn't be processed twice as this might cause issues. -Make sure the block processing and the saving of the last processed nonce should be atomic. -::: +## Connecting the MultiversX DeFi Wallet to xExchange App -#### 2.3. Querying the transactions +You can now connect xExchange to the MultiversX DeFi wallet in real-time. With this connection, you will be able to log in to the exchange using the MultiversX DeFi wallet extension in a few steps. +Follow these steps to proceed: -The system fetches the response and iterates over each successful transaction and determine if any address from the integrator is involved. +**Step 1:** To get started, go to the [MultiversX Exchange](https://xexchange.com) page on the right section of the page, click on the _“connect”_ button. +![wallet_extension_step17](/wallet/wallet-extension/xexchange.png) -### 3. Transaction handling +**Step 2:** Select "**_MultiversX DeFi Wallet_**" from the options displayed. -After identifying a relevant transaction in step 2.3 (the sender or the receiver is an integrator's address) actions could be taken on integrator's side. +![wallet_extension_step15](/wallet/wallet-extension/login.png) -It is recommended that the integrator performs some balances checks before triggering internal transfers. +**Step 3:** Lastly, enter your password and click on the wallet address you want to connect to. -For example, if the receiver is an integrator's address, the integrator can update its balance on internal storage systems. +![wallet_extension_step16](/wallet/wallet-extension/extension_login.png) +- In a split second, the MultiversX Exchange home page automatically reloads. You’ll notice your account has been added to the right section of the page. -### Mentions +![wallet_extension_step18](/wallet/wallet-extension/logged.png) -- steps 2 and 3 should be executed in a continuous manner while always keeping record of the last processed nonce, in order to ensure - that no transaction is skipped. -- other usual actions such as transferring (from time to time) all addresses funds to a hot wallet could also be implemented. +Successful 🎉 -## Finality of the transactions / number of confirmations +## Guardian -The hyperblock includes only finalized transactions so only one confirmation is needed. The integrator however has the flexibility to wait for any number of additional confirmations. +You can use the MultiversX DeFi wallet with [Guardians](/developers/guard-accounts). It is as simple as it was before, with the only mention that you have to introduce the 2FA code for guardian signature when requested: +![guardian_extension1](/wallet/wallet-extension/guardian_extension1.png) -## Balances check +--- -From time to time, or for safety reasons before performing a transaction, an integrator would want to check the balance of some -addresses. This can be performed via [Get address balance endpoint](/sdk-and-tools/rest-api/addresses#get-address-balance). +### Mvx Esdt Safe +# Mvx-ESDT-Safe +![To Sovereign](../../static/sovereign/to-sovereign.png) -## Useful tools and examples +The ability to transfer tokens from the Main Chain to any Sovereign Chain is essential, since every Sovereign can connect to the Main MultiversX Chain. As a result, the customizable Sovereign can leverage any token available on the default network. Another great feature is the possibility of executing smart contracts inside the Sovereign Chain through this contract. -MultiversX SDKs or tools can be used for signing transactions and performing accounts management. +This contract has three main modules: [`deposit`](#deposit), [`execute_operation`](#executing-an-operation) and [`register_token`](#registering-tokens). -A complete list and more detailed information can be found on the [accounts management](/integrators/accounts-management) and -[signing transaction](/integrators/creating-transactions) sections. -There is also an example that matches the above-presented workflow and can be found on the Go SDK for MultiversX, [sdk-go](https://github.com/multiversx/mx-sdk-go/tree/main/examples/examplesFlowWalletTracker). +## Deposit +### Main Chain deposit to Sovereign Chain transfer flow +1. User deposits the tokens he wishes to transfer in the `Mvx-ESDT-Safe` contract deployed on the Main Chain. +2. An observer is monitoring the Main Chain. +3. Sovereign network receives extended shard header. +4. Incoming transactions processor handles and processes the new transaction. -However, other SDKs can be used as well for handling accounts management or transaction signing. +### Deposit Endpoint +```rust + #[payable("*")] + #[endpoint] + fn deposit( + &self, + to: ManagedAddress, + optional_transfer_data: OptionalValueTransferDataTuple, + ) +``` ---- +One key aspect of cross chain transfers from MultiversX Main Chain to a Sovereign Chain is being able to transfer tokens and also execute a smart contract call within single transaction. The `#[payable("*")]` annotation means that the endpoint can receive any tokens that will transferred. If those tokens are from a Sovereign Chain they will be burned otherwise they will be saved in the Smart Contract`s account. The checks enabled for the transfer of tokens are the following: -### ESDT tokens integration guide +- If the token is whitelisted or not blacklisted, in that case the tokens can be transferred. +- If the fee is enabled, the smart contract assures that the fee is paid. +- If there are maximum 10 transfers in the transaction. -## **Introduction** -Integrating ESDT tokens support can be done alongside native EGLD integration, so one should refer to the [egld-integration-guide](/integrators/egld-integration-guide). +If the deposit also includes the `optional_transfer_data` parameter it will also have some extra checks regarding the cross-chain execution of endpoints: -The only differences are internal ways to store ESDT tokens alongside with their token identifier and number of decimals and different approaches -for identifying and parsing ESDT transactions. +- The gas limit must be under the specified limit. +- The endpoint that has to be executed is not blacklisted. -## **ESDT transactions parsing** -Considering that the platform which wants to support ESDT tokens already supports EGLD transfers, this section will -provide the additional steps that are to be integrated over the existing system. +At the end of the `deposit` endpoint, all the extra tokens will be refunded to the caller and an event will be emitted since the bridging process is complete. -If so, all the transactions on the network are being parsed so the integrator will check if the sender or receiver of the transaction -is an address that is interested in. -In addition to this, one can check if the transaction is a successful ESDT transfer. If so, then the transferred token, the amount and the -receiver can be further parsed. -One has to follow these steps: -- check if the transaction is an ESDT transfer (data field format is `ESDTTransfer@@`. More details [here](/tokens/fungible-tokens#transfers)) -- parse the tokens transfer details from Logs&Events. More details [here](/tokens/fungible-tokens#parse-fungible-tokens-transfer-logs) +```rust +#[event("deposit")] +fn deposit_event( + &self, + #[indexed] dest_address: &ManagedAddress, + #[indexed] tokens: &MultiValueEncoded>, + event_data: OperationData, +) +``` +This log event will emit the destination address and the tokens which will be transferred to the Sovereign Chain. -### Example -Let's suppose we are watching these addresses: -- `erd1zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3z92425g3zygs3mthts` -- `erd1venxvenxvenxvenxvenxvenxvenxvenxvenxvenxvenxvenxvenq5ezmpv` +:::note +The source code for the endpoint can be found [here](https://github.com/multiversx/mx-sovereign-sc/blob/main/mvx-esdt-safe/src/deposit.rs). +::: -And we support the following tokens: -- `TKN-99hh44` (hex encoded: `544b4e2d393968683434`) -- `TKN2-77hh33` (hex encoded: `544b4e322d373768683333`) +## Executing an Operation -Therefore, we will look for transactions that have the following structure: -``` -TokenTransferTransaction { - Sender: erd1.... - Receiver: erd1zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspavcaj OR erd1venxvenxvenxvenxvenxvenxvenxvenxvenxvenxvenxvenxvenq5ezmpv - Value: 0 - GasLimit: 60000000 - Data: "ESDTTransfer" + - "@" + // 544b4e2d393968683434 OR 544b4e322d373768683333 - "@" -} +```rust +#[endpoint(executeBridgeOps)] + fn execute_operations( + &self, + hash_of_hashes: ManagedBuffer, + operation: Operation +) ``` -*For more details about how arguments have to be encoded, check [here](/developers/sc-calls-format).* +- `hash_of_hashes`: hash of all hashes of the operations that were sent in a round +- `operation`: the details of the cross-chain execution -Any other transaction that does not follow this structure has to be omitted. +To ensure that the cross-chain execution is will be successful, the following checks must be passed: -When we find such a transaction, we will fetch the logs of the transaction, as described [here](/tokens/fungible-tokens#parse-fungible-tokens-transfer-logs). -The logs will provide us information about the receiver, the token and the amount to be transferred. If the log is there, we are sure that -the transfer is successful, and we can start processing with the extracted data. +1. Calculate the hash of the *Operation* received as a parameter. +2. Verify that the given *Operation’s* hash is registered by the Header-Verifier smart contract. +3. Mint tokens or get them from the account. +4. Distribute the tokens. +5. Emit confirmation event or fail event if needed. +As the 2nd point specifies, the `Header-Verifier` smart contract plays an important role in the cross-chain execution mechanism. In the [`Header-Verifier`](header-verifier.md) section there will also be a description for the important endpoints within this contract. -## **Sending ESDT tokens** -Sending ESDT tokens to a given recipient can be done via preparing and broadcasting to the network a transaction that -follows the format described [here](/tokens/fungible-tokens#transfers). +:::note +The source code for the endpoint can be found [here](https://github.com/multiversx/mx-sovereign-sc/blob/main/mvx-esdt-safe/src/execute.rs). +::: -Also, there is support for building tokens transfer transaction on many SDKs. A few examples are: -- [sdk-js - ESDTTransferPayloadBuilder](https://github.com/multiversx/mx-sdk-js-core/blob/main/src/tokenTransferBuilders.ts) -- [erdjava - ESDTTransferBuilder](https://github.com/multiversx/mx-sdk-erdjava/blob/main/src/main/java/multiversx/esdt/builders/ESDTTransferBuilder.java) +## Important Endpoint Related Structures +This subsection outlines the key data structures that enable robust cross-chain operations. It details how an *Operation* is composed of its destination address, one or more token transfers defined by `OperationEsdtPayment`, and the contextual metadata provided by `OperationData`. Additionally, `TransferData` specifies the parameters needed for executing remote smart contract calls, collectively ensuring precise control over cross-chain interactions. +```rust +#[derive(TopEncode, TopDecode, NestedEncode, NestedDecode, TypeAbi, ManagedVecItem, Clone)] +pub struct Operation { + pub to: ManagedAddress, + pub tokens: ManagedVec>, + pub data: OperationData, +} +``` +- `to`: specifies the destination of the *Operation* +- `tokens`: represents one or more token transfers associated with the operation +- `data`: encapsulates additional instructions or parameters that guide the execution of the operation -## **Balances check** -From time to time, or for safety reasons before performing a transaction, an integrator would want to check the tokens balance of some -addresses. This can be performed via [Get address token balance endpoint](/tokens/fungible-tokens#get-balance-for-an-address-and-an-esdt-token). +```rust +pub struct OperationEsdtPayment { + pub token_identifier: TokenIdentifier, + pub token_nonce: u64, + pub token_data: EsdtTokenData, +} +``` +This struct describes a single token transfer action within an *Operation*. Each Operation can have one or more of such payments, with that enabling the transfer of a variety of tokens during a cross-chain transaction. -## **Getting tokens properties** -Each token has some properties such as the name, the ticker, the token identifier or the number of decimals. -These properties can be fetched via an API call described [here](/tokens/fungible-tokens#get-esdt-token-properties). +- `token_identifier`: used for the identification of the token +- `token_nonce`: if the token is Non-Fungible or Semi-Fungible, it will have a custom nonce, if not the value will be 0 +- `token_data`: a structure holding metadata and other token properties +```rust +pub struct OperationData { + pub op_nonce: TxId, + pub op_sender: ManagedAddress, + pub opt_transfer_data: Option>, +} +``` -## **Useful tools** -- ESDT documentation can be found [here](/tokens/fungible-tokens). -- ESDT API docs can be found [here](/tokens/fungible-tokens#rest-api). -- sdk-js helper functions can be found [here](https://github.com/multiversx/mx-sdk-js-core/blob/release/v9/src/esdtHelpers.ts). -- sdk-js token transfer transactions builder can be found [here](https://github.com/multiversx/mx-sdk-js-core/blob/main/src/tokenTransferBuilders.ts). -- erdjava token transfer transactions builder can be found [here](https://github.com/multiversx/mx-sdk-erdjava/blob/main/src/main/java/multiversx/esdt/builders/ESDTNFTTransferBuilder.java). -- [@multiversx/sdk-transaction-decoder](https://www.npmjs.com/package/@multiversx/sdk-transaction-decoder). +`OperationData` encapsulates the needed information for the *Operation* that needs to be executed. This isn’t just another data definition, we’ve already seen data-related fields elsewhere. Instead, it centralizes the contextual information that *Operation* needs before, during, and after execution. ---- +- `op_nonce`: is used for the identification of each *Operation* +- `op_sender`: represents the original sender of the *Operation* +- `opt_transfer_data`: an optional `TransferData` field, when present, contains details about the cross-chain execution of another Smart Contract -### Frequently Asked Questions +```rust +pub struct TransferData { + pub gas_limit: GasLimit, + pub function: ManagedBuffer, + pub args: ManagedVec>, +} +``` -[comment]: # "mx-abstract" +`TransferData` represents the description of the remote execution of another Smart Contract. -This page contains answers to frequently asked questions about connecting an application to MultiversX, be it an exchange, wallet, dApp, Web3 indexer or data provider. +- `gas_limit`: specifies the needed gas for the execution of all other endpoints. +- `function`: the name of the endpoint that will be executed. +- `args`: the arguments for the calls. -## General information +:::note +The source code for structures can be found [here](https://github.com/multiversx/mx-sovereign-sc/blob/main/common/structs/src/operation.rs). +::: -### What is the native token of MultiversX? +## Registering tokens +As mentioned at the start of this section, in the scope a Sovereign Chain, a token that already exists inside the MultiversX Mainchain can be leveraged within the custom blockchain. It has to be firstly registered inside the `Mvx-ESDT-Safe` smart contract. The `register_token` module has the role of registering any token that will be later used inside the Sovereign Chain. -**EGLD** on Mainnet, **XeGLD** on Devnet and Testnet. The atomic unit of the native token (think of `wei` for Ethereum) is not named. +### Register any token -``` -1 EGLD = 10^18 atomic units = 1000000000000000000 atomic units +```rust + #[payable("EGLD")] + #[endpoint(registerToken)] + fn register_token( + &self, + sov_token_id: TokenIdentifier, + token_type: EsdtTokenType, + token_display_name: ManagedBuffer, + token_ticker: ManagedBuffer, + num_decimals: usize, + ) ``` -See [constants](/developers/constants). +This endpoint is how an user from a Sovereign Chain registers a token on the MultiversX Mainchain. Every token registration costs 0.05 EGLD, that's why the endpoint is `payable`. +The endpoint check if the token was not registered before and if it has a prefix. -### What kind of consensus does MultiversX use? +> Every token that was created in a Sovereign Chain has a prefix. Example: `sov-TOKEN-123456`. -See more at [secure proof of stake](/learn/consensus). +If everything is in order, the `Mvx-ESDT-Safe` smart contract will initiate an asynchronous call to the `issue_and_set_all_roles` endpoint from the _ESDTSystemSC_. When the system smart contract finishes the issue transaction, the callback inside the `Mvx-ESDT-Safe` smart contract will trigger and register the mapping of token identifier inside the token mappers: -### What is the block time (round duration)? +* `sovereign_to_multiversx_token_id_mapper(sov_token_id)` -> mvx_token_id +* `multiversx_to_sovereign_token_id_mapper(mvx_token_id)` -> sov_token_id -The block time (round duration) is [6 seconds](/developers/constants). Also see [the welcome page](/welcome/welcome-to-multiversx). +> After the execution of this endpoint, the `Mvx-ESDT-Safe` smart contract will have in its storage a pair of token identifiers. **Example**: `sov-TOKEN-123456` is the corresponding sovereign identifier for the `TOKEN-123456` identifier from the MultiversX Mainchain. You can view this feature as creating copies of MultiversX Mainchain tokens inside the Sovereign Chain. -### Does MultiversX employ sharding? +### Register the native token +Since a Sovereign Chain is a separate blockchain from the MultiversX Mainchain, it has to have a its own native token. Registering the native token is a straightforward process of just one endpoint call. -Currently, the MultiversX network has 3 regular shards, plus a special one, called the _metachain_ - this arrangement holds not only on _mainnet_, but also on _devnet_ and _testnet_. +```rust + #[payable("EGLD")] + #[only_owner] + #[endpoint(registerNativeToken)] + fn register_native_token(&self, token_ticker: ManagedBuffer, token_name: ManagedBuffer) +``` -Transactions between accounts assigned to the same shard are called _intra-shard transactions_. Transactions between accounts located in distinct shards are called _cross-shard transactions_. +The owner will have to call the `register_native_token` from the `Mvx-ESDT-Safe` smart contract in order to register the token identifier that will be used inside the Sovereign Chain as the native one. There can only be one native token so the endpoint firstly checks if if was not already registered. The fee amount for registering is the same as registering any token, 0.05 EGLD. The parameters include the `token_ticker` and `token_name`. The endpoint then initiates an asynchronous call to the _ESDTSystemSC_ to `issue_and_set_all_roles`. The newly created token is always fungible and has 18 decimals. After the issue call is finished the callback inside the `Mvx-ESDT-Safe` smart contract inserts the newly issued token identifier inside its storage. -More details about the sharded architecture of MultiversX can be found [here](/learn/sharding). -Integrators may choose to have a unified view of the network, leveraging the [hyperblock](/integrators/egld-integration-guide) abstraction. +:::note +The source code for this module can be found [here](https://github.com/multiversx/mx-sovereign-sc/blob/main/mvx-esdt-safe/src/register_token.rs). +::: -## Wallet +--- -### What signature scheme does MultiversX use? +### One Click Deployment -For transactions, [ed25519](/developers/signing-transactions) is used. +# One-Click Deployment -### What BIP-0044 coin type is being used? +## One-click local sovereign deployment in Digital Ocean -CoinType is **508**, according to: [SLIP-0044](https://github.com/satoshilabs/slips/blob/master/slip-0044.md). +At present, the only one-click deployment option is available on the Digital Ocean marketplace. This solution sets up a droplet containing a local Sovereign Chain connected to the MultiversX public testnet, complete with all essential services such as API, wallet, explorer, and more. -### What is the derivation path for wallets? +### Create Sovereign Chain droplet -The derivation path is `m/44'/508'/0'/0'/{address_index}'`. That is, the _account index_ stays fixed at `0`, while the _address index_ is allowed to vary. +- Go to [Digital Ocean marketplace](https://marketplace.digitalocean.com/apps/multiversx-testnet-sovereign-chain) for more info and to create your Sovereign Chain droplet. -## Transactions +:::note +It is recommended to choose a droplet with minimum 8 CPUs and 32GB RAM for the best performance. +::: -### What is the schema of a transaction? +:::important +The one-click deployment setup provided in this guide is designed primarily for development purposes. By default, it connects to the MultiversX public testnet and runs all services on the same machine. This setup is equivalent to the Full Local Setup but operates in the cloud, providing a convenient way to test and develop your Sovereign Chain in a hosted environment. +::: -See [transactions](/learn/transactions). +--- -### How to determine the status of a transaction? +### Other Vm -See [querying the blockchain](/integrators/querying-the-blockchain). +# Other-VM -### What can be said about transactions finality? +:::note +As the MultiversX Sovereign Chains ecosystem grows, additional VMs will be added and described here over time. SpaceVM implements every necessary interface and the new VM needs to only change the EXECUTOR part inside the SpaceVM.. +::: -A transaction is final when the block or blocks (for cross-shard transactions) that notarize it have been declared **final**. -Generally speaking, a transaction can be considered final as soon as it presents the _hyperblock coordinates_ (hyperblock nonce and hyperblock hash) when queried from the network, and these coordinates are under (older than) the [latest final hyperblock](/integrators/querying-the-blockchain#querying-finality-information). +--- -For more details, see [integration guide](/integrators/egld-integration-guide) and [querying the blockchain](/integrators/querying-the-blockchain). +### Restaking -## Accounts +# Restaking -### How does an address look like? +## Introduction -An **account** is identified by an **address**, which is the **bech32-encoded** public key of the account. -For the **bech32** encoding, the human-readable part (HRP) is `erd`. +In the blockchain space, reStaking products are emerging as a significant innovation. Eigenlayer is currently the leading platform, but new projects are continuously entering the Ethereum ecosystem with impressive valuations. Currently, over $13 billion is reStaked in Eigenlayer, despite the absence of a live product. -### Types of accounts +## General Economics and Staking Models -Both _regular user accounts_ and _smart contract accounts_ can hold tokens: EGLD and ESDT tokens (fungible, semi-fungible or non-fungible ones). +When designing the General Economics for Sovereign Chains, extensive research was conducted on various economic and staking models. This included insights from IBC (Cosmos ecosystem), the new Interchain Security model, Polygon SuperChains, Avalanche Appchains, various Ethereum Layer 2 solutions, and Eigenlayer. Each model was analyzed, incorporating the best features and discarding the less effective ones. -Regular user accounts can sign transactions using their private key. Smart contracts cannot create actual transactions, as they cannot sign them. Instead, they interact with other accounts by crafting so-called _unsigned transactions_ (or smart contract results). +## Current Proposed Economics Design -### How are accounts created? +The currently proposed economic design for Sovereign Chains incorporates a one-time reStaking model, non-custodial delegation, and allows extensive customization with native tokens. This framework enables the creation of an economic security fund that Sovereign Chains can leverage. Additionally, a general Sovereign Validator pool can be established, allowing new chains to launch using existing validators and economic resources without needing to gather new participants. -Regular user accounts get created on the blockchain when the corresponding address receives tokens for the first time. On the other hand, (user-defined) smart contract accounts are created when a smart contract is deployed. The address of a smart contract is a function of `[address of the deployer, nonce of deployment transaction]`. For a smart contract account, there is no (known) private key. +This design allows validators to be sponsored by users, meaning validators do not need to hold EGLD; instead, users can delegate their tokens. This flexibility extends to users delegating directly to Sovereign Chains. -### How to distinguish between a normal account and a smart contract? +## Enhanced Flexibility and Security -Examples of addresses: +The designed system aims to combine the benefits of Eigenlayer with added freedom for users, validators, and projects. Open markets and freedom encourage competition, opportunities, and innovation. The current system proposes one-time reStaking, the use of liquid staked assets, and even DeFi positions containing liquid staked assets. Key considerations include whether to allow multiple reStaking and if Sovereign Chains should be required to allocate a portion of rewards to the reStaking layer. -- **regular user account:** `erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th` -- **smart contract:** `erd1qqqqqqqqqqqqqpgqq66xk9gfr4esuhem3jru86wg5hvp33a62jps2fy57p` +## Multiple ReStaking -If the address (decoded as bytes) has a prefix of 8 bytes of `0x00`, then it refers to a smart contract. +Enabling multiple reStaking would allow EGLD reStakers to earn significantly higher returns, potentially doubling or tripling yields. Non-custodial reStaking ensures that users retain the base EGLD rewards, making it an attractive option for participation. +Benefits for Different Actors -## Smart Contracts +- **SovereignChain Builders**: They can utilize existing funds and validators, simplifying the process of securing their network. With pre-built contracts and a streamlined launch process, Sovereign Chains can achieve high security from the outset without distributing excessive tokens to validators. +- **Validators**: Validators can earn more by participating in multiple networks without owning EGLD, as users provide the required tokens. Validators can create their own economies by rewarding users who delegate to them, fostering a competitive environment. +- **Users**: Users face slightly higher risks of slashing but benefit from increased returns through multiple yields. They maintain a close connection with builders and validators from the beginning. +- **EGLD**: Increased staking reduces market supply, enhancing yield percentages and utility. ReStaking creates a new utility layer for EGLD, making it more appealing to investors. -There are two types of smart contracts on MultiversX: **system smart contracts** and **user-defined smart contracts**. System smart contracts are coded into the Protocol itself, while user-defined smart contracts are developed and deployed by users. The latter are written primarily in **Rust**. The Virtual Machine uses the **WASM** format, by leveraging the [Wasmer](https://wasmer.io/) engine. +## Security Considerations -### How to detect contract deployment events? +ReStaking leverages the security of ETH, with slashing risks being the primary concern. Slashing events are rare in existing PoS networks, indicating that proper economic incentives generally ensure validator honesty. However, potential systemic risks from widespread slashing events need careful management. -Look for events of type [`SCDeploy`](/developers/event-logs/contract-deploy-events). +Mitigation Strategies that we are analyzing: -### Is it possible to upgrade a smart contract? +- **Limit Slashing**: Set global and local limits on slashed EGLD. +- **Distribution of Slashed EGLD**: Distribute slashed tokens to honest validators and participants, incentivizing honest behavior. +- **Cooldown Period**: Implement a cooldown period before distributing slashed EGLD to mitigate immediate impacts. +- **Global Redistribution**: Distribute slashed EGLD globally rather than locally to spread the risk and reward. -Yes, if the `upgradeable` flag is set in the contract's [metadata](/developers/data/code-metadata). Also see [upgrading smart contracts](/developers/developer-reference/upgrading-smart-contracts). +## Conclusion + +The risk/reward ratio for reStaking supports enabling multiple reStaking, offering significant benefits to all participants. By carefully managing the associated risks, the system can provide enhanced returns and foster a thriving ecosystem. --- -### Integrators - Overview +### Security -## Introduction +# Security Considerations -If you want to integrate the MultiversX Network in your app, even if we are talking about an exchange, wallet, or a dApp that -uses its own infrastructure, please choose a direction from the following table +:::note +This version of the documentation focuses solely on the essential steps required to set up and deploy a sovereign chain on either a local or remote computer. More content will be added as it is accepted and discussed on [Agora](https://agora.multiversx.com/), or once it is implemented and available for production. +::: -## Table of contents +--- -| Name | Description | -| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -| [EGLD integration guide](/integrators/egld-integration-guide) | How to integrate MultiversX's native token EGLD. | -| [ESDT tokens integration guide](/integrators/esdt-tokens-integration-guide) | How to integrate MultiversX's ESDT tokens. | -| [Observing squad](/integrators/observing-squad) | How to run an infrastructure with a general view over all the shards. | -| [Snapshotless Observing squad](/integrators/snapshotless-observing-squad) | How to set up a light Observing squad with access to real time state only. | -| [Deep-history squad](/integrators/deep-history-squad) | How to set up an Observing squad able to resolve historical state queries. | -| [Accounts management](/integrators/accounts-management) | How to create and manage EGLD accounts. | -| [Creating transactions](/integrators/creating-transactions) | How to create and sign transactions. | -| [Querying the blockchain](/integrators/querying-the-blockchain) | How to query the MultiversX Blockchain in order to watch desired addresses or events. | -| [WalletConnect JSON-RPC Methods](/integrators/walletconnect-json-rpc-methods) | How to ensure the proper communication using WalletConnect between a wallet and a dapp. | +### Services ---- +# Introduction -### Observing Squad +## Sovereign services -The N+1 setup for connecting to the MultiversX Network +### API service -In order to integrate with the MultiversX Network and be able to [broadcast transactions](/integrators/creating-transactions) and [query blockchain data](/integrators/querying-the-blockchain) in an _optimized_ approach, one needs to set up an **on-premises Observing Squad**. +The Sovereign API is a wrapper over the Sovereign Proxy that brings a robust caching mechanism, alongside Elasticsearch historical queries support, tokens media support, delegation & staking data, and many others. -An Observing Squad is defined as a set of `N` **Observer Nodes** (one for each Shard, including the Metachain) plus an [**MultiversX Proxy**](/sdk-and-tools/proxy) instance which will connect to these Observers and provide an HTTP API (by delegating requests to the Observers). +### Lite Extras API +The Sovereign Lite Extras API includes a faucet service that allows users to obtain test tokens for their wallet. -:::tip -Currently the MultiversX Mainnet has 3 Shards, plus the Metachain. Therefore, the Observing Squad is composed of 4 Observers and one Proxy instance. -::: +### Lite Wallet +The Sovereign Lite Wallet is a lightweight version of the public wallet. It supports key functionalities such as cross-chain transfers, token issuance, token transfers, and more. -By setting up an Observing Squad and querying the blockchain data through the Proxy, the particularities of MultiversX's sharded architecture are abstracted away. **This means that the client interacting with the Proxy does not have to be concerned about sharding at all.** +### Explorer DApp +The Explorer DApp serves as the blockchain explorer for the Sovereign Chain, providing insights into transactions, blocks, and other on-chain activity. +## Software dependencies -## **System requirements** +### Node.js -The Observing Squad can be installed on multiple machines or on a single, but more powerful machine. +[Download Node.js](https://nodejs.org/en/download) or use a version manager like [NVM](https://github.com/nvm-sh/nvm) -In case of a single machine, our recommendation is as follows: +### Yarn -- 16 x CPU -- 32 GB RAM -- Disk space that can grow up to 5 TB -- 100 Mbit/s always-on Internet connection -- Linux OS (Ubuntu 20.04 recommended) +To install yarn, use the following command: +```bash +npm install --global yarn +``` -The recommended number of CPUs has been updated from `8` to `16` in April 2021, considering the increasing load over the network. +### Redis -:::tip -These specs are only a recommendation. Depending on the load over the API or the observers, one should upgrade the machine as to keep the squad synced and with good performance. -::: +To install and start redis, use the following commands: +```bash +sudo apt update +sudo apt install redis +sudo systemctl start redis +sudo systemctl enable redis +``` -## **Setup via the Mainnet scripts** +## Prerequisites -:::caution -`elrond-go-scripts-mainnet` are deprecated as of November 2022. Please use `mx-chain-scripts`, explained below. -::: +### Sovereign network deployed +Before starting the services it is required to have a full sovereign network running, see [setup guide](/sovereign/local-setup). -## **Setup via mx-chain-scripts** +--- +### Software Dependencies -## **Installation and Configuration** +# Software Dependencies -The Observing Squad can be set up using the [installation scripts](/validators/nodes-scripts/config-scripts/). Within the installation process, the `DBLookupExtension` feature (required by the Hyperblock API) will be enabled by default. +Understanding and managing software dependencies is crucial for the successful deployment and maintenance of Sovereign Chains. Dependencies ensure that all components of your blockchain network work seamlessly together. This page outlines the key dependencies required for building and operating Sovereign Chains, including software libraries, frameworks, and tools. -Clone the installer repository: +:::note +Below is the list of software needed to deploy a local Sovereign Chain. All the software dependencies will be installed by scripts in [Setup Guide](/sovereign/setup). +::: -```bash -git clone https://github.com/multiversx/mx-chain-scripts -``` +## Core Dependencies -Edit `config/variables.cfg` accordingly. For example: +### Python3 + +To install python3, use the following command: ```bash -ENVIRONMENT="mainnet" -... -CUSTOM_HOME="/home/ubuntu" -CUSTOM_USER="ubuntu" +sudo apt install python3 ``` -Additionally, you might want to set the following option, so that the logs are saved within the `logs` folder of the node: +## pipx + +To install pipx, use the following command: ```bash -NODE_EXTRA_FLAGS="-log-save" +sudo apt install pipx +pipx ensurepath +sudo pipx ensurepath --global ``` -Please check that the `CUSTOM_HOME` directory exists. Run the installation script as follows: +### mxpy -```bash -./script.sh observing_squad -``` +Ensure you are using the latest version of mxpy. Follow the installation or upgrade instructions provided [here](/sdk-and-tools/mxpy/installing-mxpy#install-using-pipx) if you haven't done so already. -After installation, 5 new `systemd` units will be available (and enabled). +### multiversx-sdk -Start the nodes and the Proxy using the command: +To install this dependency on Linux, use the following command: ```bash -./script.sh start +pip install multiversx-sdk ``` -In order to check the status of the Observing Squad, please see the section **Monitoring and trivial checks** below. +### tmux +:::note +If you want to use tmux (which is the default configuration), you should install it. +::: +To install tmux, use the following command: -## **Upgrading the Observing Squad** +```bash +sudo apt install tmux +``` -The Observing Squad can be updated using the installation scripts. +### screen +To install the screen utility on Linux, use the following command: -### **General upgrade procedure** +```bash +sudo apt install screen +``` -:::important -`elrond-go-scripts-mainnet` are deprecated as of November 2022. Users of these scripts have to migrate to [mx-chain-scripts](/validators/nodes-scripts/config-scripts/). -The migration guide can he found [here](/validators/nodes-scripts/install-update/#migration-from-old-scripts). -::: +### wget -In order to upgrade the Observing Squad - that is, both the Observers and the Proxy, one should issue the following commands: +To install wget, use the following command: ```bash -$ cd ~/mx-chain-scripts -$ ./script.sh github_pull -$ ./script.sh stop -$ ./script.sh upgrade_squad -$ ./script.sh upgrade_proxy -$ ./script.sh start +apt install wget ``` -After running the commands above, the upgraded Observing Squad will start again. The expected downtime is about 2-3 minutes. +### Docker + +While not mandatory, it is recommended to use the latest version of Docker. If you need to install it, please follow the instructions at [Docker's official documentation](https://docs.docker.com/get-docker/). +### Golang -## **February 2023 upgrade** +Ensure you are using Go version 1.20. :::note -For observing squad users that still use the old `elrond-go-scripts`: since the rebranding to `MultiversX`, the scripts have been rebranded as well to `mx-chain-scripts`. +Please note that at the time of writing this documentation, the setup scripts have been tested only on Ubuntu. ::: -In order to upgrade the squad, you first need to migrate to the new scripts, while still running the squad via the old scripts. After that, -we'll use the new scripts to upgrade the squad. - - -### **How to migrate to the new scripts** - -If you already migrated from `elrond-go-scripts` to `mx-chain-scripts`, you can skip this section. +--- -Make sure you are on the same directory as the old scripts. +### Solana L2 -```bash -$ cd ~ -$ git clone https://github.com/multiversx/mx-chain-scripts -$ cd mx-chain-scripts -$ ./script.sh migrate -``` +# Solana L2 -The above commands should clone the new scripts and migrate the old configuration files to the new ones. You may now proceed to the next section. +:::note +This documentation is not complete. More content will be added once it is accepted and discussed on Agora or once it is implemented and available for production. -### **How to upgrade to the newest version via the new scripts** +::: -In order to upgrade the squad, we first need to stop the squad, then upgrade the squad and finally start the squad again. These steps are done by: +--- -```bash -$ cd ~/mx-chain-scripts -$ ./script.sh github_pull -$ ./script.sh stop -$ ./script.sh upgrade_squad -$ ./script.sh upgrade_proxy -$ ./script.sh start -``` +### Sov Esdt Safe -After successfully migrating to the new scripts and upgrading the squad, you can now remove the old scripts. (example: `rm -rf ~/elrond-go-scripts`) +# Sov-ESDT-Safe +![From Sovereign](../../static/sovereign/from-sovereign.png) +Whether an External Owned Account (EOA) like an user wallet or another smart contract the procedure is simple. An address will be able to perform a multi tokens transfer with various types of tokens: +- Fungible Tokens +- (Dynamic) Non-Fungible Tokens +- (Dynamic) Semi-Fungible Tokens +- (Dynamic) Meta ESDT Tokens -## **Monitoring and trivial checks** +When making the deposit, the user specifies: +1. A destination address +2. `TransferData` if the execution contains a smart contract call, which contains gas, function and arguments -One can monitor the running Observers using the **termui** utility (installed during the setup process itself in the `CUSTOM_HOME="/home/ubuntu" -` folder), as follows: +## Sovereign Chain to Main Chain transfer flow +1. User deposits token to the `Sov-ESDT-Safe` smart contract. +2. Outgoing *Operations* are created at the end of the round. +3. Validators sign all the outgoing *Operations*. +4. Leader sends *Operations* to the Sovereign Bridge Service. +5. Sovereign Bridge Service sends the *Operations* to the Header-Verifier for registration and verification, and then to `Mvx-ESDT-Safe` for execution. +6. At the end of the execution success/fail, a confirmation event will be added which will be received in Sovereign through the observer and then the cross chain transfer will be completed. -```bash -~/elrond-utils/termui --address localhost:8080 # Shard 0 -~/elrond-utils/termui --address localhost:8081 # Shard 1 -~/elrond-utils/termui --address localhost:8082 # Shard 2 -~/elrond-utils/termui --address localhost:8083 # Metachain +### Deposit Endpoint +```rust + #[payable("*")] + #[endpoint] + fn deposit( + &self, + to: ManagedAddress, + optional_transfer_data: OptionalValueTransferDataTuple, + ) ``` +As you can see both the `Mvx-ESDT-Safe` and `Sov-ESDT-Safe` smart contracts have the `deposit` endpoint. At a high level, the process is the same, depositing funds or calling other smart contracts. The main differences are when each payments is processed. Since the `Sov-ESDT-Safe` smart contract doesn't have the storage mapper for the native token, the payments won't be verified if they include any payment with the native token. The enabled checks are the same: -Alternatively, one can query the status of the Observers by performing GET requests using **curl**: +- If the token is whitelisted or not blacklisted, in that case the tokens can be transferred. +- If the fee is enabled, the smart contract assures that the fee is paid. +- If there are maximum 10 transfers in the transaction. -```bash -curl http://localhost:8080/node/status | jq # Shard 0 -curl http://localhost:8081/node/status | jq # Shard 1 -curl http://localhost:8082/node/status | jq # Shard 2 -curl http://localhost:8083/node/status | jq # Metachain -``` +If the deposit also includes the `optional_transfer_data` parameter it will also have some extra checks regarding the cross-chain execution of endpoints: -The Proxy does not offer a **termui** monitor, but its activity can be inspected using **journalctl**: +- The gas limit must be under the specified limit. +- The endpoint that has to be executed is not blacklisted. -```bash -journalctl -f -u elrond-proxy.service -``` -Optionally, one can perform the following smoke test in order to fetch the latest synchronized hyperblock: +At the end of the `deposit` endpoint, all the extra tokens will be refunded to the caller and an event will be emitted since the bridging process is complete. -```bash -export NONCE=$(curl http://localhost:8079/network/status/4294967295 | jq '.data["status"]["erd_highest_final_nonce"]') -curl http://localhost:8079/hyperblock/by-nonce/$NONCE | jq +```rust +#[event("deposit")] +fn deposit_event( + &self, + #[indexed] dest_address: &ManagedAddress, + #[indexed] tokens: &MultiValueEncoded>, + event_data: OperationData, +) ``` +This log event will emit the destination address and the tokens which will be transferred to the Sovereign Chain. -## **Setup via Docker** - -The Observing Squad can be also set up using Docker. - -Clone the Observing Squad repository: - -```bash -git clone https://github.com/multiversx/mx-chain-observing-squad.git -``` +:::note +The source code for the endpoint can be found [here](https://github.com/multiversx/mx-sovereign-sc/blob/main/sov-esdt-safe/src/deposit.rs). +::: -Install docker-compose if not already installed: +--- -```bash -apt install docker-compose -``` +### Sovereign - Overview -Install and run the whole Observing Squad using the `./start_stack.sh` script from the mainnet folder: +:::note -```bash -cd mainnet -./start_stack.sh -``` +This documentation is not complete. More content will be added once it is accepted and discussed on Agora or once it is implemented and available for production. -In order to check if the Observing Squad is running, you can list the running containers: +::: -```bash -docker ps -``` +This guide provides detailed instructions on setting up, deploying, and managing a sovereign chain, along with covering various other educational topics. -In order to check the status inside a container, you can check the logs on the machine for the last synchronized block nonce: +## Table of Contents -```bash -docker exec -it 'CONTAINER ID' /bin/bash -cat logs/mx-chain-.......log -``` +1. [Introduction](/sovereign/concept) +2. [Prerequisites](/sovereign/system-requirements) +3. [Setup Guide](/sovereign/local-setup) +4. [Custom Configurations](/sovereign/custom-configurations) +5. [Managing a Sovereign Chain](/sovereign/managing-sovereign) +6. [Economics](/sovereign/token-economics) +7. [Governance](/sovereign/governance) +8. [Testing and Validation](/sovereign/testing) +9. [Security Considerations](/sovereign/security) +10. [VMs](/sovereign/vm) +11. [Interoperability](/sovereign/interoperability) +12. [How to become a validator](/sovereign/validators) -More detailed commands for installing, building and running an Observing Squad using Docker are described [here](https://github.com/multiversx/mx-chain-observing-squad.git). The images (for the Proxy and for the Observers) are published on [Docker Hub](https://hub.docker.com/u/multiversx). +## Introduction +The introduction chapter will provide an overview of what sovereign chains are and their significance in the blockchain ecosystem. It will answer questions such as: -## One click deploy in Tencent Cloud -Tencent Cloud nodes for Full Observing Squads can be easily deployed from the [Tencent Cloud Market](https://www.tencentcloud.com/market/product/P20240326183001630223531). +- What are sovereign chains? +- How do they operate within the MultiversX blockchain network? +- What are the benefits and use cases of sovereign chains? +## Prerequisites -## One click deploy in Digital Ocean -Digital Ocean droplets for Full Observing Squads can be easily deployed via our droplets available in the [Digital Ocean Marketplace](https://marketplace.digitalocean.com/apps/multiversx-full-observing-squad). +The prerequisites chapter will detail the necessary preparations before setting up a sovereign chain. It will answer questions such as: ---- +- What are the system requirements? +- What software dependencies need to be installed? -### Querying the Blockchain +## Setup Guide -This page describes how to query the Network in order to fetch data such as transactions and blocks (hyperblocks). +The setup guide will get you through the initial steps to get your sovereign chain up and running. It will answer questions such as: -:::note -On this page, we refer to the [Gateway (Proxy) REST API](/sdk-and-tools/rest-api/gateway-overview) - i.e. the one backed by an [observing squad](/integrators/observing-squad). -::: +- How do you create a new wallet? +- Where can you download the required files? +- What repositories should you use and how to prepare your environment? +- How do you deploy necessary contracts? Can you do it in an automated manner? +- What are the step-by-step instructions for manual deployment? +- How do you update sovereign configurations and manage Docker observers? +--- -## **Querying broadcasted transactions** +### Sovereign Api -In order to fetch a previously-broadcasted transaction, use: +# API service -- [get transaction by hash](/sdk-and-tools/rest-api/transactions#get-transaction) +## Deploy Sovereign Proxy service -:::note -Fetching a _recently_ broadcasted transaction may not return the _hyperblock coordinates_ (hyperblock nonce and hyperblock hash) in the response. However, once the transaction is notarized on both shards (with acknowledgement from the metachain), the hyperblock coordinates will be set and present in the response. +:::info +Proxy service is automatically deployed if the sovereign chain was started with [local setup](/sovereign/local-setup) ::: -In order to inspect the **status** of a transaction, use: +### Step 1: Get the `mx-chain-proxy-sovereign-go` Repository -- [get transaction **shallow status** by hash](/sdk-and-tools/rest-api/transactions#get-transaction-status) -- [get transaction **process status** by hash](/sdk-and-tools/rest-api/transactions#get-transaction-process-status) +Before proceeding, ensure that a **SSH key** for GitHub is configured on your machine. -For the difference between the _shallow status_ and the _process status_, see the next section. +1. Clone the GitHub repository: + ```bash + git clone git@github.com:multiversx/mx-chain-proxy-sovereign-go.git + ``` +2. Navigate to proxy directory: + ```bash + cd mx-chain-proxy-sovereign-go/cmd/proxy + ``` -## **Transaction Status** +### Step 2: Edit Proxy `config.toml` file -### Shallow status +Example: +``` +[[Observers]] + ShardId = 0 + Address = "http://127.0.0.1:10000" -The **shallow status** of a transaction indicates whether a transaction has been **handled and executed** by the network. -However, the _shallow_ status does not provide information about the transaction's **processing outcome**, and does not capture processing errors. -That is, transactions processed with errors (e.g. _user error_ or _out of gas_) have the status `success` (somehow counterintuitively). +[[Observers]] + ShardId = 4294967295 + Address = "http://127.0.0.1:10000" +``` :::note -The _shallow_ status is, generally speaking, sufficient for integrators that are only interested into simple transfers (of EGLD or custom tokens). +For sovereign proxy there are 2 Observers required for `ShardId` 0 and 4294967295. The `Address` should be the same for both. ::: -The **shallow status** of a transaction can be one of the following: - - `success` - the transaction has been fully executed - with respect to the network's sharded architecture, it has been executed in both source and destination shards. - - `invalid` - the transaction has been marked as invalid for execution at sender's side (e.g., not enough balance at sender's side, sending value to non-payable contracts etc.). - - `pending` - the transaction has been accepted in the _mempool_ or accepted and partially executed (in the source shard). +### Step 3: Start Sovereign Proxy service -### Process status +Build and run the proxy +```bash +go build +./proxy --sovereign +``` -The **process status** of a transaction indicates whether a transaction has been processed successfully or not. +## Deploy Sovereign API service -:::note -The _process_ status is, generally speaking, useful for integrators that are interested in smart contract interactions. -::: +### Step 1: Get the `mx-api-service` Repository -:::note -Fetching the _process status_ of a transaction is less efficient than fetching the _shallow status_. -::: +1. Clone the GitHub repository: + ```bash + git clone https://github.com/multiversx/mx-api-service.git + ``` -The **process status** of a transaction can be one of the following: - - `success` - the transaction has been fully executed - with respect to the network's sharded architecture, it has been executed in both source and destination shards. - - `fail` - the transaction has been processed, but with errors (e.g., _user error_ or _out of gas_), or it has been marked as invalid (see _shallow_ status). - - `pending` - the transaction has been accepted in the _mempool_ or accepted and partially executed (in the source shard). - - `unknown` - the processing status cannot be precisely determined yet. +2. Checkout the sovereign branch and navigate to testnet directory: + ```bash + cd mx-api-service && git fetch && git checkout feat/sovereign + ``` +### Step 2: Edit API config -## **Querying hyperblocks and fully executed transactions** +1. Navigate to the `config` folder: + ```bash + cd config + ``` -In order to query executed transactions, please follow: +2. Update the configuration files (we are starting from testnet configuration in this example): + - `config.testnet.yaml` - enable/disable or configure the services you need + - `dapp.config.testnet.json` - dapp configuration file -- [get hyperblock by nonce](/sdk-and-tools/rest-api/blocks#get-hyperblock-by-nonce) -- [get hyperblock by hash](/sdk-and-tools/rest-api/blocks#get-hyperblock-by-hash) +### Step 3: Start Sovereign API service + +```bash +npm install +npm run init +npm run start:testnet +``` +## Deploy Sovereign Extras service -## **Querying finality information** +The extras service only includes the `faucet` option at the moment. -In order to fetch the nonce (the height) of **the latest final (hyper) block**, one would perform the following request against the on-premises Proxy instance: +### Step 1: Get the ```mx-lite-extras-service``` Repository -``` -curl http://myProxy:8079/network/status/4294967295 +```bash +git clone https://github.com/multiversx/mx-lite-extras-service.git ``` -Above, `4294967295` is a special number - the ID of the Metachain. +### Step 2: Update extras configuration files -From the response, one should be interested into the field `erd_highest_final_nonce`, which will point to the latest final hyperblock. +- `.env.custom` - change `API_URL` and `GATEWAY_URL` with your own URLs +- `config/config.yaml` - update the faucet configuration parameters as needed -``` - "data": { - "status": { - "erd_highest_final_nonce": 54321 - ... - } - }, - ... -} +### Step 3: Start Sovereign Extras service +```bash +NODE_ENV=custom npm run start:faucet ``` ---- +Read more about deploying API service in [GitHub](https://github.com/multiversx/mx-lite-extras-service#quick-start). -### Snapshotless Observing Squad +--- -This page describes the Snapshotless Observing Squad, a type of Observing Squad optimized for real-time requests such as accounts data fetching and vm-query operations. -More details related to exposed endpoints are available [here](/sdk-and-tools/proxy#snapshotless-observers-support). +### Sovereign Explorer +# Explorer service -## Overview +## Deploy Explorer -Whenever a node is executing the trie snapshotting process, the accounts data fetching & vm-query operations can be greatly affected. -This is caused by the fact that the snapshotting operation has a high CPU and disk I/O utilization. -The nodes started with the flag `--operation-mode snapshotless-observer` will not create trie snapshots on every epoch and will also prune the trie storage in order to save space. +### Step 1: Get the `mx-explorer-dapp` Repository +```bash +git clone https://github.com/multiversx/mx-explorer-dapp.git +``` -## Setup +### Step 2: Update explorer configuration file +1. Navigate to the `src/config` folder: + ```bash + cd src/config + ``` -### Creating a Snapshotless Observing Squad from scratch +2. Update the parameters and URLs with your own configuration in `config.testnet.ts` file -If you choose to install a snapshotless Observing Squad from scratch, you should follow the instruction from the [observing squad section](/integrators/observing-squad) and remember to add in the `variables.cfg` file the operation mode in the node's extra flags definition: +Example configuration: ``` -NODE_EXTRA_FLAGS="-log-save -operation-mode snapshotless-observer" +{ + default: true, + id: 'sovereign', + name: 'Sovereign', + chainId: 'S', + adapter: 'api', + theme: 'default', + egldLabel: 'SOV', + walletAddress: 'https://localhost:3000', + explorerAddress: 'https://localhost:3003', + apiAddress: 'https://localhost:3002', + hrp: 'erd', + isSovereign: true +} ``` -After that, you can resume the normal Observing Squad installation steps. -Then, based on the needs there are multiple options concerning the proxy: -* if only a snapshotless squad is needed, nothing else should be done -* if both regular and snapshotless squads are needed: - * with two different proxies: one started with regular observers and one started with snapshotless observers, nothing else should be done - * with only one proxy (being served by all 8 observers), `IsSnapshotless = true` should be added to each observer started with this flag, in the proxy config (found at `$CUSTOM_HOME/elrond-proxy/config/config.toml`), as follows. Please note that this step is optional, although it would help the proxy to forward the requests in an efficient manner. -```toml -[[Observers]] - ShardId = 0 - Address = "http://127.0.0.1:8080" - IsSnapshotless = true +### Step 3: Start Sovereign Explorer + +```bash +yarn +npm run start-testnet ``` +Read more about deploying explorer in [GitHub](https://github.com/multiversx/mx-explorer-dapp/tree/main#quick-start). -### Converting a normal Observing Squad to a Snapshotless Observing Squad +--- -If you already have an Observing Squad, and you want to transform it into a Snapshotless Observing Squad, the easiest way is to manually edit the service file `/etc/systemd/system/elrond-node-x.service` (with `sudo`) and append the `-operation-mode snapshotless-observer` flag at the end of the `ExecStart=` line. -In the end, the file should look like: -``` -[Unit] - Description=MultiversX Node-0 - After=network-online.target +### Sovereign Wallet - [Service] - User=jls - WorkingDirectory=/home/ubuntu/elrond-nodes/node-0 - ExecStart=/home/ubuntu/elrond-nodes/node-0/node -use-log-view -log-logger-name -log-correlation -log-level *:DEBUG -rest-api-interface :8080 -log-save -profile-mode -operation-mode snapshotless-observer - StandardOutput=journal - StandardError=journal - Restart=always - RestartSec=3 - LimitNOFILE=4096 +# Wallet service - [Install] - WantedBy=multi-user.target -``` +## Deploy Lite Wallet + +### Step 1: Get the `mx-lite-wallet-dapp` Repository -Save the file, and force a reload units with the command ```bash -sudo systemctl daemon-reload +git clone https://github.com/multiversx/mx-lite-wallet-dapp.git ``` -After units reload, you can restart the nodes. - -:::caution -Even if the nodes are synced, after changing the operation mode, they will start to re-sync their state in -"snapshotless" format. The nodes should be temporarily started with the extra node flag `--force-start-from-network` that will force the node to start from network. -Let the node sync completely and then remove this extra flag and restart the node. -Failure to do so will make the node error with a message like `consensusComponentsFactory create failed: epoch nodes configuration does not exist epoch=0`. -::: - +### Step 2: Update sovereign configuration file -## One click deploy in AWS -AWS instances for Snapshotless Observing Squads can be easily deployed via our Amazon Machine Image available in the [AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-pbwpmtdtwmkgs). +1. Navigate to the `src/config` folder: + ```bash + cd src/config + ``` +2. Update the `sharedNetworks.ts` file: + - for `sovereign` item + - update the URLs with your own + - update `WEGLDid` with the sovereign native token identifier from `config.toml` -> `BaseTokenID` + - update `sovereignContractAddress` with contract address from `sovereignConfig.toml` -> `SubscribedEvents` from `OutgoingSubscribedEvents` + - for `testnet` item (or the network your sovereign is connected to) + - update `sovereignContractAddress` with contract address from `sovereignConfig.toml` -> `SubscribedEvents` from `NotifierConfig` -## One click deploy in Google Cloud -Google Cloud instances for Snapshotless Observing Squads can be easily deployed via our virtual machine image available in the [Google Cloud Marketplace](https://console.cloud.google.com/marketplace/product/multiversx-gcp-markeplace/multiversx-snapshotless-observing-squad). +### Step 3: Start Sovereign Lite Wallet +```bash +yarn install +yarn start-sovereign +``` -## One click deploy in Tencent Cloud -Tencent Cloud nodes for Snapshotless Observing Squads can be easily deployed from the [Tencent Cloud Market](https://www.tencentcloud.com/market/product/P20240326183001630223531). +Read more about deploying lite wallet in [GitHub](https://github.com/multiversx/mx-lite-wallet-dapp/tree/main#multiversx-lite-wallet-dapp). +--- -## One click deploy in Digital Ocean -Digital Ocean droplets for Snapshotless Observing Squads can be easily deployed via our droplets available in the [Digital Ocean Marketplace](https://marketplace.digitalocean.com/apps/multiversx-observing-squad). +### Standalone Evm ---- +# Standalone EVM -### WalletConnect JSON-RPC Methods +## EVM as example +In the early stages of the MultiversX VM development, there were already components built specifically for EVM compatibility. We are revisiting and reusing parts of that code. In **VM1.2**, for instance, there was a direct correspondence between EVM opcodes and the **BlockchainHook** interface, as well as a mechanism that wrapped MvX-style transaction data (**txData**) into EVM-specific `vmInput`. -The WalletConnect [Sign API](https://specs.walletconnect.com/2.0/specs/clients/sign) establishes a session between a dapp and a wallet in order to expose a set of blockchain accounts that can sign transactions and/or messages using a secure remote JSON-RPC transport with methods and events. -The following methods are already implemented in [xPortal](/wallet/xportal/) and in WalletConnect's [Web Examples](https://github.com/WalletConnect/web-examples). +## 1. VMExecutionHandlerInterface +The MultiversX protocol defines a **VMExecutionHandlerInterface** with the following functions: -- [React Wallet (Sign v2)](https://github.com/WalletConnect/web-examples/tree/main/wallets/react-wallet-v2) ([Demo](https://react-wallet.walletconnect.com/)) -- [React dApp (with standalone client) - v2](https://github.com/WalletConnect/web-examples/tree/main/dapps/react-dapp-v2) ([Demo](https://react-app.walletconnect.com/)) +```go +// RunSmartContractCreate computes how a smart contract creation should be performed +RunSmartContractCreate(input *ContractCreateInput) (*VMOutput, error) -The Transaction Object is the same for both the `mvx_signTransaction` and the `mvx_signTransactions` methods. -To get the transaction object into a ready-to-serialize, plain JavaScript object, one can use `.toPlainObject()` from `@multiversx/sdk-core` or [any other available SDKs](/sdk-and-tools/overview). +// RunSmartContractCall computes the result of a smart contract call and how the system must change after the execution +RunSmartContractCall(input *ContractCallInput) (*VMOutput, error) +``` +The **SCProcessor** from `mx-chain-sovereign-go` prepares the input information for these functions. We aim to avoid modifying the **SCProcessor** itself; instead, all necessary abstractions will be implemented at the EVM level. +## 2. Input Preparation: EVMInputCreator -## mvx_signTransactions +When a contract creation request is made (via *ContractCreateInput), an EVMInputCreator component will: +- Convert the `ContractCreateInput` into an EVMInput. +- Invoke the actual EVM smart contract logic. -Sign a list of transactions. -This method returns a signature and any additional properties ( e.g. Guardian info ) that must be applied to each of the provided Transactions before broadcasting them on the network. +The EVM itself is taken from the official Go implementation ([evm.go](https://github.com/ethereum/go-ethereum/blob/master/core/vm/evm.go) in go-ethereum). +## 3. Abstraction Layer: MultiversX & EVM Interfaces -### Parameters +To allow the EVM to function within MultiversX, we introduce a layer that bridges EVM interfaces with MultiversX components. The core interface it uses is the `BlockchainHookInterface`, which grants access to critical blockchain data, state, and transaction information. -```text -1. `Object` - Signing parameters: - 1.1. `transactions` : `Array` - Array of Transactions - 1.1.1 `Object` - Transaction Object - 1.1.1.1 `nonce` : `String` - The Nonce of the Sender. - 1.1.1.2 `value` : `String` - The Value to transfer, as a string representation of a Big Integer (can be "0"). - 1.1.1.3 `receiver` : `String` - The Address (bech32) of the Receiver. - 1.1.1.4 `sender` : `String` - The Address (bech32) of the Sender. - 1.1.1.5 `gasPrice` : `Number` - The desired Gas Price (per Gas Unit). - 1.1.1.6 `gasLimit` : `Number` - The maximum amount of Gas Units to consume. - 1.1.1.7 `data` : `String | undefined` - The base64 string representation of the Transaction's message (data). - 1.1.1.8 `chainID` : `String` - The Chain identifier. ( `1` for Mainnet, `T` for Testnet, `D` for Devnet ) - 1.1.1.9 `version` : `String | undefined` - The Version of the Transaction (e.g. 1). - 1.1.1.10 `options` : `String | undefined` - The Options of the Transaction (e.g. 1). - 1.1.1.11 `guardian` : `String | undefined` - The Address (bech32) of the Guardian. - 1.1.1.12 `receiverUsername` : `String | undefined` - The base64 string representation of the Sender's username. - 1.1.1.10 `senderUsername` : `String | undefined` - The base64 string representation of the Receiver's username. -``` +### 3.1 Reading & Writing to Storage +- **Reading Storage**: When an EVM opcode attempts to read data from the storage (e.g., `readStorageFromTrie(key)`), it should invoke `blockchainHook.ReadFromStorage(scAddress, key)`. Internally, this call goes through the `storageContext` component, which manages reads from local cache if a key has already been accessed or modified during the current transaction. -### Returns +- **Writing to Storage**: When writing to storage, the EVM opcode should call `SetStorageToAddress(address, key)` in the `storageContext`. -```text -1. `Object` - 1.1. `signatures` : `Array` - 1.1.1 `Object` - corresponding signature and optional properties response for the provided transaction - 1.1.1.1 `signature` : The Signature (hex-encoded) of the Transaction. - 1.1.1.2 `guardian` : `String | undefined` - The Address (bech32) of the Guardian. - 1.1.1.3 `guardianSignature` : `String | undefined` - The Guardian's Signature (hex-encoded) of the Transaction. - 1.1.1.4 `options` : `Number | undefined` - The Version of the Transaction (e.g. 1). - 1.1.1.5 `version` : `Number | undefined` - The Options of the Transaction (e.g. 1). -``` +### 3.2 Finalizing State Changes +After EVM execution finishes, we need to commit the resulting state changes to the blockchain. The EVM will use the `outputContext` component, which (together with the `storageContext`) tracks modified accounts and storages. It also creates the final `vmOutput`, which the `scProcessor` in `mx-chain-sovereign-go` will then validate and apply to the blockchain (the trie) if everything is correct. -### Example +## 4. Gas Metering -```javascript -// Request -{ - "id": 1, - "jsonrpc": "2.0", - "method": "mvx_signTransactions", - "params": { - "transactions": [ - { - "nonce": 42, - "value": "100000000000000000", - "receiver": "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", - "sender": "erd1uapegx64zk6yxa9kxd2ujskkykdnvzlla47uawh7sh0rhwx6y60sv68me9", - "gasPrice": 1000000000, - "gasLimit": 70000, - "data": "Zm9vZCBmb3IgY2F0cw==", // base64 representation of "food for cats" - "chainID": "1", - "version": 1 - }, - { - "nonce": 43, - "value": "300000000000000000", - "receiver": "erd1ylzm22ngxl2tspgvwm0yth2myr6dx9avtx83zpxpu7rhxw4qltzs9tmjm9", - "sender": "erd1uapegx64zk6yxa9kxd2ujskkykdnvzlla47uawh7sh0rhwx6y60sv68me9", - "gasPrice": 1000000000, - "gasLimit": 70000, - "data": "Zm9vZCBmb3IgZG9ncw==", // base64 representation of "food for dogs" - "chainID": "1", - "version": 1 - } - ] - } -} +EVM gas metering is handled internally within the EVM code. The VMExecutionHandler can receive a new gas schedule via: -// Result -{ - "id": 1, - "jsonrpc": "2.0", - "result": { - "signatures": [ - { - "signature": "1aa6cdd9f614e2a1cedcc207e6e7c574674c9b05e98f31035cac89fcca2673ca9273c48823418cf44696f64a2c535ab3784f680a0c6d6e84b960c33e586cb30b" - }, - { - "signature": "43127c0ac3d5b124ced9c15e884940fb3c1256c463a74db33c1842fa323971e1f43725eea62225c6b2f9b2634edf68ad2e315241df734d60c41b920dec85b60a" - } - ] - } -} +```go +GasScheduleChange(newGasSchedule map[string]map[string]uint64) ``` +This function provides the cost of each opcode as a map. The EVM needs the appropriate wrapper functions to load these costs into its **OPCODES** structure. -## mvx_signTransaction +## 5. Implementation Steps: Integrating EVM -This method returns a signature and any additional properties ( e.g. Guardian info ) that must be applied to the transaction before broadcasting it on the network. -Similar to `mvx_signTransactions`, but only one Transaction can be signed at a time instead of a list of transactions. -The same logic applies to the Transaction Object here too. +- Start from the SpaceVM code. +- Replace the current executor (WASMER) with the EVM executor. +- During EVM opcode interpretation, invoke the `storageContext` and `meteringContext` functions to manage state changes and track gas consumption. +Once these steps are complete, the underlying EVM logic should effectively run on MultiversX. -### Parameters {#mvx_signTransaction-parameters} +## 6. Address Conversion: 20 Bytes vs. 32 Bytes -```text -1. `Object` - Signing parameters: - 1.1. `transaction` : `Object` - Transaction Object - 1.1.1 `nonce` : `String` - The Nonce of the Sender. - 1.1.2 `value` : `String` - The Value to transfer, as a string representation of a Big Integer (can be "0"). - 1.1.3 `receiver` : `String` - The Address (bech32) of the Receiver. - 1.1.4 `sender` : `String` - The Address (bech32) of the Sender. - 1.1.5 `gasPrice` : `Number` - The desired Gas Price (per Gas Unit). - 1.1.6 `gasLimit` : `Number` - The maximum amount of Gas Units to consume. - 1.1.7 `data` : `String | undefined` - The base64 string representation of the Transaction's message (data). - 1.1.8 `chainID` : `String` - The Chain identifier. ( `1` for Mainnet, `T` for Testnet, `D` for Devnet ) - 1.1.9 `version` : `String | undefined` - The Version of the Transaction (e.g. 1). - 1.1.10 `options` : `String | undefined` - The Options of the Transaction (e.g. 1). - 1.1.11 `guardian` : `String | undefined` - The Address (bech32) of the Guardian. - 1.1.12 `receiverUsername` : `String | undefined` - The base64 string representation of the Sender's username. - 1.1.10 `senderUsername` : `String | undefined` - The base64 string representation of the Receiver's username. -``` +EVM addresses are 20-bytes long, whereas MultiversX uses 32-byte addresses. To avoid changing the broader MultiversX system, the EVM will use internal transformers: +- **Internal EVM Calls**: Within the EVM, contracts use the last 20 bytes of the corresponding 32-byte MvX address. +- At runtime, the full 32-byte address is still known, and when a storage `read` or `write` occurs, the EVM prefixes the last 20 bytes with 10 bytes of zeros plus a 2-byte `VMType` (the standard MvX smart contract addressing scheme). -### Returns {#mvx_signTransaction-returns} +### 6.1 Calling EVM Contracts from EVM -```text -1. `Object` - corresponding signature and optional properties response for the provided transaction - 1.1 `signature` : The Signature (hex-encoded) of the Transaction. - 1.2 `guardian` : `String | undefined` - The Address (bech32) of the Guardian. - 1.3 `guardianSignature` : `String | undefined` - The Guardian's Signature (hex-encoded) of the Transaction. - 1.4 `options` : `Number | undefined` - The Version of the Transaction (e.g. 1). - 1.5 `version` : `Number | undefined` - The Options of the Transaction (e.g. 1). -``` +When an EVM-based smart contract calls another EVM contract, it uses the 20-byte address. Internally, the system prefixes these 20 bytes with the deterministic overhead (10 bytes of zeros and 2 bytes for `VMType`) to fetch the appropriate contract code from the accounts trie before running it. +### 6.2 Token Storage in EVM -### Example {#mvx_signTransaction-example} +Token balances (like ERC20) live in the contract's own storage. The contract will use the last 20 bytes of a user’s MvX address when recording ownership or balances. If an opcode like `GetCaller` is executed, it returns only the last 20 bytes from the `ContractCallInput.Sender`. -```javascript -// Request -{ - "id": 1, - "jsonrpc": "2.0", - "method": "mvx_signTransaction", - "params": { - "transaction": { - "nonce": 42, - "value": "100000000000000000", - "receiver": "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r", - "sender": "erd1ylzm22ngxl2tspgvwm0yth2myr6dx9avtx83zpxpu7rhxw4qltzs9tmjm9", - "gasPrice": 1000000000, - "gasLimit": 70000, - "data": "Zm9vZCBmb3IgY2F0cw==", // base64 representation of "food for cats" - "chainID": "1", - "version": 1 - } - } -} +### 6.3 Calling WasmVM from EVM -// Result -{ - "id": 1, - "jsonrpc": "2.0", - "result": { - "signature": "5845301de8ca3a8576166fb3b7dd25124868ce54b07eec7022ae3ffd8d4629540dbb7d0ceed9455a259695e2665db614828728d0f9b0fb1cc46c07dd669d2f0e" - } -} -``` +MultiversX WasmVM expects 32-byte addresses. If an EVM contract tries to invoke a WasmVM contract using only 20 bytes, the call will fail due to incorrect argument size. Consequently, when the EVM calls a WasmVM contract, it must supply a full 32-byte address. +:::note +In most cases, the EVM contracts will call only other EVM contracts. However, bridging to WasmVM is still feasible, for example, when claiming ESDT tokens through an ERC wrapper contract. +::: -## mvx_signMessage +## 7. WASM VM and the `ExecuteOnDestOnOtherVM` Function -This method returns a signature for the provided message from the requested signer address. +The **WASM VM** supports a public function `ExecuteOnDestOnOtherVM` via the **BlockchainHook** interface. If a new VM is fully integrated, it can be added to the `vmContainer` component with a new **baseAddress**. Below is an example table illustrating potential base addresses for different VMs: +| VM Name | Address Suffix | Notes | +|-------------|----------------------|----------------------------------------------------------------------------------| +| **SpaceVM** | 05 | Standard base address for the WASM VM | +| **System VM** | 255 | Standard base address (example) for the System VM | +| **EVM** | To Be Decided | Will be assigned upon integration to ensure address derivation works properly | -### Parameters {#mvx_signMessage-parameters} +From the `SCAddress`, the protocol looks at bytes **10** and **11** to determine which VM should be called. Once EVM integration is complete, it will receive its own base address and will adjust how the **CreateContract** opcode calculates deployed contract addresses. -```text -1. `Object` - Signing parameters: - 1.1. `message` : `String` - the message to be signed - 1.2. `address` : `String` - bech32 formatted MultiversX address ( erd1... ) -``` +### 7.1 Synchronous Execution +When the EVM executes a `DelegateCall` opcode, it will invoke an internal function of the new EVM implementation that checks whether execution should occur in the EVM itself or a different VM. If it needs to run on another VM, it calls `blockchainHook.ExecuteOnDestOnOtherVM`. -### Returns {#mvx_signMessage-returns} +- **Returning `VMOutput`**: This function returns a `VMOutput`, which can be merged into the current `outputContext` and `storageContext` via the `PushContext`-type public functions. -```text -1. `Object` - 1.1. `signature` : `String` - corresponding signature for the signed message -``` +In the **WASM VM**, if a smart contract calls `ExecuteOnDest`, the VM decides where the execution should take place. For asynchronous calls, the same logic applies: +- **Intra-Shard**: The system calls `ExecuteOnDestOnOtherVM`. +- **Cross-Shard**: On the destination shard, the **scProcessor** determines which VM to invoke and continues accordingly. -### Example {#mvx_signMessage-example} +## 8. ESDT ↔ ERC20 & ESDTNFT ↔ ERC721 -```javascript -// Request -{ - "id": 1, - "jsonrpc": "2.0", - "method": "mvx_signMessage", - "params": { - "message": "food for cats", - "address": "erd1uapegx64zk6yxa9kxd2ujskkykdnvzlla47uawh7sh0rhwx6y60sv68me9" - } -} +Bridging MultiversX ESDT standards with common Ethereum-based token standards (ERC20, ERC721, etc.). This introduces several key differences in token handling, especially around **token transfers** and **approval mechanisms**. -// Result -{ - "id": 1, - "jsonrpc": "2.0", - "result": { - "signature": "513fb2fa5ac39282ffc3aa90a89024b77057ac4542199673b05601302668bdda36c1076952f4c7445f4c6487a4263d51f72dff325012ab3f236594546ef54408" - } -} -``` +### 8.1 ESDT Transfer Model +On MultiversX, transfers typically use a **`transferAndExecute`** paradigm: +- The sender (token owner) explicitly initiates a transfer of tokens and, in the same operation, calls a smart contract endpoint to process further actions (e.g., swapping, staking, etc.). +### 8.2 ERC20 Transfer Model +In the Ethereum ecosystem, the common workflow is: +1. **Approval**: A user grants a smart contract (SC) permission to spend tokens on their behalf by calling `approve(scAddress, amount)`. +2. **Transfer**: The SC (now approved) calls `transferFrom(user, destination, amount)` to pull tokens from the user’s balance. -## mvx_signNativeAuthToken +This design allows third-party contracts to move funds from a user’s wallet without a new, explicit approval each time. However, it also opens the door to potential exploits: a malicious dApp can trick users into granting excessive approvals, which might be exploited later to drain funds. -A dApp (and its backend) might want to reliably assign an off-chain user identity to a MultiversX address. On this purpose, the signing providers allow a login token to be used within the login flow - this token is signed using the wallet of the user. Afterwards, a backend application would normally [verify the signature](/sdk-and-tools/sdk-js/sdk-js-signing-providers/#verifying-the-signature-of-a-login-token) of the token. +### 8.3 The Wrapper/SafeESDT Contract -The functionality is mostly the same as `mvx_signMessage`, only in this case instead of signing the provided message, the wallet will sign a special format including the requested signer address and the provided login token in the form of `${address}${token}`. +Because MultiversX prohibits direct “pull” transfers of ESDTs (a fundamental security decision), bridging to ERC-like workflows requires an **intermediary contract**—often called a **wrapper** or **safeESDT** contract: +1. **Deposit**: A user deposits their ESDT tokens into the wrapper contract. +2. **Allow**: The user can specify which addresses (e.g., other SCs) are allowed to withdraw a certain amount of these deposited tokens. +3. **Transfer**: The contract implements an ERC20-like `transferFrom()` functionality. When an external EVM-based SC tries to “pull” tokens, it actually interacts with this safeESDT contract, which checks permissions and only then completes the transfer if authorized. +4. **Withdrawal**: The user can reclaim any unspent tokens from the wrapper contract when they wish. -### Parameters {#mvx_signNativeAuthToken-parameters} +In the EVM environment, an operation like `safeESDTContract.transferFrom(user, scAddress, amount)` would mimic the ERC20 approach. Under the hood, the **blockchainHook** would manage a synchronous call to the other VM. -```text -1. `Object` - Signing parameters: - 1.1. `token` : `String` - the loginToken to be signed - 1.2. `address` : `String` - bech32 formatted MultiversX address ( erd1... ) -``` +### 8.4 Extending to Other ERC Standards +A similar wrapper approach can be adopted for other token types: -### Returns {#mvx_signNativeAuthToken-returns} +- **ERC721 (NFTs)**: An **ESDTNFT** wrapper can track ownership and minted tokens, providing `approve()` and `transferFrom()` methods that mirror standard ERC721 functionality. +- **ERC1155**: This multi-token standard can likewise be “wrapped”, allowing ESDT-based multi-tokens to be interfaced with EVM-based dApps expecting ERC1155 contracts. -```text -1. `Object` - 1.1. `signature` : `String` - corresponding signature for the signed token -``` +By handling all "pull" transfers inside dedicated wrapper contracts, MultiversX preserves its **secure-by-design** “push” transfer model while still enabling compatibility with dApps that rely on ERC-style approvals. +### Claiming ESDT Tokens from an ERC20 Balance -### Example {#mvx_signNativeAuthToken-example} +This diagram illustrates how a user claims an ESDT token originally held in an ERC20 contract on the EVM side. The process involves burning ERC20 tokens, calling a WASM VM wrapper contract, and finally minting ESDT tokens to the user. -```javascript -// Request -{ - "id": 1, - "jsonrpc": "2.0", - "method": "mvx_signNativeAuthToken", - "params": { - "token": "aHR0cHM6Ly9kZXZuZXQueGV4Y2hhbmdlLmNvbQ.c6191feb77da75e1acb3c5c3e8d4053be370d925fe7a78c7958ff5edc63d0c8c.86400.eyJ0aW1lc3RhbXAiOjE2OTM3NjQ1ODh9", - "address": "erd1uapegx64zk6yxa9kxd2ujskkykdnvzlla47uawh7sh0rhwx6y60sv68me9" - } -} +```mermaid +sequenceDiagram + participant U as User + participant E as ERC20 Contract (EVM) + participant W as WASM VM Wrapper -// Result -{ - "id": 1, - "jsonrpc": "2.0", - "result": { - "signature": "2789172fd8e0f3b81767392b4f3450807a5894e5c704073d18a0d5e4d0819cd8fac53ef8ba3e3b0430481d6e396f67ae484ae1f295befa766e49a3abfdf76e0a" - } -} + U->>E: 1) Call ERC20 contract to burn tokens + E->>W: 2) callContract(ERCWrapper) on WASM VM
(includes burn details) + Note over W: Registers the token under a 20-byte address
(EVM only knows 20 bytes) + U->>W: 3) User claims tokens from the WASM VM wrapper + W->>W: 3a) Checks last 20 bytes == callInput.CallerAddress[12:32] + W->>U: 4) Mints and sends ESDT tokens to OriginalCaller ``` +--- -## mvx_signLoginToken +### System Requirements -Exactly the same functionality as `mvx_signNativeAuthToken`, only the login token format differs. The Wallet can display a different UI based on the login token method request. +# System Requirements +:::note + This is a living document. More content will be added once it is implemented and available for production. As this documentation evolves, some sections may be updated or modified to reflect the latest developments and best practices. Community feedback and contributions are encouraged to help improve and refine this guide. Please note that the information provided is subject to change and may not always reflect the latest updates in the technology or procedures. s +::: -## mvx_cancelAction +This page outlines the recommended system requirements for running a Sovereign Chain node. -Wallets can implement this method to improve the UX. It is used to transmit that the user wishes to renounce on a triggered action. Close a sign transaction modal or a sign message modal, etc. +The hardware requirements for running a Sovereign Chain validator node generally depend on the node configuration and may evolve as the Sovereign network undergoes upgrades. If the Sovereign Chain does not serve any special function (such as AI, DA, DePIN, etc.), the minimum requirements should align with those for running a MultiversX node. +## Processor -### Parameters {#mvx_cancelAction-parameters} +It is preferable to use a **4 x dedicated/physical** CPUs, either Intel or AMD, with ```SSE4.1``` and ```SSE4.2``` flags (use lscpu to verify). The CPUs must be ```SSE4.1``` and ```SSE4.2``` capable, otherwise the node won't be able to use the Wasmer 2 VM available through the VM 1.5 (and above) and the node will not be able to sync blocks from the network. -```text -1. `Object` - Action parameters - 1.1. `action` : `String | undefined` - Current action to be cancelled ( for ex. `cancelSignTx` ) -``` +:::caution +If the system chosen to host the node is a VPS, the host must have dedicated CPUs. Using shared CPUs can hinder your node's performance that will result in a decrease of node's rating and eventually the node might get jailed. +::: +:::tip +We are promoting using processors that support the fma or fma3 instruction set since it is widely used by our VM. Displaying the available CPU instruction set can be done using the Linux shell command sudo lshw or lscpu +::: -### Returns {#mvx_cancelAction-parameters} +## Memory -`void` +It is recommended to use at least 16GB RAM. + +## Disk space +Disk space is usually the primary bottleneck for node operators. At the time of writing, for running a node with the **chain-sdk** binary you would need at least 200 GB SSD. +As well as storage capacity, MultiversX nodes rely on fast read and write operations. This means HDDs and cheaper SSDs can sometimes struggle to sync the blockchain. -### Example {#mvx_cancelAction-parameters} +## Bandwidth -```javascript -// Request -{ - "id": 1, - "jsonrpc": "2.0", - "method": "mvx_cancelAction", - "params": { - "action": "cancelSignTx" - } -} -``` +It is important to have a stable and reliable internet connection, especially for running a validator because downtime can result in missed rewards or penalties. It is recommended to have at least 100 Mbit/s always-on internet connection. Running a node also requires a lot of data to be uploaded and downloaded so it is better to use an ISP that does not have a capped data allowance and if it does you would need at least 4 TB/month data plan. --- -## Advanced -### Architecture +### Testing -import useBaseUrl from '@docusaurus/useBaseUrl'; -import ThemedImage from '@theme/ThemedImage'; +# Testing and Validation +:::note -# Architecture +This documentation is not complete. More content will be added once it is accepted and discussed on Agora or once it is implemented and available for production. -Ad-Astra Bridge is a system that allows the transfer of ERC20 tokens between EVM-compatible chains and the MultiversX network. -Currently, there are 2 bridges available: between the Ethereum and MultiversX networks and between the BSC and MultiversX networks. -The system is composed of several contracts and relayers that work together to facilitate the transfer of tokens. +::: -Without providing many details regarding the smart contracts interactions, this is a simplified view of the entire bridge architecture. - - +--- +### Token Economics -## EVM-compatible chains contracts -The repository for the Solidity contracts used on the EVM-compatible side can be found here: https://github.com/multiversx/mx-bridge-eth-sc-sol -The main contracts are described below: -1. **Safe**: A contract that allows users to deposit ERC20 tokens that they want to transfer to the MultiversX network; -2. **Bridge**: A contract that facilitates the transfer of tokens from MultiversX to an EVM-compatible chain. Only the relayers are allowed to use this contract. +# Token Economics +## Introduction -## MultiversX contracts -The repository for the Rust contracts used on the MultiversX side can be found here: https://github.com/multiversx/mx-bridge-eth-sc-rs -1. **Safe**: A contract that allows users to deposit ESDT tokens that they want to transfer to EVM-compatible networks; -2. **Bridge**: A contract that facilitates the transfer of tokens from the EVM-compatible chain to MultiversX. As the Bridge contract on the EVM-compatible chain side, this - the contract is allowed to be operated by the registered relayers; -3. **MultiTransfer**: A helper contract that is used to perform multiple token transfers at once; -4. **BridgedTokensWrapper**: A helper contract that is used to support wrapping the same token from multiple chains into a single ESDT token; -5. **BridgeProxy**: A helper contract that is used to store and handle the smart contract execution and the possible refund operation after the swap is done. +Sovereign Chains represent an important step forward for MultiversX ecosystem, allowing each chain to operate independently with its own set of rules, governance, and most importantly, its own token economy (tokenomics). Unlike traditional blockchain models where a single token often dominates, Sovereign Chains enable the creation and management of unique tokens tailored to the specific needs and goals of each chain. +## The Flexibility of Token Economies -## Relayers -The repository for the code that the relayers use can be found here: https://github.com/multiversx/mx-bridge-eth-go +One of the core benefits of Sovereign Chains is the flexibility in designing token economies. Each Sovereign Chain can develop a token that aligns perfectly with its specific use case, community, and economic model. This allows for a highly customized approach to incentivizing behavior, securing the network, and ensuring the long-term sustainability of the chain. -For each existing bridge, the following list applies: -- **5 Relayers** are managed by the MultiversX Foundation; -- **5 Relayers** are distributed to the MultiversX validators community, with each validator having one relayer. +## Key Components of Token Economics ---- +### Token Creation and Distribution: +- Initial Supply: Sovereign Chains can decide the total initial supply of their token, whether it’s a fixed supply or an inflationary model. +- Distribution Mechanisms: Tokens can be distributed through a variety of methods including initial coin offerings (ICOs), airdrops, or mining. -### Axelar Amplifier Setup +### Utility and Functionality: +- Transaction Fees: Tokens can be used to pay for transaction fees within the chain, ensuring smooth and cost-effective operations. +- Staking and Governance: Tokens often play a crucial role in governance, allowing holders to vote on important decisions and proposals. Additionally, tokens can be staked to secure the network and earn rewards. -# Axelar Amplifier setup for MultiversX +### Incentive Structures: +- Reward Programs: To encourage participation and loyalty, Sovereign Chains can implement reward programs for activities such as staking, providing liquidity, or contributing to the network's development. +- Burn Mechanisms: To control inflation and increase scarcity, some chains might implement token burning mechanisms where a portion of the tokens is permanently removed from circulation. -## Prerequisites +## Advantages of Sovereign Chain Token Economies -- have an [Axelar Validator](https://docs.axelar.dev/validator/setup/overview/) running (node, tofnd & vald) +- Customization: Sovereign Chains can tailor their tokenomics to fit the specific needs and goals of their ecosystem. +- Innovation: By having control over their own economic model, Sovereign Chains can experiment with innovative incentive structures and governance models. +- Independence: Each chain operates independently, reducing the risk of systemic failures and ensuring greater resilience. -## Become an Amplifier Verifier +--- -For more detailed information check out the [Become a Verifier](https://docs.axelar.dev/validator/amplifier/verifier-onboarding/) Axelar docs. +### Token Management -You can skip this if already having an Amplifier Verifier up and running. +# Sovereign Deposit Tokens Guide -### Set up `tofnd` +## Main Chain -> Sovereign Chain -If running on the same machine as the Axelar Validator, the existing `tofnd` can be used. +1. Navigate to `/mx-chain-sovereign-go/scripts/testnet/sovereignBridge`. -If you want to setup on a new machine, then you can setup `tofnd` using Docker: + Update the configuration file `config/configs.cfg` with the token settings you prefer. Example: + ```ini + # Issue Main Chain Token Settings + TOKEN_TICKER=TKN + TOKEN_DISPLAY_NAME=Token + NR_DECIMALS=18 + INITIAL_SUPPLY=111222333 + ``` -``` -docker pull axelarnet/tofnd:v1.0.1 -docker run -p 50051:50051 --env MNEMONIC_CMD=auto --env NOPASSWORD=true --env ADDRESS=0.0.0.0 -v tofnd:/.tofnd axelarnet/tofnd:v1.0.1 -``` +3. Source the script: + ```bash + source script.sh + ``` -### Set up `ampd` +4. Issue a token on the main chain: + ```bash + issueToken + ``` -Setup the `ampd` process using Docker: +5. Deposit the token in the smart contract: + ```bash + depositTokenInSC + ``` -``` -docker pull axelarnet/axelar-ampd:v1.3.1 -``` +## Sovereign Chain -> Main Chain -Make sure that the `ampd` process can communicate with `tofnd`. +1. Navigate to `/mx-chain-sovereign-go/scripts/testnet/sovereignBridge`. -To view your Verifier address you can run: `docker run axelarnet/axelar-ampd:v1.3.1 verifier-address` + Update the configuration file `config/configs.cfg` with the sovereign token settings you prefer. Example: + ```ini + # Issue Sovereign Token Settings + TOKEN_TICKER_SOVEREIGN=SVN + TOKEN_DISPLAY_NAME_SOVEREIGN=SovToken + NR_DECIMALS_SOVEREIGN=18 + INITIAL_SUPPLY_SOVEREIGN=333222111 + ``` -### Configure the verifier +2. Source the script: + ```bash + source script.sh + ``` -You need to create a configuration file at `~/.ampd/config.toml` and add the required configuration depending on your environment. +3. Issue a new token on the local sovereign chain: + ```bash + issueTokenSovereign + ``` -For complete configuration files for different environments, check out the [Configure the verifier](https://docs.axelar.dev/validator/amplifier/verifier-onboarding/#configure-the-verifier) section in the Axelar Amplifier docs. +### Steps to transfer tokens: -Example basic `config.toml` for mainnet: +:::info +- Ensure the sovereign bridge contract has the BurnRole for the token you want to bridge. All new tokens have `ESDTBurnRoleForAll` enabled. If disabled, register the burn role: + ```bash + setLocalBurnRoleSovereign + ``` +::: -``` -# replace with your Axelar mainnet node -tm_jsonrpc="http://127.0.0.1:26657" -tm_grpc="tcp://127.0.0.1:9090" -event_buffer_cap=100000 +- Register the sovereign token identifier on the main chain bridge contract: + ```bash + registerSovereignToken + ``` -[service_registry] -cosmwasm_contract="axelar1rpj2jjrv3vpugx9ake9kgk3s2kgwt0y60wtkmcgfml5m3et0mrls6nct9m" +- Deposit the token in the smart contract on the sovereign chain: + ```bash + depositTokenInSCSovereign + ``` -[broadcast] -batch_gas_limit="20000000" -broadcast_interval="1s" -chain_id="axelar-dojo-1" -gas_adjustment="2" -gas_price="0.007uaxl" -queue_cap="1000" -tx_fetch_interval="1000ms" -tx_fetch_max_retries="15" +--- -[tofnd_config] -batch_gas_limit="10000000" -key_uid="axelar" -party_uid="ampd" -url="http://127.0.0.1:50051" +### Token Types -[[handlers]] -cosmwasm_contract="axelar14a4ar5jh7ue4wg28jwsspf23r8k68j7g5d6d3fsttrhp42ajn4xq6zayy5" -type="MultisigSigner" -``` +import useBaseUrl from '@docusaurus/useBaseUrl'; +import ThemedImage from '@theme/ThemedImage'; -You need to configure additional `handlers` for each chain you want to support. Check out the [ampd README file](https://github.com/axelarnetwork/axelar-amplifier/blob/main/ampd/README.md) for more information. -Find below an example for configuring handlers for **MultiversX**. -### Activate and run the verifier +# Token Types -For more information check out the [Axelar docs](https://docs.axelar.dev/validator/amplifier/verifier-onboarding/#activate-and-run-the-verifier). +With the release of the bridge v3.0 & v3.1 different token types are allowed to be bridged. By token type, we refer to how the +tokens are locked/burnt or unlocked/minted along with the chain side on which they are native. The decision of how the token should +be configured in the bridge depends on the availability of the minter/burner role on the EVM-compatible chain side along with +the marketing decision of "on which chain the token should be native (a.k.a. where it was first minted)" -Find below basic instructions for mainnet: -1. Bond your verifier: `ampd bond-verifier amplifier 50000000000 uaxl` +**1. Mint/Burn & Native on MultiversX < - > EVM-chain has Mint/Burn & Non-Native** -2. Register public key: +This bridge token-type configuration has the advantage of not holding a single token in the bridge. +The swapped tokens are minted/burned on both sides. The total token quantity on both chains equals the (minted - burned) on MultiversX +added with the (minted - burned) on the EVM-compatible chain side. The initial supply & mint is done on MultiversX. -`ampd register-public-key ecdsa` + + -`ampd register-public-key ed25519` -3. Register support for chains for which you have configured handlers: `ampd register-chain-support amplifier flow ethereum multiversx [MORE_CHAINS]` +**2. Mint/Burn & Non-Native on MultiversX < - > EVM-chain has Mint/Burn & Native** -Run the `ampd` process with `docker run axelarnet/axelar-ampd:v1.3.1` +This has the same advantages as 1. but the initial minting is done on the EVM-compatible chain side. -## Add support for MultiversX to Verifier -### Running a MultiversX Observing Squad +**3. Mint/Burn & Non-Native on MultiversX < - > EVM-chain has Locked/Unlocked & Native** -For security reasons, you will need to run your own MultiversX Observing Squad, which is a collection of nodes, one node for each MultiversX shard + the Proxy API service. This API will be used by the Verifier to get transactions from the MultiversX network in order to be able to verify them. +This bridge token configuration type will need to be initiated on the EVM-compatible chain side and the tokens transferred to +MultiversX will be locked in the bridge contract (no minter role is needed on the EVM-compatible chain as opposed to 1 & 2) and +will be unlocked when swaps from MultiversX to the EVM-compatible chain are done. On the MultiversX side, mint/burn +actions will be performed. The total quantity of tokens on both chains will be equal to the supply on the EVM-compatible chain. Everything that is +locked in the bridge contract will equal to what was (minted - burned) on MultiversX side. -You can find detailed steps in the [MultiversX Observing Squad docs](https://docs.multiversx.com/integrators/observing-squad). There exist installation scripts that making setting up an Observing Squad easy. +:::warning +This configuration will also require an intermediate token as to allow the bridging on more than one EVM-compatible chain. +It also can create discrepancies between the allowed supplies to bridge between multiple chains. +::: -Below you can find basic information on how to setup a squad for mainnet: +Example: if tokens are being brought to MultiversX from EVM-compatible chain 1 and then the tokens are bridged out from +MultiversX to EVM-compatible chain 2, then the bridge for EVM-compatible chain 2, at some point, will be unable to process +swaps because it will run out of its intermediary tokens. The solution here is to manually bridge in the reversed order +as described (an operation that consumes time & fees). -1. Clone the `mx-chain-scripts` repo: `git clone https://github.com/multiversx/mx-chain-scripts` + + -2. Edit the `config/variables.cfg` according, for example: +**Note:** The diagram above is a little bit misleading because the ERC20 contracts hold the address/balance ledgers inside +them. For the sake of simplicity, the tokens are depicted as stored inside the bridge **Safe** contracts +(just as MultiversX ESDTs). -``` -ENVIRONMENT="mainnet" -... -CUSTOM_HOME="/home/ubuntu" -CUSTOM_USER="ubuntu" -``` +--- -3. Setup the Observing Squad: `./script.sh observing_squad` +### Transfer Flows -4. Start the nodes & the Proxy: `./script.sh start` +import useBaseUrl from '@docusaurus/useBaseUrl'; +import ThemedImage from '@theme/ThemedImage'; -### Updating Verifier `config.toml` file -In order to support MultiversX, first you need to add the two required handlers at the end of your `~/.ampd/config.toml` file: +# Transfer Flows -#### Devnet +The main functionality of the bridge is to transfer tokens from one network to another. For example, a user can transfer tokens from an EVM-compatible chain to MultiversX or from MultiversX to an EVM-compatible chain. +Besides the main functionality, there is the possibility to call a smart contract on MultiversX when doing a swap. -``` -[[handlers]] -type = 'MvxMsgVerifier' -cosmwasm_contract = 'axelar1sejw0v7gmw3fv56wqr2gy00v3t23l0hwa4p084ft66e8leap9cqq9qlw4t' -# replace with your MultiversX Proxy URL -proxy_url = 'http://127.0.0.1:8079' -[[handlers]] -type = 'MvxVerifierSetVerifier' -cosmwasm_contract = 'axelar1sejw0v7gmw3fv56wqr2gy00v3t23l0hwa4p084ft66e8leap9cqq9qlw4t' -# replace with your MultiversX Proxy URL -proxy_url = 'http://127.0.0.1:8079' -``` +# 1. Simple token transfer functionality -#### Testnet +## 1.a. EVM-compatible chain to MultiversX -``` -[[handlers]] -type = 'MvxMsgVerifier' -cosmwasm_contract = 'TBD' -# replace with your MultiversX Proxy URL -proxy_url = 'http://127.0.0.1:8079' +Let's suppose Alice has x tokens on an EVM-compatible chain and wants to transfer them to MultiversX at the address she owns +(or it might be Bob's address on MultiversX, the bridge does not care). The steps and flow are the following: +* Alice deposits the ERC20 tokens that she wants to transfer to the MultiversX network in the EVM-compatible chain's **Safe** contract; +* The EVM-compatible chain's **Safe** contract groups multiple deposits into batches; +* After a certain period of time (defined by the finality parameters of the EVM-compatible chain), each batch becomes final and starts being processed by the relayers; +* The relayers propose, vote, and perform the transfer using MultiversX's **Bridge** contract with a consensus of a minimum of 7 out of 10 votes; +* On the MultiversX network, the same amount of ESDT tokens are minted or released, depending on the token's settings; +* The destination address receives the equivalent amount of ESDT tokens on the MultiversX network. -[[handlers]] -type = 'MvxVerifierSetVerifier' -cosmwasm_contract = 'TBD' -# replace with your MultiversX Proxy URL -proxy_url = 'http://127.0.0.1:8079' -``` -#### Mainnet +## 1.b. MultiversX chain to an EVM-compatible chain -``` -[[handlers]] -type = 'MvxMsgVerifier' -cosmwasm_contract = 'TBD' -# replace with your MultiversX Proxy URL -proxy_url = 'http://127.0.0.1:8079' +Now let's suppose Alice wants her x tokens on MultiversX to transfer them to an EVM-compatible chain in the address she owns +(or it might be Bob's address on the EVM-compatible chain, again, the bridge does not care). The steps and flow are the following: -[[handlers]] -type = 'MvxVerifierSetVerifier' -cosmwasm_contract = 'TBD' -# replace with your MultiversX Proxy URL -proxy_url = 'http://127.0.0.1:8079' -``` +* Alice deposits the ESDT tokens that she wants to transfer to the EVM-compatible network in MultiversX's **Safe** contract; +* The MultiversX's **Safe** contract groups multiple deposits into batches; +* After a certain period of time, each batch becomes final and ready to be processed by the relayers; +* The relayers propose, vote, and perform the transfer using the EVM-compatible chain's **Bridge** contract with a consensus of exactly 7 out of 10 votes; +* The user receives the equivalent amount of ERC20 tokens on their recipient address on the EVM-compatible network minus the fee for this operation; +* On the MultiversX network, the ESDT tokens that were transferred are burned or locked, depending on the token's settings. -### Register MultiversX chain +# 2. Token transfer with smart-contract call on MultiversX side -1. (optional) If you have not done so already, first register the `ed25519` public key: `ampd register-public-key ed25519` +Starting with bridge v3.0, swaps from the EVM-compatible chains can invoke a smart contract on the MultiversX side. +Let's suppose Alice has x tokens on an EVM-compatible chain and wants to transfer them to MultiversX to a contract while invoking +a function on that contract. The steps and flow are the following: -2. Then register support for the `multiversx` chain: `ampd register-chain-support amplifier multiversx` +* Alice deposits the ERC20 tokens that she wants to transfer to the MultiversX network in the EVM-compatible chain's **Safe** contract, + on a special endpoint also providing the MultiversX's contract address, function, the parameters for the invoked function, and a minimum + gas-limit to be used when invoking the function; +* The EVM-compatible chain's **Safe** contract groups multiple deposits into batches, regardless of whether the deposits are of this type or 1.a. type; +* After a certain period of time (defined by the finality parameters of the EVM-compatible chain), each batch becomes final and starts being processed by the relayers; +* The relayers propose, vote, and perform the transfer using MultiversX's **Bridge** contract with a consensus of a minimum of 7 out of 10 votes; +* On the MultiversX network, the same amount of ESDT tokens are minted or released, depending on the token's settings; +* The minted or released tokens, along with the smart-contract call parameters (contract address, function, parameters, and minimum gas limit) are then moved in + the specialized contract called **BridgeProxy**; +* On the **BridgeProxy** contract there is an endpoint for executing the smart-contract call. Alice or any MultiversX entity willing to + spend the gas for execution can call the endpoint. The minimum gas limit for the execution is the one specified by Alice, or it can be higher; +* The entity triggering the execution flow will pay the gas limit and the **BridgeProxy** will handle the execution which can have 2 outcomes: + * The call was successful: in this case, the contract will be credited with the tokens sent by Alice, and the invoked function would have produced the desired effects; + * The call was unsuccessful: in this case, the **BridgeProxy** received back the tokens and will mark the transfer as failed. + * The **BridgeProxy** can then be called on another endpoint to attempt the refund mechanism. Alice or any other MultiversX entity willing + to spend gas limit can invoke that endpoint; + * Whoever calls the **BridgeProxy** refund endpoint, will pay the gas limit for the reversed transfer operation. This will also attempt to subtract the fee + as for any normal MultiversX to EVM-compatible chain transfer; + * The transfer is placed in a MultiversX batch and eventually, Alice will get her tokens back on the originating EVM-compatible chain minus the fee. + As stated, the operations on the **BridgeProxy** can be done manually, or by using the **scCallsExecutor** tool provided here https://github.com/multiversx/mx-bridge-eth-go/blob/feat/v3.1/cmd/scCallsExecutor -At this point you can restart the `ampd` process and you should be able to validate MultiversX messages. +The README.md file contained in this directory is a good place to start on how to manually configure the tool and run it (on a dedicated host or VM) ---- +## Notes regarding smart-contract invoked on MultiversX from an EVM-compatible chain: -### Bitcoin L2 +The next diagram explains what happens with a token transfer with a smart-contract call in the direction EVM-compatible chain -> MultiversX when the tokens are unlocked/minted on the MultiversX chain. +The transfer is stored in the **BridgeProxy** (Step 1) and then, anyone can initiate the execution (Step 2). The tokens reach the 3-rd-party smart contract along with the function required to be called +and the provided parameters. -# Bitcoin L2 + + -:::note +As stated above, this is the "happy flow" in which the smart contract call succeeds on the 3-rd-party contract. But what +happens if the invoked function fails? This is described in the next diagram. Step 1 was identical to the previous diagram +and it was omitted. Step 2 got a new step 2.c in which the tokens return to the **BridgeProxy** contract and the whole transfer +is marked as failed and ready to be refunded on the original source EVM-compatible chain **to the original sender address**. +There is another Step 3 involved, in which, anyone can call the refund method. -This documentation is not complete. More content will be added once it is accepted and discussed on Agora or once it is implemented and available for production. + + + +Step 2 and Step 3 can be automatically triggered with the help of the **scCallsExecutor** tool referenced above. +:::important +The SC call data will need to be assembled from the EVM-compatible chain side and should respect the MultiversX Rust Framework +encoding for all provided parameters. ::: ---- +The following resource will exemplify how the correct endpoint is to be called on the EVM-compatible chain side: +https://github.com/multiversx/mx-bridge-eth-sc-sol/blob/main/tasks/depositSC.ts -### Concept +The correct encoded data can be generated by using the `encodeCallData` function provided in this typescript implementation +https://github.com/multiversx/mx-sdk-js-bridge/blob/main/helpers/encodeCallData.ts +or, using the `EncodeCallDataStrict` function from the Golang implementation +https://github.com/multiversx/mx-bridge-eth-go/blob/feat/v3.1/parsers/multiversxCodec.go -# Concept +--- -:::note +### Validators -This documentation is not complete. More content will be added once it is accepted and discussed on Agora or once it is implemented and available for production. +# How to become a validator -The content created here is derived from: -- [Sovereign Shards - MIP7 - part - 1](https://agora.multiversx.com/t/sovereign-shards-mip-7-part-1/185) +:::note +This version of the documentation focuses solely on the essential steps required to set up and deploy a sovereign chain on either a local or remote computer. More content will be added as it is accepted and discussed on [Agora](https://agora.multiversx.com/), or once it is implemented and available for production. ::: -The MultiversX blockchain utilizes a fully sharded architecture designed for scalability, processing tens of thousands of transactions per second, and allowing developers to deploy decentralized applications using smart contracts. The in-protocol token standard, ESDT, facilitates simple, inexpensive, secure and scalable token transfers. As the network expands, new shards are created, enabling horizontal scaling. Additionally, parallelization within the same shard supports vertical scaling. The system is decentralized, public, open, and uses EGLD as its base token. +--- -However, certain systems and applications require more customization than what a general-purpose network like MultiversX can offer. App-specific blockchains are tailored to run specific applications, providing several benefits: +### Vm Intro -- **Increased Performance**: Dedicating blockspace and computational resources to one concept can improve transaction throughput, reduce latency, and lower fees. This is important for applications needing high performance, such as gaming, real world use cases or DeFi. +# Introduction -- **Flexibility and Sovereignty**: Developers can customize various aspects of their blockchain, including the security model, fee token and model, governance mechanism, and VM. This customization can attract and retain users by offering unique incentive schemes, tokenomics models, and user experiences. +The MultiversX protocol is designed so that integrating a new executor, a new processor, or even a completely new VM is straightforward. In essence, any new VM only needs to implement the `VMExecutionHandler` interface. Currently, there are two VMs running on MultiversX: -Sovereign Chains aim to provide an SDK that allows developers to create highly efficient chains connected to the global MultiversX Network. This enables composability, interoperability, and simplified usability across all chains, leveraging existing infrastructure and enhancing user experience. +- **SpaceVM**: Handles general smart contracts running on WASM. +- **systemVM**: Specialized for builtin system smart contracts written in Go. -## Historical Context +For sovereign shards, we introduced the option for a WasmVM smart contract to call a systemVM smart contract through the `BlockchainHookInterface`, specifically via the `ExecuteOnDestOnOtherVM` endpoint. This is necessary because both VMs reside in the same shard on sovereign shards. On the mainnet, however, WasmVM contracts can interact with systemVM contracts only through an asynchronous call, since systemVM exists exclusively on the metachain. -### Issues with Layer2 Solutions +When considering the EVM or other VMs, on MultiversX, it’s important to note that developers will likely want to interact with WasmVM contracts as well. Put simply, a smart contract should be able to interact with both WasmVM and other VM contracts in a uniform way, and that abstraction must happen at the VM level. The WasmVM already handles this by smartly calling the `ExecuteOnDestOnOtherVM` endpoint as needed. -The concept of Sovereign Chains is introduced to address some of the most encountered issues: +If we look under the hood we have SpaceVM which can actually accommodate multiple **EXECUTORS**. In the case of current mainnet/sovereign SpaceVM the **EXECUTOR** is *WASMER2.0* with a few additions from our side. Wasmer is written in rust and SpaceVM is written in GO. The SpaceVM has a big set of **OP_CODES**, from storage handling, to memory handling to crypto operations, big floats, and more. These **OP_CODES** are represented as pointer functions, written in GO and they have an access pointer. The executor, our modified Wasmer, receives this set of functions as a LIBRARY and when a SmartContract calls one of the **OP_CODES**, this calls the internal library added to WASMER which gets executed in the GO code of SpaceVM. -- **Unified Liquidity and Application Ecosystem:** Sovereign Chains eliminate the need for fragmented multi-signature wallets by providing a unified ecosystem. This ensures that liquidity is not dispersed and applications can operate seamlessly without silos. +:::note +Adding a new VM means introducing a new Executor under SpaceVM architecture. +::: -- **Simplified User Experience:** With Sovereign Chains, users interact within a single ecosystem that supports all necessary operations, eliminating the need for multiple gas tokens. This streamlines the user experience, making interactions more intuitive and user-friendly. +--- -- **Built-In Interoperability and Composability:** Sovereign Chains are designed with interoperability and composability as core features. Applications deployed on Sovereign Chains can interact seamlessly, leveraging built-in protocols that ensure secure and efficient cross-chain operations. +### Wallets - Overview -- **Seamless Fund Movement and Deployment Adaptation**: Users can move funds effortlessly across different shards and deployments within the Sovereign Chain ecosystem. This flexibility simplifies adaptation to various applications and enhances overall adoption. +Wallets give access to your funds and MultiversX applications. Only you should have access to your wallet. -## Concept of Sovereign Chains -**High-Level Features** +## MultiversX Wallets -- **High Performance**: Capable of >4000 DeFi/Gaming transactions per second, achieved by a new consensus model that dedicates 90-95% of time to processing. Nevertheless this model will also be launched on the mainchain. -- **Configurable Features**: Options for private vs. public chains, staking setups, smart contracts, ESDTs, and customizable gas and transaction fees. +The private key associated to an address (erd1...) needs to be stored in a specific format. In order to use that private +key to sign transactions and perform various actions, one needs a wallet. -**Virtual Machine (VM) Configurations** +There are multiple ways you can store your funds. This page will present some of them. -- **Default VMs**: SystemVM (GO) and WasmVM. -- **Custom VMs**: - - Ethereum Compatible VM; - - Bitcoin Compatible VM; - - Solana Compatible VM; -**Built-in Cross-Chain Mechanism** +## Table of contents + +| Name | Description | +| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| [xPortal App](https://xportal.com/) | Digital wallet and global payments app that allows you to exchange and securely store money on your mobile phone. | +| [Web Wallet](/wallet/web-wallet) | MultiversX Web Wallet | +| [xAlias](/wallet/xalias) | Single sign-on solution for Web3, powered by Web2 (Google Sign-In). | +| [Web Wallet - tokens operations](/wallet/create-a-fungible-token) | Learn how to perform tokens operation inside Web Wallet. | +| [MultiversX DeFi Wallet](/wallet/wallet-extension/) | MultiversX DeFi Wallet Extension | +| [Ledger](/wallet/ledger) | Ledger Hardware Wallet | +| [Keystore](/wallet/keystore) | Learn more about how to use the **keystore** file. | + +--- -A built-in cross-chain mechanism facilitates token transfers between the sovereign chain and the MultiversX network without relayers. The process ensures seamless interoperability and security through validator verification and proof systems. +### Web Wallet -## Cross-Chain Mechanism Concept +Use the wallet to send, receive and store EGLD in a secure manner. Includes automations for interacting with staking products and ecosystem pools. -### Mainchain to Sovereign Chain Concept +You can log in on the Web Wallet using a keystore file, using the xPortal App, using a Ledger device or a pem file. For now, we are going to create a keystore wallet and connect on the Web Wallet using it. -#### 1. Sovereign Notifier Service and Header Inclusion -Sovereign Notifier Service +## **Create a new wallet** -Validators on the Sovereign Chain should run a notifier service to monitor cross-chain transactions from the MultiversX mainchain. This service can connect to the validators' own nodes or public notifier services to receive notification events. +Go to [https://wallet.multiversx.com](https://wallet.multiversx.com/) & carefully acknowledge the instructions provided. -Header Inclusion Process +Click on "Create a new wallet": -- *Detection*: When the leader validator detects a new header from the MultiversX mainchain, it attempts to include this header in the current block it is building. -- *Adding* `ShardHeaderHash`: The leader specifically adds the MultiversX `ShardHeaderHash` to the `SovereignShardHeader`. -- *Transaction Processing*: If there are cross-chain transactions from the mainchain, the protocol mandates the leader and validators to process all these transactions before adding the new `ShardHeaderHash`. This mimics the cross-shard transaction processing in MultiversX. +![img](/wallet/web-wallet/wallet_landing_page.png) -ShardHeader Inclusion +Carefully read and acknowledge the information, then click "Continue". -1. A `SovereignShardHeader` can contain multiple ShardHeaderHashes. -2. The leader adds only finalized ShardHeaders from the mainchain. -3. New MultiversX HeaderHash cannot be added if previous transactions are not executed. +![img](/wallet/web-wallet/wallet_create.png) -#### 2. Execution of Incoming Transactions -Creating Miniblocks -- *Transaction Collection*: The leader collects all incoming cross-chain transactions and creates a miniblock named `“INCOMING TXS”`. -- *Data Preparation*: The notifier service prepares relevant data, including proof of execution, data availability, correctness, and finality. -- *Data Sending*: This prepared data is pushed to the Sovereign Chain client. +## **Save your secret phrase! This is very important.** -Validator Verification +The words, numbered in order, are your Secret Phrase. They are just displayed on your screen once and not saved on a server or anywhere in the world. You only get one chance to save them - please do so now. -1. Validators on the Sovereign Chain verify the leader's miniblock creation. -2. Validators perform header verification, incoming transaction creation, proof verification, and finality checks. -3. If verification fails, validators do not sign the block, preventing its creation. +Click the “copy” (two rectangles) button and then paste them into a text file. If your pets don’t usually find important pieces of paper to be delicious, you could even write the words down. -Gas-Free Execution +![img](/wallet/web-wallet/wallet_mnemonic.png) -1. Incoming cross-chain transactions are executed without gas on the Sovereign Chain. - - these transactions are treated as DestME ESDT transfers, ensuring fast execution and complete integration with the MultiversX chain. +The next page is a test to see if you actually have saved the Secret Phrase. Enter the random words as indicated there and press "Continue". -#### 3. UI and Smart Contract on MultiversX Chain +![img](/wallet/web-wallet/wallet_quiz.png) -The intention is that the user should not feel the complexity of transferring a token from one chain to the other. That's why in 3 easy steps a cross-chain transaction should be felt as successful: +You are one step away from getting your Keystore File. First, encrypt it with a password. Make sure it’s the strong kind, with 8 characters, at least one UPPER-CASE letter, a $ymb@l and numb3r. -1. Users execute a transaction on the MultiversX chain directed towards the cross-chain contract. -2. Upon successful transaction on the mainchain, the user receives tokens on the Sovereign Chain. -3. The execution and transfer are done as the initial user, maintaining seamless cross-chain interaction. +![img](/wallet/web-wallet/wallet_password.png) -But how do we achieve that? Even though the main components are going to be described at several stages in the documentation, from conceptual point of view we have the: +In case you forget this password, you can get a new Keystore File with your secret phrase. Remembering it is always better. -ESDTSafe Contract +Congratulations, you have a new wallet! The associated Keystore File was downloaded to wherever your browser saves files by default. The file has the actual address of the wallet as default name, something like “erd….json”. You can rename it to “something.json” so it’s easier to manage, if you want. -- Functionality: Users can deposit tokens and specify the destination address and execution call. -- Endpoint: `deposit@address@gasLimit@functionToCall@arguments` receiving a `MultiESDTNFTTransfer`. -- Verification: The contract verifies the validity of the address and generates a specific `logEvent`. +![img](/wallet/web-wallet/wallet_created.png) -`logEvent` Structure -```json -Identifier: deposit -Address: scAddress -Topics: address, LIST -Data: localNonce (increasing), originalSender, gasLimit, functionToCall, arguments -``` -Customizable Features +## **Access a wallet** -- General Fee Model: Configurable fee and fee token. -- Whitelist/Blacklist: Tokens can be whitelisted or blacklisted for transfer. +Go to https://wallet.multiversx.com/ and click on "Access Existing" Make sure the “Keystore file” access method is selected, click Browse and locate your Keystore File [erd1… .json], then put in your password and click "Access Wallet". -#### 4. Sovereign Chain Cross-Chain Execution -Preparation and Miniblock Creation +![img](/wallet/web-wallet/wallet_login.png) -1. The notifier service generates an Extended Shard Header structure and an Incoming Txs miniblock based on logEvents. -2. Validators finalize blocks by including mainchain shard headers and all incoming transactions. +And you’re in! Your EGLD address is on top, you can use the “copy” button (the two rectangles) to copy it to the clipboard. -Token ID Collision Avoidance +![img](/wallet/web-wallet/wallet_welcome.png) -To avoid token ID collisions, each deployed Sovereign Chain adds a unique prefix or increasing nonce when registering token IDs on the ESDT SC. This ensures clear distinction of token origins on the Sovereign Chain. -#### 5. Mainchain to Sovereign Shard Process +## **Overview of your EGLD balance** - Notifier Service: Validators receive notifications of mainchain bridge transactions. - Header Inclusion: Leaders include finalized MultiversX ShardHeaders in the SovereignShardHeader. - Transaction Execution: Leaders collect and execute incoming transactions, forming an INCOMING TXS miniblock. - Validator Verification: Validators verify and sign the block, ensuring all transactions are processed. - Gas-Free Execution: Transactions are executed without gas on the Sovereign Shard, treated as DestME ESDT transfers. +After logging into your wallet, your EGLD balances are immediately visible and displayed in easy to follow boxes. -### Sovereign Chain to MultiversX Mainchain +![img](/wallet/web-wallet/wallet_balance_overview.png) -The Sovereign Chain to MultiversX Mainchain cross-chain communication process involves synchronizing the mainchain with the Sovereign Chain, handling cross-chain transactions, and ensuring smooth token transfers between the two. This description provides a conceptual explanation of the roles, processes, and smart contract interactions involved. +- **Available:** Freely transferable EGLD balance +- **Stake Delegation:** Amount of EGLD delegated towards a Staking Services provider +- **Legacy Delegation:** Amount of EGLD delegated towards MultiversX Community Delegation +- **Staked Nodes:** Amount of EGLD locked for your staked nodes -#### Responsibilities of Sovereign Validators -Sovereign Validators have two primary responsibilities: +## **Send a transaction** -1. Syncing with the mainchain and pushing bridge transactions. -2. Managing transfers from Sovereign to Main and vice versa. +Click "Send" on the right-hand section of the wallet: -Types of Tokens +![img](/wallet/web-wallet/wallet_send.png) -There are two types of tokens involved in the bridging process: +Input the destination address & amount, and then click "Send". -- **Token Type A**: Tokens initially originating from Main to Sovereign, with liquidity held in the safe contract on Main. -- **Token Type B**: New tokens first issued on the Sovereign Chain, requiring creation and specific roles (`localMint`, `nftCreate`) for the `esdt-safe` contract. +![img](/wallet/web-wallet/wallet_send_tx.png) -**Cross-Chain tx Process** + Verify the destination and amount and click "Confirm". -Token Deposit +![img](/wallet/web-wallet/wallet_confirm_tx.png) -**1. Users Deposit Tokens:** - Users on Sovereign deposit tokens into the `esdt-safe` contract on the Sovereign Chain. Now tokens are either burned (for Token Type A) or kept in the safe (for Token Type B). +After confirming the transaction you can see the progress and completion of the transaction. -**2. Block Creation and Finalization:** - The Sovereign chain creates the next block and finalizes it. Outgoing transactions (Crossing to MultiversX) are compiled into compressed operations data. +![img](/wallet/web-wallet/wallet_success_tx.png) -**3. Header Hash Addition:** - The leader adds the hash of the outgoing transaction data to the header of the Sovereign Chain block. Validators reach consensus, signing the header, which the leader combines using BLS multi-signature. +You can always review your transaction history in the "Transactions" menu on the left-hand side of the wallet. -**4. Posting to Mainchain:** - The signed header and list of outgoing transactions are posted to the MultiversX chain. The transaction data on MultiversX contains the signed header of the Sovereign chain, the outgoing operations data hash, and the full arguments of the outgoing operations. +![img](/wallet/web-wallet/wallet_transactions.png) -**5. Contract Validation and Execution:** - The contract on MultiversX validates the signed header and BLS multi-signature. It verifies if -```rust -hash(outgoing operations data) = header.outgoingOpsHash -``` +## **Receiving EGLD in your wallet** -The bridge contract executes the operations, performing `TransferESDT/MultiTransferESDTNFT` for *Token Type A* and `minting/creating NFTs` for *Token Type B*. +After logging into your wallet, as described above, you will be able to see your wallet address and share it with others, so they can send you EGLD. -**Finality and Consensus** +Your address is immediately visible on the top part of the wallet. You can copy the address by pressing the copy button (two overlapping squares). -To ensure the integrity and synchronization between the Sovereign Chain and the mainchain, the following steps are taken: +You can also click "Receive" on the right-hand side to see a QR code for the address, which can be scanned to reveal the public address. -- The Sovereign chain header includes an outgoing operations hash, which encapsulates the details of the operations to be executed on the mainchain. -- Once the outgoing operations are executed on the mainchain in subsequent blocks, the Sovereign chain remains synchronized by validating every MultiversX header hash. -- The finality gadget plays an important role by ensuring that a header with *Nonce X* on the Sovereign chain is only finalized after confirming that the mainchain has executed the outgoing operations from the previous header. This interconnected process maintains consistency and order of operations. +![img](/wallet/web-wallet/wallet_receive.png) -**Incentives and Validation** -To encourage validators to ensure the smooth operation of bridge transactions and maintain network integrity, the following incentive and validation mechanisms are implemented: +## **Testnet and Devnet faucet** -- **Incentive Structure Proposal:** Validators are incentivized to push outgoing transactions to the mainchain by distributing collected fees among them. Specifically, 10% of the fees are allocated to the leader, while the remaining 90% is distributed among the other validators. This incentivization ensures active participation and prompt processing of transactions. +You can request test tokens from [Testnet Wallet](https://testnet-wallet.multiversx.com) or [Devnet Wallet](https://devnet-wallet.multiversx.com) in the `Faucet` tab. -- **Validation Process:** Finality is crucial to maintaining the order and integrity of transactions. The system ensures that outgoing operations are executed in sequence without any gaps. This is achieved by generating `logEvents` on the mainchain, which are then pushed to the Sovereign Chain. These `logEvents` serve as proof of execution, ensuring validators can verify the completion and correctness of transactions, thereby maintaining a reliable and secure cross-chain operation. +The faucet is only available once in a given time period. Other alternatives for getting test tokens are: -#### Smart Contracts for Cross Chain Tx +- request tokens on [Telegram - Validators chat](https://t.me/MultiversXValidators); +- use a third-party faucet, such as [https://r3d4.fr/faucet](https://r3d4.fr/faucet); +- request tokens on [Discord - NETWORKS](https://discord.gg/multiversxbuilders) category. -Process: -1. `SovereignMultiSigContract` is created and is the parent of the *ESDTSafe* contract, and only the `sovereignMultiSigContract` is allowed to transfer out funds from the `ESDTSafe` contract. +## **Guardian** -2. The OutGoingTXData BLS MultiSigned by the validators will be put inside a mainchain transaction and sent by the leader of the Sovereign Chain for the current block. This transaction contains a set of token operations for a set of addresses. -``` -txData = bridgeOps@LIST, gasLimit,functionToCall, Arguments>>@Nonce@BLSMultiSig -``` +Starting with Altair release, a new section for Guardian feature is available in the wallet interface: -The `sovereignMultiSigContract` first verifies if the BLSMultiSig is valid and whether it is signed by 67% of the BLSPubkeys registered in the sovereignMultiSigContract. If yes, the contract calls with the ESDTSafe contract to transfer the set of tokens. The ESDTSafe contract will iterate on the given list and make a multiESDTNFTTransfer to the given addresses with the given tokens. +![img](/wallet/web-wallet/guardian_feature1.png) -The contract emits a logEvent the same way as mainchain to sovereign ESDTSafe SC does, in order to keep track of the processed outgoing transactions. This logEvent will be pushed towards the sovereign shard, in order to notarize the finalization of processing of the OutGoingTxData. This will close the loop of processing and offer utmost security for all funds. -``` -Identifier = bridgeOps -Address = scAddress -Topics = sender, hash(outGoingTxData) -Data = nonce - increasing from internal storage -``` -The created event is an attestation that the `outGoingTx` was executed on the mainchain, and using this attestation on the sovereign chain, the rewards can be distributed from the accumulated fees. +### **Registering the Guardian** ---- +The initial step involves registering your guardian. To begin, access the wallet menu and select the Guardian feature. From there, you should have the option to register and set it up. -### Cross Chain Execution +![img](/wallet/web-wallet/guardian_step1.png) -# Introduction +To establish a Guardian, you will need to utilize an authenticator app that is capable of generating two-factor authentication (2FA) codes. This app will enable you to set up and manage the Guardian feature effectively. -When we take a look at the blockchain industry, we observe a segregated ecosystem lacking cohesion, interoperability and teamwork. The vision lead to the Blockchain Revolution, knows as “Web3” — a new era of the internet that is user-centered, emphasizing data ownership and decentralized trust. +:::note +Please use the **Authenticator app name tag** to assign a recognizable label within the authenticator app, which will help you locate it easily when searching in the app. Once you have installed the authenticator app, scan the provided QR code using the app and enter the Guardian Code into the designated fields (refer to the below image). +::: -Sovereign Chains will dismantle the barriers between isolated blockchain networks by allowing smart contracts to seamlessly interact across different Sovereign Chains and the main MultiversX chain. -This cross-chain interoperability is crucial for fostering an environment where decentralized apps (dApps) can utilize functionalities or assets from across the ecosystem. +:::important +If this is the first time when you are doing the registration, you don't have to select the **I cannot scan a QR code, show me the manual setup key**. +::: -## What is Cross-Chain Execution? +![img](/wallet/web-wallet/guardian_step2.png) -Cross-Chain execution is the ability of a smart contracts or a decentralized applications on one blockchain to invoke actions on another blockchain. This feature allows for smart contract execution or transfer of funds from one chain to another, enabling developers to build applications that are chain agnostic. +**Check & Confirm** the transaction: -## Cross-Chain Execution within Sovereign Chains +![img](/wallet/web-wallet/guardian_step3.png) -Since a Sovereign Chain is a separate blockchain with a different rule-set from the MultiversX blockchain, there has to be a way of communication between them. The interaction is being done by Smart Contracts, the Sovereign Bridge Service and Nodes. -> The MultiversX blockchain will be referred as the _MultiversX Mainchain_ in the further sections. +### **Activation period** -![To Sovereign](../../static/sovereign/to-sovereign.png) +After successfully completing all the previously mentioned steps, you will need to wait for the registration period to conclude. For the Mainnet, this registration period will extend over a duration of **20 epochs**. -When a transaction starts from the MultiversX Mainchain, either from a wallet or a smart contract, it goes through the `Mvx-ESDT-Safe` smart contract. The Observer nodes monitor the events that the deposit transaction emits and then the Sovereign Nodes notarize the state changes inside the Sovereign Chain. This notarization means the end of a Cross-Chain transfer. +:::important +During the waiting period for your Guardian to become active, all interactions with the blockchain will continue to function as usual. There should be no noticeable changes or differences in the way transactions are processed. +::: -![From Sovereign](../../static/sovereign/from-sovereign.png) +![img](/wallet/web-wallet/guardian_step4.png) +### **Verify Access** -When the transaction is generated from the Sovereign Chain, the `Sov-ESDT-Safe` smart contract must be used. This smart contract will generate events and from that point, the sovereign nodes, at the end of the round, will read all the outgoing operations (plus all the unconfirmed operations), then these are signed by all the validators, then send to the bridge service, which will make the transaction on main chain +As depicted in the above-presented picture, you have the possibility to check if you still have access to your authenticator app by clicking the *Verify Access* button. -This is a high-level description of the whole process, the smart contracts that take place in it are far more detailed and have a lot of specific scenarios and behaviours. The current Sovereign Chain suite consists of four main contracts, here is the high-level description for some of the cross chain smart contracts: +![img](/wallet/web-wallet/guardian_step41.png) -## Mvx-ESDT-Safe & Sov-ESDT-Safe -The two contracts have the same role: to facilitate a cross-chain execution depending on what side the process starts. The reason for the prefix of `Sov` and `Mvx` is to show where the smart contract is deployed, `Sov` means that the contract is deployed on a Sovereign Chain and `Mvx` that is deployed on the MultiversX Mainchain. There will be an in-depth description of each smart contract in the upcoming modules ([`Mvx-ESDT-Safe`](mvx-esdt-safe.md) and [`Sov-ESDT-Safe`](sov-esdt-safe.md)). The description will consist of flows for the cross-chain interactions, important modules and endpoints. +If you still have access to the correct authenticator app, you will receive a success message indicating that the activation process is complete. In such a case, you will not need to repeat the entire registration process from the beginning once the activation period is over. -Cross-chain transfers imply sending funds through the smart contracts mentioned above. There are two types of bridging mechanism available: *Lock&Send* and *Burn&Mint*. +![img](/wallet/web-wallet/guardian_step42.png) -#### Lock and Send -Lock & Send: a custodial bridging mechanism in which the source-chain tokens are locked inside the bridge contract, while an equal amount is minted on the destination chain. The locked balance on the source chain backs the circulating supply on the sovereign chain 1-for-1 until the tokens are returned and unlocked. +In the event that you lose access to the authenticator app, you will be required to go through the entire process again by clicking on **"Change Guardian"** and following all the aforementioned steps, starting with the registration of the Guardian. This means you will need to repeat the steps described earlier in order to set up the Guardian feature anew. -#### Burn and Mint -Mint & Burn: a non-custodial bridging mechanism where the source-chain tokens are burned (permanently removed from supply) and an identical amount is minted on the destination chain, so the total supply moves between chains without any tokens being held in escrow. +![img](/wallet/web-wallet/guardian_step43.png) -## Fee-Market -Since every Sovereign Chain will have a customizable fee logic, it was paramount that this configuration had to be separated into a different contract. The rules set inside this contract are: -* fee per transferred token -* fee per gas unit -* users whitelist to bypass the fee -This contract is also present in the MultiversX Mainchain and in any Sovereign Chain. +### **Guarding your account** -## Header-Verifier -Any cross-chain transaction that happens inside a Sovereign Chain is called and *operation*. The main role of this contract is to verify operations. They have to be signed by the validators of the Sovereign Chain. If the operation is successfully verified it will be registered and then can be executed by the `Mvx-ESDT-Safe` smart contract. All the *BLS keys* of the validators will be stored inside this contract. The in-depth description of how those _operations_ are registered can be found in the [`Header-Verifier`](header-verifier.md) module. +Once the activation period has elapsed, you should find the option to "Guard your account" enabled, as indicated in the picture provided above. By clicing it, will enable the account guarding feature for your account. -:::note -The source for the smart contracts can be found at the official [MultiversX Sovereign Chain SCs repository](https://github.com/multiversx/mx-sovereign-sc). +:::important +In order to Guard your account, a normal ```GuardAccount``` self-transaction will be sent to the blockchain. ::: -## Sovereign Bridge Service -This feature facilitates the execution of outgoing operations. This service is an application that receives Sovereign operations. After that, it will call the `execute_operation` endpoint from the `Mvx-ESDT-Safe` smart contract. The registration and execution of operations looks like this: +![img](/wallet/web-wallet/guardian_step6.png) -- For N operations there is only one [register transaction](mvx-esdt-safe.md#registering-a-set-of-operations) inside the Header-Verifier smart contract. -- N transactions for the [execution](mvx-esdt-safe.md#executing-an-operation) of N operations inside the ESDT-Safe smart contract, one execution transaction per operation. +Upon the successful completion of a transaction, your account will be guarded, signifying that the account guardian feature is active. **Congratulations! Your funds are now secure and protected.** -> There can be one or more services deployed in the network at the same time. +![img](/wallet/web-wallet/guardian_step44.png) ---- +As depicted in the image, 3 new options are now available to the user. +- [Verify Access](/wallet/web-wallet#verify-access); +- [Change Guardian](/wallet/web-wallet#changing-your-guardian); +- [Unguard Account](/wallet/web-wallet#unguarding-your-account). -### Custom Configurations +:::info +New information has been added to the guardian dashboard. -# Custom Configurations +![img](/wallet/web-wallet/guardian_step46.png) -## Sovereign network customisations +**Your current guardian is** the **guardian address.** Once you enable the account guardian feature, any outgoing transaction initiated by this account will be considered a guarded transaction. This means that such transactions will require two signatures: one from the account owner, as before, and another from the account guardian, which is the address you can see here. -The Sovereign Chain SDK is built with flexibility in mind, allowing you to tailor it to your specific needs. This page highlights various customizations you can apply to make your network unique. +To obtain the guardian's signature for a transaction, you don't need to take any action. The TCS (Trusted Co-Signer Service - Guardian Service) will automatically provide the valid guardian signature for the user's transaction whenever a user with a guarded account sends a transaction from their wallet and provides a valid two-factor authentication (2FA) code. The wallet manages the interaction with the TCS, so the user only needs to enter a valid 2FA code. -### config.toml +::: -- `GeneralSettings.ChainID` - defines your unique chain identifier -- `EpochStartConfig.RoundsPerEpoch` - defines how many round are in each epoch -### economics.toml +### **Sending a guarded transaction** -- `GlobalSettings.GenesisTotalSupply` - total native ESDT supply at genesis -- `GlobalSettings.YearSettings` - adjust the inflation rate each year -- `FeeSettings` - adjust the fee settings as needed +Once you have set a guardian and initiated the ```GuardAccount``` transaction, all subsequent transactions must be guarded to be validated by the blockchain. This means that you must enter the 2FA code in order to proceed with these transactions when using the web-wallet. -### ratings.toml +Let's take for example a simple eGLD transfer transaction: -- `General` - adjust the rating parameters as needed +![img](/wallet/web-wallet/guardian_sendTx1.png) -### systemSmartContractsConfig.toml +Confirm the transaction: -- `ESDTSystemSCConfig.ESDTPrefix` - the prefix for all issued tokens -- `ESDTSystemSCConfig.BaseIssuingCost` - base cost for issuing a token -- `StakingSystemSCConfig.NodeLimitPercentage` [[docs](https://docs.multiversx.com/validators/staking-v4/#how-does-the-dynamic-node-limitation-work)] +![img](/wallet/web-wallet/guardian_sendTx2.png) -### sovereignConfig.toml +In the last step, you will need to check your authenticator tool to retrieve the Guardian code (2FA code) and enter it for verification. See picture below: -- `GenesisConfig.NativeESDT` - Native ESDT identifier for the Sovereign Chain +![img](/wallet/web-wallet/guardian_sendTx3.png) -### prefs.toml -The `OverridableConfigTomlValues` will overwrite the parameters in the config files. Make sure that your new config parameters are not overwritten by this file. +### **Unguarding your account** -:::note -These are just a few examples that you can adjust to make the Sovereign Chain unique. All the files you could adjust when creating a Sovereign Chain can be found in the [deployment guide](/sovereign/distributed-setup#step-4-edit-the-sovereign-configuration). -::: +Unguarding your account consists of sending a guarded ```UnGuardAccount``` transaction to your own address. You can do it by simply using the **Unguard Account** button from the Guardian section: -:::note -We will continue to add configurations for features such as token-less chains, gas-less chains, and other customizations at a later stage, following their implementation. -::: +![img](/wallet/web-wallet/guardian_step44.png) ---- +And confirming the transaction after checking the validity of it: -### Disclaimer +![img](/wallet/web-wallet/guardian_unguard2.png) -# Disclaimer +And validating it via the authenticator app: -:::note - This is a living document. More content will be added as it is accepted and discussed on Agora, or once it is implemented and available for production. As this documentation evolves, some sections may be updated or modified to reflect the latest developments and best practices. Community feedback and contributions are encouraged to help improve and refine this guide. Please note that the information provided is subject to change and may not always reflect the latest updates in the technology or procedures. All information and changes will be communicated and discussed with the community. -::: +![img](/wallet/web-wallet/guardian_step45.png) -## Support and Scope +Due to the transaction being guarded, there is no cooldown period required. The transaction will be processed instantly without any delay. -While MultiversX provides the **chain sdk** core code and scripts necessary to start a sovereign chain, it is important to note that the infrastructure for all aspects of sovereign setup is not provided. MultiversX focus is on delivering the tools and resources needed to launch and maintain a sovereign chains. However, users may need to describe the need for additional support or resources or for more specific or advanced requirements by using Agora forum or other support channels that are available for them to use. -### What MultiversX Provides +### **Changing your guardian** -- **Core Code**: The fundamental codebase necessary to run a sovereign chain. -- **Scripts**: Automated scripts to assist with the initial setup and deployment processes. -- **Documentation**: Guides and manuals (like this one) to help you understand and implement the core functionalities. -- **Technical Support**: Technical support especially for potential issues or configurations that are caused by the above mentioned materials. +The Guardians feature allows you to change your guardian in situations where you suspect your account has been compromised or if you have lost access to your Authenticator. -### What will be supported in the limit of capacity +![img](/wallet/web-wallet/guardian_change7.png) -- **Comprehensive Technical Support**: In-depth, personalized support for all potential issues or configurations. -- **Custom Solutions**: Any specific customizations or bespoke solutions required by individual users. -- **Operational Management**: Day-to-day management and operational support for running a sovereign chain. +This can be achieved by clicking th *Change Guardian* button from the Guardian's main dashboard. Afterwards you will be faced with two options: -### Living Document -As blockchain technology and the concept of sovereign chains continue to evolve, this documentation will also undergo changes. MultiversX may add new sections, update existing content, or deprecate information that becomes outdated. These changes aim to ensure that the documentation remains relevant and useful for all users. +#### **Changing guardian with 20 epochs pre-registration time** -MultiversX commits to transparency and collaboration in this process. All significant updates and changes will be communicated to the community through appropriate channels, and feedback will be actively sought to ensure the documentation meets the needs of its users. A weekly technical call will be scheduled where all issues, concerns and problems can be directly addressed to the development team. +1. **If you have lost access to the authenticator app**, you can address this issue by selecting the option *"I cannot access my guardian"* from the window mentioned below. -### Community Involvement +![img](/wallet/web-wallet/guardian_change1.png) -Your feedback is important in helping MultiversX improve the documentation. We encourage community members to participate in discussions on Agora and other platforms, sharing their experiences, challenges, and suggestions. Together, we can create a more robust and comprehensive resource for everyone involved in the MultiversX ecosystem. +2. And follow the normal guardian registration process of registering a **new guardian**: ---- +![img](/wallet/web-wallet/guardian_change3.png) -For any questions or further assistance, please refer to the [Engage the developer community section](https://multiversx.com/builders-hub) or the [official agora forum](https://agora.multiversx.com/). +3. Introduce the 2FA code provided by your Authenticator App for the **new guardian**: ---- +![img](/wallet/web-wallet/guardian_change4.png) -### Distributed Setup +:::note +The above described process will need 20 epochs for your Guardian to become active. If you did not lose access to the authenticator app there is no reason why you should proceed this way. If you will have a pending guardian for 20 epochs which was not registered by you it means that your account has been compromised and you must move your funds to a safe account. +::: -# Distributed Setup +It is important to be aware of certain indicators that indicate the necessity of changing your guardian. One such indicator is the presence of a red shield and frame, as depicted in the image below. This visual cue serves as a signal that prompts you to take action and investigate the situation of your account. -## Create distributed Sovereign Chain configuration +![img](/wallet/web-wallet/guardian_change2.png) -This guide will help you deploy a public Sovereign Chain with real validators, enabling a truly decentralized setup. At its core, blockchain technology—and Sovereign Chains in particular—are designed to operate in a decentralized manner, powered by multiple independent validators. This ensures transparency, security, and resilience, as no single entity has control over the entire system. Unlike other guides we’ve provided, which focus on local setups, this solution emphasizes decentralization by involving multiple stakeholders in the validation process. By following the steps below, the owner can create the Sovereign Chain configuration for the network: +The Guardian Dashboard will signal the fact that you have an unconfirmed guardian change request: -### Step 1: Get the `mx-chain-sovereign-go` Repository +![img](/wallet/web-wallet/guardian_change5.png) -Before proceeding, ensure that a **SSH key** for GitHub is configured on your machine. +Showcasing the details of the request: -1. Clone the GitHub repository: - ```bash - git clone git@github.com:multiversx/mx-chain-sovereign-go.git - ``` +![img](/wallet/web-wallet/guardian_change6.png) -2. Navigate to project directory: - ```bash - cd mx-chain-sovereign-go - ``` +If you were the one who initiated the request to change the guardian and you still have access to the currently active guardian, you will encounter two additional options: -### Step 2: Seeder Build +- Skip the cooldown period by clicking the **Confirm Change** button: By selecting this option, you can proceed with the change of the guardian without waiting for the cooldown period to elapse. -Build and run the seed node -```bash -cd cmd/seednode -go build -./seednode -rest-api-interface 127.0.0.1:9091 -log-level *:DEBUG -log-save -``` +- Cancel the Guardian Change and continue using your current guardian by clicking the **Cancel Change** button: Choosing this option allows you to abort the guardian change process and maintain your existing guardian without any modifications. -You should have an output similar to the one displayed below. The highlighted part is important and will be used later. +Both of the actions will be done by sending a guarded ```SetGuardian``` self-transaction. -| Seednode addresses: | -|---------------------------------------------------------------------------------------------| -| `/ip4/`127.0.0.1`/tcp/10000/p2p/16Uiu2HAmSY5NpuqC8UuFHunJensFbBc632zWnMPCYfM2wNLuvAvL` | -| `/ip4/`192.168.10.100`/tcp/10000/p2p/16Uiu2HAmSY5NpuqC8UuFHunJensFbBc632zWnMPCYfM2wNLuvAvL` | +:::important +If it was not you the one triggering the change of the guardian, it may be the case that your account has been compromised and the scammer is trying to change the guardian to a new one, that he can control. Don't worry, he has to wait 20 epochs in order to have his own guardian becoming active. By then you can: + - move the funds to a different account, that you control, by using the still active guardian. In this case never **Confirm Change.** + - instantly cancel the pending guardian by using the **Cancel Change**, and play the "owning" game with the scammer. **Not recommended.** -:::info -All the validator nodes will have to connect to this seed node. +We recommend the first solution. Even though you have your funds staked, the unbound period (10 epochs) is shorther than the guardian activation period so that you have enough time to unstake and safely move them to a safe account. ::: -### Step 3: Sovereign node build -Build the sovereign node -```bash -cd .. -cd cmd/sovereignnode/ -go build -v -ldflags="-X main.appVersion=v0.0.1" -``` +#### **Instantly changing guardian** -:::info -Use your own custom version instead of `v0.0.1`. -::: +To instantly change your guardian you must use the Change Guardian. You must follow a different set of steps compared to the previous *Change Guardian* way: -### Step 4: Edit the sovereign configuration +1. Use an authenticator app for registering the **new guardian**. You must have access to your already active guardian which means that you don't have to select **"I cannot access my guardian"** option. -Node configs can be found in `cmd/node/config`. Below are the files and folders: -``` -gasSchedules folder -genesisContracts folder -genesis.json* -genesisSmartContracts.json -nodesSetup.json* -api.toml -config.toml -economics.toml -enableEpochs.toml -enableRounds.toml -external.toml -fullArchiveP2P.toml -p2p.toml -prefs.toml -ratings.toml -systemSmartContractsConfig.toml -``` +![img](/wallet/web-wallet/guardian_change8.png) -_Note: Files marked with * will be discussed later in the document._ +2. Open your authenticator app, scan the QR code and register your **new guardian**. -Sovereign configs can be found in `cmd/sovereignnode/config` -``` -enableEpochs.toml -prefs.toml -sovereignConfig.toml -``` +![img](/wallet/web-wallet/guardian_change9.png) -#### Minimum recommended changes +3. Enter the 2FA code of the **new guardian** from the authenticator app. -1. Move the config files from `/node/config` into `/sovereignnode/config`, except _economics.toml_, _enableEpochs.toml_, _prefs.toml_. -2. Config changes: - 1. **config.toml** - 1. GeneralSettings.ChainID - 2. EpochStartConfig.RoundsPerEpoch - 2. **p2p.toml** - 1. KadDhtPeerDiscovery:InitialPeerList = `[/ip4/PUBLIC_IP/tcp/10000/p2p/16Uiu2HAmSY5NpuqC8UuFHunJensFbBc632zWnMPCYfM2wNLuvAvL]` - - PUBLIC_IP is the IP of the machine where seed node is running, the other part is seed node address - 3. **systemSmartContractsConfig.toml** - 1. ESDTSystemSCConfig.ESDTPrefix - 2. StakingSystemSCConfig.NodeLimitPercentage [[docs](https://docs.multiversx.com/validators/staking-v4/#how-does-the-dynamic-node-limitation-work)] - 4. **sovereignConfig.toml** - 1. GenesisConfig.NativeESDT -3. Other changes: - - Use the [custom configuration](/sovereign/custom-configurations) page to see more configs we recommend to be changed +![img](/wallet/web-wallet/guardian_change10.png) -### Step 5: Genesis configuration +4. Check and Confirm the transaction. -#### `genesis.json` +![img](/wallet/web-wallet/guardian_change11.png) -This file should contain all the genesis addresses that will be funded and will be validators. Adjust as needed. +5. Enter the 2FA code of the guardian you want to change. :::note -The sum of `supply` should be equal to `GenesisTotalSupply` from economics.toml +This is the 2FA code of your previous guardian that you have to enter, **not the new guardian's one**. ::: -Example with 2 validators: -``` -[ - { - "address": "erd1a2jq3rrqa0heta0fmlkrymky7yj247mrs54g6fyyx8dm45menkrsmu3dez", - "supply": "10000000000000000000000000", - "balance": "9997500000000000000000000", - "stakingvalue": "2500000000000000000000", - "delegation": { - "address": "", - "value": "0" - } - }, - { - "address": "erd1pn564xpwk4anq9z50th3ae99vplsf7d2p55cnugf00eu0gcq6gdqcg7ytx", - "supply": "10000000000000000000000000", - "balance": "9997500000000000000000000", - "stakingvalue": "2500000000000000000000", - "delegation": { - "address": "", - "value": "0" - } - } -] -``` +![img](/wallet/web-wallet/guardian_change12.png) -#### `nodesSetup.json` +6. Awesome! Wait for your transaction to be processed. From now on your account will be guarded by the new guardian that you recently registered. -This file contains all the initial nodes. Adjust as needed. +![img](/wallet/web-wallet/guardian_change13.png) -:::note -- `consensusGroupSize` should be equal to `minNodesPerShard` -- each node pair contains one genesis address associated with a validator public key -- `startTime` should be a timestamp from the future, the time when the network will start -- `roundDuration` is the duration in milliseconds per round -- `metaChainConsensusGroupSize` and `metaChainMinNodes` should always be 0 -::: +--- -Example: -``` -{ - "startTime": 1733138599, - "roundDuration": 6000, - "consensusGroupSize": 2, - "minNodesPerShard": 2, - "metaChainConsensusGroupSize": 0, - "metaChainMinNodes": 0, - "hysteresis": 0, - "adaptivity": false, - "initialNodes": [ - { - "pubkey": "6a1ee46baa8da9279f53addbfbc61a525604eb42d964bd3a25bf7f34097c3b3a31706728718ccdbe3d43386c37ec3011df6ceb4188e14025ab149bd568cafaba18a78b51e71c24046c5276a187a6c1d6da83e30590a6025875b8f6df8984ec05", - "address": "erd1a2jq3rrqa0heta0fmlkrymky7yj247mrs54g6fyyx8dm45menkrsmu3dez", - "initialRating": 0 - }, - { - "pubkey": "40f3857218333f0b2ba8592fc053cbaebec8e1335f95957c89f6c601ce0758372ba31c30700f10f25202d8856bb948055f9f0ef53dea57b62f013ee01c9dc0346a2b3543f2b4d423166ee1981b310f2549fb879d4cd89de6c392d902a823d116", - "address": "erd1pn564xpwk4anq9z50th3ae99vplsf7d2p55cnugf00eu0gcq6gdqcg7ytx", - "initialRating": 0 - } - ] -} -``` +### Web Wallet Tokens + +## **Introduction** + +**ESDT** stands for _eStandard Digital Token_. + +MultiversX network natively supports the issuance of custom tokens, without the need for contracts such as ERC20, but addressing the same use cases. +You can create and issue an ESDT token from [MultiversX web wallet](https://wallet.multiversx.com/) in a few steps. Let's go over these steps. -___ + +## **Prerequisites** + +- A wallet on MultiversX Network. +- 0.05 EGLD issuance fee +- fees for transactions + + +## **Creating a fungible token from Web Wallet** + +To get started, open up the [MultiversX web wallet](https://wallet.multiversx.com/). You can create a new wallet if you do not have one or import your existing wallet. Here is a [guide](https://docs.multiversx.com/wallet/web-wallet/) to help you navigate. + +On the left sidebar, you will notice the **ISSUE** section. + +![sidebar](/wallet/wallet-tokens/sidebar.png) + +Click on **Tokens**. + +![issue-token}](/wallet/wallet-tokens/issue-token.png) :::note -At this point, a `config` folder should be created that will contain all the .toml files and genesis configuration. This folder should be shared with the other validators so they will be able to join the network. +The Web Wallet will handle the preparation of the transaction. Therefore, if you'd want a token with a supply of 10 and 2 decimals, you should simply put 10 as supply and 2 as number of decimals. ::: -## Join a Sovereign Chain as validator/observer +When creating a token, you are required to provide the token name, a ticker, the initial supply, and the number of decimals. +In addition to these, tokens' properties should be set. -### Sovereign validator setup +Useful resources: -Each validator should have: -- **walletKey.pem** - wallet that will be funded at genesis [[docs](/validators/key-management/wallet-keys)] -- **validatorKey.pem** (or **allValidatorsKey.pem** if multi key node) - validator key [[docs](/validators/key-management/validator-keys/#how-to-generate-a-new-key)] -- **config** folder - received from Sovereign Chain creator +- [Token parameters format](/tokens/fungible-tokens#parameters-format) - constraints about length, charset and so on. +- [Token properties](/tokens/fungible-tokens#configuration-properties-of-an-esdt-token) - what the properties stand for. -### Sovereign validator/observer node start +Enter the required details. Next, click on **_Continue_** button to proceed. You will have to review the transaction and sign it, if everything looks good. -The following commands will start the sovereign validator node with the configuration from **config** folder and with the **validatorKey** (or multi key from **allValidatorsKey**). -Adjust the flags as needed. You can find all the available flags in `/mx-chain-sovereign-go/cmd/sovereignnode/flags.go` +Once the transaction is processed, your token will be issued. -#### # single key -``` -./sovereignnode --validator-key-pem-file ./config/validatorKey.pem --profile-mode --log-save --log-level *:INFO --log-logger-name --log-correlation --use-health-service --rest-api-interface :8080 --working-directory ~/my_validator_node -``` -#### # multi key -``` -./sovereignnode --all-validator-keys-pem-file ./config/allValidatorsKey.pem --profile-mode --log-save --log-level *:INFO --log-logger-name --log-correlation --use-health-service --rest-api-interface :8080 --working-directory ~/my_validator_node -``` +### **Finding the token identifier** -#### # observer -``` -./sovereignnode --profile-mode --log-save --log-level *:INFO --log-logger-name --log-correlation --use-health-service --destination-shard-as-observer 0 --rest-api-interface :8080 --operation-mode db-lookup-extension --working-directory ~/my_observer_node -``` +The token identifier of a token is unique. It is composed by the token ticker, a `-` char, followed by 6 random hex characters. Example: `MTKN-c66c30`. -### Staking transaction +Because the token identifier isn't deterministic, it can be found only after issuing it. There are 2 ways of finding it: -Before staking, a node is a mere observer. After staking, the node becomes a validator, which means that it will be eligible for consensus and will earn rewards. You can find the documentation how to make the staking transaction with mxpy [here](/validators/staking#staking-through-mxpy). +1. On the Explorer page of the issue transaction, you will see a Smart Contract Result which has a data field similar to: `@4d544b4e2d373065323338@152d02c7e14af6800000`. + On the right side, choose `Smart` and you will able to see the decoded parameters. In this example, the token identifier is `MTKN-c66c30`. -## Deploy services +![Token issue SCR](/wallet/wallet-tokens/scr-issue-token.png) -You can find the documentation on how to deploy services [here](/sovereign/services). +2. From the Web Wallet, go to `TOKENS` tab from the left sidebar, and you can see the token there, including its identifier. ---- +![Token view in Web Wallet](/wallet/wallet-tokens/web-wallet-token-display.png) -### Dual Staking -# Dual Staking +## **Transfer a token from your wallet** +You can transfer an amount of a token to another account. To get started, open up the [MultiversX web wallet](https://wallet.multiversx.com/). -:::note -This version of the documentation focuses solely on the essential steps required to set up and deploy a sovereign chain on either a local or remote computer. More content will be added as it is accepted and discussed on [Agora](https://agora.multiversx.com/), or once it is implemented and available for production. -::: +Navigate to the `Tokens` tab, and click on `Send` for the token you want to transfer. ---- +![Web Wallet Tokens page](/wallet/wallet-tokens/web-wallet-tokens-page.png) -### Ethereum L2 +On the pop-up, introduce the recipient and the amount you want to send. Then press `Send`. -# Ethereum L2 +![Web Wallet Transfer Token](/wallet/wallet-tokens/web-wallet-transfer-token.png) -:::note +Once the transaction is successfully executed, the recipient should receive the amount of tokens. -This documentation is not complete. More content will be added once it is accepted and discussed on Agora or once it is implemented and available for production. -::: +## **Managing a token from Web Wallet** + +At the time of writing, a dashboard for tokens owners is still under construction. Meanwhile, token operations have to be done +manually, by following the transaction formats described [here](/tokens/fungible-tokens/#management-operations). --- -### Governance +### Webhooks -# Governance +The web wallet webhooks allow you to build or setup integrations for dapps or payment flows. +The web wallet webhooks are links that point the user of the wallet to either login or populate a "send transaction" form with the provided arguments. Once the action is performed, the user is redirected to the provided callback URL along with a success or error status. -:::note -This version of the documentation focuses solely on the essential steps required to set up and deploy a sovereign chain on either a local or remote computer. More content will be added as it is accepted and discussed on [Agora](https://agora.multiversx.com/), or once it is implemented and available for production. -::: ---- +## **Login hook** -### Governance - Overview +This is useful when you need to find the user's wallet address. A common use case is that, starting from this address you can query the API for the wallet's balance or recent transactions. -This page provides an overview of the On-chain Governance module that will be available on the `v1.6.x` release. +### URL Parameters +`https://wallet.multiversx.com/hook/login?callbackUrl=https://example.com/` -## Table of contents +| Param | Required | Description | +| ------------- | ----------------------------------------- | ----------------------------------------------------- | +| callbackUrl | REQUIRED | The URL the user should be redirected to after login. | -| Name | Description | -|-----------------------------------------------------------------------------------|--------------------------------------------------------------------------| -| [Interacting with the on-chain governance SC](/governance/governance-interaction) | Interacting with the governance system smart contract. | +Upon a successful login, the user is redirected back to the callback URL along which the user's address is appended. +### Callback URL Parameters -## Overview +`https://example.com/?address=erd1axhx4kenjlae6sknq7zjg2g4fvzavv979r2fg425p62wkl84avtqsf7vvv` -The MultiversX network enables on-chain governance by issuing special types of transactions. This mechanism increases decentralization by allowing community members to directly propose and vote on changes, such as protocol upgrades or configuration adjustments. +| Param | Description | +| ------------- | ------------------------------- | +| address | The users's Address (bech32). | -- **Anyone** can create a proposal by paying the proposal fee and specifying: - - a **commit hash** (unique identifier, usually a GitHub commit or spec reference) - - a **start epoch** and an **end epoch** (the voting window). -- **Any staked user** (direct or delegated) can vote during the active period. -- Voting power is proportional to stake: - `voting_power = staked_value` (linear voting). +## **Send transaction hook** -## Implementation details +This is useful when you need to prepopulate a transaction required to send an EGLD amount or pre-populate the transaction's data field with a smart contract function invocation. -### Proposals +### URL Parameters {#send-transaction-url-parameters} -In order for a proposal to be submitted, the following requirements need to be met: -- For a period of at least 15 days, the proposal needs to be published on Agora for public debate and analysis; -- Each proposal requires paying a `ProposalFee` (currently **500 EGLD**); -- Each proposal costs around 51 million of gas units to be submitted. -- The starting epoch of the proposal's voting period needs to be set inside an interval of 30 epochs from the epoch in which the proposal was submitted; +`https://wallet.multiversx.com/hook/transaction?receiver=erd1qqqqqqqqqqqqqpgqxwakt2g7u9atsnr03gqcgmhcv38pt7mkd94q6shuwt&value=0&gasLimit=250000000&data=claimRewards&callbackUrl=https://example.com/` -After a proposal is created, the following rules will apply: -- If the proposal passes, the fee is refunded to the issuer. -- If the proposal fails, a penalty fee (called `LostProposalFee`) is deducted from the initial proposal fee, and the remaining amount is returned to the proposer. -- If the proposal is vetoed, the fee is slashed (transferred to the **Community Governance Pool**) or reduced by the configured `LostProposalFee`. -- Proposals cannot be duplicated: the same commit hash cannot be submitted twice. -- Proposals can be **cancelled** by the issuer **before the voting period starts**. (introduced in v1.10.0 Barnard release) -- Lost proposal fees may either be accumulated in the governance contract (retrievable by the contract owner) or redirected to a **Community Governance Pool**, depending on configuration. +| Param | Required | Description | +| ------------- | ----------------------------------------- | ----------------------------------------------------- | +| receiver | REQUIRED | The receiver's Address (bech32). | +| value | REQUIRED | The Value to transfer (can be zero). | +| gasLimit | OPTIONAL | The maximum amount of Gas Units to consume. | +| data | OPTIONAL | The message (data) of the Transaction. | +| callbackUrl | OPTIONAL | The URL the user should be redirected to after login. | +### Callback URL Parameters {#send-transaction-callback-url-parameters} -### Voting -- The voting starts from the provided starting epoch and lasts at most *10 days*. -- There are four vote types: **Yes**, **No**, **Abstain**, and **Veto**. -- Voting consumes gas (approx. 6 million units). -- Voting power is derived from staked and delegated EGLD. -- Delegation and liquid staking contracts can forward user votes. -- Voting power is **linear** with stake: the more EGLD staked or delegated, the higher the voting power. -- The contract tracks both **used voting power/stake** (applied in a proposal) and **total available voting power/stake** for each address. +`https://example.com/?status=success&txHash=48f68a2b1ca1c3a343cbe14c8b755934eb1a4bb3a4a5f7068bc8a0b52094cc89&address=erd1axhx4kenjlae6sknq7zjg2g4fvzavv979r2fg425p62wkl84avtqsf7vvv` -### Quorum and thresholds -A proposal can pass only if all conditions are met: +| Param | Description | +| ------------- | ----------------------------------------- | +| status | Success / failure of sending transaction. | +| txHash | The transaction's hash. | -- **Quorum**: at least `MinQuorum%` of the total voting power must participate. (currently at least **20%** of total voting power) -- **Acceptance threshold**: YES / (YES + NO + VETO) ≥ **66.67%** (`MinPassThreshold`) -- **Veto threshold**: if VETO votes exceed **33%** of total participating voting power, the proposal is rejected and the proposal fee is slashed. +--- -### Closing and cleanup -**Proposal closing permissions:** -- **Before voting starts:** Only the proposal issuer can close the proposal. -- **After voting ends:** Any user can close the proposal. +### Whitelist Requirements -**Effects of closing:** -1. Finalizes the proposal outcome (passed/failed/vetoed). -2. Unlocks any locked funds. -3. Updates the proposal status in the governance system. +# Whitelist Requirements -**Cleanup options:** -- The `clearEndedProposals` function can be called to clean up expired but unclosed proposals. -- This helps maintain system efficiency by removing inactive proposals. -### Governance configuration -All thresholds and fees are defined in `GovernanceConfigV2`: -- `MinQuorum` -- `MinPassThreshold` -- `MinVetoThreshold` -- `ProposalFee` -- `LostProposalFee` -- `LastProposalNonce` +Before enabling a token to be sent via the Ad-Astra bridge, the token must be whitelisted. +The whitelisting process is performed with the help of the MultiversX team. -These values are set by the system and can be updated by contract owner calls. +## Whitelist requirements -### Example -Let's suppose we have the following addresses that cast the following votes: -- `alice`: staked value **2000 EGLD** that vote **Yes** -- `bob`: staked value **3000 EGLD** that vote **Yes** -- `charlie`: staked value **4000 EGLD** that vote **Yes** -- `delta`: staked value **1500 EGLD** that vote **No** +1. The token issuer must issue the token on the MultiversX network and submit a branding request manually or using [https://assets.multiversx.com](https://assets.multiversx.com); +2. The token issuer must assign the MINT & BURN role to the **BridgedTokensWrapper** or the **Safe** contract, depending on the +token type configuration. -The totals are: -- Quorum = `(2000+3000+4000+1500) = 10,500 EGLD` -- **Yes** = `9000 EGLD` -- **No** = `1500 EGLD` -- **Abstain** = `0` -- **Veto** = `0` +```rust +RolesAssigningTransaction { + Sender:
+ Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 0 + GasLimit: 60000000 + Data: "setSpecialRole" + + "@" + + + "@" + + + "@" + + + "@" + +} +``` -Assume: -- total staked in the system = `20,000 EGLD` -- `MinQuorum` = 20% -- `MinPassThreshold` = 50% -- `MinVetoThreshold` = 33% +Example: -Evaluation: -- Quorum is satisfied: `10,500 > 4000` -- Yes > No: `9000 > 1500` -- Yes is ≥ 50% of votes: `9000 / 10,500 = 85.7%` -- Veto is below 33%: `0 < 3465` +Let's suppose we want to whitelist the token `TKN-001122` on the Ethereum bridge and the token configuration type is 1. +(mint/burn tokens on both ends, native on MultiversX) -To sum it all, the proposal **passes**. +The transaction will look like: + +```rust +RolesAssigningTransaction { + Sender:
+ Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u + Value: 0 + GasLimit: 60000000 + Data: "setSpecialRole" + + "@544b4e2d303031313232" + + "@000000000000000005004ab1cd3d291159a38df7d2a1c498c9d7a7e3047ccc48" + + "@45534454526f6c654c6f63616c4d696e74" + + "@45534454526f6c654c6f63616c4275726e" +} +``` + +3. Depending on the token configuration (valid for 1. & 2. configuration types), the minter & burner role should be granted on +the EVM-compatible chain side (depending on the ERC20 contract variant, `addMinter` and `addBurner` endpoints can be used); +4. MultiversX team will complete the whitelisting process by issuing the required transactions. + +For reference, this is the list of the known smart contracts: +* **Wrapper** `erd1qqqqqqqqqqqqqpgq305jfaqrdxpzjgf9y5gvzh60mergh866yfkqzqjv2h` +* **Ethereum Safe** `erd1qqqqqqqqqqqqqpgqf2cu60ffz9v68r0h62sufxxf67n7xprue3yq4ap4k2` +* **BSC Safe** `erd1qqqqqqqqqqqqqpgqa89ts8s3un2tpxcml340phcgypyyr609e3yqv4d8nz` + +:::warning +To ensure the correct functioning of the bridge, as a MultiversX token owner please ensure the following points are met: +* if you make use of the transfer-role on your token, remember to grant the role also on the **Safe**, **MultiTransfer**, **BridgedTokensWrapper**, and **BridgeProxy** contracts; +* do not freeze the above-mentioned contracts; +* do not wipe tokens on the above-mentioned contracts. + +Failure to comply with these rules will force the bridge owner to blacklist the token in order to restore the correct functioning of the bridge. +::: --- -### Governance interaction +### xAlias -### Introduction +**xAlias** is a _single sign-on_ solution for Web3, powered by Google Sign-In (Web2). It allows new users (not yet proficient in blockchain technologies) to quickly and easily create blockchain wallets (without the need of seed phrases), then start right away and interact with MultiversX dApps. -The interaction with the governance system smart contract is done through correctly formatted transactions to submit actions and through the usage of the vm-query REST API calls for reading the proposal(s) status. +It's a _self-custody_ wallet, and it's _convertible_ to a conventional Web3 wallet at a later point. +:::important +**For dApp developers:** xAlias exposes **the same [URL hooks and callbacks](/wallet/webhooks)** as the [Web Wallet](/wallet/web-wallet). Therefore, integrating xAlias is **identical to integrating the Web Wallet** (with one trivial exception: the configuration of the URL base). See [Signing Providers for dApps](/sdk-and-tools/sdk-js/sdk-js-signing-providers). +::: -### Creating a proposal -The proposal creation transaction has the following parameters: +## Before you begin -```rust -GovernanceProposalTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla - Value: 500 EGLD - GasLimit: 51000000 - Data: "proposal" + - "@" + - "@" + - "@" -} -``` +If you don't already have a Google account, [set up one](https://accounts.google.com/signup). -The value parameter represents the mandatory proposal submission deposit of 500 EGLD. -The proposal identifier is a hex string containing exactly 40 characters. This is the git commit hash on which the proposal is made. +## Sign Up with xAlias -The starting & ending epochs should be an even-length hex string containing the starting epoch and the ending epoch. During this time frame, lasting at most 10 days, the votes can be cast. +Navigate to **[xAlias.com](https://xalias.com)**, then click on **Get Started** to reach the **Sign Up** screen: ->**Note:** When providing the starting epoch, it should be taken into consideration that the governance contract enforcing a configured maximum gap of 30 epochs between the current epoch and the proposal’s starting epoch. +![img](/wallet/xalias/xalias_signup_first.png) -After issuing the proposal, there is a log event generated having the `proposal` identifier that will contain the following encoded topics: +Then, click on **Authenticate**, which redirecteds you to Google Sign-In. -- `nonce` as encoded integer which uniquely identifies the proposals -- `identifier` is the provided 40 hex characters long identifier -- `start epoch` as encoded integer for the starting epoch -- `end epoch` as encoded integer for the ending epoch +![img](/wallet/xalias/xalias_signup_google_choose_account.png) +Pick the Google account you want to use, then click on **Confirm**. -### Voting a proposal using the direct staked or delegation-system amount +![img](/wallet/xalias/xalias_signup_google_confirm.png) -Any wallet that has staked EGLD (either direct staked or through the delegation sub-system) can cast a vote for a proposal. +Next, you'll have to **Authorize** xAlias to store and access its own data on your Google Drive account: -```rust -GovernanceVoteTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla - Value: 0 EGLD - GasLimit: 6000000 - Data: "vote" + - "@" + - "@" -} -``` +![img](/wallet/xalias/xalias_signup_second.png) -The value parameter for the voting transaction must be set to 0, since the function is non-payable. +Read the Google consent screen, then click on **Allow**. -The `nonce` is the hex encoded value of the proposal's unique nonce and the `vote_type` can be one of the following values: -- for **Yes**: `796573`; -- for **No**: `6e6f`; -- for **Abstain**: `6162737461696e`; -- for **Veto**: `7665746f`. +![img](/wallet/xalias/xalias_signup_authorize_google.png) -The vote value for the account that will vote a proposal is the sum of all staked values along with the sum of all delegated values in the delegation sub-system. +At the end of the Sign Up flow, you will be asked to back-up your xAlias account, as a document file, which can be either received by email or downloaded directly: -After issuing the vote, a log event is generated containing the `proposal` identifier and the following encoded topics: +![img](/wallet/xalias/xalias_signup_backup_file.png) -- `nonce` as encoded integer which uniquely identifies the proposals -- `vote_type` as encoded string representing the vote -- `total_stake` total staked EGLD for the sender address -- `total_voting_power` total available voting power for the sender address +To confirm the back-up and complete the flow, enter the confirmation code from the received (or downloaded) document: -Example response: +![img](/wallet/xalias/xalias_signup_backup_code.png) -```json -{ - "returnData": [ - "WQ==", (nonce: hex = 59 -> decimal = 89) - "eWVz", (vote_type: hex = "79657" -> "yes") - "BxRA1dqcrTxyQw==", (total_stake: hex = "071440d5da9cad3c7243" -> 33430172142116630458947 -> ~33430 EGLD) - "BxRA1dqcrTxyQw==", (total_voting_power: hex = "071440d5da9cad3c7243" -> 33430172142116630458947 -> ~33430 EGLD) - ] -} -``` +Congratulations, you have successfully **created your xAlias account**! -### Voting a proposal through smart contracts (delegation voting) +## Sign In -Whenever we deal with a smart contract that delegated through the delegation sub-system or owns staked nodes it is the out of scope of the metachain's governance contract to track each address that sent EGLD how much is really staked (if any EGLD is staked). +You can always sign-in to your xAlias account by navigating to **[xAlias.com](https://xalias.com)**, then clicking on **Sign In**. You will be asked to confirm the Google account, then reach the **xAlias Dashboard**. -That is why we offered an endpoint to the governance smart contact that can be called **only by a shard smart contract** and the governance contract will record the address provided, the vote type and vote value. -This is very useful whenever implementing liquid-staking-like smart contracts. The liquid-staking contract knows the balance for each user, so it will delegate the call to the governance contract providing the corresponding voting power. +## xAlias Dashboard -:::important -The maximum value for the staked value & the voting power for the liquid-staking contract is known by the governance contract, so it can not counterfeit the voting. If, due to a bug the liquid-staking contract allows, for example, for a user to vote with a larger quota, the liquid-staking contract will deplete its maximum allowance making other voting transactions (on his behalf) to fail. +Upon the initial sign-up, and each time you sign-in to xAlias, you will be presented the **xAlias Dashboard**. + +Here, you will be able to see the wallet address (the one starting with _erd1_) and share it with others, so they can send you EGLD or other tokens.‌ Additionally, you can click on **Open in Explorer** and see the all the blockchain transactions associated with your wallet address (blockchain address). + +![img](/wallet/xalias/xalias_dashboard.png) + + +## Use a MultiversX dApp with xAlias + +:::note +The screenshots below are from the [**MultiversX dApp Template**](https://devnet.template-dapp.multiversx.com). ::: -The user that delegated through a liquid-staking-like contract in order to vote on a proposal, sends a transaction to the liquid-staking-like contract. The smart contract then creates the GovernanceVoteThroughDelegationTransaction based on the user’s transaction inputs, forwarding the vote and the computed voting power to the governance contract. +:::important +**For dApp developers:** if your dApp doesn't yet support **xAlias** as a signing provider, **we recommend that you enable the integration, and reach a broader audicence** (wider user base for your dApp). Please follow [Signing Providers for dApps](/sdk-and-tools/sdk-js/sdk-js-signing-providers) for technical details. +::: -```rust -GovernanceVoteThroughDelegationTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla - Value: 0 EGLD - GasLimit: 51000000 - Data: "delegateVote" + - "@" + - "@" + - "@" + - "@" -} -``` +If you've stumbled upon a MultiversX dApp that you'd like to use and it supports xAlias, follow the **Login** or **Connect** flow of the dApp, then pick **xAlias** (as your Web3 wallet). -The value parameter for the voting transaction must be set to 0, since the function is non-payable. +![img](/wallet/xalias/xalias_dapp_login.png) -The `nonce` is the hex encoded value of the proposal's unique nonce and the `vote_type` can be one of the following values: -- for **Yes**: `796573`; -- for **No**: `6e6f`; -- for **Abstain**: `6162737461696e`; -- for **Veto**: `7665746f`. +Then, you will reach the following consent screen: -The `account address handled by the smart contract` is the address handled by the smart contract that will delegate the vote towards the governance smart contract. This address will be recorded for casting the vote. +![img](/wallet/xalias/xalias_dapp_consent.png) -The `vote_balance` is the amount of stake the address has in the smart contract. The governance contract will "believe" that this is the right amount as it impossible to verify the information. The balance will diminish the total voting power the smart contract has. +Upon confirmation, you will be redirected to the dApp (which is informed about your blockchain address - **not your email address, of course**). -After issuing the vote, a log event is generated containing the `proposal` identifier and the following encoded topics: +Then, as a user of the dApp (of any dApp), you might reach a point where you need to **sign a transaction** - then, you will be redirected to xAlias: -- `nonce` as encoded integer which uniquely identifies the proposals -- `vote_type` as encoded string representing the vote -- `voter` account address handled by the smart contract -- `total_stake` total staked EGLD for the sender address -- `total_voting_power` total available voting power for the sender address +![img](/wallet/xalias/xalias_dapp_sign_transaction.png) -Example response: +... or you might need to sign a message: -```json -{ - "returnData": [ - "WQ==", (nonce: hex = 59 -> decimal = 89) - "eWVz", (vote_type: hex = "79657" -> "yes") - "ATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeE=", (voter: hex = "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1" -> erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th) - "BxRA1dqcrTxyQw==", (total_stake: hex = "071440d5da9cad3c7243" -> 33430172142116630458947 -> ~33430 EGLD) - "BxRA1dqcrTxyQw==", (total_voting_power: hex = "071440d5da9cad3c7243" -> 33430172142116630458947 -> ~33430 EGLD) - ] -} -``` +![img](/wallet/xalias/xalias_dapp_sign_message.png) -### Closing a proposal +## Sign Out -A proposal can be closed by the issuer before the voting period starts but with an early closing fee equal to `LostProposalFee`. A proposal can also be closed by anyone in an epoch that is strictly higher than the end epoch value provided when the proposal was opened. +To sign out from xAlias, navigate to **[xAlias.com](https://xalias.com)**, then click on **Sign Out**. -```rust -CloseProposalTransaction { - Sender: - Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla - Value: 0 EGLD - GasLimit: 51000000 - Data: "closeProposal" + - "@" -} -``` +:::note +Note that disconnecting from a dApp doesn't sign you out from xAlias. +::: -#### Rules for closing -- The proposal can be closed by the issuer before the voting starts or by anyone after voting ended. -- If the proposal **passes** → the full proposal fee is refunded. -- If the proposal **fails** or is **vetoed** → the refund is reduced by the `LostProposalFee`. -- Once a proposal is closed, it cannot be reopened. -- Closing also finalizes the vote tally (the proposal is marked as `Passed` or not, based on the results). +--- +### xPortal -### Querying the status of a proposal +Experience the power of convenience and security in one place. Built by MultiversX, [xPortal](https://xportal.com/) is the go-to app for all crypto needs. Securely store your cryptocurrencies and NFTs, effortlessly swap tokens, and soon, enjoy the perks of our upcoming debit cards with exciting cashback rewards. Join millions of users who trust xPortal for seamless finance management, gamified missions & AI avatar creation, global payments, and a social experience like no other. -The status of any proposal can be queried at any time through `vm-values/query` REST API endpoints provided by the gateway/API. +:::info +On this page, we will start presenting features from xPortal, beginning with the Invisible Guardians feature. Subsequently, we will introduce other features in the following sections. +::: -```bash -https://.multiversx.com/vm-values/query -``` -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla", - "funcName": "viewProposal", - "args": [""] -} -``` +## What are Invisible Guardians? -- The argument `nonce` is the proposal nonce in hex format. -- The response will contain the following json definition where all fields are base64-encoded: +Invisible Guardians is a variation of the Guardians feature tailored for xPortal users. It is specifically designed to deliver the same level of security introduced by the Guardians feature while maintaining the same ease of use xPortal users are accustomed to. -```json -{ - "returnData": [ - "", (amount locked by proposer) - "", (unique identifier of the proposal) - "", (proposal number) - "", (address of the proposer) - "", (epoch when voting starts) - "", (epoch when voting ends) - "", (current quorum stake: sum of stake that participated) - "", (total stake voting YES) - "", (total stake voting NO) - "", (total stake vetoing the proposal) - "", (total stake abstaining) - "", - "" - ] -} -``` +![img](/wallet/xportal/activate_guardian.jpg) -Example response: -```json -{ - "returnData": [ - "Gxrk1uLvUAAA", (proposal locked amount: 500 EGLD denominated = 500 * 10^18) - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABg==", (commit hash: 0x0000...006) - "AQ==", (nonce: 1) - "aj88GtqHy9ibm5ePPQlG4aqLhpgqsQWygoTppckLa4M=", (proposer address) - "bQ==", (starting epoch: 109) - "bg==", (ending epoch: 110) - "ntGU2xmyOMAAAA==", (quorum: 750000 EGLD denominated) - "", (yes votes: 0) - "", (no votes: 0) - "ntGU2xmyOMAAAA==", (veto votes: 750000 EGLD denominated) - "", (abstain votes: 0) - "dHJ1ZQ==", (proposal closed: true) - "ZmFsc2U=" (proposal passed: false) - ] -} -``` +## How do Invisible Guardians work? +When active, the feature enables an invisible guardian which is encrypted and stored locally on your device, silently co-signing every transaction. This acts as an additional security layer and safeguards the account in situations where, one way or another, the secret phrase has been compromised. -### Querying an address voting status (direct staking or system delegation) -The voting status of a certain address can be queried at any time by using the `vm-values/query` REST API endpoints provided by the gateway/API. +## How do I activate an Invisible Guardian on my account? +The Guardians feature can be accessed in the Security section, which can be found in the xPortal Settings. There, users can activate an Invisible Guardian on their account by going through a few simple steps: -```bash -https://gateway.multiversx.com/vm-values/query -``` +- Create a backup password for your guarded account +![img](/wallet/xportal/inv_guardian_step1.jpg) +- Enable the Invisible Guardian through a blockchain transaction +![img](/wallet/xportal/inv_guardian_step2.jpg) +- Wait for the 20-day cooldown period to pass +![img](/wallet/xportal/inv_guardian_step4.jpg) +- Activate the Invisible Guardian through another blockchain transaction +![img](/wallet/xportal/inv_guardian_step5.jpg) -With the following payload: -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla", - "funcName": "viewDelegatedVoteInfo", - "args": ["", "
"] -} -``` +## Can I deactivate the Invisible Guardian feature once it was activated? -- `proposal_nonce` → the proposal identifier (nonce, hex-encoded). -- `address` → the bech32 address of the account to check. +The Invisible Guardian can be deactivated at any time, but it requires that a transaction is sent and signed by the Guardian currently in place on that account. -> **Note:** The older function `viewUserVoteHistory` (which returned lists of proposal nonces) is now considered legacy. -> Use `viewDelegatedVoteInfo` for detailed voting power and stake information. -The response will contain the following json definition where all fields are base64-encoded: +## Can I change my Guardian? -```json -{ - "returnData": [ - "", (voting power used by this address on the proposal) - "", (stake associated with the used power) - "", (total available voting power for the address) - "" (total stake considered in governance for the address) - ] -} -``` +Yes, the Invisible Guardian can be changed, and there are two ways in which this action can be performed: -Example for an address that voted: -```json -{ - "returnData": [ - "Cg==", (used power: 100) - "ZAA=", (used stake: 100) - "A+g=", (total power: 1000) - "Gg4M=", (total stake: 1000) - ] -} -``` +- If the user has access to the current Guardian the change can be done immediately by signing a transaction; +- If the user doesn’t have access to the current Guardian the process implies a 20-day cooldown period before the change of Guardian takes place. -In this example, the queried address voted on this proposal with **100 stake**, which translated into **100 voting power**. The proposal overall had **1000 total stake** and **1000 total voting power** recorded. -Example for an address that did not vote: +## Why do I have to wait 20 days to activate or change an Invisible Guardian? -```json -{ - "returnData": [ - "AA==", (used power: 0) - "AA==", (used stake: 0) - "A+g=", (total power: 1000) - "A+g=", (total stake: 1000) - ] -} -``` +The 20-day cooldown period is designed to safeguard staked funds and relieve pressure on users. +Given that it might take longer than 10 days to safely transfer funds to a secure account, we have extended this period by an additional 10 days. +This not only allows the user ample time to react if their account is compromised, but it also provides a buffer for the user to counteract potential threats. -### Querying the voting power of an address -The voting power of a certain address can be queried at any time by using the `vm-values/query` REST API endpoints provided by the gateway/API. +## What implications should I be aware of before activating an Invisible Guardian on my account? -```bash -https://gateway.multiversx.com/vm-values/query -``` +Aside from the extra security and the peace of mind that goes hand in hand with activating an Invisible Guardian on your account, there are a few things everyone should be aware of beforehand: +- You will only be able to process transactions from xPortal or by logging in with xPortal +- You won’t be able to import your secret phrase into other wallet apps -With the following payload: -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla", - "funcName": "viewVotingPower", - "args": [""] -} -``` +## What if something happens to my device or I change it? -Here's an example of the response: +The Invisible Guardian is stored locally on your device, but the first step of setting it up is creating an encrypted backup containing the Guardian. This makes importing it on a different device as easy as it gets, all you need to do is to provide the password associated with the guarded backup. -```json -{ - "returnData": [ - "Q8M8GTdWSAAA" (1250000000000000000000 -> 1250 EGLD) - ] -} -``` -### Querying the configuration of the Governance contract +## What happens if my secret phrase is compromised? -The configuration of the Governance System Smart Contract can be queried at any time by using the `vm-values/query` REST API endpoints provided by the gateway/API. +If your secret phrase is compromised and the Invisible Guardian is active, whoever has access to the secret phrase will be unable to process any transactions from your account. They will be able though to request the change of the Guardian. This process lasts 20 days. -```bash -https://gateway.multiversx.com/vm-values/query -``` +You will be able to cancel any request of this kind, but our recommendation is to transfer all your assets (staked and unstaked) to a new wallet. -With the following payload: -```json -{ - "scAddress": "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla", - "funcName": "viewConfig", - "args": [] -} -``` +## How would I know if my account is compromised? -The response will contain the following json definition where all fields are base64-encoded: +A red warning message is displayed in your xPortal account every time a Guardian is enabled, changed or disabled. If it was you who processed this change, you can confirm that the action was performed by you and the message disappears. -```json -{ - "returnData": [ - "", (the amount of EGLD required to issue a new proposal) - "", (the amount of EGLD that the issuer loses if the proposal fails) - "", (the minimum percentage of total voting power required for a proposal to be considered valid) - "", (the minimum percentage of veto votes needed to reject a proposal and slash its fee) - "", (the minimum percentage of YES votes required for a proposal to pass) - "" (the nonce of the last created proposal) - ] -} -``` +If the action was not performed by you, we strongly recommend moving all your assets to a new wallet within 20 days. -Here's how a response might look like: -```json -{ - "returnData": [ - "NTAwMDAwMDAwMDAwMDAwMDAwMDAw", ("500000000000000000000" -> 500 EGLD) - "MTAwMDAwMDAwMDAwMDAwMDAwMDA=", ("10000000000000000000" -> 10 EGLD) - "MC4yMDAw", ("0.2000" -> 20%) - "MC4zMzAw", ("0.3300" -> 33%) - "MC42NjY3", ("0.6667" -> 66.67%) - "MA==", ("0" -> 0) - ], -} -``` +Also, to ensure maximum protection we strongly recommend you access your account periodically. --- -### Header Verifier - -# Header-Verifier +## Terminology +### Terminology -As mentioned in the [Introduction](cross-chain-execution.md) the `Header-Verifier` smart contract is responsible to verify signatures, store the *BLS Keys* of the validators and register incoming *Operations*. +A sourced reference for MultiversX terminology. Foundational terms are included on purpose: what is obvious to readers fluent in the docs is not obvious to first-timers. Every entry carries a Source line; features in phased rollout (for example Supernova) are noted as such. -## Registering a set of *Operations* -```rust - #[endpoint(registerBridgeOps)] - fn register_bridge_operations( - &self, - signature: BlsSignature, - bridge_operations_hash: ManagedBuffer, - operations_hashes: MultiValueEncoded, - ) -``` +## Blockchain foundations -Any *Operation* before being executed has to be registered in this smart contract. The reason behind this is that the hash will be verified and it will be locked until the operation is executed by the `Mvx-ESDT-Safe` contract. +Plain-language entries for readers new to MultiversX or to blockchains. The terms here are general concepts, defined with their MultiversX specifics. The MultiversX-specific machinery follows in later sections. -The registering endpoint operates as follows: -1. Verifies that `bridge_operations_hash` is not found in the `hash_of_hashes_history` storage mapper, otherwise it will return an error. -2. Verifies that the hash of all `operations_hashes` matches the `bridge_operations_hash`, otherwise, the endpoint will return an error. -3. All `operations_hashes` are stored in the smart contract's storage with the status `OperationsHashStatus::NotLocked`. -4. The `bridge_operations_hash` is added to the `hash_of_hashes_history` storage mapper. +### Blockchain -## Locking *Operations* -```rust - #[endpoint(lockOperationHash)] - fn lock_operation_hash(&self, hash_of_hashes: ManagedBuffer, operation_hash: ManagedBuffer) -``` +A shared, append-only ledger maintained by a network of nodes that agree on its contents through a consensus mechanism. -The Header-Verifier has a system in place for locking *Operation* hashes. Locking those registered hashes prevents any unwanted behaviour when executing or removing an *Operation* hash. Remember that the execution of *Operations* can only be done by the `Mvx-ESDT-Safe` smart contract. This endpoint when called will follow this flow: +Context: MultiversX is a layer-1 blockchain that settles its own transactions. Its ledger is partitioned across shards and secured by Secure Proof of Stake. -1. Check if the caller is the `Mvx-ESDT-Safe` smart contract. -2. Check if the *Operation* is registered. -3. If the hash is not locked set the status in the storage as locked or else return panic. +See also: [Layer 1](#layer-1-l1), [Shard (Execution Shard)](#shard-execution-shard), [Consensus](#consensus), [Secure Proof of Stake](#secure-proof-of-stake-spos). +Read more: [docs.multiversx.com/learn/sharding](https://docs.multiversx.com/learn/sharding). -:::note -The hash can be in two different states: `OperationHashStatus::NotLocked` or `OperationHashStatus::Locked` -::: +--- -## Removing *Operations* -```rust - #[endpoint(removeExecutedHash)] - fn remove_executed_hash(&self, hash_of_hashes: &ManagedBuffer, operation_hash: &ManagedBuffer) -``` +### Layer 1 (L1) -After registering and executing an *Operation* the status of the hash associated to it must be removed from the Header-Verifier's internal storage. This endpoint will be called by the `Mvx-ESDT-Safe` smart contract after the execution of the *Operation* is successful. The steps are pretty clear: +A base blockchain that settles transactions on its own network rather than depending on another chain for security. -1. Check if the caller is the `Mvx-ESDT-Safe` smart contract. -2. Remove the status of the hash from storage. +Context: MultiversX is a layer-1, in contrast to layer-2 networks that post their data or proofs to a separate base chain. Its scaling comes from sharding rather than from an external settlement layer. -:::note -The source code for this contract can be found [here](https://github.com/multiversx/mx-sovereign-sc/blob/main/header-verifier/src/lib.rs). -::: +See also: [Blockchain](#blockchain), [Adaptive State Sharding](#adaptive-state-sharding), [Sovereign Chains](#sovereign-chains). +Read more: [docs.multiversx.com/learn/sharding](https://docs.multiversx.com/learn/sharding). --- -### Interoperability +### Block -# Interoperability and Modular Design +A batch of ordered transactions, cryptographically linked to the previous block, that extends the ledger. -Sovereign Chains are designed to serve as an interoperability layer between different ecosystems. Ecosystem partners and builders have the opportunity to create a set of components that can be activated or deactivated based on the specific needs of a particular Sovereign Chain. But how will sovereign chains achieve that? +Context: On MultiversX each shard produces its own blocks, one per round, and the metachain notarizes them. A block becomes final once the shard's validators reach the consensus threshold. -## Virtual Machine Structure +See also: [Round](#round), [Shard (Execution Shard)](#shard-execution-shard), [Metachain](#metachain), [Finality](#finality). +Read more: [docs.multiversx.com/learn/consensus](https://docs.multiversx.com/learn/consensus). -SpaceVM has two parts, developed in GO and Rust: +--- -**1. One part handles**: -- OPCODES -- State management -- Transfers - -**2. The other part functions as the executor.** +### Transaction -Starting from the address model, multiple VMs are enabled to run within the system. There is a clear implementation of how these VMs can call each other independently, facilitated by the blockchainHook. TODO: add information about the implementation. +A signed instruction from an account to move value, call a smart contract, or invoke a built-in function. -## Modular Design and Multi-VM Capabilities +Context: A MultiversX transaction carries a sender, receiver, value, data, gas limit, gas price, nonce, and chain ID, plus optional guardian and relayer fields. It is signed by the sender's key and gossiped to the network for inclusion in a block. -The basic idea is to utilize the modular design and multi-VM capabilities to create direct connections to existing technologies from the market. +See also: [Account Nonce](#account-nonce), [Gas and Fees](#gas-and-fees), [Built-in Functions](#built-in-functions), [Onchain Guardian](#onchain-guardian). +Read more: [docs.multiversx.com/developers/transactions/tx-overview](https://docs.multiversx.com/developers/transactions/tx-overview/); [github.com/multiversx/mx-sdk-js-core](https://github.com/multiversx/mx-sdk-js-core). -### Ethereum L2 +--- -### Bitcoin L2 +### Account -### Solana L2 +An entry in the ledger identified by an address, holding a balance, a nonce, and (for contracts) code and storage. + +Context: MultiversX has user accounts, controlled by a key, and smart contract accounts, controlled by code. Both use the same bech32 address format; a contract account additionally holds its deployed code and storage. + +See also: [Bech32 Addressing](#bech32-addressing-erd1), [Account Nonce](#account-nonce), [Smart Contract](#smart-contract), [Account State Trie](#account-state-trie). +Read more: [docs.multiversx.com/developers/account-management](https://docs.multiversx.com/developers/account-management/). --- -### Key Components +### Smart Contract -# Key Components +A program deployed to the blockchain that runs deterministically when called, holding its own balance and storage. -:::note +Context: On MultiversX, smart contracts compile to WebAssembly and most are written in Rust with the `multiversx-sc` framework. A contract is itself an account, callable by transactions and by other contracts. -This documentation is not complete. More content will be added once it is accepted and discussed on Agora or once it is implemented and available for production. +See also: [WASM VM](#wasm-vm-and-wasmer), [multiversx-sc (mx-sdk-rs)](#multiversx-sc-mx-sdk-rs), [Account](#account), [Tx Syntax](#tx-syntax). +Read more: [docs.multiversx.com/developers/overview](https://docs.multiversx.com/developers/overview/). -::: +--- -Sovereign Chains mark significant progress for MultiversX in the field of appchains. While the concept is straightforward to discuss in general terms, developers, builders, or other entities looking to launch a sovereign chain need to consider several critical components. For clarity, we will divide these components into four major categories and describe the importance and role of each one. +### Consensus -![img](/sovereign/sovereign-1.png) +The process by which the validators of a network agree on the next block and the order of its transactions. +Context: MultiversX uses Secure Proof of Stake: a proposer is chosen at random each round and the shard's validators sign the block, which becomes final once a two-thirds threshold is reached. -### **1. Sovereign Cross-Chain Smart Contracts** +See also: [Secure Proof of Stake](#secure-proof-of-stake-spos), [Consensus Group](#consensus-group), [Equivalent Consensus Proof](#equivalent-consensus-proof-ecp), [Finality](#finality). +Read more: [docs.multiversx.com/learn/consensus](https://docs.multiversx.com/learn/consensus). -In order to have a native *connection* with the MultiversX MainChain three smart contracts have to be deployed: +--- -#### **```esdt-safe``` Contract** +### Proof of Stake (PoS) -The ```esdt-safe``` smart contract, which operates on both the mainchain and the sovereign chain, is the primary contract responsible for token transfers. +A consensus family in which the right to validate is tied to staked tokens rather than to computational work. -Mainchain Endpoints: +Context: Validators lock EGLD as stake and are selected to produce and sign blocks; misbehavior can cost rewards or stake. MultiversX's variant is Secure Proof of Stake. -- ```deposit```: This endpoint performs a ```MultiESDTNFTTransfer``` to the contract, locking the tokens on the mainchain and initiating their transfer to the sovereign chain (tokens on the mainchain remain locked in the smart contract, while those on the sovereign chain are burned). -- ```executeBridgeOps```: Called by the bridge service wallet, this endpoint transfers tokens on the mainchain. It first verifies that these operations have been previously stored in the multisig contract before executing. -- ```registerToken```: Issues a token and maps it to a sovereign token ID, granting the mainchain contract the rights to mint/burn and requiring a payment of 0.05 EGLD for the token issuance on the mainchain. +See also: [Secure Proof of Stake](#secure-proof-of-stake-spos), [Staking](#staking), [Validator (node)](#validator-node), [Byzantine Fault Tolerance](#byzantine-fault-tolerance-bft). +Read more: [docs.multiversx.com/learn/consensus](https://docs.multiversx.com/learn/consensus). -Sovereign Chain Endpoint: +--- -- ```deposit```: Burns the token being sent to the mainchain. `registerToken` must be called beforehand to enable bridging, and the contract must have the *burn right* set. The default `ESDTRoleBurnForAll` role is assigned to any issued token. If this role is deactivated, a transaction is required to assign the *burn role*, facilitated by the script `setLocalBurnRoleSovereign`. +### Byzantine Fault Tolerance (BFT) -#### **Fee Market Contract** +The property of a consensus protocol that lets the network agree correctly even if some participants fail or act maliciously, up to a threshold. -This contract allows you to specify the fee for deposit transactions. The fee structure can vary and includes: -- No fee; -- Fixed fee with a specific token; -- Fee with any token (currently, this requires a check through an xexchange contract, which is not yet operational); +Context: MultiversX tolerates faulty or adversarial validators below one-third of a shard's set; a block needs at least two-thirds of signatures to finalize, so a dishonest minority cannot forge agreement. -The fee is applicable either per token in the `MultiESDTNFTTransfer` action or based on the gas used in the deposit transaction. +See also: [Secure Proof of Stake](#secure-proof-of-stake-spos), [Consensus](#consensus), [Equivalent Consensus Proof](#equivalent-consensus-proof-ecp). +Read more: [docs.multiversx.com/learn/consensus](https://docs.multiversx.com/learn/consensus). -#### **MultiSig Verifier** +--- -The `multisig` smart contract, operates solely on the mainchain, and is responsible for managing and validating the multi-signature operations for bridge transactions. It registers the public keys of the validators from the sovereign chain. It also validates the multi-signature received on this endpoint which is called by the bridge service: -- `registerBridgeOps` - here, the multisig is validated, the hashes of the bridge operations are verified, and the hashes are recorded (these hashes are checked during the execution of executeBridgeOps). +### Finality -### **2. Sovereign Notifier** +The point at which a block and its transactions become irreversible. -:::note -The Sovereign Notifier exports outport blocks to Sovereign Chain. It needs to be a server outport host driver. -::: +Context: MultiversX has deterministic finality: a block is final the moment its consensus proof is produced, with no probabilistic confirmation window. Finality is distinct from block time, which is how often blocks are produced. -There is only one Sovereign Notifier per Sovereign Chain, started specifically on the shard where the Sovereign's `esdt-safe` contract is deployed. It is subscribed to the address of the `esdt-safe` contract (automatically set during genesis by scripts). It listens for events with the IDs `deposit` and `execute`. When it detects an event, it sends the event to the sovereign chain as an incoming operation. The nodes then recognize the operation, process it, and the funds are credited to the account. +See also: [Deterministic Finality](#deterministic-finality), [Equivalent Consensus Proof](#equivalent-consensus-proof-ecp), [Block Time](#block-time), [Supernova](#supernova). +Read more: [docs.multiversx.com/learn/consensus](https://docs.multiversx.com/learn/consensus). -### **3. Sovereign Cross-Chain TX Service** +--- -The Sovereign Cross-Chain TX service, connected via gRPC, manages the interaction between the mainchain and the sovereign chain for token transfers and other operations. +### Deterministic Finality -For the setup described in this documentation, there is a single cross-chain tx service in operation (recommended it is for every validator from Sovereign Chain to have a running cross-chain tx service for each instance). It has a wallet attached from the mainchain and contains addresses from the cross-chain contracts (`multisig` for registration and `esdt-safe` for execution), along with various security certificates. +Finality reached at a defined moment, when the consensus threshold is met, rather than becoming more probable as further blocks are added. -#### Functionality: -- **Outgoing Operations**: The service listens for outgoing operations created from logs of deposit events in esdt-safe on the sovereign chain. -- **Validation and Signing**: These operations are validated, signed, hashed, and processed by all validators on the sovereign chain. -- **Register and Execute**: Once the service receives a validated operation, it sends the operation to the registerBridgeOps endpoint in the multisig contract and then to the executeBridgeOps endpoint in the esdt-safe contract for execution. +Context: On MultiversX a block is final once its Equivalent Consensus Proof is broadcast, and exactly one valid proof can exist per block, so equivocation is not possible. Probabilistic-finality chains, by contrast, treat a block as final only after enough subsequent blocks. Supernova preserves deterministic finality while cutting the time to reach it. -### **4. Sovereign Chain** +See also: [Finality](#finality), [Equivalent Consensus Proof](#equivalent-consensus-proof-ecp), [Supernova](#supernova), [Byzantine Fault Tolerance](#byzantine-fault-tolerance-bft). +Read more: [docs.multiversx.com/learn/consensus](https://docs.multiversx.com/learn/consensus). --- -### Keystore files +### Block Time -The MultiversX keystore is a JSON file that holds a mnemonic (seed phrase), encrypted with a password (as chosen by the user). Thus, the keystore provides users with a reliable and convenient method for managing their hot wallets and protecting their assets. +The interval between consecutive blocks on a shard. +Context: MultiversX block time is roughly 6 seconds on mainnet today. The Supernova upgrade is designed to reduce it to a 600-millisecond target; Supernova is pre-mainnet, so this reduction is not yet live. Block time bounds how quickly transactions can be included, and is distinct from finality, which is when a block becomes irreversible. -## MultiversX Keystore +See also: [Round](#round), [Finality](#finality), [Supernova](#supernova). +Read more: [docs.multiversx.com/learn/consensus](https://docs.multiversx.com/learn/consensus); MultiversX, "Supernova: decoupling consensus and execution" ([multiversx.com/blog/supernova-decoupling-consensus-and-execution](https://multiversx.com/blog/supernova-decoupling-consensus-and-execution)). +--- -### Loading a keystore into the web wallet +### Mempool -Choose connecting with keystore: +The pool of valid, pending transactions a node holds before they are included in a block. -![img](/developers/keystore/connect_with_keystore.png) +Context: Because MultiversX is sharded, each shard's nodes maintain the mempool for the transactions they are responsible for. Supernova (pre-mainnet) is designed to propagate blocks by referencing transactions nodes already hold rather than rebroadcasting full block bodies. -Add your keystore and introduce the password: +See also: [Transaction](#transaction), [Shard (Execution Shard)](#shard-execution-shard), [Supernova](#supernova). +Read more: [docs.multiversx.com/learn/sharding](https://docs.multiversx.com/learn/sharding). -![img](/developers/keystore/load_json_introd_pass.png) +--- -Upon loading the keystore, the user is presented with a choice of addresses, as derived from the mnemonic held by the keystore (if the keystore has `"kind":"mnemonic"`). +### Hash -![img](/developers/keystore/bech32.png) +A fixed-size fingerprint of data produced by a one-way function, used to link blocks, identify transactions, and commit to state. +Context: MultiversX uses cryptographic hashes throughout: to chain blocks, to derive the randomness for proposer selection, and to compute the transaction hash that identifies a transaction. -### **How does a MultiversX Keystore look like?** +See also: [Block](#block), [Verifiable Random Function](#verifiable-random-function-vrf), [Account State Trie](#account-state-trie). +Read more: [docs.multiversx.com/learn/consensus](https://docs.multiversx.com/learn/consensus). -Here is an example: +--- -```json -{ - "version": 4, - "id": "5b448dbc-5c72-4d83-8038-938b1f8dff19", - "kind": "mnemonic", - "crypto": { - "ciphertext": "6d70fbdceba874f56f15af4b1d060223799288cfc5d276d9ebb91732f5a38c3c59f83896fa7e7eb6a04c05475a6fe4d154de9b9441864c507abd0eb6987dac521b64c0c82783a3cd1e09270cd6cb5ae493f9af694b891253ac1f1ffded68b5ef39c972307e3c33a8354337540908acc795d4df72298dda1ca28ac920983e6a39a01e2bc988bd0b21f864c6de8b5356d11e4b77bc6f75ef", - "cipherparams": { - "iv": "2da5620906634972d9a623bc249d63d4" - }, - "cipher": "aes-128-ctr", - "kdf": "scrypt", - "kdfparams": { - "dklen": 32, - "salt": "aa9e0ba6b188703071a582c10e5331f2756279feb0e2768f1ba0fd38ec77f035", - "n": 4096, - "r": 8, - "p": 1 - }, - "mac": "5bc1b20b6d903b8ef3273eedf028112d65eaf85a5ef4215917c1209ec2df715a" - } -} -``` +### Account State Trie -At first, you will see an unappealing JSON file, which appears to contain magic parameters used for numerous complex cryptographic operations with unclear and vague purpose. But if you dig a little deeper you will see that it contains: +The tree structure in which the protocol stores account balances, nonces, code, and storage, committing the whole state to a single root hash. -- **kind** - Can be `secretKey` or `mnemonic` and represents the input to be encrypted using the `cipher`; -- **ciphertext** - Your MultiversX mnemonic or secret key encrypted using the `cipher` algorithm below; -- **cipher** - The name of a symmetric AES algorithm; -- **cipherparams** - The parameters required for the `cipher` algorithm above; -- **kdf** - A key derivation function used to let you encrypt your keystore file with a password; -- **kdfparams** - The parameters required for the `kdf` algorithm above; -- **mac** - A code used to verify your password. +Context: Each shard maintains its own state trie for the accounts it holds. Storing state as a trie lets the protocol prove and verify account data efficiently, and is what state sharding partitions across shards. -Keystore files created with the first major version of the web wallet (available prior February 14th, 2023) hold the encrypted secret key, instead of the encrypted mnemonic (as the new keystore files do). Though the older files are still compatible with the new web wallet - compatibility is achieved through the aforementioned "kind" field. +See also: [Account](#account), [Adaptive State Sharding](#adaptive-state-sharding), [Hash](#hash). +Read more: [docs.multiversx.com/learn/sharding](https://docs.multiversx.com/learn/sharding). -When **kind** is set (or not set at all) to `secretKey`, the `ciphertext` field will contain the encrypted secret key, as it did before. However, when **kind** is set to `mnemonic`, the `ciphertext` field will contain the encrypted mnemonic instead. +--- -Auxiliary reference: [ERC-2335: BLS12-381 Keystore](https://eips.ethereum.org/EIPS/eip-2335). +### Move Balance + +The value-transfer and data-handling part of a transaction's cost, as opposed to smart-contract execution. + +Context: A plain EGLD transfer is a pure move-balance operation: it pays the minimum gas plus a per-data-byte cost and does not invoke the virtual machine. Contract calls add an execution component on top. The distinction is the basis of MultiversX's two-part fee model. + +See also: [Gas and Fees](#gas-and-fees), [Transaction](#transaction), [Smart Contract Results](#smart-contract-results-scrs). +Read more: [docs.multiversx.com/developers/gas-and-fees/overview](https://docs.multiversx.com/developers/gas-and-fees/overview/). --- -### Ledger +### Miniblock -You can safely store your EGLD by installing the MultiversX EGLD app on your Ledger Nano S or Ledger Nano X device. We recommend using a hardware wallet for managing large amounts of EGLD. +A container inside a block that holds an ordered list of transactions grouped by their (sender shard, receiver shard) pair. +Context: A single MultiversX block can contain several miniblocks, one per shard-to-shard direction. The miniblock is the atomic unit of cross-shard execution: either the entire miniblock is processed, or none of its transactions are applied and it is retried in the next round. -## Before you begin +See also: [Block](#block), [Cross-Shard Transaction](#cross-shard-transaction), [Metablock](#metablock), [Cross-Shard Atomicity](#cross-shard-atomicity). +Read more: [docs.multiversx.com/learn/transactions](https://docs.multiversx.com/learn/transactions). -1. [Set up](https://support.ledger.com/hc/en-us/articles/360000613793) your Ledger device -2. [Download](https://www.ledger.com/ledger-live/download) the Ledger Live application -3. [Upgrade](https://support.ledger.com/hc/en-us/articles/360002731113) your device with the latest firmware‌ +--- +### Metablock -## **Install the MultiversX EGLD app on your Ledger** +The metachain's own block, which notarizes the finalized shard block headers rather than carrying user transactions. -Teach your Ledger device to work with EGLD by installing the MultiversX app on it.‌ +Context: When a shard block is final, the metachain records that block's header and miniblock hashes, together with its Equivalent Consensus Proof, in the next metablock. The metablock is what makes a shard block's cross-shard output visible network-wide. -- Open the Ledger Live application -- Click on the “Manager” section in the app -- Connect and unlock your Ledger device -- If prompted, allow the Manager on your device by pressing the two buttons -- In the App catalog search for “MultiversX” -- Click the “Install” button next to the MultiversX app -- Your Ledger device will display “Processing” as the app installs +See also: [Metachain](#metachain), [Notarization](#notarization), [Miniblock](#miniblock), [Hyperblock](#hyperblock). +Read more: [docs.multiversx.com/learn/transactions](https://docs.multiversx.com/learn/transactions). -![img](/wallet/ledger/ledger_live.png) +--- -Awesome, you did everything perfectly! +### Genesis Round +The first round of the first epoch of the chain, containing the network's bootstrapping phase. -## Log into your MultiversX wallet using the Ledger device +Context: MultiversX organizes time into rounds grouped into epochs; the genesis round is where the chain starts. Round length is fixed (currently 6 seconds) and an epoch is initially sized to last about 24 hours. -- Make sure your Ledger device is connected to your computer -- Log into your Ledger device -- Navigate to the MultiversX app & open it by pushing both buttons on your device -- In your web browser navigate to https://wallet.multiversx.com -- Select "Ledger" from the Login options +See also: [Round](#round), [Epoch](#epoch), [Block](#block). +Read more: [docs.multiversx.com/learn/chronology](https://docs.multiversx.com/learn/chronology). -![img](/wallet/ledger/connect_ledger.png) +--- -- A pop-up will notify you that your Ledger wants to connect. Allow it by clicking "Connect" +### Intra-Shard Transaction -![img](/wallet/ledger/paired.png) +A transaction whose sender and receiver accounts belong to the same shard, processed entirely within that one shard. -After connecting the Ledger you will be prompted to choose the address you want to connect with. Select the address then click on "Access Wallet". +Context: Because both accounts live in the same shard, an intra-shard transaction is executed and finalized in a single shard block, without the metachain-mediated round trip that cross-shard transactions require. -![img](/wallet/ledger/ledger_address.png) +See also: [Cross-Shard Transaction](#cross-shard-transaction), [Shard (Execution Shard)](#shard-execution-shard), [Bech32 Addressing](#bech32-addressing-erd1). +Read more: [docs.multiversx.com/integrators/faq](https://docs.multiversx.com/integrators/faq/); [docs.multiversx.com/learn/transactions](https://docs.multiversx.com/learn/transactions). -Your MultiversX Wallet will now open. +--- +### Cross-Shard Transaction -## Overview of your EGLD balance +A transaction whose sender and receiver accounts belong to different shards, executed asynchronously across both shards with metachain notarization in between. -After logging into your wallet, your EGLD balances are immediately visible and displayed in easy to follow boxes. +Context: A cross-shard transaction passes through the sender's shard, metachain notarization, and the receiver's shard. The source shard finalizes and ships a notarized cross-shard miniblock, and the destination shard picks it up and executes its part, so execution is asynchronous rather than atomic across the two shards. -![img](/wallet/web-wallet/wallet_balance_overview.png) +See also: [Intra-Shard Transaction](#intra-shard-transaction), [Cross-Shard Atomicity](#cross-shard-atomicity), [Miniblock](#miniblock), [Metachain](#metachain). +Read more: [docs.multiversx.com/integrators/faq](https://docs.multiversx.com/integrators/faq/); [docs.multiversx.com/learn/transactions](https://docs.multiversx.com/learn/transactions). -- **Available:** Freely transferable EGLD balance -- **Stake Delegation:** Amount of EGLD delegated towards a Staking Services provider -- **Legacy Delegation:** Amount of EGLD delegated towards MultiversX Community Delegation -- **Staked Nodes:** Amount of EGLD locked for your staked nodes +--- +### Verifiable Unpredictable Function (VUF) -## Send a transaction using the Ledger +A function that produces a random seed which is verifiable and unpredictable, used to derive the randomness that seeds proposer selection each round. -In the MultiversX web wallet: +Context: MultiversX uses a VUF to generate the new random seed carried in each block, and a Verifiable Random Function over that seed to select the block proposer. The VUF output is verifiable and unpredictable, though not uniform, which is why a separate VRF step is applied for selection. -- Click "Send" -- Input the destination address & amount -- Click Send -- In the next screen click "Confirm on Ledger"! +See also: [Verifiable Random Function (VRF)](#verifiable-random-function-vrf), [Block Proposer (Leader)](#block-proposer-leader), [Secure Proof of Stake (SPoS)](#secure-proof-of-stake-spos). +Read more: [docs.multiversx.com/learn/consensus](https://docs.multiversx.com/learn/consensus). -![img](/wallet/ledger/confirm.png) +--- -On your Ledger device: +### Mainnet -- Review the receiver address, amount & fee on your Ledger device -- Sign the transaction by pressing both buttons +The live, production MultiversX network, where transactions move real EGLD and real value. -Awesome, your transaction is now sent using your Ledger device. +Context: MultiversX mainnet launched in July 2020 and runs three execution shards plus the metachain. It is the network real applications and users transact on, as distinct from the public test networks. -![img](/wallet/ledger/tx_completed.png) +See also: [Testnet](#testnet), [Devnet](#devnet), [EGLD](#egld), [Shard (Execution Shard)](#shard-execution-shard). +Read more: [docs.multiversx.com/learn/EGLD](https://docs.multiversx.com/learn/EGLD/). + +--- +### Testnet -## Receiving EGLD in your wallet +A public test network that runs the same MultiversX protocol as mainnet, for testing applications without spending real EGLD. -After logging into your wallet, as described above, you will be able to see your wallet address and share it with others, so they can send you EGLD.‌ +Context: Testnet uses valueless test EGLD (xEGLD) obtained from a faucet. It mirrors mainnet behavior so applications can be validated before deployment. -Your address is immediately visible on the top part of the wallet, as "erd1... ". You can copy the address by pressing the copy button (two overlapping squares). ‌ +See also: [Devnet](#devnet), [Mainnet](#mainnet), [Faucet](#faucet), [Chain Simulator](#chain-simulator). +Read more: [docs.multiversx.com/learn/EGLD](https://docs.multiversx.com/learn/EGLD/). -You can also click "Receive" on the right-hand side to see a QR code for the address, which can be scanned to reveal the public address. +--- -![img](/wallet/ledger/receive.png) +### Devnet +A public development network running the same MultiversX protocol, used to try features and applications earlier than testnet. -## Guardians +Context: Devnet is the earliest public network for developers and typically carries in-development work; like testnet it uses valueless xEGLD from a faucet. Many applications run on devnet before promoting to mainnet. -To set up a guardian for a Ledger account, follow [these steps](/wallet/web-wallet.md#guardian). +See also: [Testnet](#testnet), [Mainnet](#mainnet), [Faucet](#faucet), [Chain Simulator](#chain-simulator). +Read more: [docs.multiversx.com/learn/EGLD](https://docs.multiversx.com/learn/EGLD/). -## **Need help?** +--- -- Reach out to MultiversX on your [preferred channel](https://linktr.ee/MultiversX) +### Faucet + +A service on the test networks that dispenses valueless test tokens (xEGLD) to an account, so developers can transact without spending real funds. + +Context: The web wallet on devnet and testnet includes a faucet that sends test xEGLD, subject to a rate limit; other community faucets exist. It is the standard way to fund an account for testing. + +See also: [Testnet](#testnet), [Devnet](#devnet), [Web Wallet](#web-wallet), [EGLD](#egld). +Read more: [docs.multiversx.com/wallet/web-wallet](https://docs.multiversx.com/wallet/web-wallet/); [docs.multiversx.com/learn/EGLD](https://docs.multiversx.com/learn/EGLD/). --- -### Local Setup +## Protocol and architecture -# Full Local Setup +### Adaptive State Sharding -## Deploy local Sovereign Chain +The MultiversX scaling design that splits the network into parallel shards across three dimensions at once (state, transactions, and network) and can change the number of shards in response to load. -This guide will help you deploy a full sovereign local network connected to MultiversX network. This includes all the smart contracts and dependent services needed. Follow these steps carefully to ensure a successful deployment. +Context: Each shard processes only the transactions whose sender or receiver belongs to it and stores only its own slice of global state, so capacity grows by adding shards rather than by making one chain faster. Mainnet has run a stable configuration of three execution shards plus a coordinating metachain since the July 2020 launch, because it has not been throughput-bound. "Adaptive" refers to the protocol-level ability to split or merge shards; that capability is present but latent at the current configuration. -### Step 1: Get the `mx-chain-sovereign-go` Repository +See also: [Metachain](#metachain), [Hyperblock](#hyperblock), [Cross-Shard Atomicity](#cross-shard-atomicity), [Secure Proof of Stake](#secure-proof-of-stake-spos). +Read more: [docs.multiversx.com/learn/sharding](https://docs.multiversx.com/learn/sharding); founding whitepaper, *A Highly Scalable Public Blockchain via Adaptive State Sharding and Secure Proof of Stake* ([files.multiversx.com/multiversx-whitepaper.pdf](https://files.multiversx.com/multiversx-whitepaper.pdf)). -Before proceeding, ensure that a **SSH key** for GitHub is configured on your machine. +--- -1. Clone the GitHub repository: - ```bash - git clone git@github.com:multiversx/mx-chain-sovereign-go.git - ``` +### Metachain -2. Navigate to testnet directory: - ```bash - cd mx-chain-sovereign-go/scripts/testnet - ``` +The blockchain that runs in a dedicated coordinating shard, whose responsibility is notarizing and finalizing shard block headers rather than processing user transactions. -3. Run the prerequisites script: - ```bash - ./prerequisites.sh - ``` +Context: Since the Andromeda upgrade a shard block is final within its own shard once more than two-thirds (2/3+1) of that shard's validators have signed it, independent of the metachain. The metachain's role is cross-shard: it includes the header and miniblock hashes of each new shard block in its own block, which is what lets the destination shard process that block's cross-shard output. The metachain also computes validator reshuffling at epoch boundaries and arbitrates cross-shard messaging. - :::info - The prerequisites script verifies and downloads the necessary packages to run the nodes and clones the required repositories: +See also: [Hyperblock](#hyperblock), [Cross-Shard Atomicity](#cross-shard-atomicity), [Validator Reshuffle](#validator-reshuffle-epoch-boundary), [Secure Proof of Stake](#secure-proof-of-stake-spos). +Read more: [docs.multiversx.com/integrators/faq](https://docs.multiversx.com/integrators/faq/). - - **mx-chain-deploy-sovereign-go**: Initializes the configuration for the chain and deployment parameters. - - **mx-chain-proxy-sovereign-go**: Repository for the sovereign proxy. - - **mx-chain-sovereign-bridge-go**: Repository for the cross-chain service. - - **mx-chain-tools-go**: Repository for updating elastic indices. - ::: +--- -### Step 2: Deploy Sovereign setup +### Hyperblock -#### Sovereign chain with no main chain connection +A block-like abstraction that reunites data from all shards from the perspective of a metachain block, and contains only fully executed transactions, meaning transactions executed in both their source and destination shards, where the final step is notarized in the metachain block with that nonce. -1. Update chain parameters in `variables.sh` file +Context: The hyperblock is the unit that represents synchronized cross-shard state at a point in the chain's history. It is distinct from a metachain block (the metachain's own block that notarizes shard headers). A cross-shard transaction's "hyperblock coordinates" are set once it is notarized on both shards with acknowledgment from the metachain. The concept predates the Supernova upgrade. -2. Start the chain with local scripts: -```bash -./config.sh && ./sovereignStart.sh -``` +See also: [Metachain](#metachain), [Cross-Shard Atomicity](#cross-shard-atomicity), [Supernova](#supernova). +Read more: [docs.multiversx.com/integrators/querying-the-blockchain](https://docs.multiversx.com/integrators/querying-the-blockchain/); [docs.multiversx.com/sdk-and-tools/rest-api/blocks](https://docs.multiversx.com/sdk-and-tools/rest-api/blocks/). -#### Sovereign chain with main chain connection +--- -Navigate to the `sovereignBridge` folder: -```bash -cd sovereignBridge -``` +### Cross-Shard Atomicity -1. Install the [software dependencies](/sovereign/software-dependencies) and download the cross-chain contracts by running the sovereign bridge prerequisites script: - ```bash - ./prerequisites.sh - ``` +Whether a transaction whose sender and receiver live in different shards is all-or-nothing across both. On MultiversX, cross-shard execution is asynchronous rather than atomic in that strict sense: each shard executes its part in its own block, and a step that fails on a later shard does not automatically reverse steps already completed on an earlier one. -2. Create a new wallet: - ```bash - mxpy wallet new --format pem --outfile ~/wallet.pem - ``` +Context: A cross-shard transaction passes through multiple blocks: the source shard finalizes and ships a notarized cross-shard miniblock, and the destination shard picks it up and executes it, mediated by the metachain. Because the parts execute in sequence rather than together, partial outcomes are possible. For example, a swap that completes on the source shard and forwards its result to another shard, where a follow-on step then fails, leaves the original swap in place unless the contract itself undoes it. Composability is therefore structurally asynchronous: a contract calling a contract on another shard receives the outcome in a callback, not as a synchronous return value, and is responsible for any compensating action on failure. This model has held since 2020; the Supernova upgrade compresses the wall-clock latency between the steps without changing it. - :::info - This wallet is the owner for cross chain smart contracts and is used by the sovereign bridge service. - ::: - - :::note - You can use any wallet of your choice, but for the purpose of this guide we are generating a new wallet. - ::: +See also: [Metachain](#metachain), [Hyperblock](#hyperblock), [Supernova](#supernova), [Typed Proxies](#typed-proxies). +Read more: [docs.multiversx.com/learn/sharding](https://docs.multiversx.com/learn/sharding); [github.com/multiversx/mx-chain-go](https://github.com/multiversx/mx-chain-go). -3. Get funds in this wallet on the chain (testnet/devnet/mainnet) you want the sovereign to be connected to. +--- -4. Update the configuration file `config/configs.cfg` with paths you want to use, wallet location and main chain constants. The following example shows the paths you have to use in order to connect to public MultiversX testnet: - ``` - # Sovereign Paths - SOVEREIGN_DIRECTORY="~/sovereign" - TXS_OUTFILE_DIRECTORY="${SOVEREIGN_DIRECTORY}/txsOutput" - CONTRACTS_DIRECTORY="${SOVEREIGN_DIRECTORY}/contracts" +### Secure Proof of Stake (SPoS) - # Owner Configuration - WALLET="~/wallet.pem" +MultiversX's Byzantine-fault-tolerant proof-of-stake consensus mechanism, in which validators are assigned to per-shard consensus groups at random and a verifiable random function selects block proposers and signers. - # Main chain network config - MAIN_CHAIN_ELASTIC=https://testnet-index.multiversx.com - PROXY = https://testnet-gateway.multiversx.com - CHAIN_ID = T +Context: Eligibility to validate depends on staked EGLD. A verifiable random function selects the block proposer each round, and validator signatures are aggregated with a BLS multi-signature scheme into a single proof. Since the Andromeda upgrade the full shard validator set signs each block, and a block is final once at least two-thirds have signed, giving single-block deterministic finality. SPoS is paired with adaptive state sharding as the two foundational designs from the 2018 whitepaper. - # Native token - NATIVE_ESDT_TICKER=SOV - NATIVE_ESDT_NAME=SovToken - ``` +See also: [Adaptive State Sharding](#adaptive-state-sharding), [Validator Reshuffle](#validator-reshuffle-epoch-boundary), [Metachain](#metachain). +Read more: [docs.multiversx.com/learn/consensus](https://docs.multiversx.com/learn/consensus); founding whitepaper ([files.multiversx.com/multiversx-whitepaper.pdf](https://files.multiversx.com/multiversx-whitepaper.pdf)). - :::note - - **SOVEREIGN_DIRECTORY, OUTFILE_PATH, CONTRACTS_DIRECTORY** - represent the paths to the location where the deployment scripts will generate the outputs. - - **WALLET** - should represent the wallet generated at Step 2.2. - - **MAIN_CHAIN_ELASTIC** - represents the elasticsearch db from the main chain - - **PROXY** - in this case, for the purpose of the test, the used proxy is the testnet one. Of course that the proper proxy should be used when deploying your own set of contracts depending on the development phase of your project. - - **CHAIN_ID** - should represent the chain ID of the chain where the contracts are to be deployed. - - **"1"** for Mainnet; - - **"D"** for Devnet; - - **"T"** for Testnet; - - or use you own local network ID - - **NATIVE_ESDT_TICKER** - represents the ticker from which the native esdt will be generated, and updated in configs - - **NATIVE_ESDT_NAME** - represents the native esdt name - ::: +--- -5. Source the script: - ```bash - source script.sh - ``` +### Validator Reshuffle (Epoch Boundary) -6. Create and deploy main chain observer: - ```bash - createAndDeployMainChainObserver - ``` - - :::info - After running this command, one should wait until the observer is fully synchronized. - ::: +The pseudo-random redistribution of a portion of each shard's validators to other shards at every epoch boundary. -6. Deploy all cross-chain contracts on main chain and deploy Sovereign Chain with all required services: - ```bash - deploySovereignWithCrossChainContracts sov - ``` +Context: At each epoch boundary (approximately 24 hours) up to one-third of each shard's validators are moved to new shards, computed by the metachain using its randomness source. The security rationale: if a validator cannot predict which shard it will be in next epoch, an attacker cannot cheaply concentrate a corrupt majority inside a single shard. This mechanism predates Supernova and is unchanged by it. A validator is a node that has staked at least 2500 EGLD and participates in consensus. - :::info - `deploySovereignWithCrossChainContracts` command will: - - deploy all smart contracts on the main chain and update the sovereign chain configuration - - set up sovereign nodes and deploy the main chain observer - - use `sov` as the default prefix for ESDT tokens on the sovereign chain - (can be changed to any 1–4 character lowercase alphanumeric string and **must be unique across all deployed sovereign chains**) - - if no prefix is provided, a random one will be generated - ::: +See also: [Secure Proof of Stake](#secure-proof-of-stake-spos), [Metachain](#metachain), [Adaptive State Sharding](#adaptive-state-sharding). +Read more: [docs.multiversx.com/learn/sharding](https://docs.multiversx.com/learn/sharding) (node shuffling). -### Step 3: Deploy services +--- -You can find the documentation on how to deploy services [here](/sovereign/services). +### Onchain Guardian -___ +An opt-in, protocol-level two-factor authentication standard in which a guarded account requires a second signature from a designated guardian before a transaction is valid. -## Stop and clean local Sovereign Chain +Context: When an account enables a Guardian, transactions must carry a guardian co-signature in addition to the owner's signature, and the guardian can be a standard authenticator app (such as Google Authenticator or Authy) or another service. Because the second factor is enforced by the protocol rather than by an application, a leaked seed phrase alone is not sufficient to move funds from a guarded account. The standard shipped to mainnet in July 2023. Guarded transactions cost an additional 50,000 gas. -1. Navigate to `mx-chain-sovereign-go/scripts/testnet/sovereignBridge`. - Source the script: - ```bash - source script.sh - ``` +See also: [Relayed Transactions](#relayed-transactions-v1--v2--v3), [NativeAuth](#nativeauth). +Read more: [docs.multiversx.com/developers/built-in-functions](https://docs.multiversx.com/developers/built-in-functions/); The Block, "MultiversX introduces on-chain 2FA" (July 14, 2023, [theblock.co/post/239592/multiversx-on-chain-2fa](https://www.theblock.co/post/239592/multiversx-on-chain-2fa)). -2. Stop and clean the chain, and all sovereign services: - ```bash - stopAndCleanSovereign - ``` +--- + +### Relayed Transactions (V1 / V2 / V3) + +A mechanism that lets one account (the relayer) pay the gas fees for another account's transaction, so the sending account can transact without holding EGLD for fees. + +Context: Versions 1 and 2 wrapped the inner transaction in the relayer's transaction and are being deprecated. Version 3 adds explicit `relayer` and `relayerSignature` fields directly on the transaction. For V3 the relayer field must be set before either party signs, the sender and relayer must be in the same shard, and the transaction costs an additional 50,000 gas. Relayed V3 was introduced in the Spica upgrade. + +See also: [Spica](#spica), [Onchain Guardian](#onchain-guardian). +Read more: [docs.multiversx.com/developers/relayed-transactions](https://docs.multiversx.com/developers/relayed-transactions/); MultiversX SDK reference ([github.com/multiversx/mx-sdk-js-core](https://github.com/multiversx/mx-sdk-js-core)). --- -### Managing Sovereign +### WASM VM (and Wasmer) -# Managing Sovereign Chains +The WebAssembly virtual machine in which MultiversX smart contracts execute. Contracts compile to the `wasm32` target and run inside this VM rather than on a chain-specific bytecode interpreter. -:::note -This version of the documentation focuses solely on the essential steps required to set up and deploy a sovereign chain on either a local or remote computer. More content will be added as it is accepted and discussed on [Agora](https://agora.multiversx.com/), or once it is implemented and available for production. -::: +Context: Most contracts are written in Rust against the `multiversx-sc` framework and compiled to optimized WebAssembly. The VM uses a Wasmer-based execution engine to run the WASM modules. ESDT token transfers do not invoke the VM at all, which is part of why token operations are cheaper than contract-based token systems on other chains. The VM identifier for WASM contracts is `0x0500`. + +See also: [ESDT](#esdt-estandard-digital-token), [sc-meta](#sc-meta), [Storage Mappers](#storage-mappers), [Tx Syntax](#tx-syntax). +Read more: [docs.multiversx.com/learn/space-vm](https://docs.multiversx.com/learn/space-vm); [github.com/multiversx/mx-chain-vm-go](https://github.com/multiversx/mx-chain-vm-go). --- -### MultiversX DeFi Wallet +### ESDT (eStandard Digital Token) -Popularly referred to as the "future of money," MultiversX currently has a robust web wallet extension known as the **MultiversX DeFi Wallet**. It is a powerful browser extension for the MultiversX Wallet that effectively automates and reduces the steps and time required for users to interact with MultiversX Decentralized apps. +MultiversX's native token standard, used to issue and manage fungible, semi-fungible, and non-fungible tokens at the protocol level without deploying a token-logic smart contract. -The MultiversX DeFi Wallet can be installed on Firefox, Chrome, Brave, and other chromium-based browsers. This extension is free and secure, with compelling features that allow you to create a new wallet or import existing wallets, manage multiple wallets on the MultiversX mainnet, and store MultiversX tokens such as EGLD, ESDT, or NFTs on the MultiversX Network with easy accessibility. +Context: Because token accounting is handled by the protocol, issuing or transferring an ESDT does not require the virtual machine, which lowers cost and complexity relative to contract-based standards. Token identifiers take the form `TICKER-randomhex` for fungible tokens, with a nonce suffix for non-fungible and semi-fungible tokens (`TICKER-randomhex-nonceHex`). A non-fungible token has a nonce greater than zero and a quantity of one; a semi-fungible token has a nonce greater than zero and a quantity greater than one. -Let's walk through the steps on how to install and set up the MultiversX DeFi Wallet extension: +See also: [WASM VM](#wasm-vm-and-wasmer), [EGLD](#egld), [Bech32 Addressing](#bech32-addressing-erd1). +Read more: [docs.multiversx.com/tokens/intro](https://docs.multiversx.com/tokens/intro) (expansion and protocol-level handling); [docs.multiversx.com/tokens/esdt-tokens](https://docs.multiversx.com/tokens/esdt-tokens/). +--- -## Prerequisites -Wallets don't have custody of your funds, you do. Before utilizing the MultiversX DeFi Wallet Extension, certain preliminary steps must be taken to ensure its proper use. +### Bech32 Addressing (erd1…) +The address format used on MultiversX, in which public keys are encoded as Bech32 strings with the human-readable prefix `erd`. -### Add MultiversX DeFi Wallet to your browser +Context: A typical address looks like `erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th`. The prefix (the human-readable part, HRP) defaults to `erd`. Addresses are 32-byte public keys; the shard an address belongs to is derived from the last bytes of the public key, which is how the protocol routes a transaction to the correct shard. Smart contract addresses are a recognizable subclass of the same format. -- On the [Chrome Web Store Extension](https://chrome.google.com/webstore/category/extensions), search for **MultiversX DeFi Wallet.** and add it to your browser. +See also: [ESDT](#esdt-estandard-digital-token), [Adaptive State Sharding](#adaptive-state-sharding), [EGLD](#egld). +Read more: MultiversX SDK `Address` reference ([github.com/multiversx/mx-sdk-js-core](https://github.com/multiversx/mx-sdk-js-core)). - ![MultiversX-defi-chrome](/wallet/wallet-extension/add_extension.png) +--- -- Confirm the action in the pop-up. +### EGLD - ![wallet_extension_step2](/wallet/wallet-extension/confirm_extension.png) +The native token of MultiversX, used to pay transaction fees, secure the network through staking, and fund validator rewards. -- You should receive a notification that the extension has been added successfully. +Context: EGLD ("eGold") was introduced at the July 2020 mainnet launch, redenominated from the prior ERD token at 1,000 ERD to 1 EGLD. At launch the project marketed a fixed maximum supply of 31.4 million EGLD. That fixed cap was removed by the Economic Evolution proposal in October 2025, which moved EGLD to a tail-inflation model. One EGLD is divisible to 18 decimals (1 EGLD = 10^18 of its smallest unit). A validator must stake at least 2500 EGLD. - ![wallet_extension_step3](/wallet/wallet-extension/extension_installed.png) +See also: [Economic Evolution](#economic-evolution), [Staking V5](#staking-v5-and-the-emission-split), [KPI-Gated Emissions](#kpi-gated-emissions), [Secure Proof of Stake](#secure-proof-of-stake-spos). +Read more: CoinDesk, "Elrond Launches Onto Mainnet, Reduces Token Supply by 99%" (July 31, 2020); MultiversX, "The MultiversX Economic Evolution" (Oct 22, 2025, [multiversx.com/blog/the-multiversx-economic-evolution-a-blueprint-for-growth-and-strategic-expansion](https://multiversx.com/blog/the-multiversx-economic-evolution-a-blueprint-for-growth-and-strategic-expansion)). +--- -### Set up MultiversX DeFi wallet +### KPI-Gated Emissions -- Once it has been successfully installed, click on the extension to get started. +An emissions design in which a portion of EGLD's annual issuance is released only if the ecosystem meets defined performance indicators, and is otherwise locked. -- You will be presented with two options: you can either _Create new wallet_ or _Import existing wallet._ +Context: Of the annual emission, the two growth-oriented buckets (the ecosystem growth fund and the user growth dividend, 20% each, so 40% combined) are conditional; the staking-reward and protocol-sustainability buckets are not. Four indicators are evaluated quarterly on three-month rolling averages: the staking ratio (target 65 to 70%), protocol revenue measured through fee burns (target above 10% quarter-on-quarter growth), DeFi activity (aggregate TVL and 24-hour volume targets), and price growth (a market indicator weighted in a composite score). Unmet targets lock the unused tokens at quarter-end, prorated to the share of the target achieved, enforced by smart contract. The distribution model is re-evaluated annually by governance vote. The mechanism is specified in the Economic Evolution framework, which is being rolled out in phases. - ![MultiversX-defi-example-welcome-page](/wallet/wallet-extension/extension.png) +See also: [Economic Evolution](#economic-evolution), [Staking V5](#staking-v5-and-the-emission-split), [EGLD](#egld), [Governance Proposal Process](#governance-proposal-process). +Read more: "A competitive economic framework for MultiversX: toward revenue and reflexive value accrual," MultiversX Agora ([agora.multiversx.com/t/...530](https://agora.multiversx.com/t/a-competitive-economic-framework-for-multiversx-toward-revenue-and-reflexive-value-accrual/530)); MultiversX, "The MultiversX Economic Evolution" (Oct 22, 2025, [multiversx.com/blog/the-multiversx-economic-evolution-a-blueprint-for-growth-and-strategic-expansion](https://multiversx.com/blog/the-multiversx-economic-evolution-a-blueprint-for-growth-and-strategic-expansion)). +--- -### Create a new wallet +### Shard (Execution Shard) -**Step 1:** To get started, first install the [MultiversX DeFi wallet extension](https://chrome.google.com/webstore/detail/multiversx-defi-wallet/dngmlblcodfobpdpecaadgfbcggfjfnm). +One of the parallel partitions of the network, each processing a subset of transactions and holding a slice of global state. -**Step 2:** Open up the extension and click on _‘’Create new wallet”_. +Context: Mainnet runs three execution shards plus the coordinating metachain. An account is assigned to a shard by the last bytes of its address, and a transaction is processed by the shard or shards of its sender and receiver. Adding shards is how the network scales throughput. -**Step 3:** Next, a secret phrase consisting of a set of 24 secret words will be displayed. Safely backup these secret words. We strongly recommend you write them down or copy and store them in a safe place like a password manager. These secret words are the key to your wallet account and cannot be recovered if lost. +See also: [Adaptive State Sharding](#adaptive-state-sharding), [Metachain](#metachain), [Cross-Shard Atomicity](#cross-shard-atomicity), [Bech32 Addressing](#bech32-addressing-erd1). +Read more: [docs.multiversx.com/learn/sharding](https://docs.multiversx.com/learn/sharding). -![MultiversX-defi-example-secret-phrase](/wallet/wallet-extension/mnemonic_extension.png) +--- -**Step 4:** Before proceeding to the next step, confirm that you have safely stored your secret phrase. +### Shard Split and Merge (Resharding) -**Step 5:** For further verification, you will be prompted to input some of the secret words. +The protocol-level ability to change the number of shards by splitting a shard into two when load rises, or merging shards when it falls, without halting the network. -![MultiversX-defi-example-authentication](/wallet/wallet-extension/confirm_secret_phrase.png) +Context: This is the "adaptive" part of adaptive state sharding. The machinery exists in the protocol, but mainnet has run a stable configuration of three execution shards plus the metachain since 2020 and has not been throughput-bound, so resharding has not been triggered in production. It is therefore an architectural capability rather than an exercised mainnet behavior. -**Step 6:** Create a password that will be used to access the wallets stored in the MultiversX DeFi wallet extension. Ensure you keep this password safe as it will be needed to access your wallets regularly. Please note that this password cannot be recovered if lost. +See also: [Adaptive State Sharding](#adaptive-state-sharding), [Shard (Execution Shard)](#shard-execution-shard), [Metachain](#metachain), [Validator Reshuffle](#validator-reshuffle-epoch-boundary). +Read more: [docs.multiversx.com/learn/sharding](https://docs.multiversx.com/learn/sharding); founding whitepaper ([files.multiversx.com/multiversx-whitepaper.pdf](https://files.multiversx.com/multiversx-whitepaper.pdf)). -![MultiversX-defi-example-password](/wallet/wallet-extension/extension_password.png) +--- -**Step 7:** Completed! Your MultiversX DeFi Wallet has been successfully created and set to be used. +### Epoch -![MultiversX-defi-example-successful](/wallet/wallet-extension/wallet_extension_created.png) +The protocol's day-scale time unit, currently about 24 hours, used for validator reshuffling, staking state changes, and reward computation. +Context: Many protocol events are denominated in epochs rather than wall-clock time. Validator reshuffling happens at epoch boundaries, the unstake-to-withdraw waiting period is 10 epochs, and a guardian becomes active 20 epochs after it is set. An epoch is divided into many rounds. -### Import existing wallet +See also: [Round](#round), [Validator Reshuffle](#validator-reshuffle-epoch-boundary), [Unbonding Period](#unbonding-period), [Onchain Guardian](#onchain-guardian). +Read more: [docs.multiversx.com/validators/staking](https://docs.multiversx.com/validators/staking/). -Do you already have a wallet? +--- -Then there is no need to create a new one. The MultiversX Wallet Extension provides an option to import your existing wallet. However, to import an existing wallet you must have access to its **secret (recovery) phrase**. +### Round -The MultiversX wallet has a set of 24-words, which serve as your wallet’s secret phrase. Using a secret phrase to import an existing wallet does not affect your wallet in any way. +The fixed time slot in which one block is proposed per shard. -**To get started:** +Context: In each round one validator is selected as proposer and the shard's validators sign the proposed block. Round length sets the block time: roughly 6 seconds today, which the Supernova upgrade (pre-mainnet) is designed to reduce to a 600-millisecond target. Many rounds make up an epoch. -**Step 1:** With the [MultiversX DeFi wallet extension](https://chrome.google.com/webstore/detail/multiversx-defi-wallet/dngmlblcodfobpdpecaadgfbcggfjfnm) installed. Click on ‘’Import existing wallet”. +See also: [Epoch](#epoch), [Secure Proof of Stake](#secure-proof-of-stake-spos), [Supernova](#supernova). +Read more: [docs.multiversx.com/learn/consensus](https://docs.multiversx.com/learn/consensus). -![MultiversX-defi-example-welcome-page](/wallet/wallet-extension/extension.png) +--- -**Step 2:** Next, enter your 24-word secret phrase. You can either enter these words one at a time or you can simply paste in the words using the "_paste_" icon. +### Smart Contract Results (SCRs) -![MultiversX-defi-example-import-wallet](/wallet/wallet-extension/write_secret_phase.png) +The result objects the protocol generates from smart-contract execution, used to deliver outcomes, value, and cross-shard effects back to callers and other contracts. -**Step 3:** Enter in your wallet password and confirm this password. +Context: A single transaction that calls a contract can produce one or more SCRs, including the gas refund and any cross-shard continuation of the call. SCRs are first-class records on the network, visible in explorers, rather than ordinary transactions. They are how MultiversX represents the asynchronous, cross-shard parts of a contract call. -![MultiversX-defi-example-password](/wallet/wallet-extension/extension_password.png) +See also: [Cross-Shard Atomicity](#cross-shard-atomicity), [Built-in Functions](#built-in-functions), [Gas and Fees](#gas-and-fees). +Read more: [docs.multiversx.com/developers/developer-reference/sc-api-functions](https://docs.multiversx.com/developers/developer-reference/sc-api-functions/). -**Step 4:** Completed! Your MultiversX DeFi Wallet has been successfully imported and set to be used. +--- -![MultiversX-defi-example-successful](/wallet/wallet-extension/wallet.png) +### Built-in Functions +Protocol-side functions that execute without a dedicated smart contract as receiver, handled directly by the protocol. -## Key features +Context: Roughly thirty built-in functions cover token and account operations. Examples include ESDTTransfer and MultiESDTNFTTransfer (token transfers), ESDTNFTCreate (mint), ClaimDeveloperRewards, ChangeOwnerAddress, SetUserName (herotag via DNS), and SetGuardian and GuardAccount. Because they run protocol-side, they are cheaper than equivalent contract code and available chain-wide without deploying anything. -Now you have a wallet registered in the MultiversX DeFi Wallet Extension and it's ready to use. Great! Here's what you can do with this wallet: +See also: [ESDT](#esdt-estandard-digital-token), [System Smart Contracts](#system-smart-contracts), [Onchain Guardian](#onchain-guardian), [Herotag](#herotag). +Read more: [docs.multiversx.com/developers/built-in-functions](https://docs.multiversx.com/developers/built-in-functions/). +--- -### Send to a wallet +### System Smart Contracts -One of the key features of this extension is that it allows you to send funds from your wallet to another wallet. To use this feature, you will need to have some funds in your wallet before proceeding. +Protocol-level contracts deployed at fixed, well-known addresses that implement core functions such as staking, ESDT issuance, delegation, and governance. -**To get started** +Context: Unlike user-deployed contracts, these ship with the protocol and live at reserved addresses. Issuing a token, staking a node, creating a delegation contract, or submitting a governance proposal are all transactions sent to a system smart contract. They are the protocol's own onchain logic. -**Step 1:** Go to the MultiversX Wallet extension, enter your password and click on _“**Send**”_. +See also: [ESDT](#esdt-estandard-digital-token), [Staking](#staking), [Delegation](#delegation), [Governance Proposal Process](#governance-proposal-process). +Read more: [docs.multiversx.com/validators/staking/staking-smart-contract](https://docs.multiversx.com/validators/staking/staking-smart-contract/); [docs.multiversx.com/developers/built-in-functions](https://docs.multiversx.com/developers/built-in-functions/). -![MultiversX-defi-example-step9](/wallet/wallet-extension/wallet.png) +--- -**Step 2:** Enter the address of the wallet you intend to send to and the amount. +### Gas and Fees -![MultiversX-defi-example-send](/wallet/wallet-extension/send.png) +MultiversX's transaction-cost model, in which a transaction declares a gas limit and gas price and the fee is computed from two components: moving value and data, and executing contract code. -_(Optional)_ **Step 3**: Enter the data. This is a description of the transaction or any information you wish to pass through. +Context: A simple EGLD transfer pays only the value-movement component (a minimum gas limit plus a per-data-byte cost). A contract call adds a second component, charged at the gas price times a gas-price modifier that is typically lower. Gas paid beyond what a call actually consumes is refunded. This split is what lets the protocol route most of a contract call's fee to the contract's developer. -**Step 4:** Click on the _“Continue”_ button to complete the transaction. +See also: [Developer Revenue Share](#developer-revenue-share), [Smart Contract Results](#smart-contract-results-scrs), [Relayed Transactions](#relayed-transactions-v1--v2--v3). +Read more: [docs.multiversx.com/developers/gas-and-fees/overview](https://docs.multiversx.com/developers/gas-and-fees/overview/). +--- -### Lock/unlock +### Gas Refund -After 60 minutes of being inactive, the extension automatically locks itself. You can unlock it at any time using your password. In addition, you can lock the extension manually, by clicking the **“lock”** icon in the header. +The return of gas a transaction reserved but did not consume, paid back to the sender as part of the transaction's results. +Context: A caller sets a gas limit up front; if execution uses less, the difference is refunded rather than kept. The refund is delivered as a smart contract result and is visible on the transaction in explorers. This is why setting a generous gas limit on a contract call does not, by itself, cost the full amount. -### Deposit to a wallet +See also: [Gas and Fees](#gas-and-fees), [Gas-Price Modifier](#gas-price-modifier), [Smart Contract Results](#smart-contract-results-scrs). +Read more: [docs.multiversx.com/developers/gas-and-fees/overview](https://docs.multiversx.com/developers/gas-and-fees/overview/). -A deposit can be made to your wallet using the wallet extension. This feature allows you to share your QR code or wallet address to receive a token deposit. To get started: +--- -- Open up your MultiversX Wallet extension. +### Gas-Price Modifier -- Next, click on the _"*deposit*"_ and share your QR code or wallet address. +A protocol factor, less than one, applied to the execution portion of a contract call's fee, so the gas spent running contract code is charged at a lower rate than the base gas price. - ![MultiversX-defi-example-deposit](/wallet/wallet-extension/receive.png) +Context: A transaction's cost splits into moving value and data (charged at the full gas price) and executing contract code (charged at the gas price times the modifier). The modifier lowers the effective cost of computation and is part of how the fee model keeps contract calls affordable. It applies only to the execution component, not to the value-movement component. +See also: [Gas and Fees](#gas-and-fees), [Move Balance](#move-balance), [Gas Refund](#gas-refund), [Developer Revenue Share](#developer-revenue-share). +Read more: [docs.multiversx.com/developers/gas-and-fees/overview](https://docs.multiversx.com/developers/gas-and-fees/overview/). -### Transactions history +--- -On the wallet extension dashboard, the wallet records all transactions sent and received in your wallet. If you are a new user, it says "_No transactions found for this wallet_" until you make your first transaction. +### Herotag -![MultiversX-defi-example-step9](/wallet/wallet-extension/wallet.png) +A human-readable name mapped to a MultiversX address through the protocol's onchain Distributed Name Service (DNS), so funds can be sent to a name instead of a full address. +Context: Instead of an `erd1...` address, a user can register and share a herotag (a short handle). The mapping is stored onchain through DNS smart contracts and resolved by wallets and explorers; the username is set with the SetUserName built-in function. It is MultiversX's naming-service equivalent. -### Networks +See also: [Bech32 Addressing](#bech32-addressing-erd1), [Built-in Functions](#built-in-functions). +Source: [docs.multiversx.com/welcome/terminology](https://docs.multiversx.com/welcome/terminology/); xPortal Help Center, "What is a herotag?". -In the settings section on your extension dashboard, you can connect to the different networks provided by MultiversX, such as the mainnet, testnet, and devnet. +--- -Choose either of these networks. +### Sovereign Chains +A framework and SDK for launching application-specific chains that connect to the MultiversX network while remaining independently configurable. -## Connecting the MultiversX DeFi Wallet to xExchange App +Context: A Sovereign Chain sets its own parameters (public or private, staking model, gas and fee rules, supported tokens) and runs a consensus configuration that dedicates most of its time to processing for higher throughput on a dedicated workload. It is MultiversX's approach to appchains and interoperability, announced in 2024. -You can now connect xExchange to the MultiversX DeFi wallet in real-time. With this connection, you will be able to log in to the exchange using the MultiversX DeFi wallet extension in a few steps. -Follow these steps to proceed: +See also: [Adaptive State Sharding](#adaptive-state-sharding), [ESDT](#esdt-estandard-digital-token). +Read more: [docs.multiversx.com/sovereign/overview](https://docs.multiversx.com/sovereign/overview/); [docs.multiversx.com/sovereign/concept](https://docs.multiversx.com/sovereign/concept/). -**Step 1:** To get started, go to the [MultiversX Exchange](https://xexchange.com) page on the right section of the page, click on the _“connect”_ button. +--- -![wallet_extension_step17](/wallet/wallet-extension/xexchange.png) +### SpaceVM -**Step 2:** Select "**_MultiversX DeFi Wallet_**" from the options displayed. +The name of the MultiversX virtual machine that executes smart contracts, a WebAssembly engine also documented under WASM VM. -![wallet_extension_step15](/wallet/wallet-extension/login.png) +Context: SpaceVM runs contracts compiled to the wasm32 target and is stateless: during execution a contract cannot write directly to storage or the blockchain, and the accumulated changes are applied only at the end, and only on success. Reading global state is permitted at any time. -**Step 3:** Lastly, enter your password and click on the wallet address you want to connect to. +See also: [WASM VM (and Wasmer)](#wasm-vm-and-wasmer), [Stateless Execution](#stateless-execution), [Smart Contract](#smart-contract). +Read more: [docs.multiversx.com/learn/space-vm](https://docs.multiversx.com/learn/space-vm). -![wallet_extension_step16](/wallet/wallet-extension/extension_login.png) +--- -- In a split second, the MultiversX Exchange home page automatically reloads. You’ll notice your account has been added to the right section of the page. +### Stateless Execution -![wallet_extension_step18](/wallet/wallet-extension/logged.png) +The MultiversX VM property that a smart contract cannot write directly to storage or the blockchain during execution; changes accumulate in a transient structure and are committed only at the end, and only if execution succeeds. -Successful 🎉 +Context: This design removes the need for reverting operations: a failed execution leaves global state untouched because nothing was written until success. Reading global state is allowed at any time during execution. +See also: [SpaceVM](#spacevm), [WASM VM (and Wasmer)](#wasm-vm-and-wasmer), [Smart Contract Results (SCRs)](#smart-contract-results-scrs). +Read more: [docs.multiversx.com/learn/space-vm](https://docs.multiversx.com/learn/space-vm). -## Guardian +--- -You can use the MultiversX DeFi wallet with [Guardians](/developers/guard-accounts). It is as simple as it was before, with the only mention that you have to introduce the 2FA code for guardian signature when requested: +### Restaking (Sovereign Chains) -![guardian_extension1](/wallet/wallet-extension/guardian_extension1.png) +A model in which EGLD holders stake to help secure a sovereign chain and earn additional yield on top of base EGLD staking rewards, without giving up custody. ---- +Context: Restaking lets the security of the MultiversX mainchain extend to sovereign chains and lets participants earn extra returns for backing them. It is part of how sovereign chains bootstrap validator participation. -### Mvx Esdt Safe +See also: [Sovereign Chains](#sovereign-chains), [Dual Staking (Sovereign Chains)](#dual-staking-sovereign-chains), [Staking](#staking), [Delegation](#delegation). +Read more: [docs.multiversx.com/sovereign/restaking](https://docs.multiversx.com/sovereign/restaking/). -# Mvx-ESDT-Safe -![To Sovereign](../../static/sovereign/to-sovereign.png) +--- -The ability to transfer tokens from the Main Chain to any Sovereign Chain is essential, since every Sovereign can connect to the Main MultiversX Chain. As a result, the customizable Sovereign can leverage any token available on the default network. Another great feature is the possibility of executing smart contracts inside the Sovereign Chain through this contract. +### Dual Staking (Sovereign Chains) -This contract has three main modules: [`deposit`](#deposit), [`execute_operation`](#executing-an-operation) and [`register_token`](#registering-tokens). +A design in which a validator earns rewards from both the MultiversX mainchain and a sovereign chain at the same time. +Context: Dual staking aims to let validators contribute to and be rewarded by a sovereign chain while continuing to validate the mainchain, improving participation efficiency across the two. -## Deposit -### Main Chain deposit to Sovereign Chain transfer flow -1. User deposits the tokens he wishes to transfer in the `Mvx-ESDT-Safe` contract deployed on the Main Chain. -2. An observer is monitoring the Main Chain. -3. Sovereign network receives extended shard header. -4. Incoming transactions processor handles and processes the new transaction. +See also: [Sovereign Chains](#sovereign-chains), [Restaking (Sovereign Chains)](#restaking-sovereign-chains), [Staking](#staking). +Read more: [docs.multiversx.com/sovereign/dual-staking](https://docs.multiversx.com/sovereign/dual-staking/). -### Deposit Endpoint -```rust - #[payable("*")] - #[endpoint] - fn deposit( - &self, - to: ManagedAddress, - optional_transfer_data: OptionalValueTransferDataTuple, - ) -``` +--- -One key aspect of cross chain transfers from MultiversX Main Chain to a Sovereign Chain is being able to transfer tokens and also execute a smart contract call within single transaction. The `#[payable("*")]` annotation means that the endpoint can receive any tokens that will transferred. If those tokens are from a Sovereign Chain they will be burned otherwise they will be saved in the Smart Contract`s account. The checks enabled for the transfer of tokens are the following: +### Header Verifier -- If the token is whitelisted or not blacklisted, in that case the tokens can be transferred. -- If the fee is enabled, the smart contract assures that the fee is paid. -- If there are maximum 10 transfers in the transaction. +A component that verifies mainchain block headers on a sovereign chain, so cross-chain transfers act only on data proven final on the origin chain. -If the deposit also includes the `optional_transfer_data` parameter it will also have some extra checks regarding the cross-chain execution of endpoints: +Context: The header verifier checks proofs of execution and finality before a sovereign chain accepts a cross-chain operation, which is what makes bridging between the mainchain and a sovereign chain secure. -- The gas limit must be under the specified limit. -- The endpoint that has to be executed is not blacklisted. +See also: [Sovereign Chains](#sovereign-chains), [ESDT-Safe](#esdt-safe), [Cross-Shard Message Buffer](#cross-shard-message-buffer). +Read more: [docs.multiversx.com/sovereign/concept](https://docs.multiversx.com/sovereign/concept/). +--- -At the end of the `deposit` endpoint, all the extra tokens will be refunded to the caller and an event will be emitted since the bridging process is complete. +### ESDT-Safe +The bridge contract that locks or receives tokens on one side of a cross-chain transfer and emits the events that trigger the corresponding action on the other side. -```rust -#[event("deposit")] -fn deposit_event( - &self, - #[indexed] dest_address: &ManagedAddress, - #[indexed] tokens: &MultiValueEncoded>, - event_data: OperationData, -) -``` +Context: On a sovereign or bridge setup, the ESDT-Safe contract accepts a token deposit with a destination address and function call, then logs an event that the relaying infrastructure uses to complete the transfer on the other chain. It is paired with a counterpart safe contract on the other side. -This log event will emit the destination address and the tokens which will be transferred to the Sovereign Chain. +See also: [Sovereign Chains](#sovereign-chains), [Header Verifier](#header-verifier), [Ad-Astra Bridge](#ad-astra-bridge), [Bridged Tokens](#bridged-tokens). +Read more: [docs.multiversx.com/sovereign/concept](https://docs.multiversx.com/sovereign/concept/). -:::note -The source code for the endpoint can be found [here](https://github.com/multiversx/mx-sovereign-sc/blob/main/mvx-esdt-safe/src/deposit.rs). -::: +--- -## Executing an Operation +### Ad-Astra Bridge -```rust -#[endpoint(executeBridgeOps)] - fn execute_operations( - &self, - hash_of_hashes: ManagedBuffer, - operation: Operation -) -``` -- `hash_of_hashes`: hash of all hashes of the operations that were sent in a round -- `operation`: the details of the cross-chain execution +The MultiversX cross-chain bridge that transfers tokens between MultiversX and EVM-compatible chains such as Ethereum, using safe and bridge contracts on each side operated by a relayer set. -To ensure that the cross-chain execution is will be successful, the following checks must be passed: +Context: The bridge is run by ten relayers, five managed by the MultiversX Foundation and five by community validators, which together operate the contracts under a quorum. It moves tokens between ERC20 and ESDT form. -1. Calculate the hash of the *Operation* received as a parameter. -2. Verify that the given *Operation’s* hash is registered by the Header-Verifier smart contract. -3. Mint tokens or get them from the account. -4. Distribute the tokens. -5. Emit confirmation event or fail event if needed. +See also: [Bridged Tokens](#bridged-tokens), [Axelar (Bridge)](#axelar-bridge), [ESDT-Safe](#esdt-safe), [WEGLD (Wrapped EGLD)](#wegld-wrapped-egld). +Read more: [docs.multiversx.com/bridge/architecture](https://docs.multiversx.com/bridge/architecture/). -As the 2nd point specifies, the `Header-Verifier` smart contract plays an important role in the cross-chain execution mechanism. In the [`Header-Verifier`](header-verifier.md) section there will also be a description for the important endpoints within this contract. +--- -:::note -The source code for the endpoint can be found [here](https://github.com/multiversx/mx-sovereign-sc/blob/main/mvx-esdt-safe/src/execute.rs). -::: +### Axelar (Bridge) -## Important Endpoint Related Structures -This subsection outlines the key data structures that enable robust cross-chain operations. It details how an *Operation* is composed of its destination address, one or more token transfers defined by `OperationEsdtPayment`, and the contextual metadata provided by `OperationData`. Additionally, `TransferData` specifies the parameters needed for executing remote smart contract calls, collectively ensuring precise control over cross-chain interactions. -```rust -#[derive(TopEncode, TopDecode, NestedEncode, NestedDecode, TypeAbi, ManagedVecItem, Clone)] -pub struct Operation { - pub to: ManagedAddress, - pub tokens: ManagedVec>, - pub data: OperationData, -} -``` +A second cross-chain path for MultiversX that uses Axelar's verifier network to reach a broader set of chains, requiring a MultiversX observing squad as part of the setup. -- `to`: specifies the destination of the *Operation* -- `tokens`: represents one or more token transfers associated with the operation -- `data`: encapsulates additional instructions or parameters that guide the execution of the operation +Context: Alongside the Ad-Astra bridge, the Axelar integration lets MultiversX interoperate with the chains Axelar connects, with MultiversX acting as a verifier chain in Axelar's Amplifier model. -```rust -pub struct OperationEsdtPayment { - pub token_identifier: TokenIdentifier, - pub token_nonce: u64, - pub token_data: EsdtTokenData, -} -``` +See also: [Ad-Astra Bridge](#ad-astra-bridge), [Bridged Tokens](#bridged-tokens), [Observing Squad](#observing-squad). +Read more: [docs.multiversx.com/bridge/axelar](https://docs.multiversx.com/bridge/axelar/). -This struct describes a single token transfer action within an *Operation*. Each Operation can have one or more of such payments, with that enabling the transfer of a variety of tokens during a cross-chain transaction. +--- -- `token_identifier`: used for the identification of the token -- `token_nonce`: if the token is Non-Fungible or Semi-Fungible, it will have a custom nonce, if not the value will be 0 -- `token_data`: a structure holding metadata and other token properties +### Bridged Tokens -```rust -pub struct OperationData { - pub op_nonce: TxId, - pub op_sender: ManagedAddress, - pub opt_transfer_data: Option>, -} -``` +Tokens created on a non-native chain to represent an asset from another chain, kept at parity with the original by the bridge either minting and burning or locking and unlocking. -`OperationData` encapsulates the needed information for the *Operation* that needs to be executed. This isn’t just another data definition, we’ve already seen data-related fields elsewhere. Instead, it centralizes the contextual information that *Operation* needs before, during, and after execution. +Context: Whether a bridged token uses a mint-and-burn or lock-and-unlock mechanism depends on which chain is the token's native chain and whether a mint role is available. On MultiversX these appear as ESDTs that mirror an ERC20 (or the reverse), maintained one-to-one by the bridge. -- `op_nonce`: is used for the identification of each *Operation* -- `op_sender`: represents the original sender of the *Operation* -- `opt_transfer_data`: an optional `TransferData` field, when present, contains details about the cross-chain execution of another Smart Contract +See also: [Ad-Astra Bridge](#ad-astra-bridge), [Axelar (Bridge)](#axelar-bridge), [ESDT (eStandard Digital Token)](#esdt-estandard-digital-token), [WEGLD (Wrapped EGLD)](#wegld-wrapped-egld). +Read more: [docs.multiversx.com/bridge/token-types](https://docs.multiversx.com/bridge/token-types/). -```rust -pub struct TransferData { - pub gas_limit: GasLimit, - pub function: ManagedBuffer, - pub args: ManagedVec>, -} -``` +--- -`TransferData` represents the description of the remote execution of another Smart Contract. +## Consensus and security -- `gas_limit`: specifies the needed gas for the execution of all other endpoints. -- `function`: the name of the endpoint that will be executed. -- `args`: the arguments for the calls. +The mechanics behind Secure Proof of Stake, and the Supernova consensus model layered on top of it. -:::note -The source code for structures can be found [here](https://github.com/multiversx/mx-sovereign-sc/blob/main/common/structs/src/operation.rs). -::: +### Consensus Group -## Registering tokens -As mentioned at the start of this section, in the scope a Sovereign Chain, a token that already exists inside the MultiversX Mainchain can be leveraged within the custom blockchain. It has to be firstly registered inside the `Mvx-ESDT-Safe` smart contract. The `register_token` module has the role of registering any token that will be later used inside the Sovereign Chain. +The set of validators responsible for proposing and signing a shard's block in a given round. -### Register any token +Context: Since the Andromeda upgrade the consensus group is the shard's full validator set (about 400 nodes), fixed for the epoch, rather than a small subset re-drawn each round. One member is selected as proposer each round; the rest sign, and a block finalizes once at least two-thirds have signed. -```rust - #[payable("EGLD")] - #[endpoint(registerToken)] - fn register_token( - &self, - sov_token_id: TokenIdentifier, - token_type: EsdtTokenType, - token_display_name: ManagedBuffer, - token_ticker: ManagedBuffer, - num_decimals: usize, - ) -``` +See also: [Secure Proof of Stake](#secure-proof-of-stake-spos), [Block Proposer](#block-proposer-leader), [Validator Reshuffle](#validator-reshuffle-epoch-boundary), [Equivalent Consensus Proof](#equivalent-consensus-proof-ecp). +Read more: [docs.multiversx.com/learn/consensus](https://docs.multiversx.com/learn/consensus). -This endpoint is how an user from a Sovereign Chain registers a token on the MultiversX Mainchain. Every token registration costs 0.05 EGLD, that's why the endpoint is `payable`. -The endpoint check if the token was not registered before and if it has a prefix. +--- -> Every token that was created in a Sovereign Chain has a prefix. Example: `sov-TOKEN-123456`. +### Block Proposer (Leader) -If everything is in order, the `Mvx-ESDT-Safe` smart contract will initiate an asynchronous call to the `issue_and_set_all_roles` endpoint from the _ESDTSystemSC_. When the system smart contract finishes the issue transaction, the callback inside the `Mvx-ESDT-Safe` smart contract will trigger and register the mapping of token identifier inside the token mappers: +The validator chosen to build and broadcast the block in a given round. -* `sovereign_to_multiversx_token_id_mapper(sov_token_id)` -> mvx_token_id -* `multiversx_to_sovereign_token_id_mapper(mvx_token_id)` -> sov_token_id +Context: Selection is random and verifiable: each validator computes a score by hashing its public key with the round's randomness, and the lowest score becomes proposer. The proposer assembles the block and aggregates the other validators' signatures into the consensus proof. -> After the execution of this endpoint, the `Mvx-ESDT-Safe` smart contract will have in its storage a pair of token identifiers. **Example**: `sov-TOKEN-123456` is the corresponding sovereign identifier for the `TOKEN-123456` identifier from the MultiversX Mainchain. You can view this feature as creating copies of MultiversX Mainchain tokens inside the Sovereign Chain. +See also: [Consensus Group](#consensus-group), [Verifiable Random Function](#verifiable-random-function-vrf), [BLS Multi-Signature](#bls-multi-signature), [Equivalent Consensus Proof](#equivalent-consensus-proof-ecp). +Read more: [docs.multiversx.com/learn/consensus](https://docs.multiversx.com/learn/consensus). -### Register the native token -Since a Sovereign Chain is a separate blockchain from the MultiversX Mainchain, it has to have a its own native token. Registering the native token is a straightforward process of just one endpoint call. +--- -```rust - #[payable("EGLD")] - #[only_owner] - #[endpoint(registerNativeToken)] - fn register_native_token(&self, token_ticker: ManagedBuffer, token_name: ManagedBuffer) -``` +### Verifiable Random Function (VRF) -The owner will have to call the `register_native_token` from the `Mvx-ESDT-Safe` smart contract in order to register the token identifier that will be used inside the Sovereign Chain as the native one. There can only be one native token so the endpoint firstly checks if if was not already registered. The fee amount for registering is the same as registering any token, 0.05 EGLD. The parameters include the `token_ticker` and `token_name`. The endpoint then initiates an asynchronous call to the _ESDTSystemSC_ to `issue_and_set_all_roles`. The newly created token is always fungible and has 18 decimals. After the issue call is finished the callback inside the `Mvx-ESDT-Safe` smart contract inserts the newly issued token identifier inside its storage. +A function that produces randomness which is unpredictable in advance but verifiable afterward, used to drive proposer and validator selection. -:::note -The source code for this module can be found [here](https://github.com/multiversx/mx-sovereign-sc/blob/main/mvx-esdt-safe/src/register_token.rs). -::: +Context: MultiversX derives each round's randomness from the previous block's seed, so no participant can predict or bias proposer selection, yet anyone can verify the result. The same randomness source drives epoch validator reshuffling. + +See also: [Block Proposer](#block-proposer-leader), [Secure Proof of Stake](#secure-proof-of-stake-spos), [Validator Reshuffle](#validator-reshuffle-epoch-boundary). +Read more: [docs.multiversx.com/learn/consensus](https://docs.multiversx.com/learn/consensus). --- -### One Click Deployment +### BLS Multi-Signature -# One-Click Deployment +A signature scheme that aggregates many validators' signatures on a block into a single, compact signature. -## One-click local sovereign deployment in Digital Ocean +Context: MultiversX validators sign the block hash and the proposer aggregates at least two-thirds of the signatures into one 96-byte signature. Aggregation keeps consensus messages small, which is part of how the network reaches finality quickly. -At present, the only one-click deployment option is available on the Digital Ocean marketplace. This solution sets up a droplet containing a local Sovereign Chain connected to the MultiversX public testnet, complete with all essential services such as API, wallet, explorer, and more. +See also: [Equivalent Consensus Proof](#equivalent-consensus-proof-ecp), [Consensus Group](#consensus-group), [Secure Proof of Stake](#secure-proof-of-stake-spos). +Read more: [docs.multiversx.com/learn/consensus](https://docs.multiversx.com/learn/consensus). -### Create Sovereign Chain droplet +--- -- Go to [Digital Ocean marketplace](https://marketplace.digitalocean.com/apps/multiversx-testnet-sovereign-chain) for more info and to create your Sovereign Chain droplet. +### Equivalent Consensus Proof (ECP) -:::note -It is recommended to choose a droplet with minimum 8 CPUs and 32GB RAM for the best performance. -::: +The aggregated BLS signature, from at least two-thirds of a shard's validators, that finalizes a block. -:::important -The one-click deployment setup provided in this guide is designed primarily for development purposes. By default, it connects to the MultiversX public testnet and runs all services on the same machine. This setup is equivalent to the Full Local Setup but operates in the cloud, providing a convenient way to test and develop your Sovereign Chain in a hosted environment. -::: +Context: A block is final the moment its ECP is broadcast, and only one valid ECP can exist for a given block, which makes equivocation impossible. The ECP is what gives MultiversX single-block, deterministic finality, and is what let the Andromeda upgrade remove the old confirmation block. + +See also: [BLS Multi-Signature](#bls-multi-signature), [Deterministic Finality](#deterministic-finality), [Andromeda](#andromeda), [Notarization](#notarization). +Read more: [docs.multiversx.com/learn/consensus](https://docs.multiversx.com/learn/consensus); [docs.multiversx.com/integrators/faq](https://docs.multiversx.com/integrators/faq/). --- -### Other Vm +### Notarization -# Other-VM +The metachain's recording of a finalized shard block header, which enables cross-shard settlement. -:::note -As the MultiversX Sovereign Chains ecosystem grows, additional VMs will be added and described here over time. SpaceVM implements every necessary interface and the new VM needs to only change the EXECUTOR part inside the SpaceVM.. -::: +Context: A shard block is already final within its own shard once more than two-thirds of its validators have signed it (its Equivalent Consensus Proof). Notarization is the separate step in which the metachain includes that block's header and aggregate signature in its own block, making the block visible network-wide so other shards can act on its cross-shard output. ---- +See also: [Metachain](#metachain), [Hyperblock](#hyperblock), [Cross-Shard Atomicity](#cross-shard-atomicity), [Equivalent Consensus Proof](#equivalent-consensus-proof-ecp). +Read more: [docs.multiversx.com/integrators/faq](https://docs.multiversx.com/integrators/faq/). -### Restaking +--- -# Restaking +### Agree-then-Run -## Introduction +The Supernova consensus model in which validators first agree on the order of transactions and then execute them, rather than agreeing on execution results. -In the blockchain space, reStaking products are emerging as a significant innovation. Eigenlayer is currently the leading platform, but new projects are continuously entering the Ethereum ecosystem with impressive valuations. Currently, over $13 billion is reStaked in Eigenlayer, despite the absence of a live product. +Context: Validators still agree on the execution results of previous blocks, but the current block's execution result is no longer on the critical path for that block's consensus. Decoupling ordering from execution reduces the required block time, so consensus for one block can proceed while the previous block is still executing. This is what lets Supernova target sub-second blocks while preserving deterministic finality. -## General Economics and Staking Models +See also: [Pipelined Consensus](#pipelined-consensus), [Partial-Precommit Execution Dispatch](#partial-precommit-execution-dispatch), [Supernova](#supernova), [Deterministic Finality](#deterministic-finality). +Read more: MultiversX, "Supernova: decoupling consensus and execution" ([multiversx.com/blog/supernova-decoupling-consensus-and-execution](https://multiversx.com/blog/supernova-decoupling-consensus-and-execution)). -When designing the General Economics for Sovereign Chains, extensive research was conducted on various economic and staking models. This included insights from IBC (Cosmos ecosystem), the new Interchain Security model, Polygon SuperChains, Avalanche Appchains, various Ethereum Layer 2 solutions, and Eigenlayer. Each model was analyzed, incorporating the best features and discarding the less effective ones. +--- -## Current Proposed Economics Design +### Pipelined Consensus -The currently proposed economic design for Sovereign Chains incorporates a one-time reStaking model, non-custodial delegation, and allows extensive customization with native tokens. This framework enables the creation of an economic security fund that Sovereign Chains can leverage. Additionally, a general Sovereign Validator pool can be established, allowing new chains to launch using existing validators and economic resources without needing to gather new participants. +A consensus design in which voting for each block runs in parallel with its own execution, rather than waiting for execution to complete before proceeding. -This design allows validators to be sponsored by users, meaning validators do not need to hold EGLD; instead, users can delegate their tokens. This flexibility extends to users delegating directly to Sovereign Chains. +Context: Under Supernova, consensus and execution each run on their own pipeline and proceed in parallel. If execution extends past the block slot it overlaps with the next block's consensus, and it does not block it. This removes execution from the critical path of agreement and is the main source of Supernova's block-time reduction. -## Enhanced Flexibility and Security +See also: [Agree-then-Run](#agree-then-run), [Partial-Precommit Execution Dispatch](#partial-precommit-execution-dispatch), [Supernova](#supernova). +Read more: MultiversX, "Supernova: decoupling consensus and execution" ([multiversx.com/blog/supernova-decoupling-consensus-and-execution](https://multiversx.com/blog/supernova-decoupling-consensus-and-execution)). -The designed system aims to combine the benefits of Eigenlayer with added freedom for users, validators, and projects. Open markets and freedom encourage competition, opportunities, and innovation. The current system proposes one-time reStaking, the use of liquid staked assets, and even DeFi positions containing liquid staked assets. Key considerations include whether to allow multiple reStaking and if Sovereign Chains should be required to allocate a portion of rewards to the reStaking layer. +--- -## Multiple ReStaking +### Partial-Precommit Execution Dispatch -Enabling multiple reStaking would allow EGLD reStakers to earn significantly higher returns, potentially doubling or tripling yields. Non-custodial reStaking ensures that users retain the base EGLD rewards, making it an attractive option for participation. -Benefits for Different Actors +The Supernova mechanism by which validators begin executing a block as soon as their local validation passes, before casting their vote. -- **SovereignChain Builders**: They can utilize existing funds and validators, simplifying the process of securing their network. With pre-built contracts and a streamlined launch process, Sovereign Chains can achieve high security from the outset without distributing excessive tokens to validators. -- **Validators**: Validators can earn more by participating in multiple networks without owning EGLD, as users provide the required tokens. Validators can create their own economies by rewarding users who delegate to them, fostering a competitive environment. -- **Users**: Users face slightly higher risks of slashing but benefit from increased returns through multiple yields. They maintain a close connection with builders and validators from the beginning. -- **EGLD**: Increased staking reduces market supply, enhancing yield percentages and utility. ReStaking creates a new utility layer for EGLD, making it more appealing to investors. +Context: The MvX consensus flow has three phases: block broadcast, voting (which follows local validation), and finalization (where a proof is produced). Validators start executing during the voting phase, so state changes are ready by the time finalization completes. Cross-shard transactions produced during this window are accepted only once the originating shard's block is proven final. -## Security Considerations +See also: [Pipelined Consensus](#pipelined-consensus), [Cross-Shard Message Buffer](#cross-shard-message-buffer), [Cross-Shard Atomicity](#cross-shard-atomicity), [Supernova](#supernova). +Read more: MultiversX, "Supernova: decoupling consensus and execution" ([multiversx.com/blog/supernova-decoupling-consensus-and-execution](https://multiversx.com/blog/supernova-decoupling-consensus-and-execution)). -ReStaking leverages the security of ETH, with slashing risks being the primary concern. Slashing events are rare in existing PoS networks, indicating that proper economic incentives generally ensure validator honesty. However, potential systemic risks from widespread slashing events need careful management. +--- -Mitigation Strategies that we are analyzing: +### Cross-Shard Message Buffer -- **Limit Slashing**: Set global and local limits on slashed EGLD. -- **Distribution of Slashed EGLD**: Distribute slashed tokens to honest validators and participants, incentivizing honest behavior. -- **Cooldown Period**: Implement a cooldown period before distributing slashed EGLD to mitigate immediate impacts. -- **Global Redistribution**: Distribute slashed EGLD globally rather than locally to spread the risk and reward. +The mechanism, mediated by the metachain, that ensures cross-shard transactions are accepted only once the originating shard's execution results are proven final. -## Conclusion +Context: Cross-shard transactions wait for a finality proof from the originating shard before the destination shard acts on them. Block headers may be broadcast early to reduce propagation time, but they are not considered for any operation until a proof exists. This ensures that pipelining does not weaken cross-shard safety. -The risk/reward ratio for reStaking supports enabling multiple reStaking, offering significant benefits to all participants. By carefully managing the associated risks, the system can provide enhanced returns and foster a thriving ecosystem. +See also: [Cross-Shard Atomicity](#cross-shard-atomicity), [Partial-Precommit Execution Dispatch](#partial-precommit-execution-dispatch), [Metachain](#metachain), [Supernova](#supernova). +Read more: MultiversX, "Supernova: decoupling consensus and execution" ([multiversx.com/blog/supernova-decoupling-consensus-and-execution](https://multiversx.com/blog/supernova-decoupling-consensus-and-execution)). --- -### Security +## Tokens -# Security Considerations +These entries detail the token types and controls built on ESDT, which is defined under Protocol and architecture. +### Meta-ESDT -:::note -This version of the documentation focuses solely on the essential steps required to set up and deploy a sovereign chain on either a local or remote computer. More content will be added as it is accepted and discussed on [Agora](https://agora.multiversx.com/), or once it is implemented and available for production. -::: +A token type that carries per-nonce metadata like an NFT but holds fungible quantities, used for tokens whose units share attributes such as locked or staked positions. + +Context: Technically a special case of the semi-fungible type, a Meta-ESDT behaves like a fungible token with attributes attached at each nonce. MultiversX ecosystem positions such as locked tokens (for example LKMEX and XMEX) are Meta-ESDTs. + +See also: [ESDT](#esdt-estandard-digital-token), [ESDT Roles](#esdt-roles-special-roles), [Dynamic NFTs](#dynamic-nfts-dynamic-tokens). +Read more: [docs.multiversx.com/tokens/nft-tokens](https://docs.multiversx.com/tokens/nft-tokens/). --- -### Services +### Dynamic NFTs (Dynamic Tokens) -# Introduction +Tokens registered as dynamic so their metadata and attributes can change after issuance rather than being fixed at mint. -## Sovereign services +Context: Static NFTs freeze their attributes at creation. A dynamic token can be updated by an address holding the appropriate role, which enables evolving game items, upgradable collectibles, and similar use cases. Dynamic token support was added in the Spica upgrade. -### API service +See also: [ESDT](#esdt-estandard-digital-token), [ESDT Roles](#esdt-roles-special-roles), [Spica](#spica). +Read more: [docs.multiversx.com/tokens/nft-tokens](https://docs.multiversx.com/tokens/nft-tokens/). -The Sovereign API is a wrapper over the Sovereign Proxy that brings a robust caching mechanism, alongside Elasticsearch historical queries support, tokens media support, delegation & staking data, and many others. +--- -### Lite Extras API -The Sovereign Lite Extras API includes a faucet service that allows users to obtain test tokens for their wallet. +### ESDT Roles (Special Roles) -### Lite Wallet -The Sovereign Lite Wallet is a lightweight version of the public wallet. It supports key functionalities such as cross-chain transfers, token issuance, token transfers, and more. +Per-address permissions that authorize specific token operations such as creating, burning, or modifying a token. -### Explorer DApp -The Explorer DApp serves as the blockchain explorer for the Sovereign Chain, providing insights into transactions, blocks, and other on-chain activity. +Context: Roles are assigned to addresses by the token manager. Examples include ESDTRoleNFTCreate (mint), ESDTRoleNFTBurn (burn), ESDTRoleNFTAddQuantity (increase SFT supply), and ESDTRoleNFTUpdateAttributes (change attributes). Roles let a project delegate controlled minting and management without giving up ownership of the token. -## Software dependencies +See also: [ESDT](#esdt-estandard-digital-token), [Meta-ESDT](#meta-esdt), [Dynamic NFTs](#dynamic-nfts-dynamic-tokens). +Read more: [docs.multiversx.com/tokens/nft-tokens](https://docs.multiversx.com/tokens/nft-tokens/). -### Node.js +--- -[Download Node.js](https://nodejs.org/en/download) or use a version manager like [NVM](https://github.com/nvm-sh/nvm) +### NFT Royalties -### Yarn +A creator royalty set at mint, expressed as a value from 0 to 10000 (0% to 100%) and applied to qualifying transfers. -To install yarn, use the following command: -```bash -npm install --global yarn -``` +Context: The royalty is stored as a property of the token, letting the creator receive a percentage of supported sales. The 0 to 10000 range encodes a percentage with two decimals of precision (for example, 500 is 5%). -### Redis +See also: [ESDT](#esdt-estandard-digital-token), [ESDT Roles](#esdt-roles-special-roles). +Read more: [docs.multiversx.com/tokens/nft-tokens](https://docs.multiversx.com/tokens/nft-tokens/). -To install and start redis, use the following commands: +--- -```bash -sudo apt update -sudo apt install redis -sudo systemctl start redis -sudo systemctl enable redis -``` +### Fungible Token -## Prerequisites +An ESDT whose units are identical and interchangeable, like a currency or a points balance. -### Sovereign network deployed +Context: Fungible ESDTs have a nonce of zero and a supply set by the number of decimals chosen at issuance. They are managed at protocol level, so no token contract is deployed. -Before starting the services it is required to have a full sovereign network running, see [setup guide](/sovereign/local-setup). +See also: [ESDT](#esdt-estandard-digital-token), [Token Identifier](#token-identifier-ticker), [NFT](#nft-non-fungible-token). +Read more: [docs.multiversx.com/tokens/intro](https://docs.multiversx.com/tokens/intro). --- -### Software Dependencies - -# Software Dependencies +### NFT (Non-Fungible Token) -Understanding and managing software dependencies is crucial for the successful deployment and maintenance of Sovereign Chains. Dependencies ensure that all components of your blockchain network work seamlessly together. This page outlines the key dependencies required for building and operating Sovereign Chains, including software libraries, frameworks, and tools. +A unique ESDT with a nonce greater than zero and a quantity of one, carrying its own name, attributes, royalties, and URIs. -:::note -Below is the list of software needed to deploy a local Sovereign Chain. All the software dependencies will be installed by scripts in [Setup Guide](/sovereign/setup). -::: +Context: MultiversX NFTs are issued and transferred at protocol level rather than through a token contract, and their metadata can be made updatable by issuing them as dynamic tokens. -## Core Dependencies +See also: [ESDT](#esdt-estandard-digital-token), [SFT](#sft-semi-fungible-token), [Dynamic NFTs](#dynamic-nfts-dynamic-tokens), [NFT Royalties](#nft-royalties). +Read more: [docs.multiversx.com/tokens/nft-tokens](https://docs.multiversx.com/tokens/nft-tokens/). -### Python3 +--- -To install python3, use the following command: +### SFT (Semi-Fungible Token) -```bash -sudo apt install python3 -``` +An ESDT with a nonce greater than zero and a quantity greater than one, behaving like multiple identical copies of the same non-fungible item. -## pipx +Context: SFTs suit assets that are unique as a type but issued in editions, such as event tickets or game items. Quantity is increased with the add-quantity role. -To install pipx, use the following command: +See also: [ESDT](#esdt-estandard-digital-token), [NFT](#nft-non-fungible-token), [Meta-ESDT](#meta-esdt), [ESDT Roles](#esdt-roles-special-roles). +Read more: [docs.multiversx.com/tokens/nft-tokens](https://docs.multiversx.com/tokens/nft-tokens/). -```bash -sudo apt install pipx -pipx ensurepath -sudo pipx ensurepath --global -``` +--- -### mxpy +### Token Identifier (Ticker) -Ensure you are using the latest version of mxpy. Follow the installation or upgrade instructions provided [here](/sdk-and-tools/mxpy/installing-mxpy#install-using-pipx) if you haven't done so already. +The onchain identifier of an ESDT, formed from a human-chosen ticker plus a random suffix, for example `USDC-c76f1f`. -### multiversx-sdk +Context: The random suffix keeps identifiers unique even when tickers collide. Non-fungible and semi-fungible tokens extend the identifier with a nonce (`TICKER-randomhex-nonceHex`) to address an individual token. -To install this dependency on Linux, use the following command: +See also: [ESDT](#esdt-estandard-digital-token), [Fungible Token](#fungible-token), [NFT](#nft-non-fungible-token). +Read more: [docs.multiversx.com/tokens/esdt-tokens](https://docs.multiversx.com/tokens/esdt-tokens/). -```bash -pip install multiversx-sdk -``` +--- -### tmux +### Liquid Staking (lsEGLD) -:::note -If you want to use tmux (which is the default configuration), you should install it. -::: -To install tmux, use the following command: +A mechanism that issues a transferable ESDT representing staked EGLD, so the staked position can be used elsewhere while it still earns staking rewards. -```bash -sudo apt install tmux -``` +Context: A user stakes EGLD into the liquid-staking contract and receives lsEGLD in return; the token can be used across DeFi while the underlying stake continues to accrue rewards. It addresses the illiquidity of ordinary staking. -### screen +See also: [Staking](#staking), [Delegation](#delegation), [ESDT](#esdt-estandard-digital-token). +Read more: [github.com/multiversx/mx-liquid-staking-sc](https://github.com/multiversx/mx-liquid-staking-sc). -To install the screen utility on Linux, use the following command: +--- -```bash -sudo apt install screen -``` +### WEGLD (Wrapped EGLD) -### wget +An ESDT that represents EGLD one-to-one, letting the native token be used in contexts that expect a standard token rather than the protocol's native currency. -To install wget, use the following command: +Context: EGLD is the native coin and is not itself an ESDT, so smart contracts and DeFi that operate on ESDTs use WEGLD, minted by locking EGLD in a wrapping contract and redeemable one-to-one. It is the MultiversX equivalent of wrapped-native tokens on other chains (such as WETH on Ethereum). -```bash -apt install wget -``` +See also: [EGLD](#egld), [ESDT](#esdt-estandard-digital-token), [Fungible Token](#fungible-token), [Liquid Staking](#liquid-staking-lsegld). +Read more: [github.com/multiversx/mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs) (`contracts/core/wegld-swap`); [docs.multiversx.com/tokens/intro](https://docs.multiversx.com/tokens/intro). -### Docker +--- -While not mandatory, it is recommended to use the latest version of Docker. If you need to install it, please follow the instructions at [Docker's official documentation](https://docs.docker.com/get-docker/). +### Token Properties (canFreeze, canWipe, canPause, ...) -### Golang +The configurable flags set on an ESDT at issuance that control which management operations its manager may later perform. -Ensure you are using Go version 1.20. +Context: The properties include canPause (halt all transfers except mint and burn), canFreeze (freeze a specific account's balance), canWipe (wipe a frozen account's tokens), canChangeOwner (transfer token management), canUpgrade (change the properties), canAddSpecialRoles (assign roles), and canCreateMultiShard. On mainnet since epoch 432, the older canMint and canBurn are no longer effective; local mint and burn roles are used instead. -:::note -Please note that at the time of writing this documentation, the setup scripts have been tested only on Ubuntu. -::: +See also: [ESDT (eStandard Digital Token)](#esdt-estandard-digital-token), [ESDT Roles (Special Roles)](#esdt-roles-special-roles), [Freeze and Wipe](#freeze-and-wipe), [Pause (Token)](#pause-token), [Local Mint and Burn](#local-mint-and-burn). +Read more: [docs.multiversx.com/tokens/fungible-tokens](https://docs.multiversx.com/tokens/fungible-tokens/). --- -### Solana L2 - -# Solana L2 +### Freeze and Wipe -:::note +Token-manager operations that freeze a specific account's balance of a token (blocking transfers to and from it) and, for an already frozen account, wipe out its tokens, reducing supply. -This documentation is not complete. More content will be added once it is accepted and discussed on Agora or once it is implemented and available for production. +Context: Freezing requires the token's canFreeze property, and wiping requires canWipe and a previously frozen account. The pair exists to support regulatory or compliance actions; freezing is reversible, whereas wiping permanently removes the tokens. -::: +See also: [ESDT (eStandard Digital Token)](#esdt-estandard-digital-token), [Token Properties (canFreeze, canWipe, canPause, ...)](#token-properties-canfreeze-canwipe-canpause-), [Pause (Token)](#pause-token), [ESDT Roles (Special Roles)](#esdt-roles-special-roles). +Read more: [docs.multiversx.com/tokens/fungible-tokens](https://docs.multiversx.com/tokens/fungible-tokens/). --- -### Sov Esdt Safe +### Pause (Token) -# Sov-ESDT-Safe -![From Sovereign](../../static/sovereign/from-sovereign.png) +A token-manager operation that prevents all transactions of a token except minting and burning, reversible with an unpause. -Whether an External Owned Account (EOA) like an user wallet or another smart contract the procedure is simple. An address will be able to perform a multi tokens transfer with various types of tokens: -- Fungible Tokens -- (Dynamic) Non-Fungible Tokens -- (Dynamic) Semi-Fungible Tokens -- (Dynamic) Meta ESDT Tokens +Context: Pausing requires the token's canPause property to be set to true. It halts transfers of the token network-wide until the manager unpauses it. -When making the deposit, the user specifies: -1. A destination address -2. `TransferData` if the execution contains a smart contract call, which contains gas, function and arguments +See also: [ESDT (eStandard Digital Token)](#esdt-estandard-digital-token), [Token Properties (canFreeze, canWipe, canPause, ...)](#token-properties-canfreeze-canwipe-canpause-), [Freeze and Wipe](#freeze-and-wipe). +Read more: [docs.multiversx.com/tokens/fungible-tokens](https://docs.multiversx.com/tokens/fungible-tokens/). -## Sovereign Chain to Main Chain transfer flow -1. User deposits token to the `Sov-ESDT-Safe` smart contract. -2. Outgoing *Operations* are created at the end of the round. -3. Validators sign all the outgoing *Operations*. -4. Leader sends *Operations* to the Sovereign Bridge Service. -5. Sovereign Bridge Service sends the *Operations* to the Header-Verifier for registration and verification, and then to `Mvx-ESDT-Safe` for execution. -6. At the end of the execution success/fail, a confirmation event will be added which will be received in Sovereign through the observer and then the cross chain transfer will be completed. +--- -### Deposit Endpoint -```rust - #[payable("*")] - #[endpoint] - fn deposit( - &self, - to: ManagedAddress, - optional_transfer_data: OptionalValueTransferDataTuple, - ) -``` -As you can see both the `Mvx-ESDT-Safe` and `Sov-ESDT-Safe` smart contracts have the `deposit` endpoint. At a high level, the process is the same, depositing funds or calling other smart contracts. The main differences are when each payments is processed. Since the `Sov-ESDT-Safe` smart contract doesn't have the storage mapper for the native token, the payments won't be verified if they include any payment with the native token. The enabled checks are the same: +### Local Mint and Burn -- If the token is whitelisted or not blacklisted, in that case the tokens can be transferred. -- If the fee is enabled, the smart contract assures that the fee is paid. -- If there are maximum 10 transfers in the transaction. +Protocol operations that increase or decrease a token's supply, performed by an address holding the corresponding local role. -If the deposit also includes the `optional_transfer_data` parameter it will also have some extra checks regarding the cross-chain execution of endpoints: +Context: An address with the ESDTRoleLocalMint role can mint new units and one with ESDTRoleLocalBurn can burn units. On mainnet since epoch 432, global mint and burn are disabled, so local mint and burn (via these roles) are the way to change a token's supply. -- The gas limit must be under the specified limit. -- The endpoint that has to be executed is not blacklisted. +See also: [ESDT (eStandard Digital Token)](#esdt-estandard-digital-token), [ESDT Roles (Special Roles)](#esdt-roles-special-roles), [Token Properties (canFreeze, canWipe, canPause, ...)](#token-properties-canfreeze-canwipe-canpause-). +Read more: [docs.multiversx.com/tokens/fungible-tokens](https://docs.multiversx.com/tokens/fungible-tokens/). +--- -At the end of the `deposit` endpoint, all the extra tokens will be refunded to the caller and an event will be emitted since the bridging process is complete. +## Network, nodes, and staking +### Node -```rust -#[event("deposit")] -fn deposit_event( - &self, - #[indexed] dest_address: &ManagedAddress, - #[indexed] tokens: &MultiValueEncoded>, - event_data: OperationData, -) -``` +A computer running the MultiversX client that relays messages with its peers. A node is either a validator or an observer. -This log event will emit the destination address and the tokens which will be transferred to the Sovereign Chain. +Context: Nodes form the peer-to-peer network that propagates transactions and blocks. Their role depends on whether they have staked to validate or run passively as observers. -:::note -The source code for the endpoint can be found [here](https://github.com/multiversx/mx-sovereign-sc/blob/main/sov-esdt-safe/src/deposit.rs). -::: +See also: [Validator](#validator-node), [Observer](#observer-node), [Observing Squad](#observing-squad). +Source: [docs.multiversx.com/welcome/terminology](https://docs.multiversx.com/welcome/terminology/). --- -### Sovereign - Overview +### Validator (node) -:::note +A node that has staked at least 2500 EGLD and participates in consensus, processing transactions and securing the network in exchange for rewards. -This documentation is not complete. More content will be added once it is accepted and discussed on Agora or once it is implemented and available for production. +Context: Validators are selected into per-round consensus groups and earn protocol rewards plus a share of fees. Each validator carries a rating, and unreliable validators can be jailed and excluded from consensus. -::: +See also: [Observer](#observer-node), [Secure Proof of Stake](#secure-proof-of-stake-spos), [Validator Rating](#validator-rating), [Jailing](#jailing), [Staking](#staking). +Read more: [docs.multiversx.com/validators/overview](https://docs.multiversx.com/validators/overview/). -This guide provides detailed instructions on setting up, deploying, and managing a sovereign chain, along with covering various other educational topics. +--- -## Table of Contents +### Observer (node) -1. [Introduction](/sovereign/concept) -2. [Prerequisites](/sovereign/system-requirements) -3. [Setup Guide](/sovereign/local-setup) -4. [Custom Configurations](/sovereign/custom-configurations) -5. [Managing a Sovereign Chain](/sovereign/managing-sovereign) -6. [Economics](/sovereign/token-economics) -7. [Governance](/sovereign/governance) -8. [Testing and Validation](/sovereign/testing) -9. [Security Considerations](/sovereign/security) -10. [VMs](/sovereign/vm) -11. [Interoperability](/sovereign/interoperability) -12. [How to become a validator](/sovereign/validators) +A passive node that follows the network and serves as a read-and-relay interface, without participating in consensus or earning rewards. -## Introduction +Context: Observers are not selected for consensus and do not carry a rating. They are used to read chain state and submit transactions, and a full set of them backs the public API. -The introduction chapter will provide an overview of what sovereign chains are and their significance in the blockchain ecosystem. It will answer questions such as: +See also: [Node](#node), [Validator](#validator-node), [Observing Squad](#observing-squad). +Read more: [docs.multiversx.com/validators/overview](https://docs.multiversx.com/validators/overview/). -- What are sovereign chains? -- How do they operate within the MultiversX blockchain network? -- What are the benefits and use cases of sovereign chains? +--- -## Prerequisites +### Observing Squad -The prerequisites chapter will detail the necessary preparations before setting up a sovereign chain. It will answer questions such as: +A set of observer nodes covering every shard plus the metachain, fronted by a MultiversX Proxy that exposes a single HTTP API over the whole network. -- What are the system requirements? -- What software dependencies need to be installed? +Context: Because state is sharded, reading the entire network requires an observer in each shard. An observing squad bundles one observer per shard, plus the metachain, with a Proxy instance, giving an application one endpoint that routes each request to the correct shard. It is the standard way to self-host full network access. -## Setup Guide +See also: [Observer](#observer-node), [Shard (Execution Shard)](#shard-execution-shard), [Metachain](#metachain). +Read more: [docs.multiversx.com/integrators/observing-squad](https://docs.multiversx.com/integrators/observing-squad/). -The setup guide will get you through the initial steps to get your sovereign chain up and running. It will answer questions such as: +--- -- How do you create a new wallet? -- Where can you download the required files? -- What repositories should you use and how to prepare your environment? -- How do you deploy necessary contracts? Can you do it in an automated manner? -- What are the step-by-step instructions for manual deployment? -- How do you update sovereign configurations and manage Docker observers? +### Validator Rating + +A per-validator reliability score that rises with successful consensus participation and falls with missed or faulty duties. + +Context: New validators start at a baseline score (50 points) and gain rating for each successful consensus they take part in. Rating reflects reliability and, if it falls too low, leads to jailing. It gives the protocol a continuous, onchain measure of node behavior. + +See also: [Validator](#validator-node), [Jailing](#jailing), [Secure Proof of Stake](#secure-proof-of-stake-spos). +Read more: [docs.multiversx.com/validators/rating](https://docs.multiversx.com/validators/rating/). --- -### Sovereign Api +### Jailing -# API service +The state in which a validator whose rating has fallen too low is excluded from consensus and earns no rewards until it is restored. -## Deploy Sovereign Proxy service +Context: A jailed validator is removed from consensus selection. The operator can return it to the active set by paying a fine and unjailing it. Jailing is the enforcement mechanism behind the rating system. -:::info -Proxy service is automatically deployed if the sovereign chain was started with [local setup](/sovereign/local-setup) -::: +See also: [Validator Rating](#validator-rating), [Validator](#validator-node), [Staking](#staking). +Read more: [docs.multiversx.com/validators/rating](https://docs.multiversx.com/validators/rating/). -### Step 1: Get the `mx-chain-proxy-sovereign-go` Repository +--- -Before proceeding, ensure that a **SSH key** for GitHub is configured on your machine. +### Staking -1. Clone the GitHub repository: - ```bash - git clone git@github.com:multiversx/mx-chain-proxy-sovereign-go.git - ``` +Locking EGLD to run a validator node, currently 2500 EGLD per node, held in a system smart contract. -2. Navigate to proxy directory: - ```bash - cd mx-chain-proxy-sovereign-go/cmd/proxy - ``` +Context: Staking makes a node eligible to validate and earn rewards. Stake above the per-node minimum is called topUp and affects how many of an operator's nodes stay active. Withdrawing requires unstaking and then waiting out the unbonding period. -### Step 2: Edit Proxy `config.toml` file +See also: [Validator](#validator-node), [Delegation](#delegation), [Staking Auction and TopUp](#staking-auction-and-topup), [Unbonding Period](#unbonding-period). +Read more: [docs.multiversx.com/validators/staking](https://docs.multiversx.com/validators/staking/). -Example: -``` -[[Observers]] - ShardId = 0 - Address = "http://127.0.0.1:10000" +--- -[[Observers]] - ShardId = 4294967295 - Address = "http://127.0.0.1:10000" -``` +### Delegation -:::note -For sovereign proxy there are 2 Observers required for `ShardId` 0 and 4294967295. The `Address` should be the same for both. -::: +Contributing EGLD toward validator nodes operated by someone else, sharing in the rewards without running a node. -### Step 3: Start Sovereign Proxy service +Context: Holders delegate to a staking provider, a custom delegation smart contract that pools participants' funds, operates the nodes, and takes a service fee from the rewards. A provider's pool has a delegation cap, the maximum amount it accepts. Delegation lowers the barrier below the 2500 EGLD per-node minimum. -Build and run the proxy -```bash -go build -./proxy --sovereign -``` +See also: [Staking](#staking), [System Smart Contracts](#system-smart-contracts), [Staking Auction and TopUp](#staking-auction-and-topup). +Read more: [docs.multiversx.com/validators/delegation-manager](https://docs.multiversx.com/validators/delegation-manager/). -## Deploy Sovereign API service +--- -### Step 1: Get the `mx-api-service` Repository +### Staking Auction and TopUp -1. Clone the GitHub repository: - ```bash - git clone https://github.com/multiversx/mx-api-service.git - ``` +The mechanism, introduced with Staking V4, that selects which staked nodes are active based on each operator's stake above the minimum, known as topUp. -2. Checkout the sovereign branch and navigate to testnet directory: - ```bash - cd mx-api-service && git fetch && git checkout feat/sovereign - ``` +Context: When more nodes are staked than there are available slots, an auction ranks operators by topUp per node and admits as many as their stake supports. This replaced the earlier first-come staking queue and ties active-set membership to economic commitment rather than arrival time. -### Step 2: Edit API config +See also: [Staking](#staking), [Staking V5](#staking-v5-and-the-emission-split), [Validator](#validator-node). +Read more: [docs.multiversx.com/validators/staking-v4](https://docs.multiversx.com/validators/staking-v4/). -1. Navigate to the `config` folder: - ```bash - cd config - ``` +--- -2. Update the configuration files (we are starting from testnet configuration in this example): - - `config.testnet.yaml` - enable/disable or configure the services you need - - `dapp.config.testnet.json` - dapp configuration file +### Unbonding Period -### Step 3: Start Sovereign API service +The waiting time between unstaking and being able to withdraw the funds, currently 10 epochs. -```bash -npm install -npm run init -npm run start:testnet -``` +Context: After an operator or delegator unstakes, the funds stay locked for the unbonding period (about 10 days at the current epoch length) before they can be unbonded and withdrawn. The delay protects network security by preventing an instant exit. -## Deploy Sovereign Extras service +See also: [Staking](#staking), [Delegation](#delegation), [Epoch](#epoch). +Read more: [docs.multiversx.com/validators/staking](https://docs.multiversx.com/validators/staking/). -The extras service only includes the `faucet` option at the moment. +--- -### Step 1: Get the ```mx-lite-extras-service``` Repository +### Unbond -```bash -git clone https://github.com/multiversx/mx-lite-extras-service.git -``` +The action of withdrawing staked or delegated EGLD back to the owner's account after it has been unstaked and the unbonding period has elapsed. -### Step 2: Update extras configuration files +Context: Unbonding is the final step of exiting stake: an operator or delegator first unstakes, waits out the unbonding period (currently 10 epochs), and then unbonds to receive the funds. It is distinct from the earlier unstake step, which only signals the intent to withdraw. -- `.env.custom` - change `API_URL` and `GATEWAY_URL` with your own URLs -- `config/config.yaml` - update the faucet configuration parameters as needed +See also: [Unstake](#unstake), [Unbonding Period](#unbonding-period), [Staking](#staking), [Delegation](#delegation). +Read more: [docs.multiversx.com/validators/staking/staking-smart-contract](https://docs.multiversx.com/validators/staking/staking-smart-contract/); [docs.multiversx.com/validators/staking](https://docs.multiversx.com/validators/staking/). -### Step 3: Start Sovereign Extras service +--- -```bash -NODE_ENV=custom npm run start:faucet -``` +### Top-up -Read more about deploying API service in [GitHub](https://github.com/multiversx/mx-lite-extras-service#quick-start). +The EGLD an operator stakes above the per-node minimum, computed as total staked EGLD minus the minimum times the number of nodes. + +Context: With the base stake fixed at 2500 EGLD per node, top-up is the excess. Top-up per node determines a node's priority in the staking auction and contributes to rewards; nodes with insufficient top-up can be left out of the active set. + +See also: [Staking Auction and TopUp](#staking-auction-and-topup), [Soft Auction](#soft-auction), [Staking](#staking), [Auction List](#auction-list). +Read more: [docs.multiversx.com/validators/staking-v4](https://docs.multiversx.com/validators/staking-v4/). --- -### Sovereign Explorer +### Auction List -# Explorer service +The set of staked nodes competing, by their top-up per node, to be selected into the active validator set for the coming epoch. -## Deploy Explorer +Context: Introduced with Staking V4, the auction list replaced the earlier first-in first-out staking queue. At each epoch a soft auction selects nodes from the auction list based on top-up; unselected nodes remain in the auction. -### Step 1: Get the `mx-explorer-dapp` Repository +See also: [Soft Auction](#soft-auction), [Top-up](#top-up), [Waiting List](#waiting-list), [Eligible Nodes](#eligible-nodes). +Read more: [docs.multiversx.com/validators/staking-v4](https://docs.multiversx.com/validators/staking-v4/). -```bash -git clone https://github.com/multiversx/mx-explorer-dapp.git -``` +--- -### Step 2: Update explorer configuration file +### Waiting List -1. Navigate to the `src/config` folder: - ```bash - cd src/config - ``` +The pool of nodes that have been selected to join but are synchronizing with their assigned shard, before they become eligible for consensus. -2. Update the parameters and URLs with your own configuration in `config.testnet.ts` file +Context: Nodes move from the auction list into the waiting list, spend time resynchronizing with the shard they were assigned, and are then shuffled into the eligible set. The waiting list keeps a node in sync so it is ready before it starts validating. -Example configuration: -``` -{ - default: true, - id: 'sovereign', - name: 'Sovereign', - chainId: 'S', - adapter: 'api', - theme: 'default', - egldLabel: 'SOV', - walletAddress: 'https://localhost:3000', - explorerAddress: 'https://localhost:3003', - apiAddress: 'https://localhost:3002', - hrp: 'erd', - isSovereign: true -} -``` +See also: [Auction List](#auction-list), [Eligible Nodes](#eligible-nodes), [Validator Reshuffle (Epoch Boundary)](#validator-reshuffle-epoch-boundary), [Soft Auction](#soft-auction). +Read more: [docs.multiversx.com/validators/staking-v4](https://docs.multiversx.com/validators/staking-v4/); [docs.multiversx.com/learn/sharding](https://docs.multiversx.com/learn/sharding). -### Step 3: Start Sovereign Explorer +--- -```bash -yarn -npm run start-testnet -``` +### Eligible Nodes -Read more about deploying explorer in [GitHub](https://github.com/multiversx/mx-explorer-dapp/tree/main#quick-start). +The active validators in the consensus set, which produce and sign blocks and earn rewards. ---- +Context: Only eligible nodes participate in consensus and receive validator rewards, and they are subject to jailing if their rating falls too low. Eligible nodes are drawn from the waiting list and reshuffled across shards at epoch boundaries. -### Sovereign Wallet +See also: [Validator (node)](#validator-node), [Waiting List](#waiting-list), [Validator Reshuffle (Epoch Boundary)](#validator-reshuffle-epoch-boundary), [Consensus Group](#consensus-group). +Read more: [docs.multiversx.com/validators/staking-v4](https://docs.multiversx.com/validators/staking-v4/). -# Wallet service +--- -## Deploy Lite Wallet +### Soft Auction -### Step 1: Get the `mx-lite-wallet-dapp` Repository +The Staking V4 selection mechanism that admits nodes from the auction list by finding the top-up threshold that fills the available slots while maximizing the number of qualifying owners and nodes. -```bash -git clone https://github.com/multiversx/mx-lite-wallet-dapp.git -``` +Context: Rather than a strict cutoff that could force operators to unstake nodes, the soft auction adjusts the required top-up per node across a range so as many eligible nodes as possible are selected each epoch. It is designed to be fairer to smaller staking providers. -### Step 2: Update sovereign configuration file +See also: [Auction List](#auction-list), [Top-up](#top-up), [Waiting List](#waiting-list), [Staking Auction and TopUp](#staking-auction-and-topup). +Read more: [docs.multiversx.com/validators/staking-v4](https://docs.multiversx.com/validators/staking-v4/). -1. Navigate to the `src/config` folder: - ```bash - cd src/config - ``` +--- -2. Update the `sharedNetworks.ts` file: - - for `sovereign` item - - update the URLs with your own - - update `WEGLDid` with the sovereign native token identifier from `config.toml` -> `BaseTokenID` - - update `sovereignContractAddress` with contract address from `sovereignConfig.toml` -> `SubscribedEvents` from `OutgoingSubscribedEvents` - - for `testnet` item (or the network your sovereign is connected to) - - update `sovereignContractAddress` with contract address from `sovereignConfig.toml` -> `SubscribedEvents` from `NotifierConfig` +### Slashing -### Step 3: Start Sovereign Lite Wallet +A penalty in which a validator that seriously misbehaves is fined and loses part or all of its staked EGLD, in addition to having its validator status removed. -```bash -yarn install -yarn start-sovereign -``` +Context: The docs describe stake slashing as reserved for serious offences such as double-signing or producing bad blocks. As of the current docs, slashing for double-signing is noted as a near-future enforcement; day-to-day unreliability is handled by the rating system and jailing rather than by slashing principal. -Read more about deploying lite wallet in [GitHub](https://github.com/multiversx/mx-lite-wallet-dapp/tree/main#multiversx-lite-wallet-dapp). +See also: [Jailing](#jailing), [Validator Rating](#validator-rating), [Staking](#staking), [Secure Proof of Stake (SPoS)](#secure-proof-of-stake-spos). +Read more: [docs.multiversx.com/validators/overview](https://docs.multiversx.com/validators/overview/). --- -### Standalone Evm +### BLS Key (Validator Key) -# Standalone EVM +The node-specific key a validator uses to sign blocks and consensus messages, stored in the node's validator key file and distinct from the wallet key that controls funds. -## EVM as example -In the early stages of the MultiversX VM development, there were already components built specifically for EVM compatibility. We are revisiting and reusing parts of that code. In **VM1.2**, for instance, there was a direct correspondence between EVM opcodes and the **BlockchainHook** interface, as well as a mechanism that wrapped MvX-style transaction data (**txData**) into EVM-specific `vmInput`. +Context: Each validator node has its own BLS key, whose public key identifies the node in staking transactions and consensus. The private key must never leave the node's host; if it is stolen and used maliciously (for example to double-sign) the associated stake can be slashed. -## 1. VMExecutionHandlerInterface -The MultiversX protocol defines a **VMExecutionHandlerInterface** with the following functions: +See also: [Key Pair (Public and Private Key)](#key-pair-public-and-private-key), [BLS Multi-Signature](#bls-multi-signature), [Multikey Nodes](#multikey-nodes), [Slashing](#slashing). +Read more: [docs.multiversx.com/validators/key-management/validator-keys](https://docs.multiversx.com/validators/key-management/validator-keys/). -```go -// RunSmartContractCreate computes how a smart contract creation should be performed -RunSmartContractCreate(input *ContractCreateInput) (*VMOutput, error) +--- -// RunSmartContractCall computes the result of a smart contract call and how the system must change after the execution -RunSmartContractCall(input *ContractCallInput) (*VMOutput, error) -``` -The **SCProcessor** from `mx-chain-sovereign-go` prepares the input information for these functions. We aim to avoid modifying the **SCProcessor** itself; instead, all necessary abstractions will be implemented at the EVM level. +### Node Redundancy -## 2. Input Preparation: EVMInputCreator +A high-availability setup in which one or more standby nodes run alongside a main validator node and take over signing only if the main node goes offline. -When a contract creation request is made (via *ContractCreateInput), an EVMInputCreator component will: -- Convert the `ContractCreateInput` into an EVMInput. -- Invoke the actual EVM smart contract logic. +Context: Standby nodes are configured with a redundancy level and stay synchronized with the main node's shard without signing, providing failover to avoid downtime and missed rewards. Two nodes must not share the same redundancy level, or they would sign in parallel and risk double-signing. -The EVM itself is taken from the official Go implementation ([evm.go](https://github.com/ethereum/go-ethereum/blob/master/core/vm/evm.go) in go-ethereum). +See also: [Multikey Nodes](#multikey-nodes), [BLS Key (Validator Key)](#bls-key-validator-key), [Slashing](#slashing), [Validator (node)](#validator-node). +Read more: [docs.multiversx.com/validators/redundancy](https://docs.multiversx.com/validators/redundancy/). -## 3. Abstraction Layer: MultiversX & EVM Interfaces +--- -To allow the EVM to function within MultiversX, we introduce a layer that bridges EVM interfaces with MultiversX components. The core interface it uses is the `BlockchainHookInterface`, which grants access to critical blockchain data, state, and transaction information. +### Multikey Nodes -### 3.1 Reading & Writing to Storage +A configuration in which a small group of nodes collectively manages many validator BLS keys, with at least one node per shard plus the metachain, instead of running one node per key. -- **Reading Storage**: When an EVM opcode attempts to read data from the storage (e.g., `readStorageFromTrie(key)`), it should invoke `blockchainHook.ReadFromStorage(scAddress, key)`. Internally, this call goes through the `storageContext` component, which manages reads from local cache if a key has already been accessed or modified during the current transaction. +Context: The nodes share a file listing all managed validator keys and coordinate consensus signing across them, which pools server resources for operators running several keys. It is an optional, more economical setup for medium-to-large staking providers. -- **Writing to Storage**: When writing to storage, the EVM opcode should call `SetStorageToAddress(address, key)` in the `storageContext`. +See also: [BLS Key (Validator Key)](#bls-key-validator-key), [Node Redundancy](#node-redundancy), [Validator (node)](#validator-node), [Staking](#staking). +Read more: [docs.multiversx.com/validators/key-management/multikey-nodes](https://docs.multiversx.com/validators/key-management/multikey-nodes/). -### 3.2 Finalizing State Changes +--- -After EVM execution finishes, we need to commit the resulting state changes to the blockchain. The EVM will use the `outputContext` component, which (together with the `storageContext`) tracks modified accounts and storages. It also creates the final `vmOutput`, which the `scProcessor` in `mx-chain-sovereign-go` will then validate and apply to the blockchain (the trie) if everything is correct. +### Merge Validator -## 4. Gas Metering +The operation that converts a standalone staked validator into a node managed by an existing delegation contract, keeping its active slot. -EVM gas metering is handled internally within the EVM code. The VMExecutionHandler can receive a new gas schedule via: +Context: Merging lets a node operator move a directly staked node under a staking provider's delegation contract without unstaking and re-entering the auction. When the node and the contract have different owners, whitelisting is required first. -```go -GasScheduleChange(newGasSchedule map[string]map[string]uint64) -``` +See also: [Delegation Contract](#delegation-contract), [Delegation Manager](#delegation-manager), [Staking](#staking), [Delegation](#delegation). +Read more: [docs.multiversx.com/validators/staking/merge-validator-delegation-sc](https://docs.multiversx.com/validators/staking/merge-validator-delegation-sc/). -This function provides the cost of each opcode as a map. The EVM needs the appropriate wrapper functions to load these costs into its **OPCODES** structure. +--- -## 5. Implementation Steps: Integrating EVM +### Delegation Manager -- Start from the SpaceVM code. -- Replace the current executor (WASMER) with the EVM executor. -- During EVM opcode interpretation, invoke the `storageContext` and `meteringContext` functions to manage state changes and track gas consumption. +The protocol's built-in factory contract, at a fixed metachain address, from which a node operator creates a delegation contract to run a staking provider. -Once these steps are complete, the underlying EVM logic should effectively run on MultiversX. +Context: An operator submits a createNewDelegationContract request to the delegation manager, depositing 1250 EGLD, and receives their own delegation contract. Using the delegation manager is the standard way to set up a staking provider, though a custom smart contract is also possible. -## 6. Address Conversion: 20 Bytes vs. 32 Bytes +See also: [Delegation Contract](#delegation-contract), [Staking provider](#staking-provider), [Delegation](#delegation), [System Smart Contracts](#system-smart-contracts). +Read more: [docs.multiversx.com/validators/delegation-manager](https://docs.multiversx.com/validators/delegation-manager/). -EVM addresses are 20-bytes long, whereas MultiversX uses 32-byte addresses. To avoid changing the broader MultiversX system, the EVM will use internal transformers: +--- -- **Internal EVM Calls**: Within the EVM, contracts use the last 20 bytes of the corresponding 32-byte MvX address. -- At runtime, the full 32-byte address is still known, and when a storage `read` or `write` occurs, the EVM prefixes the last 20 bytes with 10 bytes of zeros plus a 2-byte `VMType` (the standard MvX smart contract addressing scheme). +### Delegation Contract -### 6.1 Calling EVM Contracts from EVM +The contract that runs an individual staking provider: it tracks the provider's nodes and delegators, distributes rewards, and applies the service fee. -When an EVM-based smart contract calls another EVM contract, it uses the 20-byte address. Internally, the system prefixes these 20 bytes with the deterministic overhead (10 bytes of zeros and 2 bytes for `VMType`) to fetch the appropriate contract code from the accounts trie before running it. +Context: Created from the delegation manager with an initial 1250 EGLD from the operator (while 2500 EGLD is needed to stake a single node), the delegation contract handles staking, unstaking, and reward accounting for the pool. It also carries the pool's delegation cap and service fee. -### 6.2 Token Storage in EVM +See also: [Delegation Manager](#delegation-manager), [Staking provider](#staking-provider), [Delegation cap](#delegation-cap), [Service Fee](#service-fee). +Read more: [docs.multiversx.com/validators/delegation-manager](https://docs.multiversx.com/validators/delegation-manager/). -Token balances (like ERC20) live in the contract's own storage. The contract will use the last 20 bytes of a user’s MvX address when recording ownership or balances. If an opcode like `GetCaller` is executed, it returns only the last 20 bytes from the `ContractCallInput.Sender`. +--- -### 6.3 Calling WasmVM from EVM +### Import DB -MultiversX WasmVM expects 32-byte addresses. If an EVM contract tries to invoke a WasmVM contract using only 20 bytes, the call will fail due to incorrect argument size. Consequently, when the EVM calls a WasmVM contract, it must supply a full 32-byte address. +A node operation mode that reprocesses an existing node database from genesis, without live network synchronization, to rebuild state or reindex history. -:::note -In most cases, the EVM contracts will call only other EVM contracts. However, bridging to WasmVM is still feasible, for example, when claiming ESDT tokens through an ERC wrapper contract. -::: +Context: Import DB lets an operator replay the chain from a stored database, which is useful for reconstructing state, validating history, or populating external systems such as Elasticsearch. It can be faster than syncing from the network. -## 7. WASM VM and the `ExecuteOnDestOnOtherVM` Function +See also: [Elastic Indexer](#elastic-indexer), [Observer (node)](#observer-node), [Deep-History Squad](#deep-history-squad). +Read more: [docs.multiversx.com/validators/import-db](https://docs.multiversx.com/validators/import-db/). -The **WASM VM** supports a public function `ExecuteOnDestOnOtherVM` via the **BlockchainHook** interface. If a new VM is fully integrated, it can be added to the `vmContainer` component with a new **baseAddress**. Below is an example table illustrating potential base addresses for different VMs: +--- -| VM Name | Address Suffix | Notes | -|-------------|----------------------|----------------------------------------------------------------------------------| -| **SpaceVM** | 05 | Standard base address for the WASM VM | -| **System VM** | 255 | Standard base address (example) for the System VM | -| **EVM** | To Be Decided | Will be assigned upon integration to ensure address derivation works properly | +## Wallets and accounts -From the `SCAddress`, the protocol looks at bytes **10** and **11** to determine which VM should be called. Once EVM integration is complete, it will receive its own base address and will adjust how the **CreateContract** opcode calculates deployed contract addresses. +### Wallet -### 7.1 Synchronous Execution +Software that holds a user's keys and lets them hold assets, sign transactions, and interact with applications. -When the EVM executes a `DelegateCall` opcode, it will invoke an internal function of the new EVM implementation that checks whether execution should occur in the EVM itself or a different VM. If it needs to run on another VM, it calls `blockchainHook.ExecuteOnDestOnOtherVM`. +Context: MultiversX wallets include xPortal (mobile super-app), the web wallet, browser-extension wallets, and hardware-wallet support. The wallet holds the keys; the assets live onchain. -- **Returning `VMOutput`**: This function returns a `VMOutput`, which can be merged into the current `outputContext` and `storageContext` via the `PushContext`-type public functions. +See also: [xPortal](#xportal), [Key Pair](#key-pair-public-and-private-key), [Onchain Guardian](#onchain-guardian), [NativeAuth](#nativeauth). +Read more: [docs.multiversx.com/wallet/web-wallet](https://docs.multiversx.com/wallet/web-wallet/). -In the **WASM VM**, if a smart contract calls `ExecuteOnDest`, the VM decides where the execution should take place. For asynchronous calls, the same logic applies: +--- -- **Intra-Shard**: The system calls `ExecuteOnDestOnOtherVM`. -- **Cross-Shard**: On the destination shard, the **scProcessor** determines which VM to invoke and continues accordingly. +### xPortal -## 8. ESDT ↔ ERC20 & ESDTNFT ↔ ERC721 +MultiversX's official non-custodial mobile wallet and financial super-app for holding, sending, staking, and swapping EGLD and ESDTs, with an associated debit card. -Bridging MultiversX ESDT standards with common Ethereum-based token standards (ERC20, ERC721, etc.). This introduces several key differences in token handling, especially around **token transfers** and **approval mechanisms**. +Context: xPortal is the consumer entry point to MultiversX, combining a self-custodial wallet with payments and card features. It uses herotags so users can transact to a name instead of an address. -### 8.1 ESDT Transfer Model -On MultiversX, transfers typically use a **`transferAndExecute`** paradigm: -- The sender (token owner) explicitly initiates a transfer of tokens and, in the same operation, calls a smart contract endpoint to process further actions (e.g., swapping, staking, etc.). +See also: [Wallet](#wallet), [Herotag](#herotag), [EGLD](#egld), [ESDT](#esdt-estandard-digital-token). +Read more: [multiversx.com/ecosystem/project/xportal](https://multiversx.com/ecosystem/project/xportal); MultiversX Help Center. -### 8.2 ERC20 Transfer Model -In the Ethereum ecosystem, the common workflow is: -1. **Approval**: A user grants a smart contract (SC) permission to spend tokens on their behalf by calling `approve(scAddress, amount)`. -2. **Transfer**: The SC (now approved) calls `transferFrom(user, destination, amount)` to pull tokens from the user’s balance. +--- -This design allows third-party contracts to move funds from a user’s wallet without a new, explicit approval each time. However, it also opens the door to potential exploits: a malicious dApp can trick users into granting excessive approvals, which might be exploited later to drain funds. +### Key Pair (Public and Private Key) -### 8.3 The Wrapper/SafeESDT Contract +The cryptographic pair that controls an account: the public key derives the address, and the private key signs transactions. -Because MultiversX prohibits direct “pull” transfers of ESDTs (a fundamental security decision), bridging to ERC-like workflows requires an **intermediary contract**—often called a **wrapper** or **safeESDT** contract: +Context: Whoever holds the private key controls the account, which is why MultiversX added Guardians as an onchain second factor so a leaked key alone is not enough to move funds. -1. **Deposit**: A user deposits their ESDT tokens into the wrapper contract. -2. **Allow**: The user can specify which addresses (e.g., other SCs) are allowed to withdraw a certain amount of these deposited tokens. -3. **Transfer**: The contract implements an ERC20-like `transferFrom()` functionality. When an external EVM-based SC tries to “pull” tokens, it actually interacts with this safeESDT contract, which checks permissions and only then completes the transfer if authorized. -4. **Withdrawal**: The user can reclaim any unspent tokens from the wrapper contract when they wish. +See also: [Bech32 Addressing](#bech32-addressing-erd1), [Mnemonic](#mnemonic-seed-phrase), [Message Signing](#message-signing), [Onchain Guardian](#onchain-guardian). +Read more: [docs.multiversx.com/wallet/wallets](https://docs.multiversx.com/wallet/wallets/). -In the EVM environment, an operation like `safeESDTContract.transferFrom(user, scAddress, amount)` would mimic the ERC20 approach. Under the hood, the **blockchainHook** would manage a synchronous call to the other VM. +--- -### 8.4 Extending to Other ERC Standards +### Mnemonic (Seed Phrase) -A similar wrapper approach can be adopted for other token types: +A human-readable list of words that encodes the secret from which an account's keys are derived. -- **ERC721 (NFTs)**: An **ESDTNFT** wrapper can track ownership and minted tokens, providing `approve()` and `transferFrom()` methods that mirror standard ERC721 functionality. -- **ERC1155**: This multi-token standard can likewise be “wrapped”, allowing ESDT-based multi-tokens to be interfaced with EVM-based dApps expecting ERC1155 contracts. +Context: A mnemonic can regenerate the private key and address, so it must be kept secret; anyone with it controls the account. MultiversX wallets generate and restore accounts from a standard mnemonic. -By handling all "pull" transfers inside dedicated wrapper contracts, MultiversX preserves its **secure-by-design** “push” transfer model while still enabling compatibility with dApps that rely on ERC-style approvals. +See also: [Key Pair](#key-pair-public-and-private-key), [Keystore and PEM](#keystore-and-pem), [Onchain Guardian](#onchain-guardian). +Read more: [docs.multiversx.com/wallet/wallets](https://docs.multiversx.com/wallet/wallets/); [github.com/multiversx/mx-sdk-js-core](https://github.com/multiversx/mx-sdk-js-core). -### Claiming ESDT Tokens from an ERC20 Balance +--- -This diagram illustrates how a user claims an ESDT token originally held in an ERC20 contract on the EVM side. The process involves burning ERC20 tokens, calling a WASM VM wrapper contract, and finally minting ESDT tokens to the user. +### Keystore and PEM -```mermaid -sequenceDiagram - participant U as User - participant E as ERC20 Contract (EVM) - participant W as WASM VM Wrapper +Two file formats for storing account keys: a keystore is an encrypted JSON file unlocked by a password, while a PEM file stores the key unencrypted. - U->>E: 1) Call ERC20 contract to burn tokens - E->>W: 2) callContract(ERCWrapper) on WASM VM
(includes burn details) - Note over W: Registers the token under a 20-byte address
(EVM only knows 20 bytes) - U->>W: 3) User claims tokens from the WASM VM wrapper - W->>W: 3a) Checks last 20 bytes == callInput.CallerAddress[12:32] - W->>U: 4) Mints and sends ESDT tokens to OriginalCaller -``` +Context: Keystore files are for general use because they are password-protected. PEM files are convenient for development and automated testing but offer no protection and should not hold real funds. ---- +See also: [Key Pair](#key-pair-public-and-private-key), [Mnemonic](#mnemonic-seed-phrase), [Wallet](#wallet). +Read more: [docs.multiversx.com/sdk-and-tools/sdk-py](https://docs.multiversx.com/sdk-and-tools/sdk-py/); [github.com/multiversx/mx-sdk-js-core](https://github.com/multiversx/mx-sdk-js-core). -### System Requirements +--- -# System Requirements +### Message Signing -:::note - This is a living document. More content will be added once it is implemented and available for production. As this documentation evolves, some sections may be updated or modified to reflect the latest developments and best practices. Community feedback and contributions are encouraged to help improve and refine this guide. Please note that the information provided is subject to change and may not always reflect the latest updates in the technology or procedures. s -::: +Producing a signature over an arbitrary message, rather than a transaction, to prove control of an address without moving funds. -This page outlines the recommended system requirements for running a Sovereign Chain node. +Context: Signed messages back login schemes such as NativeAuth and let applications verify that a user controls an address. The signature is verified against the account's public key. -The hardware requirements for running a Sovereign Chain validator node generally depend on the node configuration and may evolve as the Sovereign network undergoes upgrades. If the Sovereign Chain does not serve any special function (such as AI, DA, DePIN, etc.), the minimum requirements should align with those for running a MultiversX node. +See also: [NativeAuth](#nativeauth), [Key Pair](#key-pair-public-and-private-key), [Bech32 Addressing](#bech32-addressing-erd1). +Read more: [github.com/multiversx/mx-sdk-js-core](https://github.com/multiversx/mx-sdk-js-core); [docs.multiversx.com/sdk-and-tools/sdk-js](https://docs.multiversx.com/sdk-and-tools/sdk-js/). -## Processor +--- -It is preferable to use a **4 x dedicated/physical** CPUs, either Intel or AMD, with ```SSE4.1``` and ```SSE4.2``` flags (use lscpu to verify). The CPUs must be ```SSE4.1``` and ```SSE4.2``` capable, otherwise the node won't be able to use the Wasmer 2 VM available through the VM 1.5 (and above) and the node will not be able to sync blocks from the network. +### Web Wallet -:::caution -If the system chosen to host the node is a VPS, the host must have dedicated CPUs. Using shared CPUs can hinder your node's performance that will result in a decrease of node's rating and eventually the node might get jailed. -::: +The official browser-based MultiversX wallet, at wallet.multiversx.com, for holding EGLD and tokens, signing transactions, and staking or delegating. -:::tip -We are promoting using processors that support the fma or fma3 instruction set since it is widely used by our VM. Displaying the available CPU instruction set can be done using the Linux shell command sudo lshw or lscpu -::: +Context: The web wallet supports login by keystore file, Ledger, xPortal, and PEM, and on the test networks it provides a faucet for test funds. It is a common entry point for users and, via URL hooks, for dApp login flows. -## Memory +See also: [Wallet](#wallet), [Wallet Extension (DeFi Wallet)](#wallet-extension-defi-wallet), [xAlias](#xalias), [Keystore and PEM](#keystore-and-pem). +Read more: [docs.multiversx.com/wallet/web-wallet](https://docs.multiversx.com/wallet/web-wallet/). -It is recommended to use at least 16GB RAM. +--- -## Disk space +### Wallet Extension (DeFi Wallet) -Disk space is usually the primary bottleneck for node operators. At the time of writing, for running a node with the **chain-sdk** binary you would need at least 200 GB SSD. -As well as storage capacity, MultiversX nodes rely on fast read and write operations. This means HDDs and cheaper SSDs can sometimes struggle to sync the blockchain. +The MultiversX browser-extension wallet, for Chrome, Brave, and Firefox, that creates or imports an account from a 24-word secret phrase and connects to dApps. -## Bandwidth +Context: The extension holds keys in the browser and signs transactions and messages for web applications, offering a lightweight alternative to the web wallet for day-to-day dApp use. -It is important to have a stable and reliable internet connection, especially for running a validator because downtime can result in missed rewards or penalties. It is recommended to have at least 100 Mbit/s always-on internet connection. Running a node also requires a lot of data to be uploaded and downloaded so it is better to use an ISP that does not have a capped data allowance and if it does you would need at least 4 TB/month data plan. +See also: [Wallet](#wallet), [Web Wallet](#web-wallet), [Mnemonic (Seed Phrase)](#mnemonic-seed-phrase), [mx-sdk-dapp](#mx-sdk-dapp). +Read more: [docs.multiversx.com/wallet/wallet-extension](https://docs.multiversx.com/wallet/wallet-extension/). --- -### Testing - -# Testing and Validation +### Ledger (Hardware Wallet) -:::note +A hardware wallet device that stores an account's keys offline and signs MultiversX transactions on-device using the MultiversX app. -This documentation is not complete. More content will be added once it is accepted and discussed on Agora or once it is implemented and available for production. +Context: With the MultiversX app installed through Ledger Live, a Ledger device keeps the private key isolated from the connected computer, and transactions are confirmed physically on the device. It is recommended for larger holdings. -::: +See also: [Wallet](#wallet), [Web Wallet](#web-wallet), [Key Pair (Public and Private Key)](#key-pair-public-and-private-key), [Onchain Guardian](#onchain-guardian). +Read more: [docs.multiversx.com/wallet/ledger](https://docs.multiversx.com/wallet/ledger/). --- -### Token Economics +### xAlias -# Token Economics +A single sign-on wallet that onboards users through Google Sign-In, creating a self-custody MultiversX account without a seed phrase and convertible to a conventional wallet later. -## Introduction +Context: xAlias lets non-crypto users start with a familiar Web2 login while remaining self-custodial. For developers, integrating xAlias is identical to integrating the web wallet, since it exposes the same URL hooks and callbacks. -Sovereign Chains represent an important step forward for MultiversX ecosystem, allowing each chain to operate independently with its own set of rules, governance, and most importantly, its own token economy (tokenomics). Unlike traditional blockchain models where a single token often dominates, Sovereign Chains enable the creation and management of unique tokens tailored to the specific needs and goals of each chain. +See also: [Wallet](#wallet), [Web Wallet](#web-wallet), [Mnemonic (Seed Phrase)](#mnemonic-seed-phrase), [NativeAuth](#nativeauth). +Read more: [docs.multiversx.com/wallet/xalias](https://docs.multiversx.com/wallet/xalias/). -## The Flexibility of Token Economies +--- -One of the core benefits of Sovereign Chains is the flexibility in designing token economies. Each Sovereign Chain can develop a token that aligns perfectly with its specific use case, community, and economic model. This allows for a highly customized approach to incentivizing behavior, securing the network, and ensuring the long-term sustainability of the chain. +### WalletConnect -## Key Components of Token Economics +An open protocol that connects a dApp to a compatible wallet over an encrypted channel, used on MultiversX so a dApp can request transaction and message signatures from a mobile wallet such as xPortal. -### Token Creation and Distribution: -- Initial Supply: Sovereign Chains can decide the total initial supply of their token, whether it’s a fixed supply or an inflationary model. -- Distribution Mechanisms: Tokens can be distributed through a variety of methods including initial coin offerings (ICOs), airdrops, or mining. +Context: MultiversX exposes WalletConnect JSON-RPC methods for signing, and xPortal implements them, so a user can approve actions on their phone while using a dApp on desktop. It is the vendor-agnostic path for mobile wallet integration. -### Utility and Functionality: -- Transaction Fees: Tokens can be used to pay for transaction fees within the chain, ensuring smooth and cost-effective operations. -- Staking and Governance: Tokens often play a crucial role in governance, allowing holders to vote on important decisions and proposals. Additionally, tokens can be staked to secure the network and earn rewards. +See also: [xPortal](#xportal), [Wallet](#wallet), [mx-sdk-dapp](#mx-sdk-dapp), [Message Signing](#message-signing). +Read more: [docs.multiversx.com/integrators/walletconnect-json-rpc-methods](https://docs.multiversx.com/integrators/walletconnect-json-rpc-methods/); [docs.multiversx.com/wallet/xportal](https://docs.multiversx.com/wallet/xportal/). -### Incentive Structures: -- Reward Programs: To encourage participation and loyalty, Sovereign Chains can implement reward programs for activities such as staking, providing liquidity, or contributing to the network's development. -- Burn Mechanisms: To control inflation and increase scarcity, some chains might implement token burning mechanisms where a portion of the tokens is permanently removed from circulation. +--- -## Advantages of Sovereign Chain Token Economies +## Ecosystem applications -- Customization: Sovereign Chains can tailor their tokenomics to fit the specific needs and goals of their ecosystem. -- Innovation: By having control over their own economic model, Sovereign Chains can experiment with innovative incentive structures and governance models. -- Independence: Each chain operates independently, reducing the risk of systemic failures and ensuring greater resilience. +These are widely referenced MultiversX-ecosystem products rather than parts of the base protocol. They are included because the names recur in MultiversX materials; the distinction (application, not protocol) is stated in each entry. ---- +### xExchange -### Token Management +MultiversX's flagship decentralized exchange, an automated-market-maker DEX for swapping and providing liquidity in EGLD and ESDTs, with its own governance token (MEX). -# Sovereign Deposit Tokens Guide +Context: xExchange (formerly Maiar Exchange) is an ecosystem application built on MultiversX, not a protocol feature. It uses WEGLD to pair EGLD against ESDTs and offers swaps, liquidity pools, and yield farms. -## Main Chain -> Sovereign Chain +See also: [WEGLD](#wegld-wrapped-egld), [ESDT](#esdt-estandard-digital-token), [EGLD](#egld), [Liquid Staking](#liquid-staking-lsegld). +Read more: [multiversx.com/ecosystem](https://multiversx.com/ecosystem); [xexchange.com](https://xexchange.com). -1. Navigate to `/mx-chain-sovereign-go/scripts/testnet/sovereignBridge`. +--- - Update the configuration file `config/configs.cfg` with the token settings you prefer. Example: - ```ini - # Issue Main Chain Token Settings - TOKEN_TICKER=TKN - TOKEN_DISPLAY_NAME=Token - NR_DECIMALS=18 - INITIAL_SUPPLY=111222333 - ``` +### xMoney -3. Source the script: - ```bash - source script.sh - ``` +A MultiversX-ecosystem payments product for accepting and making crypto payments, including merchant checkout and card features. -4. Issue a token on the main chain: - ```bash - issueToken - ``` +Context: xMoney (formerly Utrust) is an ecosystem company and product, not a protocol feature. Like xExchange and xPortal, it is an application in the ecosystem rather than part of the base chain. -5. Deposit the token in the smart contract: - ```bash - depositTokenInSC - ``` +See also: [xPortal](#xportal), [EGLD](#egld), [ESDT](#esdt-estandard-digital-token). +Read more: [multiversx.com/ecosystem](https://multiversx.com/ecosystem). -## Sovereign Chain -> Main Chain +--- -1. Navigate to `/mx-chain-sovereign-go/scripts/testnet/sovereignBridge`. +## Upgrades - Update the configuration file `config/configs.cfg` with the sovereign token settings you prefer. Example: - ```ini - # Issue Sovereign Token Settings - TOKEN_TICKER_SOVEREIGN=SVN - TOKEN_DISPLAY_NAME_SOVEREIGN=SovToken - NR_DECIMALS_SOVEREIGN=18 - INITIAL_SUPPLY_SOVEREIGN=333222111 - ``` +Named mainnet protocol upgrades are ratified through onchain governance. The entries below are listed in chronological order. -2. Source the script: - ```bash - source script.sh - ``` +### Sirius -3. Issue a new token on the local sovereign chain: - ```bash - issueTokenSovereign - ``` +The January 2024 upgrade that improved smart-contract upgrade handling, optimized signature verification, and added multi-key support for validators. -### Steps to transfer tokens: +Context: Sirius was the first major release ratified through MultiversX's onchain governance process, passing with 97.89% of voting stake in favor. It established the pattern of governing protocol upgrades by stake-weighted onchain vote. -:::info -- Ensure the sovereign bridge contract has the BurnRole for the token you want to bridge. All new tokens have `ESDTBurnRoleForAll` enabled. If disabled, register the burn role: - ```bash - setLocalBurnRoleSovereign - ``` -::: +See also: [Governance Proposal Process](#governance-proposal-process), [Barnard](#barnard). +Read more: MultiversX State of the Foundation Report 2025 ([files.multiversx.com/MultiversX-State-of-the-Foundation-Report-2025.pdf](https://files.multiversx.com/MultiversX-State-of-the-Foundation-Report-2025.pdf)). -- Register the sovereign token identifier on the main chain bridge contract: - ```bash - registerSovereignToken - ``` +--- -- Deposit the token in the smart contract on the sovereign chain: - ```bash - depositTokenInSCSovereign - ``` +### Vega + +The April 2024 upgrade (v1.7) that removed the staking-queue waiting period for new validators and introduced a chain-simulator environment for development and testing. + +Context: Removing the staking queue let new validators enter the active set without waiting for a slot to free up. The chain simulator gave developers a local environment that mimics protocol behavior without running a full network. + +See also: [Secure Proof of Stake](#secure-proof-of-stake-spos), [ScenarioWorld](#scenarioworld). +Read more: MultiversX State of the Foundation Report 2025 ([files.multiversx.com/MultiversX-State-of-the-Foundation-Report-2025.pdf](https://files.multiversx.com/MultiversX-State-of-the-Foundation-Report-2025.pdf)). --- -### Token Types +### Spica -import useBaseUrl from '@docusaurus/useBaseUrl'; -import ThemedImage from '@theme/ThemedImage'; +The November 2024 upgrade (v1.8) that added relayed transactions V3, dynamic NFT metadata, and onchain passkey authentication. +Context: Relayed V3 moved fee-relaying to explicit transaction fields. Dynamic NFT metadata allowed token attributes to change after minting. Onchain passkey authentication added a WebAuthn-style login path at the protocol level. -# Token Types +See also: [Relayed Transactions](#relayed-transactions-v1--v2--v3), [ESDT](#esdt-estandard-digital-token). +Read more: MultiversX State of the Foundation Report 2025 ([files.multiversx.com/MultiversX-State-of-the-Foundation-Report-2025.pdf](https://files.multiversx.com/MultiversX-State-of-the-Foundation-Report-2025.pdf)). -With the release of the bridge v3.0 & v3.1 different token types are allowed to be bridged. By token type, we refer to how the -tokens are locked/burnt or unlocked/minted along with the chain side on which they are native. The decision of how the token should -be configured in the bridge depends on the availability of the minter/burner role on the EVM-compatible chain side along with -the marketing decision of "on which chain the token should be native (a.k.a. where it was first minted)" +--- +### Andromeda -**1. Mint/Burn & Native on MultiversX < - > EVM-chain has Mint/Burn & Non-Native** +The May 2025 upgrade (v1.9) that restructured consensus, cutting time-to-finality from 12 seconds to 6 seconds and raising per-shard validator capacity. -This bridge token-type configuration has the advantage of not holding a single token in the bridge. -The swapped tokens are minted/burned on both sides. The total token quantity on both chains equals the (minted - burned) on MultiversX -added with the (minted - burned) on the EVM-compatible chain side. The initial supply & mint is done on MultiversX. +Context: Andromeda removed the legacy requirement for a separate confirmation block, so a single block carried finality. This made block time the binding latency constraint and is the predecessor that the Supernova upgrade builds on. - - +See also: [Secure Proof of Stake](#secure-proof-of-stake-spos), [Supernova](#supernova). +Read more: MultiversX, "Andromeda" release post ([multiversx.com/blog/andromeda-supernova-highspeed-highways](https://multiversx.com/blog/andromeda-supernova-highspeed-highways)); State of the Foundation Report 2025. +--- -**2. Mint/Burn & Non-Native on MultiversX < - > EVM-chain has Mint/Burn & Native** +### Barnard -This has the same advantages as 1. but the initial minting is done on the EVM-compatible chain side. +The July 2025 upgrade (v1.10) that moved the governance contracts onchain and added millisecond-level block timestamps available to smart contracts. +Context: Onchain governance contracts let proposals and voting execute as protocol-native operations. Millisecond timestamps gave contracts finer-grained time information than the prior second-level granularity. -**3. Mint/Burn & Non-Native on MultiversX < - > EVM-chain has Locked/Unlocked & Native** +See also: [Governance Proposal Process](#governance-proposal-process), [Supernova](#supernova). +Read more: MultiversX State of the Foundation Report 2025 ([files.multiversx.com/MultiversX-State-of-the-Foundation-Report-2025.pdf](https://files.multiversx.com/MultiversX-State-of-the-Foundation-Report-2025.pdf)). -This bridge token configuration type will need to be initiated on the EVM-compatible chain side and the tokens transferred to -MultiversX will be locked in the bridge contract (no minter role is needed on the EVM-compatible chain as opposed to 1 & 2) and -will be unlocked when swaps from MultiversX to the EVM-compatible chain are done. On the MultiversX side, mint/burn -actions will be performed. The total quantity of tokens on both chains will be equal to the supply on the EVM-compatible chain. Everything that is -locked in the bridge contract will equal to what was (minted - burned) on MultiversX side. +--- -:::warning -This configuration will also require an intermediate token as to allow the bridging on more than one EVM-compatible chain. -It also can create discrepancies between the allowed supplies to bridge between multiple chains. -::: +### Staking V5 (and the emission split) -Example: if tokens are being brought to MultiversX from EVM-compatible chain 1 and then the tokens are bridged out from -MultiversX to EVM-compatible chain 2, then the bridge for EVM-compatible chain 2, at some point, will be unable to process -swaps because it will run out of its intermediary tokens. The solution here is to manually bridge in the reversed order -as described (an operation that consumes time & fees). +The first phase of the Economic Evolution changes, live on mainnet since 2 December 2025 (v1.11.1), which revised how staking rewards and new EGLD issuance are distributed. - - +Context: Staking V5 implemented the move away from the fixed-supply model toward tail inflation. The annual emission is split 50% to staking rewards, 20% to an ecosystem growth fund, 20% to a user growth dividend, and 10% to protocol sustainability. This split is sometimes misreported as "50/50"; the four-way split above is the figure to use. -**Note:** The diagram above is a little bit misleading because the ERC20 contracts hold the address/balance ledgers inside -them. For the sake of simplicity, the tokens are depicted as stored inside the bridge **Safe** contracts -(just as MultiversX ESDTs). +See also: [Economic Evolution](#economic-evolution), [KPI-Gated Emissions](#kpi-gated-emissions), [EGLD](#egld), [Developer Revenue Share](#developer-revenue-share). +Read more: MultiversX, "Release: Staking V5 and New Emissions Model, Phase 1 (v1.11.1)" (Dec 2025, [multiversx.com/release/release-economics-update-phase-1-v1-11-1](https://multiversx.com/release/release-economics-update-phase-1-v1-11-1)); emission split itemized in the economic framework proposal ([agora.multiversx.com/t/...530](https://agora.multiversx.com/t/a-competitive-economic-framework-for-multiversx-toward-revenue-and-reflexive-value-accrual/530)). --- -### Transfer Flows +### Economic Evolution -import useBaseUrl from '@docusaurus/useBaseUrl'; -import ThemedImage from '@theme/ThemedImage'; +The October 2025 governance proposal that dropped EGLD's fixed-supply cap and replaced it with a tail-inflation model. +Context: Under the new framework, EGLD issuance starts at roughly 8.76% of circulating supply per year and decays toward a floor of 2% to 5%, with the decay tied to ecosystem performance indicators. Base transaction fees are split between smart-contract builders and a permanent burn, starting at 90% to builders and 10% burned and transitioning on a schedule toward a 50/50 split over eight years (see Developer Revenue Share). The proposal passed an onchain governance vote in late October 2025 with 94.55% of voting stake in favor on 41.77% participation. The first phase of protocol changes shipped as Staking V5. -# Transfer Flows +See also: [Staking V5](#staking-v5-and-the-emission-split), [KPI-Gated Emissions](#kpi-gated-emissions), [EGLD](#egld), [Developer Revenue Share](#developer-revenue-share), [Governance Proposal Process](#governance-proposal-process). +Read more: MultiversX, "The MultiversX Economic Evolution" (Oct 22, 2025, [multiversx.com/blog/the-multiversx-economic-evolution-a-blueprint-for-growth-and-strategic-expansion](https://multiversx.com/blog/the-multiversx-economic-evolution-a-blueprint-for-growth-and-strategic-expansion)); economic framework proposal on Agora ([agora.multiversx.com/t/...530](https://agora.multiversx.com/t/a-competitive-economic-framework-for-multiversx-toward-revenue-and-reflexive-value-accrual/530)). -The main functionality of the bridge is to transfer tokens from one network to another. For example, a user can transfer tokens from an EVM-compatible chain to MultiversX or from MultiversX to an EVM-compatible chain. -Besides the main functionality, there is the possibility to call a smart contract on MultiversX when doing a swap. +--- +### Supernova -# 1. Simple token transfer functionality +The MultiversX upgrade that rewrites the consensus and block-propagation pipeline to compress block time from roughly 6 seconds to 600 milliseconds, by overlapping voting with execution and decoupling execution from the consensus critical path. -## 1.a. EVM-compatible chain to MultiversX +Context: Supernova introduces execution dispatch before voting (validators begin executing a block once local validation passes, before casting their vote) and a metachain-mediated cross-shard finality gate that ensures cross-shard transactions are accepted only once the originating shard's block is proven final. Its onchain governance vote (8 to 18 January 2026) passed with 99.64% of voting stake in favor on a 33.63% quorum. Demonstrated figures: the Battle of Nodes public adversarial test (11 to 31 March 2026) sustained 120,000 transactions per second on its final day and processed over a billion transactions across thousands of community-run validators; the design target is 600ms blocks with sub-300ms intra-shard finality (a 100 to 250 millisecond intra-shard production target; a cross-shard transaction requires three rounds (sender's shard, metachain notarization, receiver's shard) and finalizes in approximately 1.8 seconds). As of June 2026 the upgrade has cleared an external security audit and is in final integration and testing toward mainnet; it is not yet live on mainnet. -Let's suppose Alice has x tokens on an EVM-compatible chain and wants to transfer them to MultiversX at the address she owns -(or it might be Bob's address on MultiversX, the bridge does not care). The steps and flow are the following: -* Alice deposits the ERC20 tokens that she wants to transfer to the MultiversX network in the EVM-compatible chain's **Safe** contract; -* The EVM-compatible chain's **Safe** contract groups multiple deposits into batches; -* After a certain period of time (defined by the finality parameters of the EVM-compatible chain), each batch becomes final and starts being processed by the relayers; -* The relayers propose, vote, and perform the transfer using MultiversX's **Bridge** contract with a consensus of a minimum of 7 out of 10 votes; -* On the MultiversX network, the same amount of ESDT tokens are minted or released, depending on the token's settings; -* The destination address receives the equivalent amount of ESDT tokens on the MultiversX network. +See also: [Cross-Shard Atomicity](#cross-shard-atomicity), [Hyperblock](#hyperblock), [Andromeda](#andromeda), [Secure Proof of Stake](#secure-proof-of-stake-spos). +Read more: MultiversX, "Supernova: decoupling consensus and execution" ([multiversx.com/blog/supernova-decoupling-consensus-and-execution](https://multiversx.com/blog/supernova-decoupling-consensus-and-execution)); test figures from xAlliance Substack, "Supernova Engine Room" series and [@MultiversX, March 31, 2026](https://x.com/MultiversX/status/2039002331640180765). +--- -## 1.b. MultiversX chain to an EVM-compatible chain +## Framework and SDK -Now let's suppose Alice wants her x tokens on MultiversX to transfer them to an EVM-compatible chain in the address she owns -(or it might be Bob's address on the EVM-compatible chain, again, the bridge does not care). The steps and flow are the following: +These terms describe the MultiversX developer toolchain: the Rust smart-contract framework (`multiversx-sc`, repository `mx-sdk-rs`) and the dApp/JS SDKs. -* Alice deposits the ESDT tokens that she wants to transfer to the EVM-compatible network in MultiversX's **Safe** contract; -* The MultiversX's **Safe** contract groups multiple deposits into batches; -* After a certain period of time, each batch becomes final and ready to be processed by the relayers; -* The relayers propose, vote, and perform the transfer using the EVM-compatible chain's **Bridge** contract with a consensus of exactly 7 out of 10 votes; -* The user receives the equivalent amount of ERC20 tokens on their recipient address on the EVM-compatible network minus the fee for this operation; -* On the MultiversX network, the ESDT tokens that were transferred are burned or locked, depending on the token's settings. +### Storage Mappers -# 2. Token transfer with smart-contract call on MultiversX side +Typed storage abstractions in the Rust smart-contract framework that present contract state (a single value, a set, a map, a list, a token) through a method on the contract trait, hiding the raw key-value encoding. -Starting with bridge v3.0, swaps from the EVM-compatible chains can invoke a smart contract on the MultiversX side. -Let's suppose Alice has x tokens on an EVM-compatible chain and wants to transfer them to MultiversX to a contract while invoking -a function on that contract. The steps and flow are the following: +Context: A mapper is declared with a `#[storage_mapper("key")]` annotation and returns a typed handle, for example `SingleValueMapper` for one value or `UnorderedSetMapper` for a set. Adding arguments to the method parameterizes the storage key, so `balance(addr)` resolves to a different slot per address. Different mappers have different storage-cost and access profiles, so the choice of mapper affects gas. The framework documents the per-mapper trade-offs in source. -* Alice deposits the ERC20 tokens that she wants to transfer to the MultiversX network in the EVM-compatible chain's **Safe** contract, - on a special endpoint also providing the MultiversX's contract address, function, the parameters for the invoked function, and a minimum - gas-limit to be used when invoking the function; -* The EVM-compatible chain's **Safe** contract groups multiple deposits into batches, regardless of whether the deposits are of this type or 1.a. type; -* After a certain period of time (defined by the finality parameters of the EVM-compatible chain), each batch becomes final and starts being processed by the relayers; -* The relayers propose, vote, and perform the transfer using MultiversX's **Bridge** contract with a consensus of a minimum of 7 out of 10 votes; -* On the MultiversX network, the same amount of ESDT tokens are minted or released, depending on the token's settings; -* The minted or released tokens, along with the smart-contract call parameters (contract address, function, parameters, and minimum gas limit) are then moved in - the specialized contract called **BridgeProxy**; -* On the **BridgeProxy** contract there is an endpoint for executing the smart-contract call. Alice or any MultiversX entity willing to - spend the gas for execution can call the endpoint. The minimum gas limit for the execution is the one specified by Alice, or it can be higher; -* The entity triggering the execution flow will pay the gas limit and the **BridgeProxy** will handle the execution which can have 2 outcomes: - * The call was successful: in this case, the contract will be credited with the tokens sent by Alice, and the invoked function would have produced the desired effects; - * The call was unsuccessful: in this case, the **BridgeProxy** received back the tokens and will mark the transfer as failed. - * The **BridgeProxy** can then be called on another endpoint to attempt the refund mechanism. Alice or any other MultiversX entity willing - to spend gas limit can invoke that endpoint; - * Whoever calls the **BridgeProxy** refund endpoint, will pay the gas limit for the reversed transfer operation. This will also attempt to subtract the fee - as for any normal MultiversX to EVM-compatible chain transfer; - * The transfer is placed in a MultiversX batch and eventually, Alice will get her tokens back on the originating EVM-compatible chain minus the fee. - As stated, the operations on the **BridgeProxy** can be done manually, or by using the **scCallsExecutor** tool provided here https://github.com/multiversx/mx-bridge-eth-go/blob/feat/v3.1/cmd/scCallsExecutor +See also: [Tx Syntax](#tx-syntax), [Typed Proxies](#typed-proxies), [sc-meta](#sc-meta), [WASM VM](#wasm-vm-and-wasmer). +Read more: [github.com/multiversx/mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs) (`framework/base/src/storage/mappers/`); MultiversX docs, storage mappers ([docs.multiversx.com/developers/developer-reference/storage-mappers](https://docs.multiversx.com/developers/developer-reference/storage-mappers/)). -The README.md file contained in this directory is a good place to start on how to manually configure the tool and run it (on a dedicated host or VM) +--- -## Notes regarding smart-contract invoked on MultiversX from an EVM-compatible chain: +### Tx Syntax -The next diagram explains what happens with a token transfer with a smart-contract call in the direction EVM-compatible chain -> MultiversX when the tokens are unlocked/minted on the MultiversX chain. -The transfer is stored in the **BridgeProxy** (Step 1) and then, anyone can initiate the execution (Step 2). The tokens reach the 3-rd-party smart contract along with the function required to be called -and the provided parameters. +The unified, fluent transaction builder in the Rust framework for every kind of outgoing transaction from a contract: native EGLD transfer, ESDT transfer, cross-contract call, deploy, upgrade, and read-only query. - - +Context: A transaction is built by chaining methods, for example `self.tx().to(&recipient).egld(&amount).transfer()`. The builder is type-checked at compile time: combinations that are not valid, such as attaching EGLD on top of an ESDT payment, fail to compile. The same builder expresses sync calls (same-shard), async calls with a callback (cross-shard), and fire-and-forget transfers, differing only in the terminal operation. The design is described in Andrei Marinica's "One Syntax to rule them all" series (2024). -As stated above, this is the "happy flow" in which the smart contract call succeeds on the 3-rd-party contract. But what -happens if the invoked function fails? This is described in the next diagram. Step 1 was identical to the previous diagram -and it was omitted. Step 2 got a new step 2.c in which the tokens return to the **BridgeProxy** contract and the whole transfer -is marked as failed and ready to be refunded on the original source EVM-compatible chain **to the original sender address**. -There is another Step 3 involved, in which, anyone can call the refund method. +See also: [Typed Proxies](#typed-proxies), [Storage Mappers](#storage-mappers), [Cross-Shard Atomicity](#cross-shard-atomicity). +Read more: [github.com/multiversx/mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs) (`framework/base/src/types/interaction/tx.rs`); [docs.multiversx.com/sdk-and-tools/sdk-rust](https://docs.multiversx.com/sdk-and-tools/sdk-rust/). - - +--- -Step 2 and Step 3 can be automatically triggered with the help of the **scCallsExecutor** tool referenced above. +### Typed Proxies -:::important -The SC call data will need to be assembled from the EVM-compatible chain side and should respect the MultiversX Rust Framework -encoding for all provided parameters. -::: +Plain Rust structs, generated from a contract's ABI, that let you call that contract (from another contract, a test, or an interactor) with full compile-time argument and return-type checking. -The following resource will exemplify how the correct endpoint is to be called on the EVM-compatible chain side: -https://github.com/multiversx/mx-bridge-eth-sc-sol/blob/main/tasks/depositSC.ts +Context: A proxy is generated by running `sc-meta all proxy` against a path declared in `sc-config.toml`; the framework reads the ABI and writes one method per endpoint. The same proxy is used in three contexts with the same method names: cross-contract calls inside a contract, blackbox tests, and interactor scripts. Proxies are plain structs (not traits), are copyable between crates, and are never hand-edited; they are regenerated when endpoints change. -The correct encoded data can be generated by using the `encodeCallData` function provided in this typescript implementation -https://github.com/multiversx/mx-sdk-js-bridge/blob/main/helpers/encodeCallData.ts -or, using the `EncodeCallDataStrict` function from the Golang implementation -https://github.com/multiversx/mx-bridge-eth-go/blob/feat/v3.1/parsers/multiversxCodec.go +See also: [Tx Syntax](#tx-syntax), [sc-meta](#sc-meta), [ScenarioWorld](#scenarioworld). +Read more: [github.com/multiversx/mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs) (`framework/meta-lib`); Andrei Marinica, "One Syntax to rule them all," Part 2 (2024). --- -### Validators +### ScenarioWorld -# How to become a validator +The blockchain test harness in the Rust framework's scenario crate, which runs contracts against a Rust implementation of the MultiversX VM for blackbox, whitebox, and JSON-scenario (Mandos) tests. +Context: A test sets up accounts and balances, deploys contracts, sends transactions, and asserts on results or storage, using the same typed proxies a real caller would. Blackbox tests exercise a contract as the blockchain would; whitebox tests reach into contract internals through a closure; scenario tests replay JSON (`.scen.json`, Mandos) files. The Rust VM runs contracts two ways: through an interpreter (the debugger) or by compiling them and executing through Wasmer (2.2 today, with an experimental Wasmer 6 integration). Blackbox and scenario tests can use either path, and scenario files additionally run against the production Go VM via `mx-scenario-go`, which guards against divergence between the two VMs; whitebox and unit tests use the Rust VM debugger only. Blackbox tests reference the built `.mxsc.json` artifact, so the contract must be built before they run. -:::note -This version of the documentation focuses solely on the essential steps required to set up and deploy a sovereign chain on either a local or remote computer. More content will be added as it is accepted and discussed on [Agora](https://agora.multiversx.com/), or once it is implemented and available for production. -::: +See also: [Typed Proxies](#typed-proxies), [sc-meta](#sc-meta), [Vega](#vega). +Read more: [github.com/multiversx/mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs) (`framework/scenario`); [docs.multiversx.com/developers/testing/rust/sc-test-overview](https://docs.multiversx.com/developers/testing/rust/sc-test-overview/). --- -### Vm Intro +### sc-meta -# Introduction +The standalone command-line tool for MultiversX smart-contract development: building contracts to WebAssembly, generating proxies, scaffolding from templates, running tests, sending transactions, and managing the framework toolchain. -The MultiversX protocol is designed so that integrating a new executor, a new processor, or even a completely new VM is straightforward. In essence, any new VM only needs to implement the `VMExecutionHandler` interface. Currently, there are two VMs running on MultiversX: +Context: Common commands include `sc-meta all build` (compile every contract under the current path to WASM and produce the `.mxsc.json` package), `sc-meta all proxy` (regenerate typed proxies), `sc-meta new --template ` (scaffold a contract), and `sc-meta test`. The build produces an optimized WASM binary plus a package bundling the WASM, the ABI, and metadata. `sc-meta` can also send transactions from the command line, deploying and interacting with contracts directly in the manner of mxpy. `sc-meta` is published as the `multiversx-sc-meta` crate. -- **SpaceVM**: Handles general smart contracts running on WASM. -- **systemVM**: Specialized for builtin system smart contracts written in Go. +See also: [Typed Proxies](#typed-proxies), [ScenarioWorld](#scenarioworld), [WASM VM](#wasm-vm-and-wasmer), [Storage Mappers](#storage-mappers). +Read more: [github.com/multiversx/mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs) (`framework/meta`); [docs.multiversx.com/developers/meta/sc-meta](https://docs.multiversx.com/developers/meta/sc-meta/). -For sovereign shards, we introduced the option for a WasmVM smart contract to call a systemVM smart contract through the `BlockchainHookInterface`, specifically via the `ExecuteOnDestOnOtherVM` endpoint. This is necessary because both VMs reside in the same shard on sovereign shards. On the mainnet, however, WasmVM contracts can interact with systemVM contracts only through an asynchronous call, since systemVM exists exclusively on the metachain. +--- -When considering the EVM or other VMs, on MultiversX, it’s important to note that developers will likely want to interact with WasmVM contracts as well. Put simply, a smart contract should be able to interact with both WasmVM and other VM contracts in a uniform way, and that abstraction must happen at the VM level. The WasmVM already handles this by smartly calling the `ExecuteOnDestOnOtherVM` endpoint as needed. +### NativeAuth -If we look under the hood we have SpaceVM which can actually accommodate multiple **EXECUTORS**. In the case of current mainnet/sovereign SpaceVM the **EXECUTOR** is *WASMER2.0* with a few additions from our side. Wasmer is written in rust and SpaceVM is written in GO. The SpaceVM has a big set of **OP_CODES**, from storage handling, to memory handling to crypto operations, big floats, and more. These **OP_CODES** are represented as pointer functions, written in GO and they have an access pointer. The executor, our modified Wasmer, receives this set of functions as a LIBRARY and when a SmartContract calls one of the **OP_CODES**, this calls the internal library added to WASMER which gets executed in the GO code of SpaceVM. +A MultiversX-native authentication scheme in which a wallet issues a signed token bound to a specific origin, a recent block hash, and an expiry, which a backend then verifies as proof of wallet control. -:::note -Adding a new VM means introducing a new Executor under SpaceVM architecture. -::: +Context: The token encodes the origin, a recent block hash, an expiry in seconds, and optional signed extra information; the user signs it with their wallet, and a server verifies the signature, the origin, and the chain state. Expiry is enforced by distance from the bound block rather than by a server clock, so once the chain passes the bound block plus the expiry, the token is invalid by construction, with no revocation list. It is used for wallet-gated APIs and single sign-on across MultiversX dApps, and is wired into the dApp SDK via `initApp({ dAppConfig: { nativeAuth: true } })`. + +See also: [Onchain Guardian](#onchain-guardian), [Bech32 Addressing](#bech32-addressing-erd1). +Read more: [github.com/multiversx/mx-sdk-dapp](https://github.com/multiversx/mx-sdk-dapp) (`src/services/nativeAuth/`); `@multiversx/sdk-native-auth-client` and `@multiversx/sdk-native-auth-server` packages. --- -### Wallets - Overview +### ABI (Application Binary Interface) -Wallets give access to your funds and MultiversX applications. Only you should have access to your wallet. +The machine-readable description of a smart contract's endpoints, arguments, return types, events, and custom types, used to encode calls and decode results. +Context: The ABI is produced when a contract is built. SDKs and the typed-proxy generator read it to convert native values to the contract's binary encoding and back, so callers do not hand-encode arguments. It is the contract's interface contract for tools and other programs. -## MultiversX Wallets +See also: [Typed Proxies](#typed-proxies), [Tx Syntax](#tx-syntax), [sc-meta](#sc-meta). +Read more: [github.com/multiversx/mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs); [docs.multiversx.com/developers/data/abi](https://docs.multiversx.com/developers/data/abi/). -The private key associated to an address (erd1...) needs to be stored in a specific format. In order to use that private -key to sign transactions and perform various actions, one needs a wallet. +--- -There are multiple ways you can store your funds. This page will present some of them. +### Managed Types +The heap-light value types the Rust framework uses inside contracts (such as `BigUint`, `ManagedBuffer`, `ManagedVec`, and `ManagedAddress`) in place of standard-library types. -## Table of contents +Context: Contracts run in a no-std WebAssembly environment without a normal allocator, so the framework provides managed equivalents whose data lives in the VM rather than in contract memory. `BigUint` is an arbitrary-precision unsigned integer; `ManagedBuffer` replaces byte vectors and strings; `ManagedVec` replaces `Vec`. Using them keeps contracts small and gas-efficient. -| Name | Description | -| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| [xPortal App](https://xportal.com/) | Digital wallet and global payments app that allows you to exchange and securely store money on your mobile phone. | -| [Web Wallet](/wallet/web-wallet) | MultiversX Web Wallet | -| [xAlias](/wallet/xalias) | Single sign-on solution for Web3, powered by Web2 (Google Sign-In). | -| [Web Wallet - tokens operations](/wallet/create-a-fungible-token) | Learn how to perform tokens operation inside Web Wallet. | -| [MultiversX DeFi Wallet](/wallet/wallet-extension/) | MultiversX DeFi Wallet Extension | -| [Ledger](/wallet/ledger) | Ledger Hardware Wallet | -| [Keystore](/wallet/keystore) | Learn more about how to use the **keystore** file. | +See also: [WASM VM](#wasm-vm-and-wasmer), [Storage Mappers](#storage-mappers), [Tx Syntax](#tx-syntax). +Read more: [github.com/multiversx/mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs) (`framework/base/src/types`); [docs.multiversx.com/developers/developer-reference](https://docs.multiversx.com/developers/developer-reference/). --- -### Web Wallet +### mxpy -Use the wallet to send, receive and store EGLD in a secure manner. Includes automations for interacting with staking products and ecosystem pools. +The MultiversX command-line tool for deploying and interacting with contracts, for sending transactions, and for performing wallet and account operations. -You can log in on the Web Wallet using a keystore file, using the xPortal App, using a Ledger device or a pem file. For now, we are going to create a keystore wallet and connect on the Web Wallet using it. +Context: mxpy wraps common developer tasks (interacting with contracts, sending transactions, managing wallets, querying the network) in a CLI. +See also: [sc-meta](#sc-meta), [Typed Proxies](#typed-proxies), [Account Nonce](#account-nonce). +Read more: [docs.multiversx.com/sdk-and-tools/mxpy](https://docs.multiversx.com/sdk-and-tools/mxpy/). -## **Create a new wallet** +--- -Go to [https://wallet.multiversx.com](https://wallet.multiversx.com/) & carefully acknowledge the instructions provided. +### Account Nonce -Click on "Create a new wallet": +A per-account counter that increments with each transaction the account sends, fixing transaction order and preventing replay. -![img](/wallet/web-wallet/wallet_landing_page.png) +Context: Every transaction must carry the sender's current nonce, and the network rejects one whose nonce is out of sequence. When sending several transactions in a row, a client fetches the latest nonce from the network and increments it locally. The nonce is account-scoped, not global. -Carefully read and acknowledge the information, then click "Continue". +See also: [Bech32 Addressing](#bech32-addressing-erd1), [Relayed Transactions](#relayed-transactions-v1--v2--v3), [mxpy](#mxpy). +Read more: [docs.multiversx.com/developers/account-management](https://docs.multiversx.com/developers/account-management/); [github.com/multiversx/mx-sdk-js-core](https://github.com/multiversx/mx-sdk-js-core). -![img](/wallet/web-wallet/wallet_create.png) +--- +### multiversx-sc (mx-sdk-rs) -## **Save your secret phrase! This is very important.** +The official Rust framework for writing, building, and testing MultiversX smart contracts, distributed in the `mx-sdk-rs` repository. -The words, numbered in order, are your Secret Phrase. They are just displayed on your screen once and not saved on a server or anywhere in the world. You only get one chance to save them - please do so now. +Context: Also called the SpaceCraft framework, it provides the contract API, managed types, storage mappers, the unified Tx syntax, typed proxies, the build tool `sc-meta`, and a Rust implementation of the VM for testing. Most MultiversX contracts are written with it. -Click the “copy” (two rectangles) button and then paste them into a text file. If your pets don’t usually find important pieces of paper to be delicious, you could even write the words down. +See also: [Storage Mappers](#storage-mappers), [Tx Syntax](#tx-syntax), [Typed Proxies](#typed-proxies), [sc-meta](#sc-meta), [Managed Types](#managed-types). +Read more: [github.com/multiversx/mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs); [docs.multiversx.com/sdk-and-tools/overview](https://docs.multiversx.com/sdk-and-tools/overview/). -![img](/wallet/web-wallet/wallet_mnemonic.png) +--- -The next page is a test to see if you actually have saved the Secret Phrase. Enter the random words as indicated there and press "Continue". +### mx-sdk-js-core -![img](/wallet/web-wallet/wallet_quiz.png) +The core JavaScript and TypeScript SDK for interacting with MultiversX: accounts, transactions, signing, queries, and smart contract calls. -You are one step away from getting your Keystore File. First, encrypt it with a password. Make sure it’s the strong kind, with 8 characters, at least one UPPER-CASE letter, a $ymb@l and numb3r. +Context: It provides the primitives (Address, Transaction, Account, network providers) used by backends, scripts, and higher-level libraries such as the dApp SDK. -![img](/wallet/web-wallet/wallet_password.png) +See also: [mx-sdk-dapp](#mx-sdk-dapp), [Account Nonce](#account-nonce), [NativeAuth](#nativeauth). +Read more: [github.com/multiversx/mx-sdk-js-core](https://github.com/multiversx/mx-sdk-js-core); [docs.multiversx.com/sdk-and-tools/sdk-js](https://docs.multiversx.com/sdk-and-tools/sdk-js/). -In case you forget this password, you can get a new Keystore File with your secret phrase. Remembering it is always better. +--- -Congratulations, you have a new wallet! The associated Keystore File was downloaded to wherever your browser saves files by default. The file has the actual address of the wallet as default name, something like “erd….json”. You can rename it to “something.json” so it’s easier to manage, if you want. +### mx-sdk-dapp -![img](/wallet/web-wallet/wallet_created.png) +A library holding the core functional logic for building MultiversX dApps: wallet-provider connections, transaction signing and tracking, and login state. +Context: It abstracts the supported wallet providers (xPortal/WalletConnect, extension, web wallet, Ledger, passkey) behind one interface and manages the sign, send, and track lifecycle. It is framework-agnostic, with React conveniences. -## **Access a wallet** +See also: [mx-sdk-js-core](#mx-sdk-js-core), [NativeAuth](#nativeauth), [Wallet](#wallet). +Read more: [github.com/multiversx/mx-sdk-dapp](https://github.com/multiversx/mx-sdk-dapp); [docs.multiversx.com/sdk-and-tools/sdk-dapp](https://docs.multiversx.com/sdk-and-tools/sdk-dapp/). -Go to https://wallet.multiversx.com/ and click on "Access Existing" Make sure the “Keystore file” access method is selected, click Browse and locate your Keystore File [erd1… .json], then put in your password and click "Access Wallet". +--- -![img](/wallet/web-wallet/wallet_login.png) +### Scenarios (testing) -And you’re in! Your EGLD address is on top, you can use the “copy” button (the two rectangles) to copy it to the clipboard. +A JSON-based test format that describes blockchain state, transactions, and expected results, runnable against both the Rust VM and the production Go VM. -![img](/wallet/web-wallet/wallet_welcome.png) +Context: Scenario files (formerly called Mandos) let the same test verify a contract under the Rust test framework and the Go VM, which guards against divergence between them. The Rust framework also offers blackbox and whitebox test styles over the same engine. +See also: [ScenarioWorld](#scenarioworld), [sc-meta](#sc-meta), [Chain Simulator](#chain-simulator). +Read more: [github.com/multiversx/mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs); [docs.multiversx.com/developers/testing/rust/sc-test-overview](https://docs.multiversx.com/developers/testing/rust/sc-test-overview/). -## **Overview of your EGLD balance** +--- -After logging into your wallet, your EGLD balances are immediately visible and displayed in easy to follow boxes. +### Chain Simulator -![img](/wallet/web-wallet/wallet_balance_overview.png) +A local binary that mimics a MultiversX network, exposing the proxy endpoints plus extra controls so developers can test against realistic behavior without a full testnet. -- **Available:** Freely transferable EGLD balance -- **Stake Delegation:** Amount of EGLD delegated towards a Staking Services provider -- **Legacy Delegation:** Amount of EGLD delegated towards MultiversX Community Delegation -- **Staked Nodes:** Amount of EGLD locked for your staked nodes +Context: Introduced with the Vega upgrade, it lets developers drive network state and fast-forward epochs for integration tests. It replicates a local testnet's behavior for faster, deterministic development. +See also: [ScenarioWorld](#scenarioworld), [MultiversX Proxy](#multiversx-proxy), [Vega](#vega). +Read more: [docs.multiversx.com/sdk-and-tools/chain-simulator](https://docs.multiversx.com/sdk-and-tools/chain-simulator/). -## **Send a transaction** +--- -Click "Send" on the right-hand section of the wallet: +### MultiversX Proxy -![img](/wallet/web-wallet/wallet_send.png) +A service that sits in front of observer nodes and exposes a single HTTP API, routing each request to the correct shard. -Input the destination address & amount, and then click "Send". +Context: The proxy is the component an observing squad uses to present the whole sharded network as one endpoint. Public gateways and self-hosted setups both run a proxy. -![img](/wallet/web-wallet/wallet_send_tx.png) +See also: [Observing Squad](#observing-squad), [MultiversX API](#multiversx-api), [Observer](#observer-node). +Read more: [docs.multiversx.com/sdk-and-tools/overview](https://docs.multiversx.com/sdk-and-tools/overview/); [docs.multiversx.com/integrators/observing-squad](https://docs.multiversx.com/integrators/observing-squad/). - Verify the destination and amount and click "Confirm". +--- -![img](/wallet/web-wallet/wallet_confirm_tx.png) +### MultiversX API -After confirming the transaction you can see the progress and completion of the transaction. +The public HTTP API (`api.multiversx.com`) that serves account, transaction, token, and network data, backed by observers and an indexer. -![img](/wallet/web-wallet/wallet_success_tx.png) +Context: It is the most common way applications read MultiversX data and submit transactions, layering a richer, indexed interface (using Elasticsearch) over the lower-level proxy. Network-specific hosts exist for devnet and testnet. -You can always review your transaction history in the "Transactions" menu on the left-hand side of the wallet. +See also: [MultiversX Proxy](#multiversx-proxy), [Observing Squad](#observing-squad), [NativeAuth](#nativeauth). +Read more: [docs.multiversx.com/sdk-and-tools/rest-api](https://docs.multiversx.com/sdk-and-tools/rest-api/). -![img](/wallet/web-wallet/wallet_transactions.png) +--- +### Contract Trait (#[contract], #[module]) -## **Receiving EGLD in your wallet** +The Rust framework's model in which a smart contract is written as a trait, annotated with `#[multiversx_sc::contract]`, whose methods become the contract's endpoints, views, and storage declarations. -After logging into your wallet, as described above, you will be able to see your wallet address and share it with others, so they can send you EGLD. +Context: Annotations on the trait's methods mark behavior: `#[init]` and `#[upgrade]` for lifecycle, `#[endpoint]` and `#[view]` for callable methods, `#[storage_mapper("key")]` for state, `#[payable]` and `#[only_owner]` for guards, and `#[event]` for logs. Reusable logic is factored into `#[multiversx_sc::module]` traits and composed by listing them as supertraits. The build tool reads this trait to generate the WASM entry points and the ABI. -Your address is immediately visible on the top part of the wallet. You can copy the address by pressing the copy button (two overlapping squares). +See also: [multiversx-sc (mx-sdk-rs)](#multiversx-sc-mx-sdk-rs), [Storage Mappers](#storage-mappers), [Reusable Modules](#reusable-modules-multiversx-sc-modules), [ABI](#abi-application-binary-interface), [sc-meta](#sc-meta). +Read more: [github.com/multiversx/mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs) (`framework/derive`); [docs.multiversx.com/developers/developer-reference](https://docs.multiversx.com/developers/developer-reference/). -You can also click "Receive" on the right-hand side to see a QR code for the address, which can be scanned to reveal the public address. +--- -![img](/wallet/web-wallet/wallet_receive.png) +### Reusable Modules (multiversx-sc-modules) +A library of ready-made contract modules in the Rust framework that a contract can inherit to gain common functionality without reimplementing it. -## **Testnet and Devnet faucet** +Context: Modules are trait-based and composed by listing them as supertraits of the contract trait. The `multiversx-sc-modules` crate ships modules for pausing, admin-only access control, governance, staking, ESDT helpers, subscriptions, DNS, token merging, bonding curves, developer-reward claiming, and long-running operations. A contract opts in by adding the module to its trait bounds and gains the module's endpoints and storage. -You can request test tokens from [Testnet Wallet](https://testnet-wallet.multiversx.com) or [Devnet Wallet](https://devnet-wallet.multiversx.com) in the `Faucet` tab. +See also: [multiversx-sc (mx-sdk-rs)](#multiversx-sc-mx-sdk-rs), [Contract Trait (#[contract], #[module])](#contract-trait-contract-module), [Storage Mappers](#storage-mappers), [Tx Syntax](#tx-syntax). +Read more: [github.com/multiversx/mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs) (`contracts/modules`); [docs.multiversx.com/developers/developer-reference](https://docs.multiversx.com/developers/developer-reference/). -The faucet is only available once in a given time period. Other alternatives for getting test tokens are: +--- -- request tokens on [Telegram - Validators chat](https://t.me/MultiversXValidators); -- use a third-party faucet, such as [https://r3d4.fr/faucet](https://r3d4.fr/faucet); -- request tokens on [Discord - NETWORKS](https://discord.gg/multiversxbuilders) category. +### Multi-Value Types +Framework types that let a contract endpoint take a variable or optional number of arguments, or return several values, by mapping a single logical type to multiple serialized argument slots. -## **Guardian** +Context: `MultiValueEncoded` carries a variable-length list of arguments or results; `OptionalValue` is a trailing optional argument; `MultiValue2<...>` through `MultiValueN` group a fixed number of values. They exist because a contract call's arguments are a flat list, and these types describe how a Rust signature maps onto that list. -Starting with Altair release, a new section for Guardian feature is available in the wallet interface: +See also: [multiversx-sc (mx-sdk-rs)](#multiversx-sc-mx-sdk-rs), [Managed Types](#managed-types), [ABI](#abi-application-binary-interface). +Read more: [github.com/multiversx/mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs) (`framework/base/src/types`); [docs.multiversx.com/developers/developer-reference](https://docs.multiversx.com/developers/developer-reference/). -![img](/wallet/web-wallet/guardian_feature1.png) +--- +### Interactors (Snippets) -### **Registering the Guardian** +The Rust framework's tooling for driving a real network, or the chain simulator, from off-chain code, using the same typed proxies that contracts and tests use. -The initial step involves registering your guardian. To begin, access the wallet menu and select the Guardian feature. From there, you should have the option to register and set it up. +Context: Built on the `multiversx-sc-snippets` crate, an interactor sends transactions and queries against a gateway or the chain simulator, so the identical proxy methods serve three settings: cross-contract calls inside a contract, blackbox tests, and live interaction. Interactors are how a project scripts deploys and end-to-end checks against devnet, testnet, or mainnet. -![img](/wallet/web-wallet/guardian_step1.png) +See also: [Typed Proxies](#typed-proxies), [ScenarioWorld](#scenarioworld), [Chain Simulator](#chain-simulator), [sc-meta](#sc-meta). +Read more: [github.com/multiversx/mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs) (`framework/snippets`); [docs.multiversx.com/sdk-and-tools/sdk-rust](https://docs.multiversx.com/sdk-and-tools/sdk-rust/). -To establish a Guardian, you will need to utilize an authenticator app that is capable of generating two-factor authentication (2FA) codes. This app will enable you to set up and manage the Guardian feature effectively. +--- -:::note -Please use the **Authenticator app name tag** to assign a recognizable label within the authenticator app, which will help you locate it easily when searching in the app. Once you have installed the authenticator app, scan the provided QR code using the app and enter the Guardian Code into the designated fields (refer to the below image). -::: +### Official SDKs -:::important -If this is the first time when you are doing the registration, you don't have to select the **I cannot scan a QR code, show me the manual setup key**. -::: +The set of MultiversX libraries for interacting with the blockchain from different languages and frameworks. -![img](/wallet/web-wallet/guardian_step2.png) +Context: The docs list sdk-js (JavaScript and TypeScript), sdk-dapp (React and framework-agnostic dApp logic), sdk-py (Python), sdk-nestjs (NestJS microservices), sdk-go (Go), plus community-referenced mxjava (Java) and erdcpp (C++). Each provides wallet, transaction, signing, query, and contract-interaction capabilities in its language. -**Check & Confirm** the transaction: +See also: [mx-sdk-js-core](#mx-sdk-js-core), [mx-sdk-dapp](#mx-sdk-dapp), [sdk-py](#sdk-py), [sdk-nestjs](#sdk-nestjs), [mxpy](#mxpy). +Read more: [docs.multiversx.com/sdk-and-tools/overview](https://docs.multiversx.com/sdk-and-tools/overview/). -![img](/wallet/web-wallet/guardian_step3.png) +--- +### sdk-py -### **Activation period** +The official Python SDK for MultiversX, used to create wallets, build and send transactions, and interact with smart contracts and the network. -After successfully completing all the previously mentioned steps, you will need to wait for the registration period to conclude. For the Mainnet, this registration period will extend over a duration of **20 epochs**. +Context: sdk-py is the Python counterpart to sdk-js, suited to backend services, scripting, and automation. The mxpy command-line tool builds on the same Python libraries. -:::important -During the waiting period for your Guardian to become active, all interactions with the blockchain will continue to function as usual. There should be no noticeable changes or differences in the way transactions are processed. -::: +See also: [Official SDKs](#official-sdks), [mxpy](#mxpy), [mx-sdk-js-core](#mx-sdk-js-core). +Read more: [docs.multiversx.com/sdk-and-tools/sdk-py](https://docs.multiversx.com/sdk-and-tools/sdk-py/). -![img](/wallet/web-wallet/guardian_step4.png) +--- +### sdk-nestjs -### **Verify Access** +A MultiversX SDK for the NestJS framework, commonly used across the MultiversX microservice ecosystem. -As depicted in the above-presented picture, you have the possibility to check if you still have access to your authenticator app by clicking the *Verify Access* button. +Context: sdk-nestjs packages building blocks for backend microservices, including native-auth handling, caching, and monitoring utilities, so services in the ecosystem share common infrastructure. -![img](/wallet/web-wallet/guardian_step41.png) +See also: [Official SDKs](#official-sdks), [NativeAuth](#nativeauth), [mx-sdk-js-core](#mx-sdk-js-core). +Read more: [docs.multiversx.com/sdk-and-tools/sdk-nestjs](https://docs.multiversx.com/sdk-and-tools/sdk-nestjs/). -If you still have access to the correct authenticator app, you will receive a success message indicating that the activation process is complete. In such a case, you will not need to repeat the entire registration process from the beginning once the activation period is over. +--- -![img](/wallet/web-wallet/guardian_step42.png) +### Endpoint -In the event that you lose access to the authenticator app, you will be required to go through the entire process again by clicking on **"Change Guardian"** and following all the aforementioned steps, starting with the registration of the Guardian. This means you will need to repeat the steps described earlier in order to set up the Guardian feature anew. +A public method of a smart contract that can be called by a transaction, declared in the Rust framework with the #[endpoint] annotation. -![img](/wallet/web-wallet/guardian_step43.png) +Context: Endpoints are the contract's callable surface. A method marked #[endpoint] is state-changing; a read-only method is marked #[view]. The build tool exposes annotated methods as the contract's WASM entry points and records them in the ABI. +See also: [View](#view), [Contract Trait (#[contract], #[module])](#contract-trait-contract-module), [ABI (Application Binary Interface)](#abi-application-binary-interface), [Payable Endpoint](#payable-endpoint). +Read more: [docs.multiversx.com/developers/developer-reference/sc-annotations](https://docs.multiversx.com/developers/developer-reference/sc-annotations/). -### **Guarding your account** +--- -Once the activation period has elapsed, you should find the option to "Guard your account" enabled, as indicated in the picture provided above. By clicing it, will enable the account guarding feature for your account. +### View -:::important -In order to Guard your account, a normal ```GuardAccount``` self-transaction will be sent to the blockchain. -::: +A read-only endpoint, declared with the #[view] annotation, intended to query contract state without changing it. -![img](/wallet/web-wallet/guardian_step6.png) +Context: A view is functionally similar to an endpoint but signals read-only intent and is typically used for queries. Views are recorded in the ABI so SDKs and explorers can call them to read contract state. -Upon the successful completion of a transaction, your account will be guarded, signifying that the account guardian feature is active. **Congratulations! Your funds are now secure and protected.** +See also: [Endpoint](#endpoint), [ABI (Application Binary Interface)](#abi-application-binary-interface), [Contract Trait (#[contract], #[module])](#contract-trait-contract-module). +Read more: [docs.multiversx.com/developers/developer-reference/sc-annotations](https://docs.multiversx.com/developers/developer-reference/sc-annotations/). -![img](/wallet/web-wallet/guardian_step44.png) +--- -As depicted in the image, 3 new options are now available to the user. -- [Verify Access](/wallet/web-wallet#verify-access); -- [Change Guardian](/wallet/web-wallet#changing-your-guardian); -- [Unguard Account](/wallet/web-wallet#unguarding-your-account). +### Init and Upgrade -:::info -New information has been added to the guardian dashboard. +The lifecycle methods of a contract: #[init] runs once when the contract is deployed, and #[upgrade] runs when the contract's code is replaced. -![img](/wallet/web-wallet/guardian_step46.png) +Context: The init constructor can take arguments to set up initial state. Since framework v1.6.0 a dedicated #[upgrade] function handles code upgrades separately from init, so migration logic is explicit. Storage persists across an upgrade, so changing the storage layout needs care. -**Your current guardian is** the **guardian address.** Once you enable the account guardian feature, any outgoing transaction initiated by this account will be considered a guarded transaction. This means that such transactions will require two signatures: one from the account owner, as before, and another from the account guardian, which is the address you can see here. +See also: [Contract Trait (#[contract], #[module])](#contract-trait-contract-module), [Code Metadata](#code-metadata), [sc-meta](#sc-meta). +Read more: [docs.multiversx.com/developers/developer-reference/sc-annotations](https://docs.multiversx.com/developers/developer-reference/sc-annotations/); [docs.multiversx.com/developers/developer-reference/upgrading-smart-contracts](https://docs.multiversx.com/developers/developer-reference/upgrading-smart-contracts/). -To obtain the guardian's signature for a transaction, you don't need to take any action. The TCS (Trusted Co-Signer Service - Guardian Service) will automatically provide the valid guardian signature for the user's transaction whenever a user with a guarded account sends a transaction from their wallet and provides a valid two-factor authentication (2FA) code. The wallet manages the interaction with the TCS, so the user only needs to enter a valid 2FA code. +--- -::: +### Payable Endpoint +An endpoint marked #[payable] so it can receive a payment (EGLD or ESDT tokens) along with the call. -### **Sending a guarded transaction** +Context: Without the payable annotation an endpoint rejects attached value. #[payable("EGLD")] or #[payable("TOKEN-ID")] narrows what is accepted; the contract reads the incoming payment through the call-value API. This is distinct from the code-metadata payable flag, which governs plain transfers to the contract. -Once you have set a guardian and initiated the ```GuardAccount``` transaction, all subsequent transactions must be guarded to be validated by the blockchain. This means that you must enter the 2FA code in order to proceed with these transactions when using the web-wallet. +See also: [Endpoint](#endpoint), [Code Metadata](#code-metadata), [ESDT (eStandard Digital Token)](#esdt-estandard-digital-token), [Tx Syntax](#tx-syntax). +Read more: [docs.multiversx.com/developers/developer-reference/sc-payments](https://docs.multiversx.com/developers/developer-reference/sc-payments/). -Let's take for example a simple eGLD transfer transaction: +--- -![img](/wallet/web-wallet/guardian_sendTx1.png) +### only_owner -Confirm the transaction: +An annotation that restricts an endpoint so only the contract's owner address may call it. -![img](/wallet/web-wallet/guardian_sendTx2.png) +Context: Marking a method #[only_owner] adds an ownership check, so administrative endpoints (for example configuration or upgrades) can be limited to the deploying account. It is the framework's built-in access guard. -In the last step, you will need to check your authenticator tool to retrieve the Guardian code (2FA code) and enter it for verification. See picture below: +See also: [Endpoint](#endpoint), [Contract Trait (#[contract], #[module])](#contract-trait-contract-module), [Reusable Modules (multiversx-sc-modules)](#reusable-modules-multiversx-sc-modules). +Read more: [docs.multiversx.com/developers/developer-reference/sc-annotations](https://docs.multiversx.com/developers/developer-reference/sc-annotations/). -![img](/wallet/web-wallet/guardian_sendTx3.png) +--- +### Contract Calls (sync, async, transfer-execute) -### **Unguarding your account** +The three ways a contract calls another contract: a synchronous call that runs inline and returns a result, an asynchronous call that runs after the current transaction and can trigger a callback, and a transfer-execute that sends value and calls a function without expecting a result. -Unguarding your account consists of sending a guarded ```UnGuardAccount``` transaction to your own address. You can do it by simply using the **Unguard Account** button from the Guardian section: +Context: A synchronous call works only when both contracts are in the same shard. An asynchronous call works across shards and reports its outcome to a #[callback]. A transfer-execute is fire-and-forget. The unified Tx syntax expresses all three, differing only in the terminal operation. -![img](/wallet/web-wallet/guardian_step44.png) +See also: [Callback](#callback), [Tx Syntax](#tx-syntax), [Cross-Shard Atomicity](#cross-shard-atomicity), [Typed Proxies](#typed-proxies). +Read more: [docs.multiversx.com/developers/developer-reference/sc-to-sc-calls](https://docs.multiversx.com/developers/developer-reference/sc-to-sc-calls/). -And confirming the transaction after checking the validity of it: +--- -![img](/wallet/web-wallet/guardian_unguard2.png) +### Callback -And validating it via the authenticator app: +A method marked #[callback] that the framework invokes automatically when an asynchronous contract call completes, receiving the call's result along with any context passed from the call site. -![img](/wallet/web-wallet/guardian_step45.png) +Context: Because cross-shard calls are asynchronous, a contract cannot read the return value inline; it reacts in a callback once the call finishes. The callback is where a contract handles success or failure of an async call, including any compensating action. -Due to the transaction being guarded, there is no cooldown period required. The transaction will be processed instantly without any delay. +See also: [Contract Calls (sync, async, transfer-execute)](#contract-calls-sync-async-transfer-execute), [Cross-Shard Atomicity](#cross-shard-atomicity), [Tx Syntax](#tx-syntax). +Read more: [docs.multiversx.com/developers/developer-reference/sc-to-sc-calls](https://docs.multiversx.com/developers/developer-reference/sc-to-sc-calls/). +--- -### **Changing your guardian** +### Code Metadata -The Guardians feature allows you to change your guardian in situations where you suspect your account has been compromised or if you have lost access to your Authenticator. +The flags set when a contract is deployed or upgraded that control contract-level properties: whether it is upgradeable, readable by other contracts, payable, and payable by smart contracts. -![img](/wallet/web-wallet/guardian_change7.png) +Context: Code metadata is fixed at deploy or upgrade time and governs how the contract may be treated afterward, for example whether it can be upgraded later or receive plain transfers. It is separate from per-endpoint annotations such as #[payable]. -This can be achieved by clicking th *Change Guardian* button from the Guardian's main dashboard. Afterwards you will be faced with two options: +See also: [Init and Upgrade](#init-and-upgrade), [Payable Endpoint](#payable-endpoint), [Contract Trait (#[contract], #[module])](#contract-trait-contract-module). +Read more: [docs.multiversx.com/developers/data/code-metadata](https://docs.multiversx.com/developers/data/code-metadata/). +--- -#### **Changing guardian with 20 epochs pre-registration time** +### Serialization (TopEncode / NestedEncode) -1. **If you have lost access to the authenticator app**, you can address this issue by selecting the option *"I cannot access my guardian"* from the window mentioned below. +The MultiversX codec that turns contract values into bytes and back, with a top-level encoding for standalone values and a nested encoding for values inside larger structures. -![img](/wallet/web-wallet/guardian_change1.png) +Context: Top-level encoding (TopEncode/TopDecode) is used where a single value stands alone, such as a call argument or result, and can drop leading zeros because its length is known from context. Nested encoding (NestedEncode/NestedDecode) is used inside collections, tuples, and structs, where each value's length must be encoded so it can be read unambiguously. -2. And follow the normal guardian registration process of registering a **new guardian**: +See also: [ABI (Application Binary Interface)](#abi-application-binary-interface), [Managed Types](#managed-types), [Multi-Value Types](#multi-value-types). +Read more: [docs.multiversx.com/developers/data/serialization-overview](https://docs.multiversx.com/developers/data/serialization-overview/). -![img](/wallet/web-wallet/guardian_change3.png) +--- -3. Introduce the 2FA code provided by your Authenticator App for the **new guardian**: +### Managed Decimal -![img](/wallet/web-wallet/guardian_change4.png) +A fixed-point decimal type in the Rust framework that pairs an integer value with a decimal scale, for representing decimal numbers inside a contract. -:::note -The above described process will need 20 epochs for your Guardian to become active. If you did not lose access to the authenticator app there is no reason why you should proceed this way. If you will have a pending guardian for 20 epochs which was not registered by you it means that your account has been compromised and you must move your funds to a safe account. -::: +Context: ManagedDecimal is a managed type suited to token amounts and rates, giving decimal arithmetic without floating point. Like other managed types its data lives in the VM rather than in contract memory. -It is important to be aware of certain indicators that indicate the necessity of changing your guardian. One such indicator is the presence of a red shield and frame, as depicted in the image below. This visual cue serves as a signal that prompts you to take action and investigate the situation of your account. +See also: [Managed Types](#managed-types), [Contract Trait (#[contract], #[module])](#contract-trait-contract-module). +Read more: [docs.multiversx.com/developers/best-practices/managed-decimal](https://docs.multiversx.com/developers/best-practices/managed-decimal/). -![img](/wallet/web-wallet/guardian_change2.png) +--- -The Guardian Dashboard will signal the fact that you have an unconfirmed guardian change request: +### Gas Limit -![img](/wallet/web-wallet/guardian_change5.png) +The maximum amount of gas a transaction may consume, declared by the sender when the transaction is built. -Showcasing the details of the request: +Context: The gas limit must fall between the network's minimum and maximum, and any gas reserved but not used is refunded to the sender. Setting it too low causes the transaction to run out of gas; setting it generously does not, by itself, cost the full amount because of the refund. -![img](/wallet/web-wallet/guardian_change6.png) +See also: [Gas and Fees](#gas-and-fees), [Gas Price](#gas-price), [Gas Refund](#gas-refund). +Read more: [docs.multiversx.com/developers/gas-and-fees/overview](https://docs.multiversx.com/developers/gas-and-fees/overview/). -If you were the one who initiated the request to change the guardian and you still have access to the currently active guardian, you will encounter two additional options: +--- -- Skip the cooldown period by clicking the **Confirm Change** button: By selecting this option, you can proceed with the change of the guardian without waiting for the cooldown period to elapse. +### Gas Price -- Cancel the Guardian Change and continue using your current guardian by clicking the **Cancel Change** button: Choosing this option allows you to abort the guardian change process and maintain your existing guardian without any modifications. +The price per unit of gas a transaction is willing to pay, declared by the sender and required to meet the network minimum. -Both of the actions will be done by sending a guarded ```SetGuardian``` self-transaction. +Context: The fee is gas consumed times gas price, with the execution part of a contract call charged at the gas price times a lower modifier. Raising the gas price does not change how much gas a transaction uses, only the rate paid per unit. -:::important -If it was not you the one triggering the change of the guardian, it may be the case that your account has been compromised and the scammer is trying to change the guardian to a new one, that he can control. Don't worry, he has to wait 20 epochs in order to have his own guardian becoming active. By then you can: - - move the funds to a different account, that you control, by using the still active guardian. In this case never **Confirm Change.** - - instantly cancel the pending guardian by using the **Cancel Change**, and play the "owning" game with the scammer. **Not recommended.** +See also: [Gas and Fees](#gas-and-fees), [Gas Limit](#gas-limit), [Gas-Price Modifier](#gas-price-modifier). +Read more: [docs.multiversx.com/developers/gas-and-fees/overview](https://docs.multiversx.com/developers/gas-and-fees/overview/). -We recommend the first solution. Even though you have your funds staked, the unbound period (10 epochs) is shorther than the guardian activation period so that you have enough time to unstake and safely move them to a safe account. -::: +--- +### Code Hash -#### **Instantly changing guardian** +A cryptographic checksum of a deployed contract's bytecode, computed by the network and stored onchain. -To instantly change your guardian you must use the Change Guardian. You must follow a different set of steps compared to the previous *Change Guardian* way: +Context: The code hash lets anyone verify a contract's bytecode, and it is the basis for confirming that a reproducible build produced exactly the bytecode that is deployed. Two builds that yield the same code hash contain identical bytecode. -1. Use an authenticator app for registering the **new guardian**. You must have access to your already active guardian which means that you don't have to select **"I cannot access my guardian"** option. +See also: [Reproducible Build](#reproducible-build), [Smart Contract](#smart-contract), [Contract Address](#contract-address). +Read more: [docs.multiversx.com/developers/reproducible-contract-builds](https://docs.multiversx.com/developers/reproducible-contract-builds/). -![img](/wallet/web-wallet/guardian_change8.png) +--- -2. Open your authenticator app, scan the QR code and register your **new guardian**. +### Reproducible Build -![img](/wallet/web-wallet/guardian_change9.png) +A deterministic contract build, run inside a pinned Docker image, that produces the same WebAssembly bytecode from the same source every time. -3. Enter the 2FA code of the **new guardian** from the authenticator app. +Context: By freezing the toolchain (compiler, wasm-opt, and dependency versions) in a versioned image, a reproducible build lets a third party recompile the source and confirm, via the code hash, that it matches the deployed contract. It is how contract bytecode is independently verified. -![img](/wallet/web-wallet/guardian_change10.png) +See also: [Code Hash](#code-hash), [sc-meta](#sc-meta), [Smart Contract](#smart-contract). +Read more: [docs.multiversx.com/developers/reproducible-contract-builds](https://docs.multiversx.com/developers/reproducible-contract-builds/). -4. Check and Confirm the transaction. +--- -![img](/wallet/web-wallet/guardian_change11.png) +### Contract Address -5. Enter the 2FA code of the guardian you want to change. +The bech32 address the network assigns to a smart contract when it is deployed, computed deterministically from the deployer's address and nonce. -:::note -This is the 2FA code of your previous guardian that you have to enter, **not the new guardian's one**. -::: +Context: A contract address is a recognizable subclass of the normal address format and identifies the contract's code and storage. Because it is derived from the deployer and nonce, it can be computed before deployment, which lets a deploy flow know the address in advance. -![img](/wallet/web-wallet/guardian_change12.png) +See also: [Account](#account), [Bech32 Addressing (erd1…)](#bech32-addressing-erd1), [Account Nonce](#account-nonce), [Smart Contract](#smart-contract). +Read more: [docs.multiversx.com/developers/smart-contracts](https://docs.multiversx.com/developers/smart-contracts/). -6. Awesome! Wait for your transaction to be processed. From now on your account will be guarded by the new guardian that you recently registered. +--- -![img](/wallet/web-wallet/guardian_change13.png) +### Blackbox and Whitebox Testing ---- +Two integration-test styles in the Rust framework: a blackbox test exercises a contract only through its public interface, while a whitebox test reaches into the contract's internals. -### Web Wallet Tokens +Context: Blackbox tests run against the contract as the blockchain would see it (and scenario files additionally run against the production Go VM), which makes them the most realistic. Whitebox tests run on the Rust VM and can access private functions and internal state for step-by-step debugging. -## **Introduction** +See also: [ScenarioWorld](#scenarioworld), [Scenarios (testing)](#scenarios-testing), [Typed Proxies](#typed-proxies), [sc-meta](#sc-meta). +Read more: [docs.multiversx.com/developers/testing/testing-overview](https://docs.multiversx.com/developers/testing/testing-overview/). -**ESDT** stands for _eStandard Digital Token_. +--- -MultiversX network natively supports the issuance of custom tokens, without the need for contracts such as ERC20, but addressing the same use cases. -You can create and issue an ESDT token from [MultiversX web wallet](https://wallet.multiversx.com/) in a few steps. Let's go over these steps. +### Gateway +The official public MultiversX HTTP API endpoint that fronts an observing squad, routing each request to the correct shard's observer. -## **Prerequisites** +Context: The gateway (gateway.multiversx.com) exposes the proxy's endpoints over the whole sharded network without requiring authentication, so applications can read data and broadcast transactions through one host. The same software can be self-hosted for private infrastructure. -- A wallet on MultiversX Network. -- 0.05 EGLD issuance fee -- fees for transactions +See also: [MultiversX Proxy](#multiversx-proxy), [Observing Squad](#observing-squad), [MultiversX API](#multiversx-api). +Read more: [docs.multiversx.com/sdk-and-tools/rest-api/gateway-overview](https://docs.multiversx.com/sdk-and-tools/rest-api/gateway-overview/). +--- -## **Creating a fungible token from Web Wallet** +### Elastic Indexer -To get started, open up the [MultiversX web wallet](https://wallet.multiversx.com/). You can create a new wallet if you do not have one or import your existing wallet. Here is a [guide](https://docs.multiversx.com/wallet/web-wallet/) to help you navigate. +A component that ingests blockchain data from an observer and indexes it into Elasticsearch, enabling historical and search queries over chain data. -On the left sidebar, you will notice the **ISSUE** section. +Context: The MultiversX API layers its richer, indexed interface over data held in Elasticsearch, which the elastic indexer populates from the network. Running the indexer is how a self-hosted setup gains historical query capability. -![sidebar](/wallet/wallet-tokens/sidebar.png) +See also: [MultiversX API](#multiversx-api), [Observer (node)](#observer-node), [Events Notifier](#events-notifier). +Read more: [docs.multiversx.com/sdk-and-tools/elastic-indexer](https://docs.multiversx.com/sdk-and-tools/elastic-indexer/). -Click on **Tokens**. +--- -![issue-token}](/wallet/wallet-tokens/issue-token.png) +### Events Notifier -:::note -The Web Wallet will handle the preparation of the transaction. Therefore, if you'd want a token with a supply of 10 and 2 decimals, you should simply put 10 as supply and 2 as number of decimals. -::: +A service that receives block events from an observer and pushes them to subscribers over a message queue or WebSocket, for real-time event streaming. -When creating a token, you are required to provide the token name, a ticker, the initial supply, and the number of decimals. -In addition to these, tokens' properties should be set. +Context: The notifier lets applications react to onchain events as they happen instead of polling the API. It is used to build reactive dApps and integrations that need push-based updates. -Useful resources: +See also: [Elastic Indexer](#elastic-indexer), [MultiversX API](#multiversx-api), [Observer (node)](#observer-node). +Read more: [docs.multiversx.com/sdk-and-tools/notifier](https://docs.multiversx.com/sdk-and-tools/notifier/). -- [Token parameters format](/tokens/fungible-tokens#parameters-format) - constraints about length, charset and so on. -- [Token properties](/tokens/fungible-tokens#configuration-properties-of-an-esdt-token) - what the properties stand for. +--- -Enter the required details. Next, click on **_Continue_** button to proceed. You will have to review the transaction and sign it, if everything looks good. +### Deep-History Squad -Once the transaction is processed, your token will be issued. +An observing squad that keeps a non-pruned history of the blockchain, so it can answer queries about an account's state at any past block. +Context: A standard observing squad retains only recent state, whereas a deep-history squad preserves historical tries, letting an application read balances or storage as they were at an arbitrary block height. It is used where historical state access is required. -### **Finding the token identifier** +See also: [Observing Squad](#observing-squad), [Snapshotless Observer](#snapshotless-observer), [MultiversX Proxy](#multiversx-proxy). +Read more: [docs.multiversx.com/integrators/deep-history-squad](https://docs.multiversx.com/integrators/deep-history-squad/). -The token identifier of a token is unique. It is composed by the token ticker, a `-` char, followed by 6 random hex characters. Example: `MTKN-c66c30`. +--- -Because the token identifier isn't deterministic, it can be found only after issuing it. There are 2 ways of finding it: +### Snapshotless Observer -1. On the Explorer page of the issue transaction, you will see a Smart Contract Result which has a data field similar to: `@4d544b4e2d373065323338@152d02c7e14af6800000`. - On the right side, choose `Smart` and you will able to see the decoded parameters. In this example, the token identifier is `MTKN-c66c30`. +An observer running in a mode that skips trie snapshots at epoch boundaries and prunes trie storage, trading historical state access for a smaller footprint. -![Token issue SCR](/wallet/wallet-tokens/scr-issue-token.png) +Context: By not creating snapshots, a snapshotless observer avoids that CPU and disk overhead and serves current-state queries and transaction broadcasting efficiently. It is the opposite trade-off to a deep-history squad. -2. From the Web Wallet, go to `TOKENS` tab from the left sidebar, and you can see the token there, including its identifier. +See also: [Observer (node)](#observer-node), [Deep-History Squad](#deep-history-squad), [Observing Squad](#observing-squad). +Read more: [docs.multiversx.com/integrators/snapshotless-observing-squad](https://docs.multiversx.com/integrators/snapshotless-observing-squad/). -![Token view in Web Wallet](/wallet/wallet-tokens/web-wallet-token-display.png) +--- +## Agent stack -## **Transfer a token from your wallet** +MultiversX's agentic-commerce stack integrates several external standards (MCP, x402, Stripe's MPP, Google's UCP and AP2, the OpenAI/Stripe ACP) and adds protocol-native pieces for agent identity (MX-8004) and a developer hub (Agent Hub). -You can transfer an amount of a token to another account. To get started, open up the [MultiversX web wallet](https://wallet.multiversx.com/). +### MX-8004 -Navigate to the `Tokens` tab, and click on `Send` for the token you want to transfer. +MultiversX's onchain agent identity standard, built as a set of four smart contracts covering Identity, Reputation, Validation, and Escrow, and positioned as semantically aligned with Ethereum's ERC-8004 patterns. -![Web Wallet Tokens page](/wallet/wallet-tokens/web-wallet-tokens-page.png) +Context: The four-contract framing is a point of difference from ERC-8004's three-contract framing; the Escrow contract is the fourth and is often omitted from third-party coverage. The standard's taxonomy (OASF) is carried in IPFS manifests rather than enforced onchain. MX-8004 is the current and canonical name and stays until further notice. (A rename was explored internally to avoid search-result collision with Ethereum's ERC-8004; it is not active, and all published assets use MX-8004.) -On the pop-up, introduce the recipient and the amount you want to send. Then press `Send`. +See also: [MCP Server](#mcp-server-multiversx), [MPP](#mpp-machine-payments-protocol), [Agent Hub](#agent-hub), [x402](#x402-multiversx-adapter). +Read more: MultiversX, "Universal Identity and Trust Layer for Agents" ([multiversx.com/blog/universal-identity-and-trust-layer-for-agents](https://multiversx.com/blog/universal-identity-and-trust-layer-for-agents)). -![Web Wallet Transfer Token](/wallet/wallet-tokens/web-wallet-transfer-token.png) +--- -Once the transaction is successfully executed, the recipient should receive the amount of tokens. +### MCP Server (MultiversX) +A Model Context Protocol server that exposes MultiversX blockchain operations as tools an AI agent or assistant can call. -## **Managing a token from Web Wallet** +Context: MCP is an open standard for connecting language models to external tools and data; the MultiversX MCP server provides tools for querying accounts, building and sending transactions, and interacting with contracts and tokens. The official MultiversX MCP package exposes 14 tools. A separate community-maintained server (referenced on the MultiversX `/ai` page) exposes 13 tools; the two should not be conflated. The companion `mx-ai-skills` repository provides 25 skills (its README states "19+", which understates the count). -At the time of writing, a dashboard for tokens owners is still under construction. Meanwhile, token operations have to be done -manually, by following the transaction formats described [here](/tokens/fungible-tokens/#management-operations). +See also: [Agent Hub](#agent-hub), [MX-8004](#mx-8004), [x402](#x402-multiversx-adapter). +Read more: [github.com/multiversx/mx-mcp](https://github.com/multiversx/mx-mcp); MultiversX `/ai` page ([multiversx.com/ai](https://multiversx.com/ai)); tool and skill counts corroborated by internal review (May 2026). --- -### Webhooks +### x402 (MultiversX adapter) -The web wallet webhooks allow you to build or setup integrations for dapps or payment flows. +A MultiversX adapter for the x402 protocol, which uses the HTTP 402 "Payment Required" status code to let a client (including an autonomous agent) pay for a resource inline over HTTP. -The web wallet webhooks are links that point the user of the wallet to either login or populate a "send transaction" form with the provided arguments. Once the action is performed, the user is redirected to the provided callback URL along with a success or error status. +Context: x402 is an open payment standard originating outside MultiversX; the adapter lets an x402 payment settle on MultiversX. It is one of several agentic-commerce standards MultiversX has built integrations for, alongside MCP, Google's UCP and AP2, and the OpenAI/Stripe ACP. The MultiversX advantage in this area is breadth of supported standards rather than dominance of agent transaction volume. +See also: [MPP](#mpp-machine-payments-protocol), [Agent Hub](#agent-hub), [MCP Server](#mcp-server-multiversx), [MX-8004](#mx-8004). +Read more: MultiversX, "MultiversX speaks Agent" ([multiversx.com/blog/multiversx-speaks-agent](https://multiversx.com/blog/multiversx-speaks-agent)); x402 protocol specification (external). -## **Login hook** +--- -This is useful when you need to find the user's wallet address. A common use case is that, starting from this address you can query the API for the wallet's balance or recent transactions. +### MPP (Machine Payments Protocol) -### URL Parameters +Stripe and Tempo's Machine Payments Protocol, a standard for autonomous machine-to-machine payments, for which MultiversX serves as a settlement venue. -`https://wallet.multiversx.com/hook/login?callbackUrl=https://example.com/` +Context: MPP is authored by Stripe and Tempo, not by MultiversX. The accurate framing is "Stripe's Machine Payments Protocol, settled on MultiversX," rather than "MultiversX MPP." Low-latency settlement is the property that makes a chain suitable for high-frequency machine payments, which is the connection to the Supernova upgrade. -| Param | Required | Description | -| ------------- | ----------------------------------------- | ----------------------------------------------------- | -| callbackUrl | REQUIRED | The URL the user should be redirected to after login. | +See also: [x402](#x402-multiversx-adapter), [Supernova](#supernova), [Agent Hub](#agent-hub), [MX-8004](#mx-8004). +Read more: MultiversX, "Stripe's Machine Payments Protocol on MultiversX" ([multiversx.com/blog/stripes-machine-payments-protocol-on-multiversx](https://multiversx.com/blog/stripes-machine-payments-protocol-on-multiversx)). -Upon a successful login, the user is redirected back to the callback URL along which the user's address is appended. +--- -### Callback URL Parameters +### Agent Hub -`https://example.com/?address=erd1axhx4kenjlae6sknq7zjg2g4fvzavv979r2fg425p62wkl84avtqsf7vvv` +MultiversX's hub for agentic development, bringing together the agent identity standard, the MCP server and skills, the payment-protocol adapters, and an agent explorer, launched in March 2026. -| Param | Description | -| ------------- | ------------------------------- | -| address | The users's Address (bech32). | +Context: The Agent Hub is the developer surface for building agents on MultiversX: it gathers the tooling (MCP server, `mx-ai-skills`, `mx-agent-kit`) and the standards (agent identity, MPP, x402, UCP, AP2, ACP) in one place. A devnet Agent Explorer surfaces registered agents. +See also: [MX-8004](#mx-8004), [MCP Server](#mcp-server-multiversx), [x402](#x402-multiversx-adapter), [MPP](#mpp-machine-payments-protocol). +Read more: MultiversX, "The MultiversX Universal Agentic Commerce Stack" ([multiversx.com/blog/the-multiversx-universal-agentic-commerce-stack](https://multiversx.com/blog/the-multiversx-universal-agentic-commerce-stack)). -## **Send transaction hook** +--- -This is useful when you need to prepopulate a transaction required to send an EGLD amount or pre-populate the transaction's data field with a smart contract function invocation. +### UCP (Universal Commerce Protocol) -### URL Parameters {#send-transaction-url-parameters} +An open commerce-orchestration standard from Google and Shopify that coordinates the steps above payment (product discovery, checkout, and post-purchase) for AI-assisted shopping, using lower-level payment and agent protocols underneath. -`https://wallet.multiversx.com/hook/transaction?receiver=erd1qqqqqqqqqqqqqpgqxwakt2g7u9atsnr03gqcgmhcv38pt7mkd94q6shuwt&value=0&gasLimit=250000000&data=claimRewards&callbackUrl=https://example.com/` +Context: UCP sits a layer above settlement: it orchestrates a purchase flow and delegates payment and agent-to-agent messaging to protocols such as AP2, A2A, and x402. Its public specification was published in January 2026 and it is wired into Google's AI shopping surfaces. MultiversX built an integration so a UCP flow can settle onchain; as with the other agentic standards, the MultiversX position is breadth of supported standards rather than control of the standard itself. -| Param | Required | Description | -| ------------- | ----------------------------------------- | ----------------------------------------------------- | -| receiver | REQUIRED | The receiver's Address (bech32). | -| value | REQUIRED | The Value to transfer (can be zero). | -| gasLimit | OPTIONAL | The maximum amount of Gas Units to consume. | -| data | OPTIONAL | The message (data) of the Transaction. | -| callbackUrl | OPTIONAL | The URL the user should be redirected to after login. | +See also: [AP2](#ap2-agent-payments-protocol), [ACP](#acp-agentic-commerce-protocol), [x402 (MultiversX adapter)](#x402-multiversx-adapter), [Agent Hub](#agent-hub). +Read more: Google Developers Blog, "Under the hood: Universal Commerce Protocol (UCP)"; Shopify Engineering, "UCP"; MultiversX agentic-stack coverage ([multiversx.com/blog/the-multiversx-universal-agentic-commerce-stack](https://multiversx.com/blog/the-multiversx-universal-agentic-commerce-stack)). -### Callback URL Parameters {#send-transaction-callback-url-parameters} +--- -`https://example.com/?status=success&txHash=48f68a2b1ca1c3a343cbe14c8b755934eb1a4bb3a4a5f7068bc8a0b52094cc89&address=erd1axhx4kenjlae6sknq7zjg2g4fvzavv979r2fg425p62wkl84avtqsf7vvv` +### AP2 (Agent Payments Protocol) -| Param | Description | -| ------------- | ----------------------------------------- | -| status | Success / failure of sending transaction. | -| txHash | The transaction's hash. | +Google's open protocol for agent-initiated payments, built around cryptographically signed "mandates" that create a non-repudiable record of what a user authorized an agent to buy. ---- +Context: AP2 chains a user's intent, the resulting cart, and the payment into signed mandates, so an agent's purchase can be proven to match what the user approved. It is rail-agnostic, with crypto settlement handled through an x402 extension, and launched in September 2025 with a broad partner list (including Mastercard, PayPal, American Express, and Coinbase). MultiversX is among the chains that can settle AP2-authorized payments. -### Whitelist Requirements +See also: [UCP](#ucp-universal-commerce-protocol), [ACP](#acp-agentic-commerce-protocol), [x402 (MultiversX adapter)](#x402-multiversx-adapter), [MX-8004](#mx-8004), [Agent Hub](#agent-hub). +Read more: Google Cloud Blog, "Announcing the Agent Payments Protocol (AP2)"; MultiversX agentic-stack coverage ([multiversx.com/blog/the-multiversx-universal-agentic-commerce-stack](https://multiversx.com/blog/the-multiversx-universal-agentic-commerce-stack)). -# Whitelist Requirements +--- +### ACP (Agentic Commerce Protocol) -Before enabling a token to be sent via the Ad-Astra bridge, the token must be whitelisted. -The whitelisting process is performed with the help of the MultiversX team. +The Agentic Commerce Protocol from OpenAI and Stripe, a standard for letting an AI assistant complete a purchase (for example, checkout inside ChatGPT), for which MultiversX has built an integration. +Context: ACP, published in September 2025, covers the checkout handoff between an assistant and a merchant, paired with Stripe's Shared Payment Token. Note a naming collision: "ACP" is also used by Virtuals for a separate Agent Commerce Protocol on Base; the MultiversX agentic stack refers to the OpenAI/Stripe Agentic Commerce Protocol. As with the other standards here, MultiversX provides an adapter rather than authoring the protocol. -## Whitelist requirements +See also: [UCP](#ucp-universal-commerce-protocol), [AP2](#ap2-agent-payments-protocol), [MPP](#mpp-machine-payments-protocol), [x402 (MultiversX adapter)](#x402-multiversx-adapter), [Agent Hub](#agent-hub). +Read more: Stripe newsroom, "Stripe and OpenAI bring Instant Checkout to ChatGPT" (Sept 2025); MultiversX agentic-stack coverage ([multiversx.com/blog/the-multiversx-universal-agentic-commerce-stack](https://multiversx.com/blog/the-multiversx-universal-agentic-commerce-stack)). -1. The token issuer must issue the token on the MultiversX network and submit a branding request manually or using [https://assets.multiversx.com](https://assets.multiversx.com); -2. The token issuer must assign the MINT & BURN role to the **BridgedTokensWrapper** or the **Safe** contract, depending on the -token type configuration. +--- -```rust -RolesAssigningTransaction { - Sender:
- Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 0 - GasLimit: 60000000 - Data: "setSpecialRole" + - "@" + + - "@" + + - "@" + + - "@" + -} -``` +### mx-agent-kit -Example: +A MultiversX toolkit that exposes blockchain actions (such as building and sending transactions or calling contracts) as functions an AI agent can invoke, so a developer can give an agent the ability to transact on MultiversX. -Let's suppose we want to whitelist the token `TKN-001122` on the Ethereum bridge and the token configuration type is 1. -(mint/burn tokens on both ends, native on MultiversX) +Context: The agent kit wraps the SDK's operations as agent-callable actions, alongside the MCP server and mx-ai-skills, and is one of the building blocks gathered under the Agent Hub. Some of the agent tooling is maintained on a core engineer's personal GitHub rather than under the `multiversx` organization, so repository provenance should be confirmed before a specific link is published as official. -The transaction will look like: +See also: [MCP Server (MultiversX)](#mcp-server-multiversx), [mx-ai-skills](#mx-ai-skills), [Agent Hub](#agent-hub), [mx-sdk-js-core](#mx-sdk-js-core). +Read more: MultiversX `/ai` page ([multiversx.com/ai](https://multiversx.com/ai)); MultiversX agentic-stack coverage. (Provenance: confirm whether the kit is hosted under the `multiversx` org or a core engineer's personal account before publishing a direct link.) -```rust -RolesAssigningTransaction { - Sender:
- Receiver: erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u - Value: 0 - GasLimit: 60000000 - Data: "setSpecialRole" + - "@544b4e2d303031313232" + - "@000000000000000005004ab1cd3d291159a38df7d2a1c498c9d7a7e3047ccc48" + - "@45534454526f6c654c6f63616c4d696e74" + - "@45534454526f6c654c6f63616c4275726e" -} -``` +--- -3. Depending on the token configuration (valid for 1. & 2. configuration types), the minter & burner role should be granted on -the EVM-compatible chain side (depending on the ERC20 contract variant, `addMinter` and `addBurner` endpoints can be used); -4. MultiversX team will complete the whitelisting process by issuing the required transactions. - -For reference, this is the list of the known smart contracts: -* **Wrapper** `erd1qqqqqqqqqqqqqpgq305jfaqrdxpzjgf9y5gvzh60mergh866yfkqzqjv2h` -* **Ethereum Safe** `erd1qqqqqqqqqqqqqpgqf2cu60ffz9v68r0h62sufxxf67n7xprue3yq4ap4k2` -* **BSC Safe** `erd1qqqqqqqqqqqqqpgqa89ts8s3un2tpxcml340phcgypyyr609e3yqv4d8nz` +### mx-ai-skills -:::warning -To ensure the correct functioning of the bridge, as a MultiversX token owner please ensure the following points are met: -* if you make use of the transfer-role on your token, remember to grant the role also on the **Safe**, **MultiTransfer**, **BridgedTokensWrapper**, and **BridgeProxy** contracts; -* do not freeze the above-mentioned contracts; -* do not wipe tokens on the above-mentioned contracts. +A MultiversX repository of pre-built "skills" (packaged capabilities) that an AI agent or assistant can load to perform common MultiversX tasks. -Failure to comply with these rules will force the bridge owner to blacklist the token in order to restore the correct functioning of the bridge. -::: +Context: The skills complement the MCP server's tools, giving agents higher-level, ready-made actions. Internal review counts 25 skills in the repository; its README states "19+", which understates the count. It is part of the agent tooling gathered under the Agent Hub. + +See also: [MCP Server (MultiversX)](#mcp-server-multiversx), [mx-agent-kit](#mx-agent-kit), [Agent Hub](#agent-hub). +Read more: MultiversX `/ai` page ([multiversx.com/ai](https://multiversx.com/ai)); skill count corroborated by internal review (May 2026). --- -### xAlias +### Agent Explorer -**xAlias** is a _single sign-on_ solution for Web3, powered by Google Sign-In (Web2). It allows new users (not yet proficient in blockchain technologies) to quickly and easily create blockchain wallets (without the need of seed phrases), then start right away and interact with MultiversX dApps. +A MultiversX explorer, hosted by the Foundation on a devnet backend, for browsing agents registered onchain along with their jobs and verification records. -It's a _self-custody_ wallet, and it's _convertible_ to a conventional Web3 wallet at a later point. +Context: The explorer (at `agents.multiversx.com`) reads real onchain data from devnet: registered agents appear as onchain NFT entries with an owner address and transaction hash, and the backend tracks jobs, verifications, and per-protocol support counts. It is a working preview on devnet, not a mainnet product, and should be labeled as such. It surfaces the agents registered under the MX-8004 identity standard. -:::important -**For dApp developers:** xAlias exposes **the same [URL hooks and callbacks](/wallet/webhooks)** as the [Web Wallet](/wallet/web-wallet). Therefore, integrating xAlias is **identical to integrating the Web Wallet** (with one trivial exception: the configuration of the URL base). See [Signing Providers for dApps](/sdk-and-tools/sdk-js/sdk-js-signing-providers). -::: +See also: [MX-8004](#mx-8004), [MX-8004 Sub-Contracts](#mx-8004-sub-contracts-identity-reputation-validation-escrow), [Agent Hub](#agent-hub), [MCP Server (MultiversX)](#mcp-server-multiversx). +Read more: MultiversX `/ai` page ([multiversx.com/ai](https://multiversx.com/ai)); Agent Explorer ([agents.multiversx.com](https://agents.multiversx.com)), a devnet preview verified by internal review (2026). +--- -## Before you begin +### MX-8004 Sub-Contracts (Identity, Reputation, Validation, Escrow) -If you don't already have a Google account, [set up one](https://accounts.google.com/signup). +The four smart contracts that make up the MX-8004 agent-identity standard: Identity (an agent's onchain identifier and metadata), Reputation (feedback and scoring), Validation (verification of an agent's work), and Escrow (holding funds for agent jobs). +Context: The first three mirror the three registries of Ethereum's ERC-8004 (Identity, Reputation, Validation); MX-8004 adds Escrow as a fourth, which is the main structural difference and is often omitted from third-party coverage. As with MX-8004 generally, the standard is not yet ratified or published at an official standards URL, and parts of the implementation live on a core engineer's personal GitHub rather than under the `multiversx` organization, so it should be described as emerging rather than as a finalized Foundation standard. -## Sign Up with xAlias +See also: [MX-8004](#mx-8004), [Agent Explorer](#agent-explorer), [Agent Hub](#agent-hub), [Herotag](#herotag). +Read more: MultiversX, "Universal Identity and Trust Layer for Agents" ([multiversx.com/blog/universal-identity-and-trust-layer-for-agents](https://multiversx.com/blog/universal-identity-and-trust-layer-for-agents)); ERC-8004 comparison and ratification status corroborated by internal review (May 2026). -Navigate to **[xAlias.com](https://xalias.com)**, then click on **Get Started** to reach the **Sign Up** screen: +--- -![img](/wallet/xalias/xalias_signup_first.png) +## Economics -Then, click on **Authenticate**, which redirecteds you to Google Sign-In. +### Developer Revenue Share -![img](/wallet/xalias/xalias_signup_google_choose_account.png) +The portion of a smart contract's transaction fees routed to the contract's owner, rather than captured by the protocol or paid only to validators. -Pick the Google account you want to use, then click on **Confirm**. +Context: Today, the owner of a called smart contract can claim 30% of the fee of each call, through the `ClaimDeveloperRewards` operation. A proposed economic framework would raise the builder share substantially: 90% of base transaction fees to the called contract's author and 10% permanently burned, rebalancing by 5% per year toward a 50/50 builder/burn split over eight years, with multi-contract transactions splitting the share in proportion to the gas each contract used and priority (tip) fees going to validators. That framework is approved in principle but not yet implemented, so the 30% claimable share remains the live figure. -![img](/wallet/xalias/xalias_signup_google_confirm.png) +See also: [Economic Evolution](#economic-evolution), [Staking V5](#staking-v5-and-the-emission-split), [KPI-Gated Emissions](#kpi-gated-emissions), [EGLD](#egld). +Read more (live, 30%): [docs.multiversx.com/developers/built-in-functions](https://docs.multiversx.com/developers/built-in-functions/) (ClaimDeveloperRewards). Read more (proposed, 90%): "A competitive economic framework for MultiversX: toward revenue and reflexive value accrual," MultiversX Agora ([agora.multiversx.com/t/...530](https://agora.multiversx.com/t/a-competitive-economic-framework-for-multiversx-toward-revenue-and-reflexive-value-accrual/530)). -Next, you'll have to **Authorize** xAlias to store and access its own data on your Google Drive account: +--- -![img](/wallet/xalias/xalias_signup_second.png) +### Governance Proposal Process -Read the Google consent screen, then click on **Allow**. +MultiversX's onchain, stake-weighted process for ratifying protocol upgrades and parameter changes, in which a proposal passes by a stake-weighted vote subject to a quorum. -![img](/wallet/xalias/xalias_signup_authorize_google.png) +Context: Voting power is proportional to staked EGLD, and proposals (protocol upgrades, economic-model changes) are ratified by an onchain vote. Sirius (late 2023/early 2024) was the first major upgrade passed this way; the governance contracts themselves were moved fully onchain in the Barnard upgrade (July 2025). Recent recorded outcomes include the Economic Evolution vote (94.55% in favor on 41.77% participation, October 2025) and the Supernova vote (99.64% in favor on 33.63% quorum, January 2026). The Foundation reports that all six governance proposals submitted across 2024 and 2025 passed. -At the end of the Sign Up flow, you will be asked to back-up your xAlias account, as a document file, which can be either received by email or downloaded directly: +See also: [Economic Evolution](#economic-evolution), [Supernova](#supernova), [Barnard](#barnard), [Sirius](#sirius). +Read more: [docs.multiversx.com/governance/overview](https://docs.multiversx.com/governance/overview/); MultiversX State of the Foundation Report 2025 ([files.multiversx.com/MultiversX-State-of-the-Foundation-Report-2025.pdf](https://files.multiversx.com/MultiversX-State-of-the-Foundation-Report-2025.pdf)). -![img](/wallet/xalias/xalias_signup_backup_file.png) +--- -To confirm the back-up and complete the flow, enter the confirmation code from the received (or downloaded) document: +### Tail Inflation -![img](/wallet/xalias/xalias_signup_backup_code.png) +An emissions model with no fixed supply cap, in which issuance continues at a declining rate rather than stopping at a maximum supply. -Congratulations, you have successfully **created your xAlias account**! +Context: The Economic Evolution proposal moved EGLD from a fixed cap to tail inflation, starting near 8.76% per year and decaying toward a 2% to 5% floor, with part of issuance gated on ecosystem KPIs. The intent is to keep funding security and growth past the point where the original cap would have been reached. +See also: [Economic Evolution](#economic-evolution), [KPI-Gated Emissions](#kpi-gated-emissions), [EGLD](#egld), [Fee Burn](#fee-burn). +Read more: MultiversX, "The MultiversX Economic Evolution" ([multiversx.com/blog/the-multiversx-economic-evolution-a-blueprint-for-growth-and-strategic-expansion](https://multiversx.com/blog/the-multiversx-economic-evolution-a-blueprint-for-growth-and-strategic-expansion)); economic framework proposal on Agora ([agora.multiversx.com/t/...530](https://agora.multiversx.com/t/a-competitive-economic-framework-for-multiversx-toward-revenue-and-reflexive-value-accrual/530)). -## Sign In +--- -You can always sign-in to your xAlias account by navigating to **[xAlias.com](https://xalias.com)**, then clicking on **Sign In**. You will be asked to confirm the Google account, then reach the **xAlias Dashboard**. +### Fee Burn +The permanent removal from supply of a portion of transaction fees. -## xAlias Dashboard +Context: Under the Economic Evolution framework, 10% of base transaction fees are burned today, rising on a schedule toward 50% over eight years as the developer share falls. Burning offsets issuance and ties supply pressure to network usage. -Upon the initial sign-up, and each time you sign-in to xAlias, you will be presented the **xAlias Dashboard**. +See also: [Developer Revenue Share](#developer-revenue-share), [Economic Evolution](#economic-evolution), [Tail Inflation](#tail-inflation), [Gas and Fees](#gas-and-fees). +Read more: economic framework proposal on Agora ([agora.multiversx.com/t/...530](https://agora.multiversx.com/t/a-competitive-economic-framework-for-multiversx-toward-revenue-and-reflexive-value-accrual/530)); MultiversX, "The MultiversX Economic Evolution" ([multiversx.com/blog/the-multiversx-economic-evolution-a-blueprint-for-growth-and-strategic-expansion](https://multiversx.com/blog/the-multiversx-economic-evolution-a-blueprint-for-growth-and-strategic-expansion)). -Here, you will be able to see the wallet address (the one starting with _erd1_) and share it with others, so they can send you EGLD or other tokens.‌ Additionally, you can click on **Open in Explorer** and see the all the blockchain transactions associated with your wallet address (blockchain address). +--- -![img](/wallet/xalias/xalias_dashboard.png) +### APR (Annual Percentage Rate) +The yearly rate of staking rewards, expressed as a percentage of the staked amount. -## Use a MultiversX dApp with xAlias +Context: A validator's or delegator's APR depends on total network stake, the node's topUp, and the staking provider's service fee. It is the headline yield figure for staking on MultiversX. -:::note -The screenshots below are from the [**MultiversX dApp Template**](https://devnet.template-dapp.multiversx.com). -::: +See also: [Staking](#staking), [Delegation](#delegation), [Service Fee](#service-fee), [Staking Auction and TopUp](#staking-auction-and-topup). +Read more: [docs.multiversx.com/economics/staking-providers-apr](https://docs.multiversx.com/economics/staking-providers-apr/). -:::important -**For dApp developers:** if your dApp doesn't yet support **xAlias** as a signing provider, **we recommend that you enable the integration, and reach a broader audicence** (wider user base for your dApp). Please follow [Signing Providers for dApps](/sdk-and-tools/sdk-js/sdk-js-signing-providers) for technical details. -::: +--- -If you've stumbled upon a MultiversX dApp that you'd like to use and it supports xAlias, follow the **Login** or **Connect** flow of the dApp, then pick **xAlias** (as your Web3 wallet). +### Service Fee -![img](/wallet/xalias/xalias_dapp_login.png) +The percentage a staking provider takes from the rewards earned on its delegators' stake. -Then, you will reach the following consent screen: +Context: The provider sets the service fee, and delegators receive rewards net of it. It is how staking providers are compensated for operating nodes. -![img](/wallet/xalias/xalias_dapp_consent.png) +See also: [Delegation](#delegation), [Staking](#staking), [APR](#apr-annual-percentage-rate). +Read more: [docs.multiversx.com/validators/delegation-manager](https://docs.multiversx.com/validators/delegation-manager/). -Upon confirmation, you will be redirected to the dApp (which is informed about your blockchain address - **not your email address, of course**). +--- -Then, as a user of the dApp (of any dApp), you might reach a point where you need to **sign a transaction** - then, you will be redirected to xAlias: +### Governance Proposal -![img](/wallet/xalias/xalias_dapp_sign_transaction.png) +An onchain proposal, identified by a code commit hash, that the community votes on to change the protocol or its parameters. -... or you might need to sign a message: +Context: Anyone can submit a proposal by paying the proposal fee (currently 500 EGLD) after at least 15 days of public debate on Agora, and setting a voting start epoch. If the proposal passes the fee is refunded; if it fails a portion is kept as a lost-proposal fee; if it is vetoed the fee is slashed. A proposal cannot be duplicated and can be cancelled by the issuer before voting starts. -![img](/wallet/xalias/xalias_dapp_sign_message.png) +See also: [Governance Proposal Process](#governance-proposal-process), [Quorum](#quorum), [Veto](#veto), [Voting Power](#voting-power). +Read more: [docs.multiversx.com/governance/overview](https://docs.multiversx.com/governance/overview/). + +--- +### Quorum -## Sign Out +The minimum share of total voting power that must participate in a governance vote for the proposal to be valid, currently at least 20%. -To sign out from xAlias, navigate to **[xAlias.com](https://xalias.com)**, then click on **Sign Out**. +Context: If participation falls below the quorum, the proposal fails regardless of how the votes split. Quorum is a configurable governance parameter that ensures a decision reflects broad participation. -:::note -Note that disconnecting from a dApp doesn't sign you out from xAlias. -::: +See also: [Governance Proposal](#governance-proposal), [Voting Power](#voting-power), [Veto](#veto), [Governance Proposal Process](#governance-proposal-process). +Read more: [docs.multiversx.com/governance/overview](https://docs.multiversx.com/governance/overview/). --- -### xPortal +### Veto -Experience the power of convenience and security in one place. Built by MultiversX, [xPortal](https://xportal.com/) is the go-to app for all crypto needs. Securely store your cryptocurrencies and NFTs, effortlessly swap tokens, and soon, enjoy the perks of our upcoming debit cards with exciting cashback rewards. Join millions of users who trust xPortal for seamless finance management, gamified missions & AI avatar creation, global payments, and a social experience like no other. +A vote type in MultiversX governance that rejects a proposal, and its threshold: if veto votes exceed 33% of the participating voting power, the proposal is rejected and its fee is slashed. -:::info -On this page, we will start presenting features from xPortal, beginning with the Invisible Guardians feature. Subsequently, we will introduce other features in the following sections. -::: +Context: Voters choose among Yes, No, Abstain, and Veto. Veto is a minority-protection mechanism, letting a sufficiently large share block a proposal that could harm the network even if a majority is in favor. Acceptance also requires Yes over Yes-plus-No-plus-Veto to reach at least 66.67%. +See also: [Governance Proposal](#governance-proposal), [Quorum](#quorum), [Voting Power](#voting-power), [Governance Proposal Process](#governance-proposal-process). +Read more: [docs.multiversx.com/governance/overview](https://docs.multiversx.com/governance/overview/). -## What are Invisible Guardians? +--- -Invisible Guardians is a variation of the Guardians feature tailored for xPortal users. It is specifically designed to deliver the same level of security introduced by the Guardians feature while maintaining the same ease of use xPortal users are accustomed to. +### Voting Power -![img](/wallet/xportal/activate_guardian.jpg) +A voter's weight in governance, equal to their staked plus delegated EGLD, applied linearly so that each EGLD counts as one unit. +Context: Voting power is derived from stake, not liquid balance, and the governance contract tracks both used and total available voting power per address. Delegation and liquid-staking contracts can forward their users' voting power. -## How do Invisible Guardians work? +See also: [Governance Proposal](#governance-proposal), [Quorum](#quorum), [Veto](#veto), [Staking](#staking). +Read more: [docs.multiversx.com/governance/overview](https://docs.multiversx.com/governance/overview/). -When active, the feature enables an invisible guardian which is encrypted and stored locally on your device, silently co-signing every transaction. This acts as an additional security layer and safeguards the account in situations where, one way or another, the secret phrase has been compromised. +--- +## Staking operations -## How do I activate an Invisible Guardian on my account? -The Guardians feature can be accessed in the Security section, which can be found in the xPortal Settings. There, users can activate an Invisible Guardian on their account by going through a few simple steps: +Practical staking and delegation vocabulary, aligned with the MultiversX docs terminology page. These describe the day-to-day actions a staker or delegator takes. -- Create a backup password for your guarded account -![img](/wallet/xportal/inv_guardian_step1.jpg) -- Enable the Invisible Guardian through a blockchain transaction -![img](/wallet/xportal/inv_guardian_step2.jpg) -- Wait for the 20-day cooldown period to pass -![img](/wallet/xportal/inv_guardian_step4.jpg) -- Activate the Invisible Guardian through another blockchain transaction -![img](/wallet/xportal/inv_guardian_step5.jpg) +### Validate +Running a validator node and contributing to the network by relaying and validating information. -## Can I deactivate the Invisible Guardian feature once it was activated? +Context: Validating requires staking at least 2500 EGLD per node and participating in consensus. It is the direct, node-operating form of securing the network, as opposed to delegating stake to someone else's nodes. -The Invisible Guardian can be deactivated at any time, but it requires that a transaction is sent and signed by the Guardian currently in place on that account. +See also: [Validator (node)](#validator-node), [Staking](#staking), [Delegate](#delegate), [Secure Proof of Stake (SPoS)](#secure-proof-of-stake-spos). +Read more: [docs.multiversx.com/validators/overview](https://docs.multiversx.com/validators/overview/). +--- -## Can I change my Guardian? +### Stake -Yes, the Invisible Guardian can be changed, and there are two ways in which this action can be performed: +Contributing to network security by delegating 1 EGLD or more toward a staking provider that operates validator nodes. -- If the user has access to the current Guardian the change can be done immediately by signing a transaction; -- If the user doesn’t have access to the current Guardian the process implies a 20-day cooldown period before the change of Guardian takes place. +Context: As a day-to-day action for a token holder, staking here means placing EGLD with a provider rather than running a node. The funds accrue rewards, and withdrawing them requires unstaking and then waiting out the unbonding period. +See also: [Staking](#staking), [Delegate](#delegate), [Stake rewards](#stake-rewards), [Unstake](#unstake). +Read more: [docs.multiversx.com/validators/delegation-manager](https://docs.multiversx.com/validators/delegation-manager/). -## Why do I have to wait 20 days to activate or change an Invisible Guardian? +--- -The 20-day cooldown period is designed to safeguard staked funds and relieve pressure on users. -Given that it might take longer than 10 days to safely transfer funds to a secure account, we have extended this period by an additional 10 days. -This not only allows the user ample time to react if their account is compromised, but it also provides a buffer for the user to counteract potential threats. +### Delegate +Contributing to network security by delegating 10 EGLD or more toward the MultiversX Community Delegation contract. -## What implications should I be aware of before activating an Invisible Guardian on my account? +Context: Delegating pools a holder's EGLD into a delegation contract that runs the nodes and takes a service fee from the rewards. It lets a holder earn staking rewards without meeting the 2500 EGLD per-node minimum or operating hardware. -Aside from the extra security and the peace of mind that goes hand in hand with activating an Invisible Guardian on your account, there are a few things everyone should be aware of beforehand: -- You will only be able to process transactions from xPortal or by logging in with xPortal -- You won’t be able to import your secret phrase into other wallet apps +See also: [Delegation](#delegation), [Stake](#stake), [Delegate rewards](#delegate-rewards), [Service Fee](#service-fee). +Read more: [docs.multiversx.com/validators/delegation-manager](https://docs.multiversx.com/validators/delegation-manager/). +--- -## What if something happens to my device or I change it? +### Stake rewards -The Invisible Guardian is stored locally on your device, but the first step of setting it up is creating an encrypted backup containing the Guardian. This makes importing it on a different device as easy as it gets, all you need to do is to provide the password associated with the guarded backup. +The income generated by funds locked to run validator nodes, which can be claimed or restaked. +Context: Rewards accrue to staked EGLD from protocol emission and a share of fees. A staker can withdraw the rewards or compound them back into stake; the yield rate is expressed as an APR that depends on total network stake and top-up. -## What happens if my secret phrase is compromised? +See also: [Stake](#stake), [APR (Annual Percentage Rate)](#apr-annual-percentage-rate), [Delegate rewards](#delegate-rewards), [Staking](#staking). +Read more: [docs.multiversx.com/economics/staking-providers-apr](https://docs.multiversx.com/economics/staking-providers-apr/). -If your secret phrase is compromised and the Invisible Guardian is active, whoever has access to the secret phrase will be unable to process any transactions from your account. They will be able though to request the change of the Guardian. This process lasts 20 days. +--- -You will be able to cancel any request of this kind, but our recommendation is to transfer all your assets (staked and unstaked) to a new wallet. +### Delegate rewards +The income generated by funds placed in delegation contracts, which can be claimed or redelegated. -## How would I know if my account is compromised? +Context: A delegator receives rewards net of the provider's service fee and can either withdraw them or redelegate to compound. This is the delegation counterpart to stake rewards. -A red warning message is displayed in your xPortal account every time a Guardian is enabled, changed or disabled. If it was you who processed this change, you can confirm that the action was performed by you and the message disappears. +See also: [Delegate](#delegate), [Delegation](#delegation), [Service Fee](#service-fee), [Stake rewards](#stake-rewards). +Read more: [docs.multiversx.com/economics/staking-providers-apr](https://docs.multiversx.com/economics/staking-providers-apr/). -If the action was not performed by you, we strongly recommend moving all your assets to a new wallet within 20 days. +--- -Also, to ensure maximum protection we strongly recommend you access your account periodically. +### Unstake ---- +Signaling the intention to unlock staked or delegated funds, which become available after the 10 epoch unbonding period. -## Terminology -### Terminology +Context: Unstaking starts the exit but does not release funds immediately; the EGLD stays locked for the unbonding period before it can be withdrawn. The final withdrawal step is unbonding. -**Metachain**: the blockchain that runs in a special shard, where the main responsibilities are not processing transactions, -but notarizing and finalizing the processed shard block headers. +See also: [Unbond](#unbond), [Unbonding Period](#unbonding-period), [Stake](#stake), [Delegation](#delegation). +Read more: [docs.multiversx.com/validators/staking](https://docs.multiversx.com/validators/staking/). -**Address**: the public key of a wallet. The MultiversX Address format is bech32, specified by the BIP 0173. -The public key always starts with an `erd1`. e.g.: `erd1sea63y47u569ns3x5mqjf4vnygn9whkk7p6ry4rfpqyd6rd5addqyd9lf2`. +--- -**Node**: a computer or server, running the MultiversX client and relaying messages received from its peers. +### Staking provider -**Validator**: a node on the MultiversX network that staked at least 2500 EGLD, that processes transactions and secures -the network by participating in the consensus mechanism, while earning rewards from the protocol and transaction fees. +A node operator that created a staking pool and accepts funds for staking. -**Observer**: a passive member of the network that can act as a read & relay interface. +Context: A staking provider runs a delegation contract that pools delegators' EGLD, operates the validator nodes, and takes a service fee from the rewards. Holders choose a provider to delegate to based on its fee and reliability. -**Validate**: the act of running a validator node and contributing to the network by relaying and -validating information. +See also: [Staking pool](#staking-pool), [Delegation Contract](#delegation-contract), [Delegate](#delegate), [Service Fee](#service-fee). +Read more: [docs.multiversx.com/validators/delegation-manager](https://docs.multiversx.com/validators/delegation-manager/). -**Stake**: contribute to the network security by delegating 1 EGLD or more towards a staking provider that operates -validator nodes. +--- -**Delegate**: contribute to the network security by delegating 10 EGLD or more towards the MultiversX Community Delegation -contract. +### Staking pool -**Stake rewards**: locked funds for running validator nodes to secure the network and generate income in form of rewards. -Rewards can be claimed or restaked. +A system smart contract that accepts funds for staking. -**Delegate rewards**: locked funds from delegation contracts also generate income in form of rewards. -These rewards can be claimed or redelegated +Context: The staking pool is the delegation contract that holds a provider's delegated EGLD and stakes it across the provider's nodes. Its size is bounded by a delegation cap. -**Unstake**: the intention to unlock staked/delegated funds, that will become available after the 10 epoch unbonding period. +See also: [Staking provider](#staking-provider), [Delegation Contract](#delegation-contract), [Delegation cap](#delegation-cap), [System Smart Contracts](#system-smart-contracts). +Read more: [docs.multiversx.com/validators/delegation-manager](https://docs.multiversx.com/validators/delegation-manager/). -**Unbond**: withdraw the funds in the original account after the 10 epoch unbonding period. +--- -**Staking provider**: node operator that created a staking pool and accepts funds for staking. +### Delegation cap -**Staking pool**: a system smart contract that accepts funds for staking. +The maximum amount of funds a staking pool accepts. -**Delegation cap**: the maximum amount of funds accepted by a staking pool. +Context: The delegation cap is set by the pool's owner and limits total delegated EGLD; a cap of zero means unlimited. The initial owner deposit and all delegated funds count toward it. -**APR**: annual percentage rate. +See also: [Staking pool](#staking-pool), [Delegation Contract](#delegation-contract), [Delegate](#delegate), [Staking provider](#staking-provider). +Read more: [docs.multiversx.com/validators/delegation-manager](https://docs.multiversx.com/validators/delegation-manager/). -**Service fee**: fee that the service providers are taking from the rewards their staking pools have received. +--- --- diff --git a/static/llms.txt b/static/llms.txt index 9bc16333a..74fa680a3 100644 --- a/static/llms.txt +++ b/static/llms.txt @@ -28,6 +28,12 @@ This documentation is organized into major sections. Each section includes tutor - [accountsesdt](https://docs.multiversx.com/sdk-and-tools/indices/accountsesdt): Elasticsearch accountsesdt index: per-address ESDT/NFT balances, identifier composition and token metadata. - [accountsesdthistory](https://docs.multiversx.com/sdk-and-tools/indices/accountsesdthistory): Elasticsearch accountsesdthistory index: historical changes for address ESDT/NFT balances with timestamps. - [accountshistory](https://docs.multiversx.com/sdk-and-tools/indices/accountshistory): Elasticsearch accountshistory index: historical snapshots of account state (balance, nonce, shard, timestamp). +- [Add a wallet login button](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/wallets/wallet-login-button): Configure UnlockPanelManager once at startup, then add a reusable connect and disconnect button that works with every registered wallet provider. [beginner, verified 2026-07-16] +- [Address utilities](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/accounts/address-utilities): The sdk-core Address toolkit, bech32/hex conversion, raw public key, shard computation, isSmartContract, and the HRP. Fully offline, no network. [beginner, verified 2026-07-16] +- [Agent or agentic engineer? Start here](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/agents): Two ways into the MultiversX cookbook. Developers copy recipes by hand; coding agents read the same CI-verified recipes from a machine-readable surface. +- [Apply a guardian to a transaction and co-sign it](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/accounts/apply-guardian-to-transaction): Turn any transaction into a guarded one with TransactionComputer.applyGuardian and co-sign it, asserting version, options, and guardian fields on devnet. [advanced, verified 2026-07-16] +- [Await a transaction on a custom condition](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/network-providers/await-transaction-on-condition): Block until a transaction matches a predicate you write with awaitTransactionOnCondition, tuning the poll interval and timeout via AwaitingOptions. [intermediate, verified 2026-07-16] +- [Await an account on a custom condition](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/network-providers/await-account-on-condition): Block until an account matches a predicate you write with awaitAccountOnCondition, tuning the poll interval and timeout via AwaitingOptions. [intermediate, verified 2026-07-16] - [Basics](https://docs.multiversx.com/developers/best-practices/best-practices-basics): Core coding practices for safe, efficient MultiversX smart contracts: structure, readability and testing. - [BigUint Operations](https://docs.multiversx.com/developers/best-practices/biguint-operations): Best practices for BigUint arithmetic: performance, safety, overflow handling and gas considerations. - [Blackbox calls](https://docs.multiversx.com/developers/testing/rust/sc-blackbox-calls): Blackbox testing API: run transactions, queries, deploys and upgrades with ScenarioWorld and typed proxies. @@ -35,25 +41,39 @@ This documentation is organized into major sections. Each section includes tutor - [blocks](https://docs.multiversx.com/sdk-and-tools/indices/blocks): Elasticsearch blocks index: block hash, header fields and example queries. - [Build a dApp in 15 minutes](https://docs.multiversx.com/developers/tutorials/your-first-dapp): Tutorial: Build a dApp in 15 minutes - [Build a Microservice for your dApp](https://docs.multiversx.com/developers/tutorials/your-first-microservice): Tutorial: Build a Microservice for your dApp +- [Build a relayed v3 transaction](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/accounts/relayed-v3-transaction): A sender who has no EGLD for gas, and a relayer who pays it, both signing the same transaction, devnet-verified, with V1/V2 deprecation flagged. [advanced, verified 2026-07-16] - [Build Reference](https://docs.multiversx.com/developers/meta/sc-build-reference): How the smart‑contract build works: meta crate, ABI generation, wasm crate code, multicontract config and CLI commands. - [Built-In Functions](https://docs.multiversx.com/developers/built-in-functions): Protocol built‑in functions called via transactions: developer rewards, ownership change, usernames, ESDT/NFT ops and guardian controls. - [C++ SDK](https://docs.multiversx.com/sdk-and-tools/erdcpp): C++ SDK: helpers and utilities for interacting with MultiversX; repositories and docs. +- [Call a contract endpoint with native JS args (NativeSerializer)](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-contract-endpoint): Call a mutable contract endpoint with plain JS arguments, auto-converted to typed values by sdk-core's NativeSerializer, verified against live devnet. [intermediate, verified 2026-07-16] +- [Call a payable endpoint with EGLD](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/call-payable-endpoint): Call a payable contract endpoint, attaching EGLD via nativeTransferAmount, against a real live devnet contract with sdk-core's controller pattern. [intermediate, verified 2026-07-16] - [Chain simulator](https://docs.multiversx.com/sdk-and-tools/chain-simulator): Chain Simulator: run local blockchain simulations for testing with configurable components. - [Chain Simulator in Adder - SpaceCraft interactors](https://docs.multiversx.com/developers/tutorials/chain-simulator-adder): Tutorial: Chain Simulator in Adder - SpaceCraft interactors +- [Claim and re-delegate rewards](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/delegation/claim-and-redelegate-rewards): Claim delegation rewards to your wallet or re-delegate (compound) them into the same MultiversX staking contract, with sdk-core DelegationController. [intermediate, verified 2026-07-16] - [CLI](https://docs.multiversx.com/developers/meta/sc-meta-cli): CLI for sc‑meta (standalone and per‑contract): commands, the ‘all’ mode, upgrade flow and helper options. - [Code Metadata](https://docs.multiversx.com/developers/data/code-metadata): Smart‑contract code metadata flags (upgradeable, readable, payable) — usage, CLI switches and bit‑flag layout. - [Composite Values](https://docs.multiversx.com/developers/data/composite-values): How composite values are encoded/decoded: managed buffers, vectors, token identifiers and nested structures. +- [Compute a contract address before deploy](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/compute-contract-address): Predict a smart contract's address before deploying it from the deployer address and deployment nonce with sdk-core's AddressComputer, fully offline. [beginner, verified 2026-07-16] - [Concept](https://docs.multiversx.com/developers/testing/scenario/concept): Concept of scenario‑based testing: composing steps to simulate blockchain interactions for contracts. - [Configuration](https://docs.multiversx.com/developers/meta/sc-config): Configure MultiversX smart contract projects: build variants, features, and multi‑contract settings. +- [Configure a network provider (Api vs Proxy)](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/network-providers/configure-network-provider): Construct an ApiNetworkProvider or ProxyNetworkProvider, tune clientName and timeout, or get one from an entrypoint. Both share one INetworkProvider interface. [beginner, verified 2026-07-16] - [Constants](https://docs.multiversx.com/developers/constants): Protocol constants and defaults referenced throughout MultiversX docs: timings, sizes and network parameters. - [Cookbook (v14)](https://docs.multiversx.com/sdk-and-tools/sdk-js/sdk-js-cookbook-v14): sdk‑js Cookbook (v14): common tasks, entrypoints, API/proxy usage and migration notes. - [Cookbook (v15)](https://docs.multiversx.com/sdk-and-tools/sdk-js/sdk-js-cookbook-v15): sdk‑js Cookbook (v15): common tasks, entrypoints, API/proxy usage and migration notes. - [Core Logic](https://docs.multiversx.com/developers/tutorials/crowdfunding/crowdfunding-p2): Tutorial: Core Logic +- [Create a delegation contract](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/delegation/create-delegation-contract): Create a new MultiversX delegation (staking-provider) contract with sdk-core DelegationController and factory, then parse the outcome for its address. [advanced, verified 2026-07-16] +- [Create a governance proposal](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/governance/create-proposal): Create a MultiversX governance proposal with sdk-core's GovernanceController and factory, reading the live proposal fee from getConfig first. [advanced, verified 2026-07-16] +- [Create an Account from a KeyPair, secret key, or mnemonic](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/accounts/account-from-keys): Build an sdk-core Account from a KeyPair, a raw secret key, or a mnemonic, fully offline, and learn which named constructors are sync versus async. [beginner, verified 2026-07-16] - [Creating Wallets](https://docs.multiversx.com/developers/creating-wallets): Create MultiversX wallets from CLI or code: generate mnemonics, derive keys and set up keystores or Ledger. +- [Custom API/Proxy request](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/network-providers/custom-api-request): Call any API or Proxy endpoint the typed provider methods do not cover, via doGetGeneric and doPostGeneric, for economics, stats, and a raw VM query. [intermediate, verified 2026-07-16] - [Custom Types](https://docs.multiversx.com/developers/data/custom-types): Define serializable custom types (structs/enums) for contracts using TopEncode/Decode and NestedEncode/Decode. +- [Decode contract events](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/decode-contract-events): Decode a smart contract's emitted events into named typed fields with sdk-core's TransactionEventsParser and an ABI, verified against a real event. [intermediate, verified 2026-07-16] +- [Decode contract return data (ABI codec)](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/decode-return-data): Decode raw contract return data and encode or decode custom struct and enum types with sdk-core's BinaryCodec, getStruct, and getEnum, fully offline. [advanced, verified 2026-07-16] - [Defaults](https://docs.multiversx.com/developers/data/defaults): Defaults in serialization and storage: built‑in defaults, types without defaults and defining custom defaults. +- [Delegate (stake) EGLD](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/delegation/delegate-stake): Delegate (stake) EGLD to an existing MultiversX delegation contract with sdk-core DelegationController and factory, then parse the staked amount. [intermediate, verified 2026-07-16] - [delegators](https://docs.multiversx.com/sdk-and-tools/indices/delegators): Elasticsearch delegators index: delegation records per provider and delegator, staked amounts and timestamps. - [Deploy a SC in 5 minutes - SpaceCraft interactors](https://docs.multiversx.com/developers/tutorials/interactors-guide): Tutorial: Deploy a SC in 5 minutes - SpaceCraft interactors +- [Deploy a smart contract](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/deploy-contract): Deploy a smart contract from WASM bytecode and constructor arguments with sdk-core's controller and factory, then parse the outcome for the new address. [intermediate, verified 2026-07-16] - [Devcontainers](https://docs.multiversx.com/sdk-and-tools/devcontainers): Devcontainers for MultiversX development in VS Code: prebuilt environments with Rust, mxpy and tools. - [Developers - Overview](https://docs.multiversx.com/developers/overview): Developer landing page: smart contracts, transactions, SDKs, tools, tutorials and where to get help. - [DEX Walkthrough](https://docs.multiversx.com/developers/tutorials/dex-walkthrough): Tutorial: DEX Walkthrough @@ -70,44 +90,72 @@ This documentation is organized into major sections. Each section includes tutor - [Execution Events](https://docs.multiversx.com/developers/event-logs/execution-events): Execution‑level events: completedTxEvent, signal and refund notifications with structures and examples. - [Extend to Any Token](https://docs.multiversx.com/developers/tutorials/crowdfunding/crowdfunding-p3): Tutorial: Extend to Any Token - [Extending sdk-js](https://docs.multiversx.com/sdk-and-tools/sdk-js/extending-sdk-js): Extend sdk‑js: customize network providers and modules to fit your dApp requirements. +- [Fetch a block](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-a-block): Fetch the latest block and a block by hash via ApiNetworkProvider, and a block by shard and nonce via ProxyNetworkProvider. Verified live against mainnet. [intermediate, verified 2026-07-16] +- [Fetch an account's on-chain state](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/accounts/fetch-account-state): Read any address's nonce, balance, username, and guarded flag, plus its key-value storage, through a network provider. Verified live against mainnet. [beginner, verified 2026-07-16] +- [Fetch an account's token balances](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/tokens/fetch-account-token-balances): Read the tokens an account holds, all fungible ESDTs, all NFTs and SFTs, and one specific token balance, via a network provider. Verified live. [intermediate, verified 2026-07-16] +- [Fetch network config and status](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/network-providers/fetch-network-config-status): Read the static network config (gas costs, shard count, round duration) and the live per-shard status (block nonce, epoch, round) via a network provider. [beginner, verified 2026-07-16] +- [Fetch token metadata](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/tokens/fetch-token-metadata): Read a token's definition, name, ticker, decimals, owner and property flags, for a fungible token and for an NFT or SFT collection. Verified live. [intermediate, verified 2026-07-16] - [Final Code](https://docs.multiversx.com/developers/tutorials/crowdfunding/final-code): Tutorial: Final Code - [Fix IDEs configuration](https://docs.multiversx.com/sdk-and-tools/troubleshooting/ide-setup): Troubleshooting IDE setup for MultiversX development (common configuration fixes). - [Fix Rust installation](https://docs.multiversx.com/sdk-and-tools/troubleshooting/rust-setup): Troubleshooting Rust toolchain setup for MultiversX (installation and environment fixes). - [Fungible tokens](https://docs.multiversx.com/tokens/fungible-tokens): ESDT fungible tokens: issuance, configuration properties, permissions, and on‑chain usage. - [Gas](https://docs.multiversx.com/developers/transactions/tx-gas): Gas handling in transactions across environments: defaults, explicit gas and examples for interactor/tests. - [Gateway overview](https://docs.multiversx.com/sdk-and-tools/rest-api/gateway-overview): Gateway REST API: endpoints, routing through proxy and interaction patterns for dApps and services. +- [Generate a mnemonic + derive keys](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/accounts/generate-mnemonic-derive-keys): Generate a BIP39 mnemonic with sdk-core and derive secret keys at several address indices, fully offline, with a determinism check and secrecy notes. [beginner, verified 2026-07-16] - [Generating scenarios](https://docs.multiversx.com/developers/testing/scenario/generating-scenarios): Ways to generate scenarios (blackbox/whitebox/interactors/manual) and reuse them across Go and Rust VMs. - [Go SDK](https://docs.multiversx.com/sdk-and-tools/sdk-go): Go SDK: libraries and utilities for interacting with MultiversX (transactions, accounts, network access). - [Google BigQuery](https://docs.multiversx.com/sdk-and-tools/google-bigquery): Google BigQuery: access and analyze MultiversX blockchain datasets with SQL. - [Guard accounts](https://docs.multiversx.com/developers/guard-accounts): Guarded (co‑signed) transactions: how guardians work, setup, and security considerations. +- [Guard and unguard an account](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/accounts/guard-unguard-account): Activate guardianship with GuardAccount and remove it with UnGuardAccount via sdk-core, including the guardian co-sign requirement, verified on devnet. [intermediate, verified 2026-07-16] - [Guardians](https://docs.multiversx.com/sdk-and-tools/sdk-dapp/internal-processes/guardians): Guardians: 2FA/guardian flows in MultiversX dApps — concepts, processes and integration notes. +- [Hash-signing a transaction](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/accounts/hash-signing-transaction): Opt a transaction into hash signing with sdk-core, assert the version/options bits, and sign the hash correctly. Includes a real v15.4.1 signing trap. [advanced, verified 2026-07-16] - [How to fix Elasticsearch mapping errors](https://docs.multiversx.com/sdk-and-tools/elastic-search-wrong-mappings-fix): Fix Elasticsearch mapping errors: common causes and steps to correct wrong mappings for MultiversX indices. - [Installing mxpy](https://docs.multiversx.com/sdk-and-tools/mxpy/installing-mxpy): Install mxpy via pipx; verify version and set up environment. - [Interactors Example](https://docs.multiversx.com/developers/meta/interactor/interactors-example): Interactor example: scaffold a project, configure endpoints, and run calls against a contract. - [Interactors Overview](https://docs.multiversx.com/developers/meta/interactor/interactors-overview): Interactors: structure, generation and usage for end‑to‑end smart contract execution and system testing. - [Intro to ESDT](https://docs.multiversx.com/tokens/intro): Introduction to ESDT token standards on MultiversX: fungible, non‑fungible and semi‑fungible basics. +- [Issue a fungible ESDT](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/tokens/issue-fungible-token): Issue a fungible ESDT token with sdk-core's TokenManagementController against DevnetEntrypoint, payload hand-verified against real devnet. [intermediate, verified 2026-07-16] +- [Issue an NFT collection + mint an NFT](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/tokens/issue-nft-collection): Issue an NFT collection and mint an NFT into it with sdk-core's TokenManagementController, every field hand-verified against real devnet. [intermediate, verified 2026-07-16] +- [Issue an SFT collection + create, add, and burn quantity](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/tokens/issue-sft-collection): Issue a semi-fungible (SFT) collection, create an SFT batch, then add and burn quantity with sdk-core's TokenManagementController on devnet. [intermediate, verified 2026-07-16] - [Iterate keys](https://docs.multiversx.com/sdk-and-tools/rest-api/iterate-keys): Iterate account storage keys via /address/iterate-keys using batching and iterator state (optional block nonce pinning). - [Java SDK](https://docs.multiversx.com/sdk-and-tools/mxjava): Java SDK: helpers and utilities for interacting with MultiversX; repository and getting started. - [JSON Structure](https://docs.multiversx.com/developers/testing/scenario/structure-json): Structure of Mandos .scen.json files: top‑level fields, step types and validation semantics with examples. - [Kotlin SDK](https://docs.multiversx.com/sdk-and-tools/erdkotlin): Kotlin SDK: libraries and helpers for MultiversX integration; repository and documentation links. - [Legacy SC calls](https://docs.multiversx.com/developers/transactions/tx-legacy-calls): Deprecated legacy contract call syntax retained for backward compatibility; prefer the unified transaction API. +- [Load an ABI](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/load-abi): Load a contract's ABI from a file, a URL, or by hand with sdk-core's Abi/AbiRegistry, then introspect its endpoints and argument types. [beginner, verified 2026-07-16] +- [Local HTTPS for dApp dev (mkcert + Vite + Next.js)](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/start-here/local-https-setup): Wallet providers refuse to connect over plain HTTP. Set up trusted HTTPS for localhost using mkcert with Vite, Next.js, or framework-agnostic plain Node. [beginner, verified 2026-07-16] +- [Local mint and burn (change a fungible token's supply)](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/tokens/local-mint-burn-supply): Increase and decrease a fungible ESDT's circulating supply with ESDTLocalMint and ESDTLocalBurn via sdk-core, plus a real SDK method-name typo. [intermediate, verified 2026-07-16] +- [Log in with a Ledger hardware wallet](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/wallets/ledger-login): A dedicated Ledger login button using ProviderFactory directly, including the anchor element the SDK renders its own device-connect and account-picker UI into. [intermediate, verified 2026-07-16] +- [Log in with the DeFi Wallet browser extension](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/wallets/defi-extension-login): A dedicated single-provider login button using ProviderFactory directly, with real extension detection and no generic multi-provider picker UI. [intermediate, verified 2026-07-16] +- [Log in with xPortal via WalletConnect](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/wallets/walletconnect-login): A dedicated xPortal login button using ProviderFactory and WalletConnect v2 directly, including the QR-code anchor element and project ID setup. [intermediate, verified 2026-07-16] - [logs](https://docs.multiversx.com/sdk-and-tools/indices/logs): Elasticsearch logs index: transaction and SC result logs with topics and data payloads. +- [Manage nonces (fetch-then-increment)](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/accounts/manage-nonces): Fetch an account's nonce from the network once, then increment it locally for every transaction sent afterward in a batch, plus recovery from a nonce gap. [intermediate, verified 2026-07-16] - [Managed Decimal](https://docs.multiversx.com/developers/best-practices/managed-decimal): Working with managed decimal arithmetic: precision, rounding, scaling and gas‑efficient patterns. - [Memory allocation](https://docs.multiversx.com/developers/meta/sc-allocator): Allocator options for MultiversX smart contracts: when to avoid heap usage, alternatives and trade‑offs. - [Messages](https://docs.multiversx.com/developers/developer-reference/sc-messages): Smart contract messaging: emitting events and composing messages for inter‑contract communication. +- [Migrate sdk-dapp v4 to v5: hook-by-hook diffs](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/start-here/v4-to-v5-migration): Side-by-side v4 vs v5 diffs for every removed hook, component, and import path. The shortest viable upgrade path for an existing v4 dApp. [intermediate, verified 2026-07-16] - [Migration](https://docs.multiversx.com/developers/transactions/tx-migration): Migrate to unified call syntax and new proxies: compatibility notes, caveats and recommended imports. +- [Migration: DappProvider to initApp, side-by-side](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/migration/dapp-provider-to-initapp): The full-app version of the DappProvider removal, every file that touches it, including the session-restore state v4 quietly handled for you. [intermediate, verified 2026-07-16] +- [Migration: useGetAccountInfo v4 vs v5](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/migration/get-account-info-v4-vs-v5): The same hook name still resolves in v5 but returns less data, silently. The easiest migration bug to miss because nothing forces a second look at it. [intermediate, verified 2026-07-16] +- [Migration: useSendTransactions to TransactionManager.send](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/migration/send-transactions-to-transaction-manager): The v4 hook this migration is named for does not exist in the real package. This recipe covers what does, batch sends and a v5 nested-array trigger. [intermediate, verified 2026-07-16] - [miniblocks](https://docs.multiversx.com/sdk-and-tools/indices/miniblocks): Elasticsearch miniblocks index: miniblock documents linking blocks and grouped transactions across shards. +- [Minimal sdk-dapp v5 app in Next.js (App Router)](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/start-here/nextjs-minimal): A working Next.js 14 App Router starter with sdk-dapp v5. Client-only init wrapper, the real next.config.js, login button, account hook, compiles strict. [beginner, verified 2026-07-16] +- [Minimal sdk-dapp v5 app in Vite + React](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/start-here/vite-react-minimal): The smallest working sdk-dapp v5 setup in a fresh Vite + React + TypeScript project. Compiles strict on the first try, with HTTPS dev built in. [beginner, verified 2026-07-16] +- [Multi-token transfer in one tx](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/transactions/multi-token-transfer): Send EGLD plus two or more ESDT/NFT/SFT tokens in one transaction with sdk-core's TransfersController, every field decoded against real devnet. [intermediate, verified 2026-07-16] - [Multi-Values](https://docs.multiversx.com/developers/data/multi-values): Multi‑value types and argument patterns: optional values, var‑args, tuples, counted lists and async call results. - [MultiversX API](https://docs.multiversx.com/sdk-and-tools/rest-api/multiversx-api): Public MultiversX API: cached wrapper over Gateway with Elasticsearch support, staking/delegation data, media handling and rate limits. - [MultiversX API WebSocket](https://docs.multiversx.com/sdk-and-tools/rest-api/ws-subscriptions): WebSocket Subscription API: real‑time blockchain streams (pulse and filtered), endpoints, payloads and client examples. +- [MultiversX SDK cookbook](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook): Atomic, copy-paste recipes for building on MultiversX with the JavaScript, TypeScript, and Rust SDKs. Every recipe's code is extracted and type-checked in CI, so what you copy is what compiles. - [MultiversX Smart Contracts](https://docs.multiversx.com/developers/smart-contracts): Smart contracts on MultiversX: Rust framework, testing, deployment and developer workflow. - [MultiversX Smart Contracts API limits](https://docs.multiversx.com/developers/contract-api-limits): Protocol limits on smart‑contract blockchain data calls: per‑transaction caps on transfers, trie reads and built‑in calls (Polaris). - [MultiversX tools on multiple platforms](https://docs.multiversx.com/sdk-and-tools/troubleshooting/multiplatform): Running MultiversX tools on multiple platforms: platform‑specific notes and fixes. - [mxpy CLI cookbook](https://docs.multiversx.com/sdk-and-tools/mxpy/mxpy-cli): mxpy CLI cookbook: common commands for accounts, transactions, contracts and localnets. +- [Native auth, token issuance, expiry, auto-logout](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/wallets/native-auth): How nativeAuth issues a bearer token on login, why loginExpiresAt is not the token's real expiry, and how LogoutManager schedules the warning toast and logout. [intermediate, verified 2026-07-16] - [NestJS SDK](https://docs.multiversx.com/sdk-and-tools/sdk-nestjs/sdk-nestjs): NestJS SDK: utilities and modules for building MultiversX‑powered backend services. - [NestJS SDK Auth utilities](https://docs.multiversx.com/sdk-and-tools/sdk-nestjs/sdk-nestjs-auth): NestJS SDK Auth utilities: authentication helpers and patterns for MultiversX integrations. - [NestJS SDK Cache utilities](https://docs.multiversx.com/sdk-and-tools/sdk-nestjs/sdk-nestjs-cache): NestJS SDK Cache utilities: caching strategies and modules for MultiversX data. - [NestJS SDK Monitoring utilities](https://docs.multiversx.com/sdk-and-tools/sdk-nestjs/sdk-nestjs-monitoring): NestJS SDK Monitoring utilities: health checks, metrics and observability for MultiversX services. +- [New contract from sc-meta new --template empty](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/new-contract-from-template): The real, unedited output of sc-meta new --template empty, then the smallest real customization on top, with every command actually run and captured. [intermediate, verified 2026-07-16] - [NFT & SFT tokens](https://docs.multiversx.com/tokens/nft-tokens): NFTs and SFTs on MultiversX: issuance, roles, properties, transfer mechanics and branding best practices. - [operations](https://docs.multiversx.com/sdk-and-tools/indices/operations): Elasticsearch operations index: unified records for transactions and SC results (type, hashes, status, participants). - [Overview](https://docs.multiversx.com/developers/testing/rust/sc-test-overview): Overview of writing Rust tests: ScenarioWorld facade, registering contracts and common state helper functions. @@ -116,12 +164,17 @@ This documentation is organized into major sections. Each section includes tutor - [Payload (data)](https://docs.multiversx.com/developers/transactions/tx-data): Data payload format: function names, argument encoding and examples for SC calls and built‑ins. - [Payments](https://docs.multiversx.com/developers/transactions/tx-payment): Token payments in transactions: EGLD and ESDT multi‑payments, rules and examples. - [Preparing SCs for Supernova](https://docs.multiversx.com/developers/best-practices/prepare-sc-supernova): Checklist to prepare smart contracts for the Supernova upgrade: timing changes, assumptions and safe migrations. +- [Propose a multisig action](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/multisig/propose-action): Propose a multisig action, a transfer-execute or an add-board-member, with sdk-core's MultisigController and factory, then parse the new action id. [advanced, verified 2026-07-16] - [Proxies](https://docs.multiversx.com/developers/transactions/tx-proxies): Smart‑contract proxies: generated call interfaces, how to generate/configure them and use in projects. - [Proxy architecture](https://docs.multiversx.com/sdk-and-tools/proxy): Gateway (Proxy) architecture: responsibilities, endpoints, deployment and interaction with nodes and API. - [Python SDK](https://docs.multiversx.com/sdk-and-tools/sdk-py): Python SDK: libraries and tooling to interact with MultiversX (accounts, transactions, contracts). +- [Query a read-only view](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/smart-contracts-call/query-contract-view): Query a smart contract's read-only view with sdk-core's SmartContractController, no wallet, no gas, no transaction, verified against live devnet. [beginner, verified 2026-07-16] - [Random Numbers in Smart Contracts](https://docs.multiversx.com/developers/developer-reference/sc-random-numbers): Randomness in MultiversX smart contracts: sources, limitations, verifiability and recommended patterns. - [rating](https://docs.multiversx.com/sdk-and-tools/indices/rating): Elasticsearch rating index: validator rating snapshots and component scores per epoch/shard. - [React Development](https://docs.multiversx.com/developers/guidelines/react-development): Team React code style and practices: branching, imports/exports, conditionals, function args and validation patterns. +- [Read a delegation contract's state](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/delegation/query-delegation-contract): Read a MultiversX staking-provider contract state with read-only queries: total stake, service fee, a delegator active stake and claimable rewards. [intermediate, verified 2026-07-16] +- [Read a multisig's state](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/multisig/read-multisig-state): Read a MultiversX multisig's quorum, board members, and pending actions with their signers using sdk-core's MultisigController, no wallet needed. [intermediate, verified 2026-07-16] +- [Read the connected account with useGetAccount](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/wallets/read-connected-account): Every AccountType field explained, which ones are optional, how to format the balance correctly, and when to reach for useGetAccountInfo instead. [beginner, verified 2026-07-16] - [receipts](https://docs.multiversx.com/sdk-and-tools/indices/receipts): Elasticsearch receipts index: execution receipts for transactions/SC results, fees, gas usage and outcomes. - [Receiver](https://docs.multiversx.com/developers/transactions/tx-to): Transaction receiver field: addressing accounts vs contracts, expected formats and routing rules. - [Relayed Transactions](https://docs.multiversx.com/developers/relayed-transactions): Relayed transactions v1–v3: relayer address/signature fields, gas computation, examples and SDK support. @@ -139,6 +192,8 @@ This documentation is organized into major sections. Each section includes tutor - [Running scenarios](https://docs.multiversx.com/developers/testing/scenario/running-scenarios): Run .scen.json scenarios via the run‑scenarios tool or generated Rust tests against Go and Rust VMs. - [Rust SDK](https://docs.multiversx.com/sdk-and-tools/sdk-rust): Rust SDK (Interactors): write typed blockchain interactions mirroring contract/test syntax; tutorials and references. - [Rust Version](https://docs.multiversx.com/developers/meta/rust-version): Supported Rust toolchains for the framework: stable vs nightly, version ranges and known issues. +- [Save and load a keystore (encrypted JSON)](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/accounts/keystore-save-load): Save a wallet to an encrypted keystore and load it back with a password, for both secret-key and mnemonic keystores, with sdk-core. Fully offline. [beginner, verified 2026-07-16] +- [Save and load a PEM (dev only)](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/accounts/pem-save-load): Save a wallet to a PEM file and load it back via Account and UserPem with sdk-core. PEM is unencrypted and dev-only; fully offline, no network. [beginner, verified 2026-07-16] - [SC to SC Calls](https://docs.multiversx.com/developers/developer-reference/sc-to-sc-calls): Patterns for smart contract‑to‑smart contract calls: synchronous vs asynchronous flows and safety tips. - [scdeploys](https://docs.multiversx.com/sdk-and-tools/indices/scdeploys): Elasticsearch scdeploys index: smart‑contract deploy/upgrade entries, code hashes and related metadata. - [Scenario Complex Values](https://docs.multiversx.com/developers/testing/scenario/values-complex): Complex scenario values: concatenation with pipes, JSON lists/maps, tuples and nested encoding notes. @@ -147,14 +202,23 @@ This documentation is organized into major sections. Each section includes tutor - [sdk-dapp](https://docs.multiversx.com/sdk-and-tools/sdk-dapp/sdk-dapp): React SDK for MultiversX: components, hooks and providers to build dApps quickly and safely. - [sdk-js](https://docs.multiversx.com/sdk-and-tools/sdk-js/sdk-js): JavaScript/TypeScript SDK: accounts, transactions, signing, smart contracts and utilities. - [SDKs and Tools - Overview](https://docs.multiversx.com/sdk-and-tools/overview): Overview of MultiversX SDKs, tools and APIs for building apps and services. +- [Send an ESDT](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/transactions/send-esdt): Send a fungible ESDT token using sdk-core's TransfersController and the Token/TokenTransfer classes against DevnetEntrypoint, no browser required. [beginner, verified 2026-07-16] +- [Send EGLD to an address](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/transactions/send-egld): Send a native EGLD transfer with sdk-core's TransfersController against DevnetEntrypoint, the backend/script pattern for when your own code holds the key. [beginner, verified 2026-07-16] - [Sender](https://docs.multiversx.com/developers/transactions/tx-from): Transaction sender field: account requirements, nonce handling and permissions when originating calls. +- [Set a guardian on an account](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/accounts/set-guardian): Nominate a guardian for an account with SetGuardian via sdk-core's AccountController and AccountTransactionsFactory, payload verified on devnet. [intermediate, verified 2026-07-16] +- [Set and unset special roles on a fungible ESDT](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/tokens/set-special-roles): Set and unset the local-mint, local-burn, and ESDT-transfer roles on a fungible ESDT with sdk-core, including a confirmed v15.4.1 unset-role bug. [intermediate, verified 2026-07-16] - [Set up a Localnet (mxpy)](https://docs.multiversx.com/developers/setup-local-testnet): Set up a localnet with mxpy: prerequisites, setup/start commands, components and logs. - [Set up a Localnet (raw)](https://docs.multiversx.com/developers/setup-local-testnet-advanced): Manual localnet setup using protocol repositories and scripts: prerequisites, configuration and running nodes. - [Setup & Basics](https://docs.multiversx.com/developers/tutorials/crowdfunding/crowdfunding-p1): Tutorial: Setup & Basics +- [Sign + verify a transaction (offline)](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-transaction): Sign a transaction offline with a raw secret key and verify it three ways with sdk-core, including tamper checks. No devnet, no broadcast. [intermediate, verified 2026-07-16] +- [Sign a message + verify a signature](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/accounts/sign-verify-message): Sign a message with an account or secret key and verify it with a UserVerifier, fully offline, including proof the check fails on a tampered signature. [beginner, verified 2026-07-16] +- [Sign and perform a multisig action](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/multisig/sign-perform-action): Sign a proposed multisig action to quorum and perform it with sdk-core's MultisigController and factory, reading its live signer state first. [advanced, verified 2026-07-16] +- [Sign and send a transaction (the working path)](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/start-here/sign-and-send): The canonical sdk-dapp v5 sign, send, and track flow. provider.signTransactions then TransactionManager.send and .track, with proper nonce handling. [beginner, verified 2026-07-16] - [Signing programmatically](https://docs.multiversx.com/developers/signing-transactions/signing-programmatically): Sign transactions programmatically using the SDKs: build, sign and broadcast flows with code snippets. - [Signing Providers for dApps](https://docs.multiversx.com/sdk-and-tools/sdk-js/sdk-js-signing-providers): Signing providers for dApps: integrate Web Wallet, Ledger and xPortal with sdk‑js in non‑sdk‑dapp setups. - [Signing Transactions](https://docs.multiversx.com/developers/signing-transactions/signing-transactions): Ways to sign MultiversX transactions: wallets, CLI and programmatic approaches, with pros and cons. - [Simple Values](https://docs.multiversx.com/developers/data/simple-values): Encoding of simple values: fixed‑width integers, big numbers and booleans (top‑level vs nested). +- [Simulate and estimate a transaction](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/network-providers/simulate-estimate-transaction): Ask the network what a transaction would cost with estimateTransactionCost and what it would do with simulateTransaction, without ever broadcasting it. [intermediate, verified 2026-07-16] - [Smart contract annotations](https://docs.multiversx.com/developers/developer-reference/sc-annotations): Index of MultiversX smart‑contract annotations (contract, module, proxy, init/upgrade, endpoints) with usage notes. - [Smart Contract API Functions](https://docs.multiversx.com/developers/developer-reference/sc-api-functions): Reference for the MultiversX smart contract API: available functions, parameters and semantics. - [Smart Contract Call Events](https://docs.multiversx.com/developers/event-logs/contract-call-events): Structure and examples of events emitted during smart‑contract calls (identifiers, topics and data). @@ -166,6 +230,7 @@ This documentation is organized into major sections. Each section includes tutor - [Smart contract payments](https://docs.multiversx.com/developers/developer-reference/sc-payments): Receiving and sending payments in contracts: code metadata payable flag, #[payable] endpoints and call‑value helpers. - [Staking smart contract](https://docs.multiversx.com/developers/tutorials/staking-contract): Tutorial: Staking smart contract - [Storage Mappers](https://docs.multiversx.com/developers/developer-reference/storage-mappers): Storage mappers for on‑chain collections: maps, sets, singletons and their performance characteristics. +- [Storage mappers: which to pick, when](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/smart-contracts-rust/storage-mapper-decision-table): A working, tested contract exercising six storage mappers side by side, with a decision table grounded in the real multiversx-sc crate source. [intermediate, verified 2026-07-16] - [System Delegation Events](https://docs.multiversx.com/developers/event-logs/system-delegation-events): Events emitted by the System Delegation contract: identifiers, topics and payload structure. - [System Smart Contracts](https://docs.multiversx.com/developers/gas-and-fees/system-smart-contracts): Gas/fee considerations for System Smart Contracts and references for staking, delegation and ESDT/NFT modules. - [tags](https://docs.multiversx.com/sdk-and-tools/indices/tags): Elasticsearch tags index: categorization labels used by explorer/API to group and filter entities. @@ -175,16 +240,22 @@ This documentation is organized into major sections. Each section includes tutor - [The dynamic allocation problem](https://docs.multiversx.com/developers/best-practices/the-dynamic-allocation-problem): Why to avoid dynamic memory allocation in contracts and use managed types to cut gas and respect VM limits. - [The MultiversX Serialization Format](https://docs.multiversx.com/developers/data/serialization-overview): Serialization in MultiversX: top‑level vs nested encoding, zero values, and per‑type rules with examples. - [Time-related Types](https://docs.multiversx.com/developers/best-practices/time-types): Time and date types in smart contracts: milliseconds vs seconds, conversions and pitfalls post‑Supernova. +- [Token lifecycle — freeze, unfreeze, pause, unpause, wipe](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/tokens/token-lifecycle-operations): Freeze/unfreeze an account's token, pause/unpause globally, and wipe with sdk-core, includes the confirmed v15.4.1 wrong-receiver bug and its fix. [advanced, verified 2026-07-16] - [tokens](https://docs.multiversx.com/sdk-and-tools/indices/tokens): Elasticsearch tokens index: ESDT/NFT collections and tokens—ticker, token ID, issuer, type and status. - [Toolchain Setup](https://docs.multiversx.com/developers/toolchain-setup): Set up the contract toolchain: Rust toolchain, sc‑meta, wasm targets, wasm‑opt and CI/CD notes. - [Tooling Overview](https://docs.multiversx.com/developers/meta/sc-meta): multiversx‑sc‑meta tooling: generate ABIs, code and interactors; build automation for smart contracts. - [Tools for signing](https://docs.multiversx.com/developers/signing-transactions/tools-for-signing): Tools for signing transactions: Web Wallet, Ledger, xPortal and CLI options with typical workflows. +- [Track a transaction (WebSocket + polling fallback)](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/transactions/track-transaction-status): Track a transaction's live status via sdk-dapp's WebSocket-with-polling-fallback tracker and the pending/successful/failed hooks. [intermediate, verified 2026-07-16] - [Transaction Overview](https://docs.multiversx.com/developers/transactions/tx-overview): Transaction structure and lifecycle: fields, validation, execution and result handling. - [transactions](https://docs.multiversx.com/sdk-and-tools/indices/transactions): Elasticsearch transactions index: fields, sample documents and common query patterns. +- [TypeScript strict-mode checklist for sdk-dapp consumers](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/wallets/typescript-strict-mode-checklist): Four real bugs found while verifying cookbook recipes against installed sdk-dapp, plus the tsconfig flags and ESLint rules that catch them. [intermediate, verified 2026-07-16] +- [Undelegate and withdraw](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/delegation/undelegate-and-withdraw): Exit a MultiversX delegation: unDelegate an amount, wait out the unbonding period, then withdraw it, using sdk-core DelegationController and factory. [intermediate, verified 2026-07-16] +- [Upgrade a smart contract](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/smart-contracts-deploy/upgrade-contract): Upgrade a deployed smart contract to new WASM bytecode with sdk-core's controller and factory, and read the upgradeContract builtin wire payload. [intermediate, verified 2026-07-16] - [Upgrading smart contracts](https://docs.multiversx.com/developers/developer-reference/upgrading-smart-contracts): How to upgrade contracts: new upgrade function (v1.6 Sirius), mxpy commands, storage handling and migration tips. - [User-defined Smart Contracts](https://docs.multiversx.com/developers/gas-and-fees/user-defined-smart-contracts): Fees and gas for user‑defined smart‑contract calls: components, computation model and examples. - [validators](https://docs.multiversx.com/sdk-and-tools/indices/validators): Elasticsearch validators index: validator public keys grouped by shard and epoch for consensus. - [Versions and Changelog](https://docs.multiversx.com/sdk-and-tools/rest-api/versions-and-changelog): Proxy API versions and changelog: notable changes, breaking notes and upgrade guidance. +- [Vote and close a proposal](https://docs.multiversx.com/sdk-and-tools/sdk-js/cookbook/governance/vote-close-proposal): Vote on and close a MultiversX governance proposal with sdk-core's GovernanceController and factory, reading a live proposal's tallies first. [advanced, verified 2026-07-16] - [WalletConnect 2.0 Migration](https://docs.multiversx.com/developers/tutorials/wallet-connect-v2-migration): Tutorial: WalletConnect 2.0 Migration - [Whitebox Framework](https://docs.multiversx.com/developers/testing/rust/whitebox-legacy): Legacy white‑box testing framework: structure, setup and scenario generation from Rust tests. - [Whitebox Functions Reference](https://docs.multiversx.com/developers/testing/rust/whitebox-legacy-functions-reference): Reference for BlockchainStateWrapper: state‑checking/getter helpers, account and ESDT/NFT utilities for tests. @@ -287,4 +358,4 @@ This documentation is organized into major sections. Each section includes tutor - [xPortal](https://docs.multiversx.com/wallet/xportal): xPortal mobile app: secure wallet, token swaps, payments, missions, AI avatar creation and upcoming debit card features. ## Terminology -- [Terminology](https://docs.multiversx.com/welcome/terminology): Glossary of MultiversX terms: Metachain, addresses, nodes, validators, staking and more. +- [Terminology](https://docs.multiversx.com/welcome/terminology): A glossary of MultiversX terms across blockchain foundations, protocol and architecture, consensus and security, tokens, nodes and staking, wallets, ecosystem applications, upgrades, and the SDK. Every entry is sourced. diff --git a/testing/cookbook-ts-ci.sh b/testing/cookbook-ts-ci.sh new file mode 100755 index 000000000..742c27917 --- /dev/null +++ b/testing/cookbook-ts-ci.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +## This script assembles the TypeScript from every cookbook recipe page and +## type-checks it under strict settings. It is the TS counterpart of +## rust-tutorial-ci.sh: that one extracts tutorial code into a crate and runs +## `cargo test`; this one extracts titled ts/tsx fences into a project and runs +## `tsc --noEmit --strict`. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# 1. Extract: titled ts/tsx fences from the recipe MDX -> project/src/recipes/. +cd "$SCRIPT_DIR/cookbook-ts" || exit 1 +npm ci || exit 1 +npm run extract || exit 1 + +# 2. Type-check: strict, no emit, across every extracted recipe at once. +cd "$SCRIPT_DIR/cookbook-ts/project" || exit 1 +npm ci || exit 1 +npm run typecheck || exit 1 diff --git a/testing/cookbook-ts/.gitignore b/testing/cookbook-ts/.gitignore new file mode 100644 index 000000000..07e6e472c --- /dev/null +++ b/testing/cookbook-ts/.gitignore @@ -0,0 +1 @@ +/node_modules diff --git a/testing/cookbook-ts/CONVENTION.md b/testing/cookbook-ts/CONVENTION.md new file mode 100644 index 000000000..33a36a2bf --- /dev/null +++ b/testing/cookbook-ts/CONVENTION.md @@ -0,0 +1,181 @@ +# Cookbook TypeScript verification: authoring convention + +This is the TypeScript counterpart of the repo's Rust tutorial CI +(`testing/rust-tutorial-ci.sh` + `testing/extract-tutorial-code/`). The Rust CI +extracts the code from a tutorial's fenced blocks into a real crate and runs +`cargo test`. This one extracts the code from cookbook recipe pages into a real +TypeScript project and runs `tsc --noEmit --strict`. Same idea, for TS: the +"Verified" badge on a recipe page means its code actually compiles in CI. + +The one rule to remember: **what is shown is what is compiled.** Every titled +`ts`/`tsx` code fence a reader sees on a recipe page is pulled out and +type-checked. If it compiles, the page is verified; if it does not, CI fails. + +## Where recipes live + +Ported recipes are Docusaurus MDX under: + +```text +docs/sdk-and-tools/sdk-js/cookbook/
/.mdx +``` + +- `
` is a grouping folder (`network-providers`, `transactions`, + `wallets`, `tokens`, ...). It also drives the sidebar sub-category. +- `` is the recipe id and the folder the extractor assembles code into. + Keep slugs unique across the whole cookbook (the extractor namespaces by slug, + not by section). + +The extractor only scans this directory, so nothing else in `docs/` is affected. + +## The fence convention + +A recipe's runnable code is authored as **titled** fenced code blocks: + +````markdown +```ts title="src/providers.ts" +// ... file contents ... +``` +```` + +- **`title=""`** is the file's path inside the recipe, relative to the + recipe root (for example `src/index.ts`, `src/lib/multiversx.ts`). This is the + same `title=` attribute Docusaurus already renders as the code-block filename + label, so the reader sees exactly the file that gets compiled. +- The fence language must be **`ts`** or **`tsx`**. Only those two are compiled. +- A recipe with several files is several titled fences on the page. The + extractor writes each to its path, so relative imports between them + (`import { x } from './providers'`) resolve exactly as they do in the real + recipe project. + +Fences **without** a `title=` (or in any other language: `bash`, `text`, `json`, +an untitled `ts` snippet) are treated as illustrative only. They are shown to the +reader but never extracted or compiled. Use an untitled `ts` fence for a throwaway +snippet you do not want type-checked (for example a one-off "generate a wallet" +aside), and a `text` fence for expected program output. + +`filename="..."` and the bare `` ```foo.ts `` shorthand are also accepted, matching +the Rust parser, but `title="..."` is the house style because Docusaurus renders it. + +### What is shown is what is compiled + +Because only titled fences compile, a recipe must show **every** file needed for +its code to type-check as a closed unit. If `App.tsx` imports `./providers`, then +`providers.tsx` must also appear as a titled fence on the page. Do not hide a +required file. If a file is pure boilerplate the reader does not need to study +(a provider bootstrap, an entry `index.ts`), still show it, under a clearly +labelled section such as "Provider bootstrap" or "Wiring it together". + +The only things the harness supplies that a recipe does not have to show are the +ambient environment types in `project/src/scaffold.d.ts` (see below). + +## Frontmatter + +Recipe pages carry the frontmatter the design-layer meta strip reads (rendered +by the `DocItem/Content` swizzle, which activates only when both `difficulty` and +`sdk_versions` are present): + +```yaml +--- +title: Send EGLD to an address +description: One-sentence summary shown in search and social cards. +difficulty: beginner # beginner | intermediate | advanced +est_minutes: 6 # optional: renders the "EST n min" chip +last_validated: "2026-07-16" # date CI last verified this page (the badge date) +sdk_versions: # object; each entry renders a version chip + sdk-core: "^15.4.0" +tags: + - sdk-core + - transaction + - typescript +# stale: true # optional: flips the badge to "Needs recheck" +--- +``` + +`difficulty` + `sdk_versions` are required for the strip to render at all. +`title`, `description`, `last_validated`, and `tags` are expected on every recipe. +`est_minutes` and `stale` are optional. + +## Running the check + +The whole pipeline is one script, mirroring `rust-tutorial-ci.sh`: + +```bash +./testing/cookbook-ts-ci.sh +``` + +It runs `npm ci` + extract in `testing/cookbook-ts/`, then `npm ci` + `tsc +--noEmit --strict` in `testing/cookbook-ts/project/`. The GitHub workflow +`.github/workflows/cookbook-ts-ci.yml` runs the same script on push / PR. + +To iterate on a single recipe faster, once dependencies are installed: + +```bash +cd testing/cookbook-ts && npm run extract +cd project && npm run typecheck +``` + +## Harness layout + +```text +testing/ + cookbook-ts-ci.sh # runner (mirror of rust-tutorial-ci.sh) + cookbook-ts/ # the extractor (mirror of extract-tutorial-code/) + parser.ts # fence parser (mirror of parser.rs) + extract.ts # assembler (mirror of extract_code.rs) + package.json # tsx + typescript + project/ # the compiled unit (mirror of the crowdfunding crate) + package.json # pinned SDK deps: sdk-core, sdk-dapp, react, ... + tsconfig.json # one strict config for every recipe + src/ + scaffold.d.ts # committed ambient types (import.meta.env) + recipes/ # git-ignored; the extractor fills this each run +.github/workflows/cookbook-ts-ci.yml +``` + +The extracted `src/recipes/**` is git-ignored, exactly as the Rust harness +git-ignores the tutorial code it writes into the crowdfunding crate. Only the +skeleton is committed. + +### Why one tsconfig for everything + +The standalone recipe projects ship two tsconfig shapes: the sdk-core +"backend/script" recipes (CommonJS, Node types) and the sdk-dapp "React" recipes +(JSX, DOM libs, bundler resolution). The harness type-checks with a single +superset config: `module: ESNext`, `moduleResolution: bundler`, `jsx: react-jsx`, +`lib` including DOM, and every strict flag the recipe projects use. Because the +harness only runs `tsc --noEmit` (it never emits or executes), the emit module +format is irrelevant, so one config checks both families. Nothing is checked more +loosely here than in a recipe's own repo. + +### The scaffold ambient types + +`project/src/scaffold.d.ts` declares `import.meta.env` so the sdk-dapp/Vite +recipes type-check without installing a bundler. It is the TS analogue of the +hand-written skeleton files (`meta/`, `sc-config.toml`) the Rust harness assumes +already exist. Recipes never need to show a `vite-env.d.ts`; the harness provides +the environment. + +## Porting a prototype recipe (checklist) + +To convert one runnable recipe project into a compliant Docusaurus page: + +1. Create `docs/sdk-and-tools/sdk-js/cookbook/
/.mdx`. +2. Write the frontmatter above. Set `last_validated` to the date you verify it, + and `sdk_versions` to the versions the recipe's `package.json` actually uses. +3. Convert prose to Docusaurus: paragraphs stay as Markdown; callouts become + admonitions (`:::note[Title]`, `:::warning[Title]`, `:::danger[Title]`), never + raw HTML. +4. Inline each source file the recipe needs as a titled fence + (` ```ts title="src/foo.ts" `), verbatim from the validated source. Include + every file required to compile, even boilerplate. Give command blocks the + `bash` language and expected-output blocks the `text` language. +5. Scrub any internal-only references from comments and prose (these are public + docs). Fix cross-links to real pages (or drop them); `onBrokenLinks` is `log`, + so broken links will not fail the build but should still be avoided. +6. Keep em-dashes sparing. +7. Add the page to `sidebars.js` under "Cookbook (recipes)". +8. Verify: + - `./testing/cookbook-ts-ci.sh` is green (the code compiles). + - `npm run build` is green and the page appears with its meta strip. + - markdownlint (`.markdownlint.jsonc`) and codespell (`.codespell`) pass on + the new file. diff --git a/testing/cookbook-ts/extract.ts b/testing/cookbook-ts/extract.ts new file mode 100644 index 000000000..a8e398ec8 --- /dev/null +++ b/testing/cookbook-ts/extract.ts @@ -0,0 +1,122 @@ +// extract.ts — assemble compilable TypeScript from the cookbook recipe MDX. +// +// TypeScript counterpart of ../extract-tutorial-code/src/extract_code.rs. +// +// The Rust CI extracts a hard-coded set of named blocks from one tutorial into +// one crate, then runs `cargo test`. The cookbook is many small recipes instead +// of one tutorial, so this walks a directory of recipe pages, and for each page +// writes every titled `ts`/`tsx` fence into its own folder inside the harness +// project: +// +// docs/.../cookbook/
/.mdx +// ```ts title="src/foo.ts" -> project/src/recipes//src/foo.ts +// +// Afterwards `tsc --noEmit --strict` in project/ type-checks every recipe at +// once (see ../cookbook-ts-ci.sh). "What is shown is what is compiled": only the +// fences a reader sees on the page are extracted; untitled/illustrative fences +// (bash, output, prose snippets) are ignored. + +import { existsSync } from "node:fs"; +import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"; +import { dirname, join, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { extractCodeBlocksFromMarkdown } from "./parser.ts"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = resolve(HERE, "..", ".."); + +/** The one directory that holds ported cookbook recipes (analogous to the + * Rust extractor's hard-coded tutorial path list). */ +const COOKBOOK_DIR = join(REPO_ROOT, "docs/sdk-and-tools/sdk-js/cookbook"); + +/** Where assembled recipes land — git-ignored, wiped and rebuilt each run. */ +const RECIPES_OUT = join(HERE, "project", "src", "recipes"); + +/** Only these fence languages are compiled. */ +const COMPILE_LANGUAGES = new Set(["ts", "tsx"]); + +async function findMdxFiles(dir: string): Promise { + const entries = await readdir(dir, { withFileTypes: true }); + const files: string[] = []; + for (const entry of entries) { + const full = join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...(await findMdxFiles(full))); + } else if (entry.name.endsWith(".mdx") || entry.name.endsWith(".md")) { + files.push(full); + } + } + return files.sort(); +} + +/** Recipe slug = the MDX file's basename (send-egld.mdx -> "send-egld"). */ +function slugFor(mdxPath: string): string { + const base = mdxPath.split("/").at(-1)!; + return base.replace(/\.mdx?$/, ""); +} + +async function main(): Promise { + if (!existsSync(COOKBOOK_DIR)) { + throw new Error(`Cookbook directory not found: ${COOKBOOK_DIR}`); + } + + // Fresh start, so a removed fence never lingers as a stale extracted file. + await rm(RECIPES_OUT, { recursive: true, force: true }); + await mkdir(RECIPES_OUT, { recursive: true }); + + const mdxFiles = await findMdxFiles(COOKBOOK_DIR); + let recipeCount = 0; + let fileCount = 0; + + for (const mdxPath of mdxFiles) { + const slug = slugFor(mdxPath); + const markdown = await readFile(mdxPath, "utf-8"); + const blocks = extractCodeBlocksFromMarkdown(markdown); + + const compilable = blocks.filter( + (b) => b.title && b.language && COMPILE_LANGUAGES.has(b.language), + ); + if (compilable.length === 0) { + continue; + } + + const seen = new Set(); + for (const block of compilable) { + const title = block.title!; + if (seen.has(title)) { + throw new Error( + `Duplicate fence title "${title}" in ${relative(REPO_ROOT, mdxPath)} ` + + `— every titled fence in a recipe must map to a unique file.`, + ); + } + seen.add(title); + + const outPath = join(RECIPES_OUT, slug, title); + await mkdir(dirname(outPath), { recursive: true }); + await writeFile(outPath, block.content, "utf-8"); + fileCount += 1; + console.log( + `Extracted ${relative(REPO_ROOT, mdxPath)} :: ${title} (${block.language})`, + ); + } + recipeCount += 1; + } + + console.log( + `\nAssembled ${fileCount} file(s) across ${recipeCount} recipe(s) into ` + + `${relative(REPO_ROOT, RECIPES_OUT)}`, + ); + + if (fileCount === 0) { + throw new Error( + "No compilable fences found. Expected titled ```ts / ```tsx fences in " + + `${relative(REPO_ROOT, COOKBOOK_DIR)}.`, + ); + } +} + +main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; +}); diff --git a/testing/cookbook-ts/package-lock.json b/testing/cookbook-ts/package-lock.json new file mode 100644 index 000000000..a9e376d2b --- /dev/null +++ b/testing/cookbook-ts/package-lock.json @@ -0,0 +1,569 @@ +{ + "name": "cookbook-ts-extractor", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cookbook-ts-extractor", + "version": "0.0.0", + "devDependencies": { + "@types/node": "^20.17.6", + "tsx": "^4.19.2", + "typescript": "^5.6.3" + }, + "engines": { + "node": ">=20.13.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/testing/cookbook-ts/package.json b/testing/cookbook-ts/package.json new file mode 100644 index 000000000..180dd26ab --- /dev/null +++ b/testing/cookbook-ts/package.json @@ -0,0 +1,18 @@ +{ + "name": "cookbook-ts-extractor", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Extracts titled ts/tsx fences from the cookbook recipe MDX into the harness project (TypeScript counterpart of extract-tutorial-code).", + "scripts": { + "extract": "tsx extract.ts" + }, + "devDependencies": { + "@types/node": "^20.17.6", + "tsx": "^4.19.2", + "typescript": "^5.6.3" + }, + "engines": { + "node": ">=20.13.1" + } +} diff --git a/testing/cookbook-ts/parser.ts b/testing/cookbook-ts/parser.ts new file mode 100644 index 000000000..ff7c7afa4 --- /dev/null +++ b/testing/cookbook-ts/parser.ts @@ -0,0 +1,150 @@ +// parser.ts — extract fenced code blocks from a Markdown / MDX string. +// +// This is the TypeScript counterpart of ../extract-tutorial-code/src/parser.rs +// (the Rust tutorial CI). It keeps the same idea and the same fence-info +// convention, so a maintainer who knows the Rust one already knows this one: +// +// ```ts title="src/main.ts" -> language "ts", title "src/main.ts" +// ```rust filename="Cargo.toml" -> language "rust", title "Cargo.toml" +// ```Cargo.toml -> title "Cargo.toml", language "toml" +// +// The Rust version leans on the `pulldown-cmark` CommonMark parser. We only need +// fenced code blocks (never indented ones, never inline code) out of our own +// recipe MDX, which is authored to a fixed convention, so a small, dependency- +// free line scanner is enough — and keeps the extractor install-light. + +export interface CodeBlock { + /** The `title=`/`filename=` value, or a bare `foo.ext` info string. */ + title?: string; + /** The first info-string token (e.g. "ts", "tsx", "bash"). */ + language?: string; + /** The verbatim code between the fences (trailing newline preserved). */ + content: string; +} + +/** Matches an opening or closing fence line: 3+ backticks or 3+ tildes. */ +const FENCE_RE = /^(\s*)(`{3,}|~{3,})(.*)$/; + +/** + * Extract every fenced code block from a Markdown / MDX document. + * + * YAML frontmatter (a leading `---` … `---` block) is stripped first so its + * contents can never be mistaken for fences. + */ +export function extractCodeBlocksFromMarkdown(markdown: string): CodeBlock[] { + const lines = stripFrontmatter(markdown).split(/\r?\n/); + const blocks: CodeBlock[] = []; + + let i = 0; + while (i < lines.length) { + const open = lines[i]!.match(FENCE_RE); + if (!open) { + i += 1; + continue; + } + + // An opening fence. Remember its exact marker so we only close on a fence + // of the same character and at least the same length (CommonMark rule). + const fenceChar = open[2]![0]!; + const fenceLen = open[2]!.length; + const info = open[3]!.trim(); + + const contentLines: string[] = []; + i += 1; + while (i < lines.length) { + const close = lines[i]!.match(FENCE_RE); + const isClose = + close !== null && + close[3]!.trim() === "" && + close[2]![0] === fenceChar && + close[2]!.length >= fenceLen; + if (isClose) { + break; + } + contentLines.push(lines[i]!); + i += 1; + } + + const { language, title } = parseCodeBlockInfo(info); + blocks.push({ + ...(title !== undefined ? { title } : {}), + ...(language !== undefined ? { language } : {}), + // Re-join with a trailing newline so extracted files end cleanly. + content: contentLines.length > 0 ? contentLines.join("\n") + "\n" : "", + }); + + i += 1; // step over the closing fence + } + + return blocks; +} + +/** + * Parse a fence info string into { language, title }. + * + * Mirrors `parse_code_block_info` in parser.rs, including the `foo.ext` + * shorthand where the first token is itself a filename. + */ +export function parseCodeBlockInfo(info: string): { + language?: string; + title?: string; +} { + if (info === "") { + return {}; + } + + const parts = info.split(/\s+/); + const first = parts[0]!; + + let language: string | undefined; + let title: string | undefined; + + // A first token that looks like a filename (`foo.ext`) is treated as the + // title, and the extension becomes the language — same as the Rust parser. + if ( + first.includes(".") && + !first.startsWith("title=") && + !first.startsWith("filename=") + ) { + title = first; + const ext = first.split(".").at(-1); + if (ext) { + language = ext; + } + } else { + language = first; + } + + for (const part of parts.slice(1)) { + if (part.startsWith("title=")) { + title = stripQuotes(part.slice("title=".length)); + } else if (part.startsWith("filename=")) { + title = stripQuotes(part.slice("filename=".length)); + } + } + + return { + ...(language !== undefined ? { language } : {}), + ...(title !== undefined ? { title } : {}), + }; +} + +/** Remove a single leading `"`/`'` pair, matching parser.rs's strip_quotes. */ +function stripQuotes(s: string): string { + return s.replace(/^["']/, "").replace(/["']$/, ""); +} + +/** Drop a leading YAML frontmatter block (`---` … `---`) if present. */ +function stripFrontmatter(markdown: string): string { + if (!markdown.startsWith("---")) { + return markdown; + } + const lines = markdown.split(/\r?\n/); + // lines[0] is the opening "---"; find the next closing "---". + for (let i = 1; i < lines.length; i += 1) { + if (lines[i]!.trim() === "---") { + return lines.slice(i + 1).join("\n"); + } + } + return markdown; +} diff --git a/testing/cookbook-ts/project/.gitignore b/testing/cookbook-ts/project/.gitignore new file mode 100644 index 000000000..c943492b6 --- /dev/null +++ b/testing/cookbook-ts/project/.gitignore @@ -0,0 +1,5 @@ +# Assembled by `cookbook-ts-extractor` on every CI run — never committed. +/src/recipes/ + +# npm +/node_modules diff --git a/testing/cookbook-ts/project/package-lock.json b/testing/cookbook-ts/project/package-lock.json new file mode 100644 index 000000000..f2b70f201 --- /dev/null +++ b/testing/cookbook-ts/project/package-lock.json @@ -0,0 +1,4750 @@ +{ + "name": "cookbook-ts-project", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cookbook-ts-project", + "version": "0.0.0", + "dependencies": { + "@multiversx/sdk-core": "^15.4.0", + "@multiversx/sdk-dapp": "^5.6.0", + "axios": "^1.7.9", + "bignumber.js": "^9.1.2", + "protobufjs": "^7.4.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^20.17.6", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "typescript": "^5.6.3" + }, + "engines": { + "node": ">=20.13.1" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT", + "optional": true + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT", + "optional": true + }, + "node_modules/@ledgerhq/devices": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.16.0.tgz", + "integrity": "sha512-brXLPzkvGM3D5YNsWQ25P5G4SmWdSNBed9W8wKoOIRLGdRfvE+bg9mzFty0iZ+aRLBkLoXwX7xKIL9zUi6LBKQ==", + "license": "Apache-2.0", + "dependencies": { + "semver": "7.7.3" + } + }, + "node_modules/@ledgerhq/errors": { + "version": "6.37.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.37.0.tgz", + "integrity": "sha512-T5yiKI5UX7ugeocdTF3TUsCIN2BH41Bio4ZeN410YFjFOf3es08n/5JyMzzKwzRgP0blG3HfBf7s7vJKqCSAeg==", + "license": "Apache-2.0" + }, + "node_modules/@ledgerhq/hw-transport": { + "version": "6.35.5", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.35.5.tgz", + "integrity": "sha512-P4+wtLewLWgxPtIb90h5kjpzXVlC6f4IBQBmvowVFkInvZt34ffXkX7wa5KfMzu4l3cqCcpNSqtPSCMp+0vuqg==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/devices": "8.16.0", + "@ledgerhq/errors": "^6.37.0", + "@ledgerhq/logs": "^6.17.0", + "events": "^3.3.0" + } + }, + "node_modules/@ledgerhq/hw-transport-web-ble": { + "version": "6.35.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-web-ble/-/hw-transport-web-ble-6.35.0.tgz", + "integrity": "sha512-nc4FAWZ0GtZO9ydDCKlvDzOEVdUXlGL/0nWPIOeCE5xxuXug5iR0uhC4Bl0qwlUCEnhewmXwLJsCFcCdr/N4QQ==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/devices": "8.16.0", + "@ledgerhq/errors": "^6.37.0", + "@ledgerhq/hw-transport": "6.35.5", + "@ledgerhq/logs": "^6.17.0", + "rxjs": "7.8.2" + } + }, + "node_modules/@ledgerhq/hw-transport-webhid": { + "version": "6.36.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-webhid/-/hw-transport-webhid-6.36.0.tgz", + "integrity": "sha512-1mKWm3LyGOgmaYlAiqbmaGoupOZHbj2Kow5sXLxKZzQa4kFvQuUdinYOWhs7T8hqJyAkz9WlHYyKM7aIs8kjNg==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/devices": "8.16.0", + "@ledgerhq/errors": "^6.37.0", + "@ledgerhq/hw-transport": "6.35.5", + "@ledgerhq/logs": "^6.17.0" + } + }, + "node_modules/@ledgerhq/hw-transport-webusb": { + "version": "6.35.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-webusb/-/hw-transport-webusb-6.35.0.tgz", + "integrity": "sha512-8fBNqzQnyR3pKLE2AVxCAc54nHMEKz9/eAYLDENVRuDp+A23lBJYWErxSyoZ6riCM19jIhRvtHjB0o4PXpa2tg==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/devices": "8.16.0", + "@ledgerhq/errors": "^6.37.0", + "@ledgerhq/hw-transport": "6.35.5", + "@ledgerhq/logs": "^6.17.0" + } + }, + "node_modules/@ledgerhq/logs": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.17.0.tgz", + "integrity": "sha512-yra33g5q/AU7+PwAws+GaVpQGUuxnDREjVBnviJjcaJLVKuLzI4pnj8Bd3nY3fypM5k1yZEYKEXfUuGFUjP2+w==", + "license": "Apache-2.0" + }, + "node_modules/@lifeomic/axios-fetch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@lifeomic/axios-fetch/-/axios-fetch-3.1.0.tgz", + "integrity": "sha512-C6ceAnh8W19KuekFsCiK1A+AMBirQFTnoEqMZQ7HE6VXJ18zjGofdXxLU8RTo2gZp/yZK5ufmPwIvRtejj1gxg==", + "license": "MIT", + "dependencies": { + "@types/node-fetch": "^2.5.10" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@lit/react": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.8.tgz", + "integrity": "sha512-p2+YcF+JE67SRX3mMlJ1TKCSTsgyOVdAwd/nxp3NuV1+Cb6MWALbN6nT7Ld4tpmYofcE5kcaSY1YBB9erY+6fw==", + "license": "BSD-3-Clause", + "optional": true, + "peerDependencies": { + "@types/react": "17 || 18 || 19" + } + }, + "node_modules/@msgpack/msgpack": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", + "integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==", + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@multiversx/sdk-bls-wasm": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-bls-wasm/-/sdk-bls-wasm-0.3.5.tgz", + "integrity": "sha512-c0tIdQUnbBLSt6NYU+OpeGPYdL0+GV547HeHT8Xc0BKQ7Cj0v82QUoA2QRtWrR1G4MNZmLsIacZSsf6DrIS2Bw==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/@multiversx/sdk-core": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-core/-/sdk-core-15.4.1.tgz", + "integrity": "sha512-1G/GS7fwED9Pl0NpwbcdXSi+986ey/OUGZycWuDCXPMMWx/qFQ0V5YMEotEqma8UM7uioVW3O5AnI7+EWOmsnQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@multiversx/sdk-transaction-decoder": "1.0.2", + "@noble/ed25519": "1.7.3", + "@noble/hashes": "1.3.0", + "bech32": "1.1.4", + "blake2b": "2.1.3", + "buffer": "6.0.3", + "ed25519-hd-key": "1.1.2", + "ed2curve": "0.3.0", + "json-bigint": "1.0.0", + "keccak": "3.0.2", + "scryptsy": "2.1.0", + "tweetnacl": "1.0.3", + "uuid": "8.3.2" + }, + "optionalDependencies": { + "@multiversx/sdk-bls-wasm": "0.3.5", + "axios": "^1.15.2", + "bip39": "3.1.0" + }, + "peerDependencies": { + "bignumber.js": "^9.0.1", + "protobufjs": "^7.5.6" + } + }, + "node_modules/@multiversx/sdk-dapp": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-dapp/-/sdk-dapp-5.7.1.tgz", + "integrity": "sha512-Tzy5xkKVrwTjylJ31AXVRWPQFtEyb2fyJTUcM8gz8dEqe/VpYH1KzCjbUVZweRcd3K9+/h83R+gT5SLmiPAwOw==", + "license": "MIT", + "dependencies": { + "@lifeomic/axios-fetch": "3.1.0", + "@multiversx/sdk-extension-provider": "5.1.2", + "@multiversx/sdk-hw-provider": "8.2.0", + "@multiversx/sdk-native-auth-client": "2.0.1", + "@multiversx/sdk-wallet-connect-provider": "6.1.5", + "@multiversx/sdk-web-wallet-cross-window-provider": "3.2.2", + "@multiversx/sdk-web-wallet-iframe-provider": "4.0.1", + "@multiversx/sdk-webview-provider": "3.2.7", + "immer": "10.1.1", + "linkifyjs": "4.3.3", + "lodash.isempty": "4.4.0", + "lodash.isequal": "4.5.0", + "lodash.isstring": "4.0.1", + "lodash.startcase": "4.4.0", + "lodash.trimend": "4.18.0", + "lodash.uniq": "4.5.0", + "socket.io-client": "4.8.3", + "zustand": "4.4.7" + }, + "optionalDependencies": { + "@multiversx/sdk-dapp-ui": ">=0.1.24 <0.2.0" + }, + "peerDependencies": { + "@multiversx/sdk-core": "^14.x || ^15.x", + "@multiversx/sdk-dapp-utils": "^3.x", + "axios": ">=1.15.0", + "bignumber.js": "^9.x", + "protobufjs": "^7.5.5" + } + }, + "node_modules/@multiversx/sdk-dapp-ui": { + "version": "0.1.24", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-dapp-ui/-/sdk-dapp-ui-0.1.24.tgz", + "integrity": "sha512-p2Dnqh9CMmlYxSwH4ks+qjqsvzQ6ak8CEIZxAJ+Xj6tYiB8r4sgqamPDkSLsxn9114nxud7bkdw2mvJ+kepRLQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@stencil/core": "4.38.1", + "@stencil/react-output-target": "1.2.0", + "@stencil/vue-output-target": "0.11.8", + "classnames": ">=2.5.1", + "lodash.capitalize": "^4.2.1", + "lodash.inrange": "^3.3.0", + "lodash.range": "^3.2.0", + "qrcode": ">=1.5.4" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@multiversx/sdk-dapp-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-dapp-utils/-/sdk-dapp-utils-3.1.0.tgz", + "integrity": "sha512-m3u2ymm+ME/+6Hbu0iNmi+CQbuImNOc8GeX+KtdW0tihQrMwEuWIECj/U5EapEFbGMlNbPb/YmwiMMfQ1bk/fQ==", + "license": "GPL-3.0-or-later", + "peer": true, + "peerDependencies": { + "@multiversx/sdk-core": "^14.0.0 || ^15.0.0", + "bignumber.js": "^9.x" + } + }, + "node_modules/@multiversx/sdk-extension-provider": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-extension-provider/-/sdk-extension-provider-5.1.2.tgz", + "integrity": "sha512-jxu18WKnc8ZLaLdl6ePcLStG2aZB1f9W0m2wPJt2UbHnPMhnk/okoYZYWlHfiF/L8WZGzQn8VXwcmras/vdwlw==", + "license": "MIT", + "peerDependencies": { + "@multiversx/sdk-core": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@multiversx/sdk-hw-provider": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-hw-provider/-/sdk-hw-provider-8.2.0.tgz", + "integrity": "sha512-WSPv72ykmPj/H9swxkD8lBbOSjnFAClGMxKntgDy5L9Uj3nUrEDbJRo0btW9ocJxRCVmkkcNoy2ze/Vp2AZngQ==", + "license": "MIT", + "dependencies": { + "@ledgerhq/devices": "8.16.0", + "@ledgerhq/errors": "6.37.0", + "@ledgerhq/hw-transport": "6.35.5", + "@ledgerhq/hw-transport-web-ble": "6.35.0", + "@ledgerhq/hw-transport-webhid": "6.36.0", + "@ledgerhq/hw-transport-webusb": "6.35.0", + "buffer": "6.0.3", + "platform": "1.3.6" + }, + "peerDependencies": { + "@multiversx/sdk-core": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@multiversx/sdk-native-auth-client": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-native-auth-client/-/sdk-native-auth-client-2.0.1.tgz", + "integrity": "sha512-J4RxpsfNnWBKOPz0vvhnReSf8V6mci11Pe3NT2awhrpBhtAvkSBUGIzgoGVf/eumrhyReRmKyMjo+u4UhGokbg==", + "license": "GPL-3.0-or-later", + "peerDependencies": { + "axios": "^1.10.0" + } + }, + "node_modules/@multiversx/sdk-transaction-decoder": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-transaction-decoder/-/sdk-transaction-decoder-1.0.2.tgz", + "integrity": "sha512-j43QsKquu8N51WLmVlJ7dV2P3A1448R7/ktvl8r3i6wRMpfdtzDPNofTdHmMRT7DdQdvs4+RNgz8hVKL11Etsw==", + "license": "MIT", + "dependencies": { + "bech32": "^2.0.0" + } + }, + "node_modules/@multiversx/sdk-transaction-decoder/node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "license": "MIT" + }, + "node_modules/@multiversx/sdk-wallet-connect-provider": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-wallet-connect-provider/-/sdk-wallet-connect-provider-6.1.5.tgz", + "integrity": "sha512-jDnLoTpr8LA+De2bp5Wv1mFmfguk5cWav9QVCWMZt9SAkYcdoskF8dYJt5Rqs40UhxPS2yy+YrRBN1KWBhhC2w==", + "license": "MIT", + "dependencies": { + "@walletconnect/sign-client": "2.23.9", + "@walletconnect/utils": "2.23.9", + "bech32": "1.1.4" + }, + "peerDependencies": { + "@multiversx/sdk-core": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@multiversx/sdk-web-wallet-cross-window-provider": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-web-wallet-cross-window-provider/-/sdk-web-wallet-cross-window-provider-3.2.2.tgz", + "integrity": "sha512-CBMrf+oNKoBa/YFDTqzItLrERaW4n0pJIlGsP67H0Vr6bUlFLIlc30qsOGvWNoZvMDeDxvhvR7crayypETJSJw==", + "license": "MIT", + "peer": true, + "dependencies": { + "qs": "6.11.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@multiversx/sdk-core": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@multiversx/sdk-web-wallet-iframe-provider": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-web-wallet-iframe-provider/-/sdk-web-wallet-iframe-provider-4.0.1.tgz", + "integrity": "sha512-zIIKX4G1xYk7XSBQsHuihOag7pUAVGMZ1q3vug9XpwBwcQvgcSwC5RHzrYQusPpVdkTHIn3qa3JHqjwNj1VtGA==", + "license": "MIT", + "dependencies": { + "@types/jest": "^29.5.11", + "@types/qs": "6.9.10", + "qs": "6.11.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@multiversx/sdk-core": "^14.0.0 || ^15.0.0", + "@multiversx/sdk-web-wallet-cross-window-provider": "^3.x" + } + }, + "node_modules/@multiversx/sdk-webview-provider": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-webview-provider/-/sdk-webview-provider-3.2.7.tgz", + "integrity": "sha512-cPDJ4N7i9IDYP+z/M/vXzgjCiAr9JSQof4yp2nzQ67EwPyFPW4fenmHvzyhtD3KYdGc48OmJ0Nk/zv2+nwQnig==", + "license": "MIT", + "peerDependencies": { + "@multiversx/sdk-core": "^14.0.0 || ^15.0.0", + "@multiversx/sdk-web-wallet-cross-window-provider": "^3.x" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.0.tgz", + "integrity": "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", + "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/ed25519": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", + "integrity": "sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/@noble/hashes": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz", + "integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz", + "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz", + "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz", + "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz", + "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz", + "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz", + "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz", + "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz", + "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "license": "MIT" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@stencil/core": { + "version": "4.38.1", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.1.tgz", + "integrity": "sha512-qImplYLSp2wSZJo3oMZ3HrTaI+uULcRB4Knrua7UT9VjN/va+TDfk4JAKwDyDfTDkD2laDPcy6QJP2S3hVxZFQ==", + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "stencil": "bin/stencil" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.10.0" + }, + "optionalDependencies": { + "@rollup/rollup-darwin-arm64": "4.34.9", + "@rollup/rollup-darwin-x64": "4.34.9", + "@rollup/rollup-linux-arm64-gnu": "4.34.9", + "@rollup/rollup-linux-arm64-musl": "4.34.9", + "@rollup/rollup-linux-x64-gnu": "4.34.9", + "@rollup/rollup-linux-x64-musl": "4.34.9", + "@rollup/rollup-win32-arm64-msvc": "4.34.9", + "@rollup/rollup-win32-x64-msvc": "4.34.9" + } + }, + "node_modules/@stencil/react-output-target": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@stencil/react-output-target/-/react-output-target-1.2.0.tgz", + "integrity": "sha512-xDNpWdRg897T3Diy5V2d8dZUdjhc4QJ/5JZdTVyv3/e9UICdJPfCY6eKp/dWWgYlJ9AUE6GLHOI1iePZmLY12A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@lit/react": "^1.0.7", + "html-react-parser": "^5.2.2", + "react-style-stringify": "^1.2.0", + "ts-morph": "^22.0.0" + }, + "peerDependencies": { + "@stencil/core": ">=3 || >= 4.0.0-beta.0 || >= 4.0.0", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@stencil/core": { + "optional": false + }, + "react": { + "optional": false + }, + "react-dom": { + "optional": false + } + } + }, + "node_modules/@stencil/vue-output-target": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.11.8.tgz", + "integrity": "sha512-R/kQoN15irgL7NJxWaUNSmwDLfoDBZjlYaXNnW3LHlF30TYfyez6pRgD7ZglSSTVktMtCXz6ZPhg0uq59VkhOw==", + "license": "MIT", + "optional": true, + "peerDependencies": { + "@stencil/core": ">=2.0.0 || >=3 || >= 4.0.0-beta.0 || >= 4.0.0", + "vue": "^3.4.38", + "vue-router": "^4.5.0" + }, + "peerDependenciesMeta": { + "@stencil/core": { + "optional": true + }, + "vue": { + "optional": false + }, + "vue-router": { + "optional": true + } + } + }, + "node_modules/@ts-morph/common": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.23.0.tgz", + "integrity": "sha512-m7Lllj9n/S6sOkCkRftpM7L24uvmfXQFedlW/4hENcuJH1HHm9u5EgxZb9uVjQSCGrbBWBkOGgcTxNg36r6ywA==", + "license": "MIT", + "optional": true, + "dependencies": { + "fast-glob": "^3.3.2", + "minimatch": "^9.0.3", + "mkdirp": "^3.0.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.9.10", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", + "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "license": "MIT", + "optional": true, + "dependencies": { + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "license": "MIT", + "optional": true, + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "vue": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "license": "MIT", + "optional": true + }, + "node_modules/@walletconnect/core": { + "version": "2.23.9", + "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.23.9.tgz", + "integrity": "sha512-ws4WG8LeagUo2ERRo02HryXRcpwIRmCQ3pHLW5gWbVReLXXIpgk6ZAfID3fEGHevIwwnHSGww+nNhNpdXyiq0g==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/jsonrpc-ws-connection": "1.0.16", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.9", + "@walletconnect/utils": "2.23.9", + "@walletconnect/window-getters": "1.0.1", + "es-toolkit": "1.44.0", + "events": "3.3.0", + "uint8arrays": "3.1.1" + }, + "engines": { + "node": ">=18.20.8" + } + }, + "node_modules/@walletconnect/core/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@walletconnect/core/node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/@walletconnect/environment": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/environment/-/environment-1.0.1.tgz", + "integrity": "sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/events": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/events/-/events-1.0.1.tgz", + "integrity": "sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==", + "license": "MIT", + "dependencies": { + "keyvaluestorage-interface": "^1.0.0", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/heartbeat": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz", + "integrity": "sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==", + "license": "MIT", + "dependencies": { + "@walletconnect/events": "^1.0.1", + "@walletconnect/time": "^1.0.2", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-provider": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.14.tgz", + "integrity": "sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.8", + "@walletconnect/safe-json": "^1.0.2", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-types": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.4.tgz", + "integrity": "sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ==", + "license": "MIT", + "dependencies": { + "events": "^3.3.0", + "keyvaluestorage-interface": "^1.0.0" + } + }, + "node_modules/@walletconnect/jsonrpc-utils": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz", + "integrity": "sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw==", + "license": "MIT", + "dependencies": { + "@walletconnect/environment": "^1.0.1", + "@walletconnect/jsonrpc-types": "^1.0.3", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/jsonrpc-ws-connection": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.16.tgz", + "integrity": "sha512-G81JmsMqh5nJheE1mPst1W0WfVv0SG3N7JggwLLGnI7iuDZJq8cRJvQwLGKHn5H1WTW7DEPCo00zz5w62AbL3Q==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.6", + "@walletconnect/safe-json": "^1.0.2", + "events": "^3.3.0", + "ws": "^7.5.1" + } + }, + "node_modules/@walletconnect/logger": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-3.0.2.tgz", + "integrity": "sha512-7wR3wAwJTOmX4gbcUZcFMov8fjftY05+5cO/d4cpDD8wDzJ+cIlKdYOXaXfxHLSYeDazMXIsxMYjHYVDfkx+nA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.2", + "pino": "10.0.0" + } + }, + "node_modules/@walletconnect/relay-api": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@walletconnect/relay-api/-/relay-api-1.0.11.tgz", + "integrity": "sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-types": "^1.0.2" + } + }, + "node_modules/@walletconnect/relay-auth": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@walletconnect/relay-auth/-/relay-auth-1.1.0.tgz", + "integrity": "sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ==", + "license": "MIT", + "dependencies": { + "@noble/curves": "1.8.0", + "@noble/hashes": "1.7.0", + "@walletconnect/safe-json": "^1.0.1", + "@walletconnect/time": "^1.0.2", + "uint8arrays": "^3.0.0" + } + }, + "node_modules/@walletconnect/relay-auth/node_modules/@noble/hashes": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", + "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/safe-json": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/safe-json/-/safe-json-1.0.2.tgz", + "integrity": "sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/sign-client": { + "version": "2.23.9", + "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.23.9.tgz", + "integrity": "sha512-Xj+hw4E6mGRyhCdVOT/RMgnG+up/Y3v0ho5PlkVozvXWeVSqHNh9DmjLuU97a7OACoGd/oHBF6g3NVqD7MgCMQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/core": "2.23.9", + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/logger": "3.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.9", + "@walletconnect/utils": "2.23.9", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/time/-/time-1.0.2.tgz", + "integrity": "sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/types": { + "version": "2.23.9", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.9.tgz", + "integrity": "sha512-IUl1PpD/Dig8IE2OZ9XtjbPohEyOZJ73xs92EDUzoIyzRtfm36g2D340pY3iu3AAdLv1yFiaZafB8Hf8RFze8A==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/types/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@walletconnect/types/node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/@walletconnect/utils": { + "version": "2.23.9", + "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.23.9.tgz", + "integrity": "sha512-C5TltCs8UPypNiteYnKSv8+ZDK2EjVDyXCxN6kA9bkA+j6KGsNIV7l9MUA8WBAvE5Gi5EcBdhD3R9Hpo/1HHqQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@msgpack/msgpack": "3.1.3", + "@noble/ciphers": "1.3.0", + "@noble/curves": "1.9.7", + "@noble/hashes": "1.8.0", + "@scure/base": "1.2.6", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.9", + "@walletconnect/window-getters": "1.0.1", + "@walletconnect/window-metadata": "1.0.1", + "blakejs": "1.2.1", + "detect-browser": "5.3.0", + "ox": "0.9.3", + "uint8arrays": "3.1.1" + } + }, + "node_modules/@walletconnect/utils/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@walletconnect/utils/node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/@walletconnect/window-getters": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/window-getters/-/window-getters-1.0.1.tgz", + "integrity": "sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/window-metadata": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz", + "integrity": "sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==", + "license": "MIT", + "dependencies": { + "@walletconnect/window-getters": "^1.0.1", + "tslib": "1.14.1" + } + }, + "node_modules/abitype": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.4.tgz", + "integrity": "sha512-dpKH+N27vRjarMVTFFkeY445VTKftzGWpL0FiT7xmVmzQRKazZexzC5uHG0f6XKsVLAuUlndnbGau6lRejClxg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "peer": true, + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "license": "ISC", + "optional": true, + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, + "node_modules/blake2b": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/blake2b/-/blake2b-2.1.3.tgz", + "integrity": "sha512-pkDss4xFVbMb4270aCyGD3qLv92314Et+FsKzilCLxDz5DuZ2/1g3w4nmBbu6nKApPspnjG7JcwTjGZnduB1yg==", + "license": "ISC", + "dependencies": { + "blake2b-wasm": "^1.1.0", + "nanoassert": "^1.0.0" + } + }, + "node_modules/blake2b-wasm": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/blake2b-wasm/-/blake2b-wasm-1.1.7.tgz", + "integrity": "sha512-oFIHvXhlz/DUgF0kq5B1CqxIDjIJwh9iDeUUGQUcvgiGz7Wdw03McEO7CfLBy7QKGdsydcMCgO9jFNBAFCtFcA==", + "license": "MIT", + "dependencies": { + "nanoassert": "^1.0.0" + } + }, + "node_modules/blakejs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cipher-base": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT", + "optional": true + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "license": "MIT", + "optional": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-browser": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz", + "integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==", + "license": "MIT" + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT", + "optional": true + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "optional": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ed25519-hd-key": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ed25519-hd-key/-/ed25519-hd-key-1.1.2.tgz", + "integrity": "sha512-/0y9y6N7vM6Kj5ASr9J9wcMVDTtygxSOvYX+PJiMD7VcxCx2G03V5bLRl8Dug9EgkLFsLhGqBtQWQRcElEeWTA==", + "license": "MIT", + "dependencies": { + "bip39": "3.0.2", + "create-hmac": "1.1.7", + "tweetnacl": "1.0.3" + } + }, + "node_modules/ed25519-hd-key/node_modules/@types/node": { + "version": "11.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", + "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==", + "license": "MIT" + }, + "node_modules/ed25519-hd-key/node_modules/bip39": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.2.tgz", + "integrity": "sha512-J4E1r2N0tUylTKt07ibXvhpT2c5pyAFgvuA5q1H9uDy6dEGpjV8jmymh3MTYJDLCNbIVClSB9FbND49I6N24MQ==", + "license": "ISC", + "dependencies": { + "@types/node": "11.11.6", + "create-hash": "^1.1.0", + "pbkdf2": "^3.0.9", + "randombytes": "^2.0.1" + } + }, + "node_modules/ed2curve": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ed2curve/-/ed2curve-0.3.0.tgz", + "integrity": "sha512-8w2fmmq3hv9rCrcI7g9hms2pMunQr1JINfcjwR9tAyZqhtyaMN991lF/ZfHfr5tzZQ8c7y7aBgZbjfbd0fjFwQ==", + "license": "Unlicense", + "dependencies": { + "tweetnacl": "1.x.x" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/engine.io-client": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", + "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT", + "optional": true + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "optional": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "optional": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "optional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "optional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/hash-base/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/hash-base/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hash-base/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/hash-base/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/hash-base/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-dom-parser": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-5.1.8.tgz", + "integrity": "sha512-MCIUng//mF2qTtGHXJWr6OLfHWmg3Pm8ezpfiltF83tizPWY17JxT4dRLE8lykJ5bChJELoY3onQKPbufJHxYA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/remarkablemark" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "domhandler": "5.0.3", + "htmlparser2": "10.1.0" + } + }, + "node_modules/html-react-parser": { + "version": "5.2.17", + "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-5.2.17.tgz", + "integrity": "sha512-m+K/7Moq1jodAB4VL0RXSOmtwLUYoAsikZhwd+hGQe5Vtw2dbWfpFd60poxojMU0Tsh9w59mN1QLEcoHz0Dx9w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/remarkablemark" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/html-react-parser" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "domhandler": "5.0.3", + "html-dom-parser": "5.1.8", + "react-property": "2.0.2", + "style-to-js": "1.1.21" + }, + "peerDependencies": { + "@types/react": "0.14 || 15 || 16 || 17 || 18 || 19", + "react": "0.14 || 15 || 16 || 17 || 18 || 19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/idb-keyval": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.3.0.tgz", + "integrity": "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT", + "optional": true + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/keccak": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz", + "integrity": "sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/keyvaluestorage-interface": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz", + "integrity": "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==", + "license": "MIT" + }, + "node_modules/linkifyjs": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz", + "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.capitalize": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", + "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.inrange": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/lodash.inrange/-/lodash.inrange-3.3.6.tgz", + "integrity": "sha512-NqOT/CVclLZKet0ALwg1joQ/XnfxePGf8L9WKvzOcTDAI0axG3Ej24qDGwLk0ejN4AVfFvs6vY5AWBB53o5piw==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.isempty": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.range": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.range/-/lodash.range-3.2.0.tgz", + "integrity": "sha512-Fgkb7SinmuzqgIhNhAElo0BL/R1rHCnhwSZf78omqSwvWqD0kD2ssOAutQonDKH/ldS8BxA72ORYI09qAY9CYg==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "license": "MIT" + }, + "node_modules/lodash.trimend": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/lodash.trimend/-/lodash.trimend-4.18.0.tgz", + "integrity": "sha512-8w2M3nZAWLN1OX/6mTPCwRlZiD/LhVyPV9l7DEbkd9wybExvg9AcCjbD19swj6oVzX5hcMZHp3/Y1b4Sl3sHKg==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "license": "(Apache-2.0 AND MIT)" + }, + "node_modules/nanoassert": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz", + "integrity": "sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ==", + "license": "ISC" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "optional": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", + "license": "MIT" + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ox": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.3.tgz", + "integrity": "sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.0.9", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "optional": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "optional": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT", + "optional": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pbkdf2": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.6.tgz", + "integrity": "sha512-BT6eelPB1EyGHo8pC0o9Bl6k6SYVhKO1jEbd3lcTrtr7XHdjP8BW1YpfCV3G9Kwkxgattk+S5q2/RvuttCsS1g==", + "license": "MIT", + "dependencies": { + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.0.0.tgz", + "integrity": "sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "slow-redact": "^0.3.0", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "optional": true, + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.2.tgz", + "integrity": "sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==", + "license": "MIT", + "optional": true + }, + "node_modules/react-style-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-style-stringify/-/react-style-stringify-1.2.0.tgz", + "integrity": "sha512-88JZckqgbfXJaGcDQoTFKRmBwHBF0Ddaxz3PL9Q+vywAJruBY+NdN+ZiKSBe7r/pWtjbt0naZdtH5oNI1X3FLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emotion/unitless": "^0.10.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC", + "optional": true + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "optional": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/ripemd160": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", + "license": "MIT", + "dependencies": { + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/scryptsy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scryptsy/-/scryptsy-2.1.0.tgz", + "integrity": "sha512-1CdSqHQowJBnMAFyPEBRfqag/YP9OF394FV+4YREIJX4ljD7OxvQRDayyoyyCk+senRjSkP6VnUNQmVQqB6g7w==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slow-redact": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/slow-redact/-/slow-redact-0.3.2.tgz", + "integrity": "sha512-MseHyi2+E/hBRqdOi5COy6wZ7j7DxXRz9NkseavNYSvvWC06D8a5cidVZX3tcG5eCW3NIyVU4zT63hw0Q486jw==", + "license": "MIT" + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "optional": true, + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/thread-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-morph": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-22.0.0.tgz", + "integrity": "sha512-M9MqFGZREyeb5fTl6gNHKZLqBQA0TjA1lea+CR48R8EBTDuWrNqW6ccC5QvjNR4s6wDumD3LTCjOFSp9iwlzaw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@ts-morph/common": "~0.23.0", + "code-block-writer": "^13.0.1" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/uint8arrays": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz", + "integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==", + "license": "MIT", + "dependencies": { + "multiformats": "^9.4.2" + } + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vue": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC", + "optional": true + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "7.5.12", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.12.tgz", + "integrity": "sha512-1xGnbYN3zbog9CwuNDQULNRrTCLIn46/WmpR1f0w6PsCYQHkylZr5vkd6kfMZYV6pRnQkcPNRyiA8LsrNKyhpg==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "optional": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/zustand": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz", + "integrity": "sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/testing/cookbook-ts/project/package.json b/testing/cookbook-ts/project/package.json new file mode 100644 index 000000000..f9aa576a0 --- /dev/null +++ b/testing/cookbook-ts/project/package.json @@ -0,0 +1,27 @@ +{ + "name": "cookbook-ts-project", + "version": "0.0.0", + "private": true, + "description": "Harness project the cookbook recipe code is extracted into and type-checked (TypeScript counterpart of the crowdfunding crate).", + "scripts": { + "typecheck": "tsc --noEmit --strict" + }, + "dependencies": { + "@multiversx/sdk-core": "^15.4.0", + "@multiversx/sdk-dapp": "^5.6.0", + "axios": "^1.7.9", + "bignumber.js": "^9.1.2", + "protobufjs": "^7.4.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^20.17.6", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "typescript": "^5.6.3" + }, + "engines": { + "node": ">=20.13.1" + } +} diff --git a/testing/cookbook-ts/project/src/scaffold.d.ts b/testing/cookbook-ts/project/src/scaffold.d.ts new file mode 100644 index 000000000..20cb723f4 --- /dev/null +++ b/testing/cookbook-ts/project/src/scaffold.d.ts @@ -0,0 +1,19 @@ +// scaffold.d.ts — ambient types the harness provides so recipe code type-checks +// without pulling in a bundler. +// +// This is committed (the extractor never touches it) and is the TS analogue of +// the hand-written skeleton files in the crowdfunding crate (meta/, sc-config +// .toml, …) that the Rust extractor assumes already exist. +// +// The sdk-dapp "React" recipes are authored for Vite and read Vite env vars via +// `import.meta.env.VITE_*`. In their own repo a `vite-env.d.ts` referencing +// `vite/client` supplies these types. The harness does not install Vite, so we +// declare just the shape the recipes rely on (VITE_* string vars) here instead. + +interface ImportMetaEnv { + readonly [key: `VITE_${string}`]: string | undefined; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/testing/cookbook-ts/project/tsconfig.json b/testing/cookbook-ts/project/tsconfig.json new file mode 100644 index 000000000..f8f1f54e0 --- /dev/null +++ b/testing/cookbook-ts/project/tsconfig.json @@ -0,0 +1,41 @@ +{ + // Strict type-check config for every extracted cookbook recipe. + // + // It is a superset of the two tsconfig shapes the standalone recipe projects + // ship: the sdk-core "backend/script" recipes (module: commonjs, node types) + // and the sdk-dapp "React" recipes (jsx, DOM libs, bundler resolution). Since + // the harness only runs `tsc --noEmit` (never emits or executes), the emit + // module format is irrelevant — so ESNext + bundler resolution type-checks + // both families with one config. Every strict flag below is carried over + // verbatim from the recipe tsconfigs, so nothing is checked more loosely here + // than in the recipe's own repo. + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + + "allowJs": false, + "noEmit": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "useDefineForClassFields": true, + "skipLibCheck": true, + + // Strictness — identical to the recipe projects' own tsconfig. + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "forceConsistentCasingInFileNames": true + }, + // src/recipes/** is filled in by the extractor; src/scaffold.d.ts is committed. + "include": ["src"] +} diff --git a/testing/cookbook-ts/tsconfig.json b/testing/cookbook-ts/tsconfig.json new file mode 100644 index 000000000..da6ce28ca --- /dev/null +++ b/testing/cookbook-ts/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["extract.ts", "parser.ts"] +}