From d1fa2969544c9b84acef39fe277f2ccd517fd636 Mon Sep 17 00:00:00 2001 From: Tudor Morar Date: Wed, 16 Jul 2025 10:08:08 +0300 Subject: [PATCH 1/4] Updated sdk-dapp documentation to v.5 (#1060) * Added sdk-dapp documentation --- docs/sdk-and-tools/sdk-dapp/sdk-dapp.md | 1326 +++++++++-------------- 1 file changed, 532 insertions(+), 794 deletions(-) diff --git a/docs/sdk-and-tools/sdk-dapp/sdk-dapp.md b/docs/sdk-and-tools/sdk-dapp/sdk-dapp.md index 37a60a4be..31e5f3c70 100644 --- a/docs/sdk-and-tools/sdk-dapp/sdk-dapp.md +++ b/docs/sdk-and-tools/sdk-dapp/sdk-dapp.md @@ -6,16 +6,23 @@ title: sdk-dapp Library used to build React dApps on MultiversX Network. :::important -The following documentation is for `sdk-dapp v2.0.0` and above. +The following documentation is for `sdk-dapp v5.0.0` and above. ::: [comment]: # (mx-abstract) ## Introduction -**sdk-dapp** is a library that holds core functional logic that can be used to create a **dApp** on MultiversX Network. +`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 **React**. +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)) [comment]: # (mx-context-auto) @@ -25,30 +32,22 @@ The GitHub repository can be found here: [https://github.com/multiversx/mx-sdk-d [comment]: # (mx-context-auto) -### npmjs - -sdk-dapp can be found on npmjs as well: https://www.npmjs.com/package/@multiversx/sdk-dapp - -[comment]: # (mx-context-auto) - ### Live demo: template-dapp -The [mx-template-dapp](https://github.com/multiversx/mx-template-dapp) that is used in [Build a dApp in 15 minutes](/developers/tutorials/your-first-dapp) is based on `sdk-dapp`. - -A live demo of the template-dapp is available at [https://template-dapp.multiversx.com/](https://template-dapp.multiversx.com/). +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) [comment]: # (mx-context-auto) ### Requirements -- Node.js version 12.16.2+ -- Npm version 6.14.4+ +- Node.js version 20.13.1+ +- Npm version 10.5.2+ [comment]: # (mx-context-auto) -## Migration from sdk-dapp 1.x +### Distribution -If you're transitioning from sdk-dapp 1.x to sdk-dapp 2.0, please read the [Migration guide](https://github.com/multiversx/mx-sdk-dapp/wiki/Migration-guide-2.0). +[npm](https://www.npmjs.com/package/@multiversx/sdk-dapp) [comment]: # (mx-context-auto) @@ -66,1019 +65,758 @@ or yarn add @multiversx/sdk-dapp ``` -If you need only the sdk-dapp basic logic, without the additional UI, consider using the `--no-optional` flag. -This will not install the packages needed for the optional UI components. - -```bash -npm install @multiversx/sdk-dapp --no-optional -``` - -or +> **Note:** Make sure you run your app on `https`, not `http`, otherwise some providers will not work. -```bash -yarn add @multiversx/sdk-dapp --no-optional -``` +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 [comment]: # (mx-context-auto) ## Usage -sdk-dapp aims to abstract and simplify the process of interacting with users' wallets and with the MultiversX Network, allowing developers to easily get started with a new application or integrate sdk-dapp into an existing application. - -This library covers two main areas: **User Identity** and **Transactions**. The API for interacting with library's logic is exposed via hooks and methods that can be called for logging in the user, getting the status of the user or sending transactions. +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. -However, to simplify usage even further, the library also comes with a default UI that already uses these hooks and methods under the hood. These UI elements can be easily customized with custom css classes. - -```jsx -import * as DappUI from "@multiversx/sdk-dapp/UI"; +```mermaid +flowchart LR + A["Signing Providers & APIs"] <--> B["sdk-dapp"] <--> C["dApp"] ``` -**Please be aware that this style of importing might also import unused code.** -To reduce the amount of dead code, you can use named imports for each component like - -```jsx -import { UnlockPage } from "@multiversx/sdk-dapp/UI/pages"; -// or -import { UnlockPage } from "@multiversx/sdk-dapp/UI/pages/UnlockPage"; -``` +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. -More on this below. +Having this knowledge, we can consider several steps needed to put a dApp together: -[comment]: # (mx-context-auto) +**Table 1.** Steps to build a dApp -### Prerequisites +| # | 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 | -There are a couple of requirements that need to be met for the application to work properly. +Each of these steps will be explained in more detail in the following sections. -
- - React - +[comment]: # (mx-context-auto) -**React** +### 1. Configuration -This library was built for applications that use React, it might not be suitable for usage with other libraries or frameworks. +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 +}; -
- - DappProvider - +initApp(config).then(() => { + render(() => , root!); // render your app +}); +``` -**DappProvider** +[comment]: # (mx-context-auto) -You need to wrap your application with the **DappProvider** component, which is exported by the library, as we need to create a global Context to be able to manipulate the data. +### 2. Provider interaction -- import the Provider: +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`. -```jsx -import { DappProvider } from "@multiversx/sdk-dapp/wrappers/DappProvider"; -//or; -import { DappProvider } from "@multiversx/sdk-dapp/wrappers"; -``` +#### 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. -- Wrap your application with this Provider. +```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 ; +}; -```jsx - ``` - -`environment` is a required key that is needed to configure the app's endpoints for a specific environment. Accepted values are `testnet`, `devnet` and `mainnet` (also configured in `EnvironmentsEnum`). - -DappProvider also accepts an optional `customNetworkConfig` object with a couple of keys. -This allows using different APIs and different connection providers to configure your network configuration. - -**All keys are optional** +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 -{ - id?: string; - name?: string; - egldLabel?: string; - decimals?: string; - digits?: string; - gasPerDataByte?: string; - walletConnectDeepLink?: string; // a string that will create a deeplink for an application that is used on a mobile phone, instead of generating the login QR code. - walletConnectV2ProjectId?: string; // a unique ProjectID needed to access the WalletConnect 2.0 Relay Cloud - walletAddress?: string; - apiAddress?: string; - explorerAddress?: string; - apiTimeout?: 4000; +// 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 + }, } ``` -
- -
- - UI Wrappers - - -**UI Wrappers** - -The library exposes a couple of Components that are connected to the redux store and are used to display various elements -when something happens inside the app: - -- `TransactionsToastList` will display new transactions in nice toasts at the bottom of the screen. This component is fully customizable. +You have the option to stop this behavior by calling `LogoutManager.getInstance().stop()` after the user has logged in. -```jsx - import {TransactionsToastList} from "@multiversx/sdk-dapp/UI/TransactionsToastList"; +```typescript +import { LogoutManager } from '@multiversx/sdk-dapp/out/managers/LogoutManager/LogoutManager'; - // all properties are optional - + loginHandler: () => { + navigate('/dashboard'); + // optional, to stop the automatic logout upon native auth expiration + LogoutManager.getInstance().stop(); + }, ``` -- `SignTransactionsModals` will show a modal when a new transaction is submitted, prompting the user to verify and sign it. - -**Important! This is required** to make transactions work, except when you use hooks to sign the transactions manually (more on this below). +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. -```jsx -import { SignTransactionsModals } from "@multiversx/sdk-dapp/UI/SignTransactionsModals"; +```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 ; +}; - - - -; ``` -`NotificationModal` Will show a modal to the user with various warnings and errors. +#### 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`. -```jsx -import { NotificationModal } from "@multiversx/sdk-dapp/UI/NotificationModal"; +```typescript +import { ProviderTypeEnum } from '@multiversx/sdk-dapp/out/providers/types/providerFactory.types'; - - - -; +const provider = await ProviderFactory.create({ + type: ProviderTypeEnum.extension +}); +await provider.login(); ``` -If you want to show custom notifications, you can use the `useGetNotification` hook to get the notifications (like insufficient funds, errors etc). - -
+> **Note:** Extension and Ledger login will only work if dApp is served over HTTPS protocol [comment]: # (mx-context-auto) -## User Identity +### 3. Displaying app data -sdk-dapp makes logging in and persisting user's session easy and hassle-free. +Depending on the framework, you can either use hooks or selectors to get the user details: -A handy component is AuthenticatedRoutesWrapper, which can be used to protect certain routes and redirect the user to login page if the user is not authenticated. +#### 3.1 React hooks -Import from sdk-dapp: +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 -```jsx -import { AuthenticatedRoutesWrapper } from "@multiversx/sdk-dapp/wrappers/AuthenticatedRoutesWrapper"; -// or -import { AuthenticatedRoutesWrapper } from "@multiversx/sdk-dapp/wrappers"; +```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 ``` -Use with routes: +Below is an example of using the hooks to display the user address and balance. -```jsx - - {appContent} - -``` +```typescript +import { useGetAccount } from '@multiversx/sdk-dapp/out/react/account/useGetAccount'; +import { useGetNetworkConfig } from '@multiversx/sdk-dapp/out/react/network/useGetNetworkConfig'; -**routes** should be an array with objects with a signature similar to this: +const account = useGetAccount(); +const { + network: { egldLabel } +} = useGetNetworkConfig(); -```typescript -{ - path: "/dashboard", - title: "Dashboard", - component: Dashboard, - authenticatedRoute: true, -} +console.log(account.address); +console.log(`${account.balance} ${egldLabel}`); ``` -The important parts that makes this component work are the flag **authenticatedRoute: true** and the key **path**, which means that this route should be accessible only to authenticated users. +#### 3.2 Store selector functions: -
- - Login UI - +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. -[comment]: # (mx-context) +```typescript +import { getAccount } from '@multiversx/sdk-dapp/out/methods/account/getAccount'; +import { getNetworkConfig } from '@multiversx/sdk-dapp/out/methods/network/getNetworkConfig'; -**Login UI** +const account = getAccount(); +const { egldLabel } = getNetworkConfig(); +``` -There are a couple of very handy React components that can be used to login the user and protect certain routes if the user is not logged in. +In order to get live updates you may need to subscribe to the store like this: -Under the `DappUI` object mentioned above, you can find 4 buttons (one for each provider) which abstract away all the logic of logging in the user and render the default UI. These buttons can be easily customized with a custom css class. -The exported buttons are: +```typescript +import { createSignal, onCleanup } from 'solid-js'; +import { getStore } from '@multiversx/sdk-dapp/out/store/store'; -- ExtensionLoginButton -- WalletConnectLoginButton -- LedgerLoginButton -- WebWalletLoginButton +export function useStore() { + const store = getStore(); + const [state, setState] = createSignal(store.getState()); -example: + const unsubscribe = store.subscribe((newState) => { + setState(newState); + }); -```jsx - -``` + onCleanup(() => unsubscribe()); -They can also be used with children - -```jsx - - <> - -

Login text

- <> -
+ return state; +} ``` -`WalletConnectLoginButton` and `LedgerLoginButton` will trigger a modal with a QR code and the ledger login UI, respectively. -These are automatically triggered by the buttons. - -If, however, you want access to these containers without the buttons, -you can easily import and use them. - -```jsx - -``` +[comment]: # (mx-context-auto) -```jsx - -``` +### 4. Transactions -All login buttons and hooks accept a prop called `redirectAfterLogin` which specifies of the user should be redirected automatically after login. -The default value for this boolean is false, since most apps listen for the "isLoggedIn" boolean and redirect programmatically. +#### 4.1 Signing transactions -Another handly component is AuthenticatedRoutesWrapper, which can be used to protect certain routes and redirect the user to login page if the user is not authenticated. +To sign transactions, you first need to create the `Transaction` object, then pass it to the initialized provider. -Import from sdk-dapp: +```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 +}); -```jsx -import { AuthenticatedRoutesWrapper } from "@multiversx/sdk-dapp/wrappers/AuthenticatedRoutesWrapper"; +await refreshAccount(); // optionally, to get the latest nonce +const provider = getAccountProvider(); +const signedTransactions = await provider.signTransactions(transactions); ``` -Use with routes: +#### 4.2 Sending and tracking transactions -```jsx - - {appContent} - -``` +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. -**routes** should be an array with objects with a signature similar to this: +> **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 -{ - path: "/dashboard", - title: "Dashboard", - component: Dashboard, - authenticatedRoute: true, -} -``` +import { TransactionManager } from '@multiversx/sdk-dapp/out/managers/TransactionManager'; +import type { TransactionsDisplayInfoType } from '@multiversx/sdk-dapp/out/types/transactions.types'; -The important parts that makes this component work are the flag **authenticatedRoute: true** and the key **path**, which means that this route should be accessible only to authenticated users. -
+const txManager = TransactionManager.getInstance(); -
-Login hooks - - -[comment]: # (mx-context-auto) +const sentTransactions = await txManager.send(signedTransactions); -### Login hooks - -This area covers the login hooks, which expose a trigger function and the login data, ready to be rendered. - -These hooks are exposed as named exports, which can be imported from sdk-dapp: +const toastInformation: TransactionsDisplayInfoType = { + processingMessage: 'Processing transactions', + errorMessage: 'An error has occurred during transaction execution', + successMessage: 'Transactions executed' +} -```jsx -import { useExtensionLogin, useWalletConnectLogin, useLedgerLogin, useWebWalletLogin } from '@multiversx/sdk-dapp/hooks'; -// or -import { useExtensionLogin } from '@multiversx/sdk-dapp/hooks/login/useExtensionLogin'; -import { useWalletConnectLogin } from '@multiversx/sdk-dapp/hooks/login/useWebWalletLogin'; -import { useLedgerLogin } from '@multiversx/sdk-dapp/hooks/login/useLedgerLogin'; -import { useWebWalletLogin } from '@multiversx/sdk-dapp/hooks/login/useWebWalletLogin';` +const sessionId = await txManager.track(sentTransactions, { + transactionsDisplayInfo: toastInformation, +}); ``` -There are 4 available hooks: - -- useExtensionLogin -- useWalletConnectLogin -- useLedgerLogin -- useWebWalletLogin - -All hooks have the same response signature: - -return type is as follows: - -```jsx -const [initiateLogin, genericLoginReturnType, customLoginReturnType] = - useLoginHook({ - callbackRoute, - logoutRoute, - onLoginRedirect, - }); -``` +#### 4.3 Using the Notifications Feed -- **initiateLogin** is a function that needs to be called for the login flow to be initiated; -- **genericLoginReturnType** is an object that is exactly the same for all hooks: +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 -{ - error: string, - loginFailed: boolean, - isLoading: boolean, - isLoggedIn: boolean -} +const notificationManager = NotificationManager.getInstance(); +await notificationManager.openNotificationsFeed(); ``` -- **customLoginReturnType** is an object that is custom for each hook and returns specific data for that login: - - - null for useExtensionLogin; +#### 4.4 Inspecting transactions - - null for useWebWalletConnect; +You can find both methods and hooks to access transactions data, as seen in the table below. - - `{ uriDeepLink: string, qrCodeSvg: svgElement }` for useWalletConnectLogin; +**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 -{ - accounts: string[]; - showAddressList: boolean; - startIndex: number; - selectedAddress: SelectedAddress | null; - onGoToPrevPage: () => void; - onGoToNextPage: () => void; - onSelectAddress: (address: SelectedAddress | null) => void; - onConfirmSelectedAddress: () => void; -} -``` - -for useLedgerLogin; - -
- -
- -Reading User State - - -[comment]: # (mx-context-auto) - -### Reading User State - -Once logged in, the user's session is persisted and can be read and deleted via a couple of handy functions. - -For logging out, the library exposes a simple function called **logout**, which can be called to clear the user data. - -the function accepts 2 arguments: - -- `callbackUrl: string (optional)` the url to redirect the user to after logging him out -- `onRedirect: (callbackUrl: string) => void (optional)` a function that will be called instead of redirecting the user. - This allows you to control how the redirect is done, for example, with react-router-dom, instead of window.location.href assignment. - _Important_ this function will not be called for web wallet logout +import { + pendingTransactionsSessionsSelector, + transactionsSliceSelector +} from '@multiversx/sdk-dapp/out/store/selectors/transactionsSelector'; +import { getStore } from '@multiversx/sdk-dapp/out/store/store'; -You can opt-in for using the `useIdleTimer` hook, which logs out the user after a period of inactivity (default set to 10 minutes). Optionally it accepts an `onLogout` function that fulfills your dapp's specific logout business logic. Make sure to call the above `logout` function inside this `onLogout` callback. +const store = getStore(); // or use useStore hook for reactivity +const pendingSessions = pendingTransactionsSessionsSelector(store.getState()); +const allTransactionSessions = transactionsSliceSelector(store.getState()); -There are 2 ways of reading the user current state: hooks (to be used inside components and for reacting to changes in the data) and simple functions (for reading data outside of React components or inside handlers). +const isSessionIdPending = + Object.keys(pendingSessions).includes(sessionId); +const currentSession = allTransactionSessions[sessionId]; +const currentSessionStatus = currentSession?.status; +const currentTransaction = currentSession?.transactions?.[0]; +const currentTransactionStatus = currentTransaction?.status; +``` -- hooks: `useGetLoginInfo, useGetAccountInfo, useGetNetworkConfig`; -- functions: `getAccount, getAccountBalance, getAccountShard, getAddress, getIsLoggedIn;` +#### 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(); +``` [comment]: # (mx-context-auto) -## Transactions +## Internal structure -The sdk-dapp library exposes a straight-forward way of sending transactions and tracking their status, with a couple of handy UI components; +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 -
-Sending Transactions - +**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. | -[comment]: # (mx-context-auto) - -### Sending Transactions +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: -The API for sending transactions is a function called **sendTransactions**: -```jsx -import { sendTransactions } from "@multiversx/sdk-dapp"; -``` - -It can be used to send a transaction with minimum information: - -```jsx -const { sessionId, error } = await sendTransactions({ - transactions: [ - { - value: '1000000000000000000', - data: 'ping', - receiver: contractAddress - }, - ], - callbackRoute?: string // (optional, defaults to window.location.pathname) the route to be redirected to after signing. Will not redirect if the user is already on the specified route; - transactionsDisplayInfo: TransactionsDisplayInfoType // (optional, default to null) custom message for toasts texts; - minGasLimit?: number (optional, defaults to 50_000); - sessionInformation?: any (optional, defaults to null) extra sessionInformation that will be passed back to you via getSignedTransactions hook; - signWithoutSending?: boolean // (optional, defaults to false), the transaction will be signed without being sent to the blockchain; - completedTransactionsDelay?: number // delay the transaction status from going into "successful" state; - redirectAfterSigning?: boolean // (optional, defaults to true), whether to redirect to the provided callbackRoute; -}); +```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 ``` -It returns a Promise that will be fulfilled with `{error?: string; sessionId: string | null;}` - -`sessionId` is the transaction's batch id which can be used to track a transaction's status and react to it. +Conceptually, these can be split into 3 main parts: -**Important! For the transaction to be signed, you will have to use either `SignTransactionsModals` defined above, in the `Prerequisites` section, -or the `useSignTransactions` hook defined below. If you don't use one of these, the transactions won't be signed** +- 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` -
- -
-Transaction Signing Flow - +Next, we will take the elements from Table 3 and detail them in the following sections. [comment]: # (mx-context-auto) -### Transaction Signing Flow +### 1. Network -Once a transaction has been submitted, you have to use either the `SignTransactionsModals` or the `useSignTransactions` hook, -for the user to be prompted in his provider (Web Wallet, MultiversX Defi Wallet Extension, xPortal, Ledger etc) to sign the transaction. +The network configuration is done in the `initApp` method, where you can make several configurations like: -If you don't want to use the default modals that appear for the user when the signing process happens, -you have to use the `useSignTransactions` hook to sign those transactions. +- specifying the environment (`devnet`, `testnet`, `mainnet`) +- overriding certain network parameters like wallet address, explorer address etc. -```jsx -const { - callbackRoute, - transactions, - error, - sessionId, - onAbort, - hasTransactions, - canceledTransactionsMessage, -} = useSignTransactions(); -``` +Once the network is configured, the `network` slice in the store will hold the network configuration. -This hook will let you know if there are any transactions and you can programmatically abort the signing process. +To query different network parameters, you can use the `getNetworkConfig` method from the `methods/network` folder. -We suggest displaying a message on the screen that confirms the transaction that needs to be signed. - -You can also get the provider via - -```jsx -const { providerType, provider } = useGetAccountProvider(); -``` - -and use that to display an appropriate message to the user. +[comment]: # (mx-context-auto) -For ledger, signing a transaction is simple if you're using the `SignTransactionsModal` component. +### 2. Provider -It is fully customizable and will take care of walking the user through the signing flow. +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. -If, however, you want to implement a different experience, you will have to use the `useSignTransactionsWithLedger` hook. +#### Initialization -it accepts the following props: +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. -```typescript -{ - onCancel: () => void; -} -``` +#### Creating a custom provider -and returns an object with the following keys: +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 -{ - onSignTransaction: () => void; - onNext: () => void; - onPrev: () => void; - waitingForDevice: boolean; - onAbort: (e: React.MouseEvent) => void; - isLastTransaction: boolean; - currentStep: number; - signedTransactions?: Record; - currentTransaction: { - transaction: Transaction; - transactionTokenInfo: { - tokenId: string; - amount: string; - receiver: string; - type?: string; - nonce?: string; - multiTxData?: string; - }; - isTokenTransaction: boolean; - tokenDecimals: number; - dataField: string; - }; -} +const provider = await ProviderFactory.create({ + type: 'custom-provider' +}); +await provider?.login(); ``` -
+#### Accessing provider methods -
-Tracking a transaction - +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). [comment]: # (mx-context-auto) -### Tracking a transaction - -The library exposes a hook called useTrackTransactionStatus; - -```jsx -import {useTrackTransactionStatus} from @multiversx/sdk-dapp/hooks; +### 3. Account -const transactionStatus = useTrackTransactionStatus({ - transactionId: sessionId, - onSuccess, - onFail, - onCancelled, -}); -``` +#### Getting account data -transactionStatus has the following information about the transaction: +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: -```typescript -{ - isPending, - isSuccessful, - isFailed, - isCancelled, - errorMessage, - status, - transactions, -} -``` - -It's safe to pass in `null` as a sessionId, so if the transaction wasn't yet sent, the hook will just return an empty object. +**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 -
-Tracking transactions' statuses - +`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. [comment]: # (mx-context-auto) -### Tracking transactions' statuses - -sdk-dapp also exposes a number of handy hooks for tracking all, pending, failed, successful and timed out transactions. +### 4. Transactions Manager -Use: +#### Overview -- `useGetPendingTransactions` to get a list of all pending transactions. -- `useGetSuccessfulTransactions` to get a list of all successful transactions. -- `useGetFailedTransactions` to get a list of all failed transactions. +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()`. -An especially useful hook called `useGetActiveTransactionsStatus` will keep you updated with the status -of all transactions at a certain point in time. +#### Features -it's return signature is +- **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 -```typescript -{ - pending: boolean - at least one transaction is pending; - hasPendingTransactions: boolean - the user has at least 1 pending transaction active; - timedOut: boolean = there are no pending transactions and at least one has timed out; - fail: boolean - there are no pending and no timedOut transactions and at least one has failed; - success: boolean - all transactions are successful and all smart contract calls have been processed successfully; -} -``` +#### Transactions Lifecycle -
+The transaction lifecycle consists of the following steps: -
-Transaction Toasts UI - +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: -[comment]: # (mx-context-auto) +**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 -### Transaction Toasts UI +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`). -sdk-dapp also exposes a toast component for tracking transactions that uses the above mentioned hooks and displays toasts with transactions statuses. +**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()`| -The toasts list is exposed via **TransactionsToastList** UI component and can be used just by rendering it inside the application. -`TransactionToastList` component renders also custom toasts. A custom toast can be added using the util function: `addNewCustomToast` and can be removed using `deleteCustomToast` +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. -When `TransactionToastList` is also used for displaying custom toasts, is enough to call `addNewCustomToast` to add new custom toast to the list; +```ts +import { createRoot } from 'react-dom/client'; +import { createCustomToast } from '@multiversx/sdk-dapp/out/store/actions/toasts/toastsActions'; -```jsx - - - - -``` - -**Important**: This has to be inside the `` children. +// 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; + } +}); -In case you don't want to use `TransactionToastList` and just display a custom toast, then you have to import `CustomToast` component +// 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' +}); + -```jsx -const customToast = addNewCustomToast( - { - toastId: 'toast-id', - message: '', - type: 'custom', - duration: 2000 - } -); - deleteCustomToast(toastId) - /> ``` -
+6. **Error Handling & Recovery** is done through a custom toast that prompts the user to take appropriate action. -
-Removing transactions manually - +#### Methods +___ -[comment]: # (mx-context-auto) - -### Removing transactions manually +#### 1. Sending Transactions -sdk-dapp takes care to change transactions' statuses and removes them when needed, -but if you need to do this manually, you can use the exposed functions for this: +In this way, all transactions are sent simultaneously. There is no limit to the number of transactions contained in the array. ```typescript -removeTransactionsToSign(sessionId); -removeSignedTransaction(sessionId); -removeAllTransactionsToSign(); -removeAllSignedTransactions(); +const transactionManager = TransactionManager.getInstance(); +const parallelTransactions: SigendTransactionType[] = [tx1, tx2, tx3, tx4]; +const sentTransactions = await transactionManager.send(parallelTransactions); ``` -
- -[comment]: # (mx-context-auto) - -## Unit testing with Jest +#### 2. Sending Batch Transactions -The sdk-dapp library exposes bundles for both CommonJS and ESModules, however, in some environments, Jest might require manual mapping of the CommonJS output. To implement it, add the following snippet inside your jest config file. +In this sequential case, each batch waits for the previous one to complete. ```typescript -moduleNameMapper: { - '@multiversx/sdk-dapp/(.*)': - '/node_modules/@multiversx/sdk-dapp/__commonjs/$1.js' -} +const transactionManager = TransactionManager.getInstance(); +const batchTransactions: SignedTransactionType[][] = [ + [tx1, tx2], + [tx3, tx4] +]; +const sentTransactions = await transactionManager.send(batchTransactions); ``` -[comment]: # (mx-context-auto) - -## sdk-dapp exports - -Since version 2.0, sdk-dapp does not have a default export object. -You have to import everything from its own separate module. Below you can find all the exports. - -You can either import everything from a module, or if you really want to make sure you're not importing anything -that is not used, you can import everything from its own file. +#### 3. Tracking Transactions -You can either go into their specific folder in the module for extra trimming, or import everything together. +The basic option is to use the built-in tracking, which displays toast notifications with default messages. -for example, these 2 imports are both valid: +```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' + } +}; -```jsx -import { - useExtensionLogin, - useGetAccountInfo, -} from "@multiversx/sdk-dapp/hooks"; +const sessionId = await transactionManager.track( + sentTransactions, + options // optional +); ``` -and +If you want to provide more human-friendly messages to your users, you can enable tracking with custom toast messages: -```jsx -import { useExtensionLogin } from "@multiversx/sdk-dapp/hooks/login"; -import { useGetAccountInfo } from "@multiversx/sdk-dapp/hooks/account"; +```typescript +const sessionId = await transactionManager.track(sentTransactions, { + transactionsDisplayInfo: { + errorMessage: 'Failed adding stake', + successMessage: 'Stake successfully added', + processingMessage: 'Staking in progress' + } +}); ``` -[comment]: # (mx-context-auto) - -### constants exports +#### 3.1 Tracking transactions without being logged in -```jsx -import { - GAS_PRICE_MODIFIER, - GAS_PER_DATA_BYTE, - GAS_LIMIT, - GAS_PRICE, - DECIMALS, - DIGITS, - mnemonicWords, - ledgerErrorCodes, - fallbackNetworkConfigurations, -} from "@multiversx/sdk-dapp/constants"; -``` - -[comment]: # (mx-context-auto) +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. -### hooks exports +Step 1. Enabling the tracking mechanism -[comment]: # (mx-context-auto) +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. -#### Login +```typescript +import { trackTransactions } from '@multiversx/sdk-dapp/out/methods/trackTransactions/trackTransactions'; -```jsx -import { - useExtensionLogin, - useLedgerLogin, - useWalletConnectLogin, - useWebWalletLogin, -} from "@multiversx/sdk-dapp/hooks/login"; +initApp(config).then(async () => { + await trackTransactions(); // enable here since by default tracking will be enabled only after login + render(() => , root!); +}); ``` -[comment]: # (mx-context-auto) +Step 2. Tracking transactions -#### Account +Then, you can track transactions by calling the `track` method from the `TransactionManager` class with a plain transaction containing the transaction hash. -```jsx -import { - useGetAccountInfo, - useGetAccountProvider, - useGetLoginInfo, -} from "@multiversx/sdk-dapp/hooks/accounts"; -``` +```typescript +import { Transaction, TransactionsConverter } from '@multiversx/sdk-core'; -[comment]: # (mx-context-auto) +const tManager = TransactionManager.getInstance(); +const txConverter = new TransactionsConverter(); +const transaction = txConverter.plainObjectToTransaction(signedTx); -#### Transactions +const hash = transaction.getHash().toString(); // get the transaction hash -```jsx -import { - useCheckTransactionStatus, - useGetActiveTransactionsStatus, - useGetFailedTransactions, - useGetPendingTransactions, - useGetSignedTransactions, - useGetSignTransactionsError, - useGetSuccessfulTransactions, - useGetTokenDetails, - useGetTransactionDisplayInfo, - useParseMultiEsdtTransferData, - useParseSignedTransactions, - useSignMultipleTransactions, - useSignTransactions, - useSignTransactionsWithDevice, - useSignTransactionsWithLedger, -} from "@multiversx/sdk-dapp/hooks/transactions"; +const plainTransaction = { ...transaction.toPlainObject(), hash }; +await tManager.track([plainTransaction]); ``` -[comment]: # (mx-context-auto) - -#### Misc - -```jsx -import { - useDebounce, - useGetNetworkConfig, - useGetNotification, - useUpdateEffect, -} from "@multiversx/sdk-dapp/hooks"; -``` +#### 3.2 Advanced Usage -[comment]: # (mx-context-auto) +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. -### services exports +```typescript +import { getStore } from '@multiversx/sdk-dapp/out/store/store'; +import { transactionsSliceSelector } from '@multiversx/sdk-dapp/out/store/selectors/transactionsSelector'; -```jsx -import { - removeTransactionsToSign, - removeSignedTransaction, - removeAllSignedTransactions, - removeAllTransactionsToSign, - isCrossShardTransaction, - sendTransactions, - signTransactions, - calcTotalFee, -} from "@multiversx/sdk-dapp/services"; +const state = transactionsSliceSelector(getStore()); +Object.entries(state).forEach(([sessionKey, data]) => { + if (sessionKey === sessionId) { + console.log(data.status); + } +}); ``` [comment]: # (mx-context-auto) -### utils exports - -[comment]: # (mx-context-auto) - -#### Account +### 5. UI Components -```jsx -import { - addressIsValid, - getAccount, - getAccountBalance, - getAccountShard, - getAddress, - getLatestNonce, - getShardOfAddress, - refreshAccount, - setNonce, - signMessage, -} from "@multiversx/sdk-dapp/utils/account"; -``` +`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. -[comment]: # (mx-context-auto) +#### 5.1 Public components -#### Operations +The business logic for these components is served by a controller. The components are: -```jsx -import { - calculateFeeLimit, - formatAmount, - nominate, - getUsdValue, -} from "@multiversx/sdk-dapp/utils/operations"; -``` +- `MvxTransactionsTable` - used to display the user's transactions -[comment]: # (mx-context-auto) +```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 + }); -#### Transactions +// and use like this: +; -```jsx -import { - getTokenFromData, - isTokenTransfer, - parseMultiEsdtTransferData, - parseTransactionAfterSigning, -} from "@multiversx/sdk-dapp/utils/transactions"; ``` -[comment]: # (mx-context-auto) +- `MvxFormatAmount` - used to format the amount of the user's balance -#### Validation -```jsx -import { - getIdentifierType, - stringIsFloat, - stringIsInteger, - isContract, - isStringBase64, -} from "@multiversx/sdk-dapp/utils"; -``` - -[comment]: # (mx-context-auto) +```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'; -#### Misc -```jsx -import { - encodeToBase64, - decodeBase64, - logout, - getTokenFromData, - getIsLoggedIn, - isSelfESDTContract, - getAddressFromDataField, -} from "@multiversx/sdk-dapp/utils"; -``` +interface IFormatAmountProps + extends Partial { + value: string; + className?: string; +} -[comment]: # (mx-context-auto) +export const FormatAmount = (props: IFormatAmountProps) => { + const { + network: { egldLabel } + } = useGetNetworkConfig(); -### Wrappers + const { isValid, valueDecimal, valueInteger, label } = + FormatAmountController.getData({ + digits: DIGITS, + decimals: DECIMALS, + egldLabel, + ...props, + input: props.value + }); -```jsx -import { - DappProvider, - AuthenticatedRoutesWrapper, - AppInitializer, -} from "@multiversx/sdk-dapp/wrappers"; + return ( + + ); +}; ``` -[comment]: # (mx-context-auto) - -### Web-specific imports -```jsx -import { useIdleTimer } from "@multiversx/sdk-dapp/web"; -``` - -[comment]: # (mx-context-auto) +#### 5.2 Internal components (advanced usage) -### UI +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. -```jsx -import { - CopyButton, - FormatAmount, - ExplorerLink, - ExtensionLoginButton, - LedgerLoginButton, - LedgerLoginContainer, - NotificationModal, - PageState, - ProgressSteps, - SignTransactionsModals, - SignWithDeviceModal, - SignWithExtensionModal, - SignWithLedgerModal, - TransactionsToastList, - TransactionToast, - Trim, - UsdValue, - WalletConnectLoginButton, - WalletConnectLoginContainer, -} from "@multiversx/sdk-dapp/UI"; +```mermaid +flowchart LR + A["Controller"] <--> B["Event Bus"] <--> C["webcomponent"] ``` -or - -```jsx -import { CopyButton } from "@multiversx/sdk-dapp/UI/CopyButton"; -import { FormatAmount } from "@multiversx/sdk-dapp/UI/FormatAmount"; -import { ExplorerLink } from "@multiversx/sdk-dapp/UI/ExplorerLink"; +```typescript +import { ComponentFactory } from '@multiversx/sdk-dapp/out/utils/ComponentFactory'; -// etc; +const modalElement = await ComponentFactory.create( + 'mvx-ledger-connect-panel' +); +const eventBus = await modalElement.getEventBus(); +eventBus.publish('TRANSACTION_TOAST_DATA_UPDATE', someData); ``` -**Important**: `shouldRenderDefaultCss` was removed from all components. +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)). [comment]: # (mx-context-auto) -## WalletConnect Setup - -Starting with the 2.0 version of the dApp SDK ( previously `@elrondnetwork/dapp-core@2.0.0` ) and `@multiversx/sdk-dapp@2.2.8` [WalletConnect 2.0](https://docs.walletconnect.com/2.0/) is available as a login and signing provider, allowing users to login by scanning a QR code with the Mobile App +## Debugging your dApp -This is an implementation of the [sdk-wallet-connect-provider](https://github.com/multiversx/mx-sdk-js-wallet-connect-provider) ( [docs](https://docs.multiversx.com/sdk-and-tools/sdk-js/sdk-js-signing-providers/#the-walletconnect-provider) ) signing provider +> **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. -A `Project ID` is required to enable the WalletConnect functionality. - -[comment]: # (mx-context-auto) +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. -### Set the Project ID +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: -In the [DappProvider](#dappprovider) wrapper a `walletConnectV2ProjectId` must be provided in the `customNetworkConfig` - -The Project ID can be generated for free here: [https://cloud.walletconnect.com/sign-in](https://cloud.walletconnect.com/sign-in) - -```jsx - +```js + resolve: { + preserveSymlinks: true, // 👈 + alias: { + src: "/src", + }, + }, ``` -The WalletConnect Project ID grants you access to the [WalletConnect Cloud Relay](https://docs.walletconnect.com/2.0/cloud/relay) that securely manages communication between the device and the dApp. +Crome Redux DevTools are by default enabled to help you debug your application. You can find the extension in the Chrome Extensions store. -If the Project ID is valid, the new functionality will work out of the box with the [Transactions and Message signing](#transactions) flows. +To build the library, run: -[comment]: # (mx-context-auto) - -## React Native support - -We are aware that there are projects out there that would like to use this library to allow users to seamlessly authenticate with the xPortal App. - -You can use this library for its utility functions, like "formatAmount, parseAmount", mnemonic words list or its constants. +```bash +npm run build +``` -However, certain architectural decisions that we made do not work out of the box with React Native runtime (neither Metro nor Re.pack). -Due to this, you cannot yet use the DappProvider wrapping logic in a React Native application. +To run the unit tests, run: -We have a couple of solutions in mind and are actively working on exploring ways to overcome these limitations. -Until then, you can use `@multiversx/sdk-*` libraries and @walletconnect to connect to the xPortal App. -There are also guide for doing this from the [community](https://github.com/S4F-IT/maiar-integration/blob/master/README) +```bash +npm test +``` \ No newline at end of file From 9bbf5bc3815e380bc31b176e7f4e236a7447be42 Mon Sep 17 00:00:00 2001 From: danielailie Date: Tue, 29 Jul 2025 11:53:08 +0300 Subject: [PATCH 2/4] Update cookbook for js --- docs/developers/relayed-transactions.md | 3 +- .../sdk-js/sdk-js-cookbook-v13.md | 1089 ------ .../sdk-js/sdk-js-cookbook-v14.md | 4 +- .../sdk-js/sdk-js-cookbook-v15.md | 3468 +++++++++++++++++ sidebars.js | 2 +- 5 files changed, 3472 insertions(+), 1094 deletions(-) delete mode 100644 docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v13.md create mode 100644 docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v15.md diff --git a/docs/developers/relayed-transactions.md b/docs/developers/relayed-transactions.md index a94243429..2290483c1 100644 --- a/docs/developers/relayed-transactions.md +++ b/docs/developers/relayed-transactions.md @@ -116,5 +116,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 v14 support](/sdk-and-tools/sdk-js/sdk-js-cookbook#relayed-transactions) - - [sdk-js v13 support (legacy)](/sdk-and-tools/sdk-js/sdk-js-cookbook-v13#preparing-a-relayed-transaction) + - [sdk-js v15 support](/sdk-and-tools/sdk-js/sdk-js-cookbook#relayed-transactions) diff --git a/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v13.md b/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v13.md deleted file mode 100644 index 9c87f5508..000000000 --- a/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v13.md +++ /dev/null @@ -1,1089 +0,0 @@ ---- -id: sdk-js-cookbook-v13 -title: Cookbook (v13) -pagination_prev: sdk-and-tools/sdk-js/sdk-js -pagination_next: null ---- - -[comment]: # (mx-abstract) - -This page will guide you through the process of handling common tasks using **sdk-js v13 (legacy, previous version)**. - -:::important -A newer variant of the **sdk-js** cookbook is available [here](/sdk-and-tools/sdk-js/sdk-js-cookbook-v13). -::: - -:::important -In order to migrate to the newer `sdk-js v14`, please follow [the migration guide](https://github.com/multiversx/mx-sdk-js-core/issues/576). -::: - -## Creating network providers - -Creating an API provider: - -```js -import { ApiNetworkProvider } from "@multiversx/sdk-core"; - -const apiNetworkProvider = new ApiNetworkProvider("https://devnet-api.multiversx.com", { clientName: "multiversx-your-client-name" }); -``` - -Creating a Proxy provider: - -```js -import { ProxyNetworkProvider } from "@multiversx/sdk-core"; - -const proxyNetworkProvider = new ProxyNetworkProvider("https://devnet-gateway.multiversx.com", { clientName: "multiversx-your-client-name" }); -``` - -Use the classes from `@multiversx/sdk-core/out/networkProviders` **only as a starting point**. -As your dApp matures, make sure you **switch to using your own network provider**, tailored to your requirements -(whether deriving from the default ones or writing a new one, from scratch) that directly interacts with the MultiversX API (or Gateway). - -On this topic, please see [extending sdk-js](/sdk-and-tools/sdk-js/extending-sdk-js). - -## Fetching network parameters - -```js -const networkConfig = await apiNetworkProvider.getNetworkConfig(); -console.log(networkConfig.MinGasPrice); -console.log(networkConfig.ChainID); -``` - -## Working with accounts - -### Synchronizing an account object - -The following snippet fetches (from the Network) the **nonce** and the **balance** of an account, and updates the local representation of the account. - -```js -import { Account } from "@multiversx/sdk-core"; - -const alice = new Account(addressOfAlice); -const aliceOnNetwork = await apiNetworkProvider.getAccount(addressOfAlice); -alice.update(aliceOnNetwork); - -console.log("Nonce:", alice.nonce); -console.log("Balance:", alice.balance.toString()); -``` - -### Managing the sender nonce locally - -When sending a bunch of transactions, you usually have to first fetch the account nonce from the network (see above), then manage it locally (e.g. increment upon signing & broadcasting a transaction): - -```js -alice.incrementNonce(); -console.log("Nonce:", alice.nonce); -``` - -:::note -Since `sdk-core v13`, the [`Transaction`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/Transaction.html) class exhibits its state as public read-write properties. For example, you can access and set the `nonce` property, instead of using `getNonce` and `setNonce`. -::: - -If you are using `sdk-core v13` or later, use `tx.nonce = ` to apply the nonce to a transaction. -For `sdk-core v12` or earlier, use the legacy `tx.setNonce()` to apply the nonce to a transaction. - -```js -notYetSignedTx.nonce = alice.getNonceThenIncrement(); -``` - -For further reference, please see [nonce management](/integrators/creating-transactions/#nonce-management). - -## Broadcasting transactions - -### Preparing a simple transaction - -:::note -Since `sdk-core v13`, the [`Transaction`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/Transaction.html) class exhibits its state as public read-write properties. For example, you can access and set the `nonce` property, instead of using `getNonce` and `setNonce`. -::: - -```js -import { Transaction } from "@multiversx/sdk-core"; - -const tx = new Transaction({ - data: Buffer.from("food for cats"), - gasLimit: 70000n, - sender: addressOfAlice.toBech32(), - receiver: addressOfBob.toBech32(), - value: 1000000000000000000n, - chainID: "D" -}); - -tx.nonce = 42n; -``` - -### Preparing a relayed transaction - -We are currently on the third iteration of relayed transactions. V1 and V2 are soon to be deactivated so we'll focus on V3. -For V3, two new fields have been added on transactions: `relayer` and `relayerSignature`. -Before the sender signs the transaction, the relayer needs to be set. After the sender has signed the transaction, the relayer can also sign the transaction and broadcast it. -Keep in mind that, for relayed V3 transactions we need an extra 50_000 gas. Let's see how we can create a relayed transaction: - -```js -import { Transaction } from "@multiversx/sdk-core"; - -const grace = await loadTestWallet("grace"); - -# alice will be our relayer, that means she is paying the gas for the transaction -const alice = await loadTestWallet("alice"); -const transactionComputer = new TransactionComputer(); - -# fetch the sender nonce of the network -const nonce = (await apiProvider.getAccount(grace.getAddress())).nonce; -# create the transaction -const transaction = new Transaction({ - receiver: grace.getAddress().bech32(), - sender: grace.getAddress().bech32(), - gasPrice: BigInt(1000000000), - gasLimit: BigInt(150000), - chainID: "D", - nonce: BigInt(nonce), - relayer: alice.getAddress(), - value: BigInt(1), -}); - -# sender signs the transaction -transaction.signature = await grace.signer.sign(transactionComputer.computeBytesForSigning(transaction)); -const buffer = transactionComputer.computeBytesForSigning(transaction); - -# relayer signs the transaction -const signature = await alice.signer.sign(Buffer.from(buffer)); -transaction.relayerSignature = signature; - -# broadcast the transaction -await proxyProvider.sendTransaction(transaction); -``` - -### Signing a transaction - -:::important -Note that the transactions **must be signed before being broadcasted**. -On the front-end, signing can be achieved using a signing provider. -On this purpose, **we recommend using [sdk-dapp](/sdk-and-tools/sdk-dapp)** instead of integrating the signing providers on your own. -::: - -:::important -For the sake of simplicity, in this section we'll use a `UserSigner` object to sign the transaction. -In real-world dApps, transactions are signed by end-users using their wallet, through a [signing provider](/sdk-and-tools/sdk-js/sdk-js-signing-providers). -::: - -```js -import { TransactionComputer, UserSigner } from "@multiversx/sdk-core"; -import { promises } from "fs"; - -const fileContent = await promises.readFile("../testwallets/alice.json", { encoding: "utf8" }); -const walletObject = JSON.parse(fileContent); -const signer = UserSigner.fromWallet(walletObject, "password"); - -const computer = new TransactionComputer(); -const serializedTx = computer.computeBytesForSigning(tx); - -tx.signature = await signer.sign(serializedTx); -``` - -### Broadcast using a network provider - -In order to broadcast a transaction, use a network provider: - -```js -const txHash = await apiNetworkProvider.sendTransaction(readyToBroadcastTx); -console.log("TX hash:", txHash); -``` - -### Wait for transaction completion - -```js -import { TransactionWatcher } from "@multiversx/sdk-core"; - -const watcherUsingApi = new TransactionWatcher(apiNetworkProvider); -const transactionOnNetworkUsingApi = await watcherUsingApi.awaitCompleted(txHash); -``` - -If, instead, you use a `ProxyNetworkProvider` to instantiate the [`TransactionWatcher`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TransactionWatcher.html), you'll need to patch the `getTransaction` method, -so that it instructs the network provider to fetch the so-called _processing status_, as well (required by the watcher to detect transaction completion). - -```js -const watcherUsingProxy = new TransactionWatcher({ - getTransaction: async (hash) => { - return await proxyNetworkProvider.getTransaction(hash, true); - } -}); - -const transactionOnNetworkUsingProxy = await watcherUsingProxy.awaitCompleted(txHash); -``` - -In order to wait for multiple transactions: - -```js -await Promise.all([ - watcherUsingApi.awaitCompleted(txHash1), - watcherUsingApi.awaitCompleted(txHash2), - watcherUsingApi.awaitCompleted(txHash3) -]); -``` - -In some circumstances, when awaiting for a transaction completion in order to retrieve its logs and events, -it's possible that these pieces of information are missing at the very moment the transaction is marked as completed - -they may not be immediately available. - -If that is an issue, you can configure the [`TransactionWatcher`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TransactionWatcher.html) to have additional **patience** -before returning the transaction object. Below, we're adding a patience of 8 seconds: - -```js -const watcherWithPatience = new TransactionWatcher(apiNetworkProvider, { patienceMilliseconds: 8000 }); -``` - -Alternatively, use [`TransactionWatcher.awaitAnyEvent()`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TransactionWatcher.html#awaitAnyEvent) or [`TransactionWatcher.awaitOnCondition()`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TransactionWatcher.html#awaitOnCondition) to customize the waiting strategy. - -For a different awaiting strategy, also see [extending sdk-js](/sdk-and-tools/sdk-js/extending-sdk-js). - -## Token transfers - -Generally speaking, in order to create transactions that transfer native tokens or ESDT tokens, one should use the [`TransferTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TransferTransactionsFactory.html) class. - -:::note -In `sdk-core v13`, the [`TransferTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TransferTransactionsFactory.html) class was extended with new methods, -to be aligned with the [SDKs specs](https://github.com/multiversx/mx-sdk-specs/blob/main/core/transactions-factories/transfer_transactions_factory.md). -The old, legacy methods are still available (see below), thus existing client code isn't affected. -::: - -:::note -In `sdk-core v13`, the [`TokenTransfer`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TokenTransfer.html) class has changed, in a non-breaking manner. -Though, from now on, it should only be used for preparing ESDT token transfers, not native EGLD transfers. - -A [`TokenTransfer`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TokenTransfer.html) object can still be instantiated using the legacy methods, e.g. `fungibleFromAmount`, `nonFungible` (which are still available), -but we recommend using the new approach instead (which, among others, makes abstraction of the number of decimals a token has). -::: - -:::tip -For formatting or parsing token amounts, see [formatting and parsing amounts](#formatting-and-parsing-amounts). -::: - -First, let's create a [`TransferTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TransferTransactionsFactory.html): - -```js -import { Token, TokenTransfer, TransactionsFactoryConfig, TransferTransactionsFactory } from "@multiversx/sdk-core"; - -// The new approach of creating a "TransferTransactionsFactory": -const factoryConfig = new TransactionsFactoryConfig({ chainID: "D" }); -const factory = new TransferTransactionsFactory({ config: factoryConfig }); -``` - -Now, we can use the factory to create transfer transactions. - -### **EGLD** transfers (value movements) - -```js -const tx1 = factory.createTransactionForNativeTokenTransfer({ - sender: addressOfAlice, - receiver: addressOfBob, - // 1 EGLD - nativeAmount: BigInt("1000000000000000000") -}); - -tx1.nonce = 42n; -``` - -### Single ESDT transfer - -```js -const tx2 = factory.createTransactionForESDTTokenTransfer({ - sender: addressOfAlice, - receiver: addressOfBob, - tokenTransfers: [ - new TokenTransfer({ - token: new Token({ identifier: "TEST-8b028f" }), - amount: 10000n - }) - ] -}); - -tx2.nonce = 43n; -``` - -### Single NFT transfer - -```js -const tx3 = factory.createTransactionForESDTTokenTransfer({ - sender: addressOfAlice, - receiver: addressOfBob, - tokenTransfers: [ - new TokenTransfer({ - token: new Token({ identifier: "TEST-38f249", nonce: 1n }), - amount: 1n - }) - ] -}); - -tx3.nonce = 44n; -``` - -### Single SFT transfer - -```js -const tx4 = factory.createTransactionForESDTTokenTransfer({ - sender: addressOfAlice, - receiver: addressOfBob, - tokenTransfers: [ - new TokenTransfer({ - token: new Token({ identifier: "SEMI-9efd0f", nonce: 1n }), - amount: 5n - }) - ] -}); - -tx4.nonce = 45n; -``` - -### Multi ESDT / NFT transfer - -```js -const tx5 = factory.createTransactionForESDTTokenTransfer({ - sender: addressOfAlice, - receiver: addressOfBob, - tokenTransfers: [ - new TokenTransfer({ - token: new Token({ identifier: "TEST-8b028f" }), - amount: 10000n - }), - new TokenTransfer({ - token: new Token({ identifier: "TEST-38f249", nonce: 1n }), - amount: 1n - }), - new TokenTransfer({ - token: new Token({ identifier: "SEMI-9efd0f", nonce: 1n }), - amount: 5n - }) - ] -}); - -tx5.nonce = 46n; -``` - -## Formatting and parsing amounts - -:::note -For formatting or parsing token amounts as numbers (with fixed number of decimals), please do not rely on `sdk-core`. Instead, use `sdk-dapp` (higher level) or `bignumber.js` (lower level). -::: - -You can format amounts using `formatAmount` from `sdk-dapp`: - -```js -import { formatAmount } from '@multiversx/sdk-dapp/utils/operations'; - -console.log("Format using sdk-dapp:", formatAmount({ - input: "1500000000000000000", - decimals: 18, - digits: 4 -})); -``` - -Or directly using `bignumber.js`: - -```js -import BigNumber from "bignumber.js"; - -BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_FLOOR }); - -console.log("Format using bignumber.js:", new BigNumber("1500000000000000000").shiftedBy(-18).toFixed(4)); -``` - -You can parse amounts using `parseAmount` from `sdk-dapp`: - -```js -import { formatAmount } from '@multiversx/sdk-dapp/utils/operations'; - -console.log("Parse using sdk-dapp:", parseAmount("1.5", 18)); -``` - -Or directly using `bignumber.js`: - -```js -console.log("Parse using bignumber.js:", new BigNumber("1.5").shiftedBy(18).decimalPlaces(0).toFixed(0)); -``` - -## Contract ABIs - -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. - -### Load the ABI from a file - -```js -import { AbiRegistry } from "@multiversx/sdk-core"; -import { promises } from "fs"; - -let abiJson = await promises.readFile("../contracts/adder.abi.json", { encoding: "utf8" }); -let abiObj = JSON.parse(abiJson); -let abi = AbiRegistry.create(abiObj); -``` - -### Load the ABI from an URL - -```js -import axios from "axios"; - -const response = await axios.get("https://github.com/multiversx/mx-sdk-js-core/raw/main/src/testdata/adder.abi.json"); -abi = AbiRegistry.create(response.data); -``` - -### 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. Let's see a simple example: - -```js -abi = AbiRegistry.create({ - "endpoints": [{ - "name": "add", - "inputs": [], - "outputs": [] - }] -}); -``` - -An endpoint with both inputs and outputs: - -```js -abi = AbiRegistry.create({ - "endpoints": [ - { - "name": "foo", - "inputs": [ - { "type": "BigUint" }, - { "type": "u32" }, - { "type": "Address" } - ], - "outputs": [ - { "type": "u32" } - ] - }, - { - "name": "bar", - "inputs": [ - { "type": "counted-variadic" }, - { "type": "variadic" } - ], - "outputs": [] - } - ] -}); -``` - -## Contract deployments - -### Load the bytecode from a file - -```js -import { Code } from "@multiversx/sdk-core"; - -const codeBuffer = await promises.readFile("../contracts/adder.wasm"); -const code = Code.fromBuffer(codeBuffer); -``` - -### Perform a contract deployment - -In `sdk-core v13`, the recommended way to create transactions for deploying -(and, for that matter, upgrading and interacting with) -smart contracts is through a [`SmartContractTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/SmartContractTransactionsFactory.html). - -The older (legacy) approach, using the method [`SmartContract.deploy()`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/SmartContract.html#deploy), is still available, however. -At some point in the future, [`SmartContract.deploy()`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/SmartContract.html#deploy) will be deprecated and removed. - -Now, let's create a [`SmartContractTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/SmartContractTransactionsFactory.html): - -```js -import { SmartContractTransactionsFactory, TransactionsFactoryConfig } from "@multiversx/sdk-core"; - -const factoryConfig = new TransactionsFactoryConfig({ chainID: "D" }); - -let factory = new SmartContractTransactionsFactory({ - config: factoryConfig -}); -``` - -If the contract ABI is available, provide it to the factory: - -```js -factory = new SmartContractTransactionsFactory({ - config: factoryConfig, - abi: abi -}); -``` - -Now, prepare the deploy transaction: - -```js -import { U32Value } from "@multiversx/sdk-core"; - -// For deploy arguments, use "TypedValue" objects if you haven't provided an ABI to the factory: -let args = [new U32Value(42)]; -// Or use simple, plain JavaScript values and objects if you have provided an ABI to the factory: -args = [42]; - -const deployTransaction = factory.createTransactionForDeploy({ - sender: addressOfAlice, - bytecode: code.valueOf(), - gasLimit: 6000000n, - arguments: args -}); -``` - -:::tip -When creating transactions using [`SmartContractTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/SmartContractTransactionsFactory.html), even if the ABI is available and provided, -you can still use [`TypedValue`](https://multiversx.github.io/mx-sdk-js-core/v13/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/v13/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")]; -``` - -::: - -Then, as [previously seen](#working-with-accounts), set the transaction nonce (the account nonce must be synchronized beforehand). - -```js -deployTransaction.nonce = deployer.getNonceThenIncrement(); -``` - -Now, **sign the transaction** using a wallet / signing provider of your choice. - -:::important -For the sake of simplicity, in this section we'll use a `UserSigner` object to sign the transaction. -In real-world dApps, transactions are signed by end-users using their wallet, through a [signing provider](/sdk-and-tools/sdk-js/sdk-js-signing-providers). -::: - -```js -const fileContent = await promises.readFile("../testwallets/alice.json", { encoding: "utf8" }); -const walletObject = JSON.parse(fileContent); -const signer = UserSigner.fromWallet(walletObject, "password"); - -const computer = new TransactionComputer(); -const serializedTx = computer.computeBytesForSigning(deployTransaction); - -deployTransaction.signature = await signer.sign(serializedTx); -``` - -Then, broadcast the transaction and await its completion, as seen in the section [broadcasting transactions](#broadcasting-transactions): - -```js -const txHash = await apiNetworkProvider.sendTransaction(deployTransaction); -const transactionOnNetwork = await new TransactionWatcher(apiNetworkProvider).awaitCompleted(txHash); -``` - -### Computing the contract address - -Even before broadcasting, -at the moment you know the _sender_ address and the _nonce_ for your deployment transaction, you can (deterministically) compute the (upcoming) address of the contract: - -```js -import { AddressComputer } from "@multiversx/sdk-core"; - -const addressComputer = new AddressComputer(); -const contractAddress = addressComputer.computeContractAddress( - Address.fromBech32(deployTransaction.sender), - deployTransaction.nonce -); - -console.log("Contract address:", contractAddress.bech32()); -``` - -### Parsing transaction outcome - -In the end, you can parse the results using a [`SmartContractTransactionsOutcomeParser`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/SmartContractTransactionsOutcomeParser.html). -However, since the `parseDeploy` method requires a [`TransactionOutcome`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TransactionOutcome.html) object as input, -we need to first convert our `TransactionOnNetwork` object to a [`TransactionOutcome`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TransactionOutcome.html), by means of a [`TransactionsConverter`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TransactionsConverter.html). - -```js -import { SmartContractTransactionsOutcomeParser, TransactionsConverter } from "@multiversx/sdk-core"; - -const converter = new TransactionsConverter(); -const parser = new SmartContractTransactionsOutcomeParser(); - -const transactionOutcome = converter.transactionOnNetworkToOutcome(transactionOnNetwork); -const parsedOutcome = parser.parseDeploy({ transactionOutcome }); - -console.log(parsedOutcome); -``` - -## Contract interactions - -In `sdk-core v13`, the recommended way to create transactions for calling -(and, for that matter, deploying and upgrading) -smart contracts is through a [`SmartContractTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/SmartContractTransactionsFactory.html). - -The older (legacy) approaches, using `SmartContract.call()`, `SmartContract.methods.myFunction()`, `SmartContract.methodsExplicit.myFunction()` and -`new Interaction(contract, "myFunction", args)` are still available. -However, at some point in the (more distant) future, they will be deprecated and removed. - -Now, let's create a [`SmartContractTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/SmartContractTransactionsFactory.html): - -```js -import { SmartContractTransactionsFactory, TransactionsFactoryConfig } from "@multiversx/sdk-core"; - -const factoryConfig = new TransactionsFactoryConfig({ chainID: "D" }); - -let factory = new SmartContractTransactionsFactory({ - config: factoryConfig -}); -``` - -If the contract ABI is available, provide it to the factory: - -```js -factory = new SmartContractTransactionsFactory({ - config: factoryConfig, - abi: abi -}); -``` - -### Regular interactions - -Now, let's prepare a contract transaction, to call the `add` function of our -previously deployed smart contract: - -```js -import { U32Value } from "@multiversx/sdk-core"; - -// For arguments, use "TypedValue" objects if you haven't provided an ABI to the factory: -let args = [new U32Value(42)]; -// Or use simple, plain JavaScript values and objects if you have provided an ABI to the factory: -args = [42]; - -const transaction = factory.createTransactionForExecute({ - sender: addressOfAlice, - contract: Address.fromBech32("erd1qqqqqqqqqqqqqpgq6qr0w0zzyysklfneh32eqp2cf383zc89d8sstnkl60"), - function: "add", - gasLimit: 5000000, - arguments: args -}); -``` - -:::tip -When creating transactions using [`SmartContractTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/SmartContractTransactionsFactory.html), even if the ABI is available and provided, -you can still use [`TypedValue`](https://multiversx.github.io/mx-sdk-js-core/v13/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/v13/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")]; -``` - -::: - -Then, as [previously seen](#working-with-accounts), set the transaction nonce (the account nonce must be synchronized beforehand). - -```js -transaction.nonce = alice.getNonceThenIncrement(); -``` - -Now, **sign the transaction** using a wallet / signing provider of your choice. - -:::important -For the sake of simplicity, in this section we'll use a `UserSigner` object to sign the transaction. -In real-world dApps, transactions are signed by end-users using their wallet, through a [signing provider](/sdk-and-tools/sdk-js/sdk-js-signing-providers). -::: - -```js -const fileContent = await promises.readFile("../testwallets/alice.json", { encoding: "utf8" }); -const walletObject = JSON.parse(fileContent); -const signer = UserSigner.fromWallet(walletObject, "password"); - -const computer = new TransactionComputer(); -const serializedTx = computer.computeBytesForSigning(transaction); - -transaction.signature = await signer.sign(serializedTx); -``` - -Then, broadcast the transaction and await its completion, as seen in the section [broadcasting transactions](#broadcasting-transactions): - -```js -const txHash = await apiNetworkProvider.sendTransaction(transaction); -const transactionOnNetwork = await new TransactionWatcher(apiNetworkProvider).awaitCompleted(txHash); -``` - -### Transfer & execute - -At times, you may want to send some tokens (native EGLD or ESDT) along with the contract call. - -For transfer & execute with native EGLD, prepare your transaction as follows: - -```js -const transactionWithNativeTransfer = factory.createTransactionForExecute({ - sender: addressOfAlice, - contract: Address.fromBech32("erd1qqqqqqqqqqqqqpgq6qr0w0zzyysklfneh32eqp2cf383zc89d8sstnkl60"), - function: "add", - gasLimit: 5000000, - arguments: args, - nativeTransferAmount: 1000000000000000000n -}); -``` - -Above, we're sending 1 EGLD along with the contract call. - -For transfer & execute with ESDT tokens, prepare your transaction as follows: - -```js -const transactionWithTokenTransfer = factory.createTransactionForExecute({ - sender: addressOfAlice, - contract: Address.fromBech32("erd1qqqqqqqqqqqqqpgq6qr0w0zzyysklfneh32eqp2cf383zc89d8sstnkl60"), - function: "add", - gasLimit: 5000000, - arguments: args, - tokenTransfers: [ - new TokenTransfer({ - token: new Token({ identifier: "UTK-14d57d" }), - amount: 42000000000000000000n - }) - ] -}); -``` - -Or, for transferring multiple tokens (NFTs included): - -```js -const transactionWithMultipleTokenTransfers = factory.createTransactionForExecute({ - sender: addressOfAlice, - contract: Address.fromBech32("erd1qqqqqqqqqqqqqpgq6qr0w0zzyysklfneh32eqp2cf383zc89d8sstnkl60"), - function: "add", - gasLimit: 5000000, - arguments: args, - tokenTransfers: [ - new TokenTransfer({ - token: new Token({ identifier: "UTK-14d57d" }), - amount: 42000000000000000000n - }), - new TokenTransfer({ - token: new Token({ identifier: "EXAMPLE-453bec", nonce: 3n }), - amount: 1n - }) - ] -}); -``` - -Above, we've prepared the [`TokenTransfer`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TokenTransfer.html) objects as seen in the section [token transfers](#token-transfers). - -### Parsing transaction outcome - -Once a transaction is completed, you can parse the results using a [`SmartContractTransactionsOutcomeParser`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/SmartContractTransactionsOutcomeParser.html). -However, since the `parseExecute` method requires a [`TransactionOutcome`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TransactionOutcome.html) object as input, -we need to first convert our `TransactionOnNetwork` object to a `TransactionOutcome`, by means of a [`TransactionsConverter`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TransactionsConverter.html). - -```js -import { SmartContractTransactionsOutcomeParser, TransactionsConverter } from "@multiversx/sdk-core"; - -const converter = new TransactionsConverter(); -const parser = new SmartContractTransactionsOutcomeParser({ - abi: abi -}); - -const transactionOutcome = converter.transactionOnNetworkToOutcome(transactionOnNetwork); -const parsedOutcome = parser.parseExecute({ transactionOutcome }); - -console.log(parsedOutcome); -``` - -### Decode transaction events - -Additionally, you might be interested into decoding the events emitted by a contract. -You can do so by means of the [`TransactionEventsParser`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TransactionEventsParser.html). - -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. - -Let's fetch [a previously-processed transaction](https://devnet-explorer.multiversx.com/transactions/05d445cdd145ecb20374844dcc67f0b1e370b9aa28a47492402bc1a150c2bab4), -to serve as an example, and convert it to a [`TransactionOutcome`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TransactionOutcome.html) (see above why): - -```js -const transactionOnNetworkMultisig = await apiNetworkProvider.getTransaction("05d445cdd145ecb20374844dcc67f0b1e370b9aa28a47492402bc1a150c2bab4"); -const transactionOutcomeMultisig = converter.transactionOnNetworkToOutcome(transactionOnNetworkMultisig); -``` - -Now, let's find and parse the event we are interested in: - -```js -import { TransactionEventsParser, findEventsByFirstTopic } from "@multiversx/sdk-core"; - -const abiJsonMultisig = await promises.readFile("../contracts/multisig-full.abi.json", { encoding: "utf8" }); -const abiMultisig = AbiRegistry.create(JSON.parse(abiJsonMultisig)); - -const eventsParser = new TransactionEventsParser({ - abi: abiMultisig -}); - -const [event] = findEventsByFirstTopic(transactionOutcomeMultisig, "startPerformAction"); -const parsedEvent = eventsParser.parseEvent({ event }); - -console.log(parsedEvent); -``` - -## Contract queries - -In order to perform Smart Contract queries, we recommend the use of [`SmartContractQueriesController`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/SmartContractQueriesController.html). -The legacy approaches that rely on [`SmartContract.createQuery()`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/SmartContract.html#createQuery) or [`Interaction.buildQuery()`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/Interaction.html#buildQuery) are still available, but they will be deprecated in the (distant) future. - -You will notice that the [`SmartContractQueriesController`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/SmartContractQueriesController.html) requires a `QueryRunner` object at initialization. -A `NetworkProvider`, slightly adapted, is used to satisfy this requirement. - -```js -import { QueryRunnerAdapter, SmartContractQueriesController } from "@multiversx/sdk-core"; - -const queryRunner = new QueryRunnerAdapter({ - networkProvider: apiNetworkProvider -}); - -let controller = new SmartContractQueriesController({ - queryRunner: queryRunner -}); -``` - -If the contract ABI is available, provide it to the controller: - -```js -controller = new SmartContractQueriesController({ - queryRunner: queryRunner, - abi: abi -}); -``` - -Let's create a query object: - -```js -const query = controller.createQuery({ - contract: "erd1qqqqqqqqqqqqqpgq6qr0w0zzyysklfneh32eqp2cf383zc89d8sstnkl60", - function: "getSum", - arguments: [], -}); -``` - -Then, run the query against the network. You will get a [`SmartContractQueryResponse`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/SmartContractQueryResponse.html) object. - -```js -const response = await controller.runQuery(query); -``` - -:::tip -The invocation of `controller.runQuery()` ultimately calls the VM query endpoints of the MultiversX REST API. -::: - -The response object contains the raw output of the query, which can be parsed as follows: - -```js -const [sum] = controller.parseQueryResponse(response); -console.log(sum); -``` - -## Explicit decoding / encoding of values - -When needed, you can use the [`BinaryCodec`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/BinaryCodec.html) to [decode and encode values](/developers/data/serialization-overview/) manually, -leveraging contract ABIs: - -```js -const abiJsonExample = await promises.readFile("../contracts/example.abi.json", { encoding: "utf8" }); -const abiExample = AbiRegistry.create(JSON.parse(abiJsonExample)); - -const abiJsonMultisig = await promises.readFile("../contracts/multisig-full.abi.json", { encoding: "utf8" }); -const abiMultisig = AbiRegistry.create(JSON.parse(abiJsonMultisig)); -``` - -:::note -The ABI files used within this cookbook are available [here](https://github.com/multiversx/mx-sdk-js-examples). -::: - -### Decoding a custom type - -Example of decoding a custom type (a structure) called `DepositEvent` from binary data: - -```js -import { BinaryCodec } from "@multiversx/sdk-core"; - -const depositCustomType = abiExample.getCustomType("DepositEvent"); -const codec = new BinaryCodec(); -let data = Buffer.from("00000000000003db000000", "hex"); -let decoded = codec.decodeTopLevel(data, depositCustomType); -let decodedValue = decoded.valueOf(); - -console.log(JSON.stringify(decodedValue, null, 4)); -``` - -Example of decoding a custom type (a structure) called `Reward` from binary data: - -```js -const rewardStructType = abiExample.getStruct("Reward"); -data = Buffer.from("010000000445474c440000000201f400000000000003e80000000000000000", "hex"); - -[decoded] = codec.decodeNested(data, rewardStructType); -decodedValue = decoded.valueOf(); -console.log(JSON.stringify(decodedValue, null, 4)); -``` - -Example of decoding a custom type (an enum) called `Action` (of [**multisig**](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/multisig) contract) from binary data: - -```js -const actionStructType = abiMultisig.getEnum("Action"); -data = Buffer.from("0500000000000000000500d006f73c4221216fa679bc559005584c4f1160e569e1000000012a0000000003616464000000010000000107", "hex"); - -[decoded] = codec.decodeNested(data, actionStructType); -decodedValue = decoded.valueOf(); -console.log(JSON.stringify(decodedValue, null, 4)); -``` - -### Encoding a custom type - -Example of encoding a custom type (a struct) called `EsdtTokenPayment` (of [**multisig**](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/multisig) contract) into binary data: - -```js -import { BigUIntValue, Field, Struct, TokenIdentifierValue, U64Value } from "@multiversx/sdk-core"; - -const paymentType = abiMultisig.getStruct("EsdtTokenPayment"); - -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")); -``` - -## Signing objects and verifying signatures - -:::note -Skip this section if you're building a **dApp**. -This section is destined for developers of **wallet-like applications** or backend (server-side) components that are concerned with signing transactions and messages. - -For **dApps**, use the available **[signing providers](/sdk-and-tools/sdk-js/sdk-js-signing-providers)** instead. -Note that we recommend using **[sdk-dapp](/sdk-and-tools/sdk-dapp)** instead of integrating the signing providers on your own. -::: - -:::note -You might also be interested into the language-agnostic overview on [signing transactions](/developers/signing-transactions). -::: - -### Signing objects - -Creating a `UserSigner` from a JSON wallet: - -```js -import { UserSigner } from "@multiversx/sdk-core"; -import { promises } from "fs"; - -const fileContent = await promises.readFile("../testwallets/alice.json", { encoding: "utf8" }); -const walletObject = JSON.parse(fileContent); -let signer = UserSigner.fromWallet(walletObject, "password"); -``` - -Creating a `UserSigner` from a PEM file: - -```js -const pemText = await promises.readFile("../testwallets/alice.pem", { encoding: "utf8" }); -signer = UserSigner.fromPem(pemText); -``` - -Signing a transaction, as we've seen [before](#signing-a-transaction): - -```js -import { Transaction, TransactionComputer } from "@multiversx/sdk-core"; - -const transaction = new Transaction({ - nonce: 91, - sender: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", - receiver: "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", - value: 1000000000000000000n, - gasLimit: 50000n, - chainID: "D" -}); - -const transactionComputer = new TransactionComputer() -let serializedTransaction = transactionComputer.computeBytesForSigning(transaction); -transaction.signature = await signer.sign(serializedTransaction); - -console.log("Signature", Buffer.from(transaction.signature).toString("hex")); -``` - -Signing an arbitrary message: - -```js -import { Message, MessageComputer } from "@multiversx/sdk-core"; - -let message = new Message({ - data: Buffer.from("hello") -}); - -const messageComputer = new MessageComputer(); -let serializedMessage = messageComputer.computeBytesForSigning(message); -message.signature = await signer.sign(serializedMessage); - -console.log("Signature", Buffer.from(message.signature).toString("hex")); -``` - -### Verifying signatures - -Creating a `UserVerifier`: - -```js -import { UserVerifier } from "@multiversx/sdk-core"; - -const aliceVerifier = UserVerifier.fromAddress(addressOfAlice); -const bobVerifier = UserVerifier.fromAddress(addressOfBob); -``` - -Verifying a signature: - -```js -serializedTransaction = transactionComputer.computeBytesForVerifying(transaction); -serializedMessage = messageComputer.computeBytesForVerifying(message); - -console.log("Is signature of Alice?", aliceVerifier.verify(serializedTransaction, transaction.signature)); -console.log("Is signature of Alice?", aliceVerifier.verify(serializedMessage, message.signature)); -console.log("Is signature of Bob?", bobVerifier.verify(serializedTransaction, transaction.signature)); -console.log("Is signature of Bob?", bobVerifier.verify(serializedMessage, message.signature)); -``` - -### Handling messages over boundaries - -Generally speaking, signed [`Message`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/Message.html) objects are meant to be sent to a remote party (e.g. a service), which can then verify the signature. - -In order to prepare a message for transmission, you can use the [`MessageComputer.packMessage()`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/MessageComputer.html#packMessage) utility method: - -```js -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/v13/classes/MessageComputer.html#unpackMessage) to reconstruct the message, prior verification: - -```js -const unpackedMessage = messageComputer.unpackMessage(packedMessage); -const serializedUnpackedMessage = messageComputer.computeBytesForVerifying(unpackedMessage); - -console.log("Unpacked message", unpackedMessage); -console.log("Is signature of Alice?", aliceVerifier.verify(serializedUnpackedMessage, message.signature)); -``` - -### Signing hashes of objects - -Under the hood, [`MessageComputer.computeBytesForSigning()`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/MessageComputer.html#computeBytesForSigning) does not compute a plain serialization of the message. -Instead, it first decorates the message (with a special prefix, plus the message length), and computes a **`keccak256` hash** of this decorated variant. -Ultimately, the signature is computed over the hash. - -However, for transactions, **by default**, the Network expects the signature to be computed over [the plain serialization](/developers/signing-transactions/#serialization-for-signing) of the transaction. -The function [`TransactionComputer.computeBytesForSigning()`](https://multiversx.github.io/mx-sdk-js-core/v13/classes/TransactionComputer.html#computeBytesForSigning) adheres to this default policy. - -The behavior can be overridden by setting the _sign using hash_ flag of `transaction.options`: - -```js -transactionComputer.applyOptionsForHashSigning(transaction); -``` - -Then, the transaction should be serialized and signed as follows: - -```js -const bytesToSign = transactionComputer.computeHashForSigning(transaction); -transaction.signature = await signer.sign(bytesToSign); -``` - -:::note -If you'd like to learn more about hash signing, please refer to the overview on [signing transactions](/developers/signing-transactions). -::: diff --git a/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v14.md b/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v14.md index 76f507f53..443d82c5e 100644 --- a/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v14.md +++ b/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v14.md @@ -1,7 +1,7 @@ --- -id: sdk-js-cookbook +id: sdk-js-cookbook-v14 title: Cookbook (v14) -pagination_prev: sdk-and-tools/sdk-js/sdk-js-cookbook-v13 +pagination_prev: sdk-and-tools/sdk-js/sdk-js pagination_next: null --- 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 new file mode 100644 index 000000000..210b90775 --- /dev/null +++ b/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v15.md @@ -0,0 +1,3468 @@ +--- +id: sdk-js-cookbook +title: Cookbook (v15) +pagination_prev: sdk-and-tools/sdk-js/sdk-js-cookbook-v14 +pagination_next: null +--- + +[comment]: # (mx-abstract) + +## Overview + +This guide walks you through handling common tasks using the MultiversX Javascript SDK (v14, 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/v14/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. + +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/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. + +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: + +```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/v14/classes/TokenManagementController.html) +- [`TokenManagementTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v14/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/v14/classes/DelegationController.html) +- [`DelegationTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v14/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 soon be deactivated, so we will focus on V3. + +For V3, two new fields have been added to transactions: +- relayer +- relayerSignature + +Signing Process: +1. The relayer must be set before the sender signs the transaction. +2. Once the sender has signed, the relayer can also sign the transaction and broadcast it. + +**Important Consideration**: +Relayed V3 transactions require an additional `50,000` gas. +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/v14/classes/MultisigController.html) +- [`MultisigTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v14/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/v14/classes/GovernanceController.html) +- [`GovernanceTransactionsFactory`](https://multiversx.github.io/mx-sdk-js-core/v14/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 publickKey = secretKey.generatePublicKey(); + + const transaction = new Transaction({ + nonce: 90n, + sender: publickKey.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/v14/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/sidebars.js b/sidebars.js index 1d85bd166..6020656fa 100644 --- a/sidebars.js +++ b/sidebars.js @@ -216,7 +216,7 @@ const sidebars = { id: "sdk-and-tools/sdk-js/sdk-js-cookbook" }, items: [ - "sdk-and-tools/sdk-js/sdk-js-cookbook-v13", + "sdk-and-tools/sdk-js/sdk-js-cookbook-v14", "sdk-and-tools/sdk-js/sdk-js-cookbook", ] }, From 3f5e4fa261bac2f216286e05e1a606d918229f29 Mon Sep 17 00:00:00 2001 From: danielailie Date: Tue, 29 Jul 2025 11:53:20 +0300 Subject: [PATCH 3/4] Update cookbook for py --- docs/sdk-and-tools/sdk-py.md | 39 ++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/docs/sdk-and-tools/sdk-py.md b/docs/sdk-and-tools/sdk-py.md index b236e8a32..abbe2e012 100644 --- a/docs/sdk-and-tools/sdk-py.md +++ b/docs/sdk-and-tools/sdk-py.md @@ -716,6 +716,32 @@ config = TransactionsFactoryConfig(chain_id="D") factory = TransferTransactionsFactory(config=config) ``` +### Estimating the Gas Limit for a Transaction + +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 + +config = TransactionsFactoryConfig(chain_id="D") +transfers_factory = TransferTransactionsFactory(config=config, gas_limit_estimator=gas_estimator) +``` + +Also, factories or controllers created through the entrypoints can use the `GasLimitEstimator` as well: + +```py +from multiversx_sdk import DevnetEntrypoint + +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 We can send native tokens (EGLD) and ESDT tokens using both the `controller` and the `factory`. @@ -1084,7 +1110,7 @@ from multiversx_sdk.abi import Abi, BigUIntValue # load the abi file abi = Abi.load(Path("contracts/adder.abi.json")) -# get the smart contracts controller +# get the smart contracts transaction factory entrypoint = DevnetEntrypoint() factory = entrypoint.create_smart_contract_transactions_factory(abi=abi) @@ -1277,7 +1303,7 @@ account.nonce = entrypoint.recall_account_nonce(account.address) # load the abi file abi = Abi.load(Path("contracts/adder.abi.json")) -# get the smart contracts controller +# get the smart contracts factory entrypoint = DevnetEntrypoint() factory = entrypoint.create_smart_contract_transactions_factory(abi=abi) @@ -1584,7 +1610,7 @@ from multiversx_sdk.abi import Abi, BigUIntValue # load the abi file abi = Abi.load(Path("contracts/adder.abi.json")) -# get the smart contracts controller +# get the smart contracts factory entrypoint = DevnetEntrypoint() factory = entrypoint.create_smart_contract_transactions_factory(abi=abi) @@ -2868,6 +2894,8 @@ transaction.relayer_signature = carol.sign_transaction(transaction) tx_hash = entrypoint.send_transaction(transaction) ``` +### Guarded Transactions + #### Creating guarded transactions using controllers 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. @@ -2993,7 +3021,7 @@ transaction = controller.create_transaction_for_deploy( gas_limit=100_000_000, ) -transaction.signature = alice.sign_transaction(transaction) +# send the transaction tx_hash = api.send_transaction(transaction) ``` @@ -3052,8 +3080,7 @@ transaction = controller.create_transaction_for_propose_transfer_execute( native_token_amount=1_000000000000000000, # 1 EGLD ) -# sign and send the transaction -transaction.signature = alice.sign_transaction(transaction) +# send the transaction tx_hash = api.send_transaction(transaction) # parse the outcome and get the proposal id From 89cd69a24efc3b9a14991fcc44181bbf0516e2b3 Mon Sep 17 00:00:00 2001 From: danielailie Date: Tue, 29 Jul 2025 11:58:07 +0300 Subject: [PATCH 4/4] Update cookbook for py --- docs/sdk-and-tools/sdk-py.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sdk-and-tools/sdk-py.md b/docs/sdk-and-tools/sdk-py.md index abbe2e012..bc958e826 100644 --- a/docs/sdk-and-tools/sdk-py.md +++ b/docs/sdk-and-tools/sdk-py.md @@ -11,10 +11,10 @@ pagination_next: sdk-and-tools/mxpy/installing-mxpy ## Overview -This page will guide you through the process of handling common tasks using the MultiversX Python SDK (libraries) **v1 (latest, stable version)**. +This page will guide you through the process of handling common tasks using the MultiversX Python SDK (libraries) **v2 (latest, stable version)**. :::important -This cookbook makes use of `sdk-py v1`. In order to migrate from `sdk-py v0` to `sdk-py v1`, please also follow [the migration guide](https://github.com/multiversx/mx-sdk-py/issues?q=label:migration). +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). ::: :::note @@ -3923,10 +3923,10 @@ If your project has multiple dependencies, we recommend using a `requirements.tx multiversx-sdk ``` -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 `1.0.0`: +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`: ```sh -git+https://git@github.com/multiversx/mx-sdk-py.git@v1.0.0#egg=multiversx_sdk +git+https://git@github.com/multiversx/mx-sdk-py.git@v2.0.0#egg=multiversx_sdk ``` If you've places all dependencies in a `requirements.txt` file, make sure you also install them by running: