diff --git a/cookbook/cookbook.md b/cookbook/cookbook.md index bf49734c..ac991db3 100644 --- a/cookbook/cookbook.md +++ b/cookbook/cookbook.md @@ -2613,6 +2613,186 @@ Flow for Creating Guarded Relayed Transactions: 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. diff --git a/cookbook/generate.py b/cookbook/generate.py index 552fccd2..7bab31ce 100644 --- a/cookbook/generate.py +++ b/cookbook/generate.py @@ -15,6 +15,7 @@ current_dir / "delegation.ts", current_dir / "relayed.ts", current_dir / "guarded.ts", + current_dir / "multisig.ts", current_dir / "governance.ts", current_dir / "addresses.ts", current_dir / "wallets.ts", diff --git a/cookbook/multisig.ts b/cookbook/multisig.ts new file mode 100644 index 00000000..c76ef873 --- /dev/null +++ b/cookbook/multisig.ts @@ -0,0 +1,188 @@ +import path from "path"; // md-ignore +import { Account, Address, DevnetEntrypoint, TransactionWatcher } from "../src"; // md-ignore +import { MultisigTransactionsOutcomeParser } from "../src/multisig/multisigTransactionsOutcomeParser"; +import { loadAbiRegistry, loadContractCode } from "../src/testutils"; +// md-start +(async () => { + // ### 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: + // - `class:MultisigController` + // - `class:MultisigTransactionsFactory` + + // #### 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 // md-as-comment + 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 // md-as-comment + 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 // md-as-comment + const txHash = await entrypoint.sendTransaction(transaction); + + // wait for transaction completion, extract multisig contract's address // md-as-comment + 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 // md-as-comment + 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 // md-as-comment + 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 // md-as-comment + 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 // md-as-comment + 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 // md-as-comment + 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 // md-as-comment + 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 // md-as-comment + 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 // md-as-comment + alice.nonce = await entrypoint.recallAccountNonce(alice.address); + + // set the nonce // md-as-comment + transaction.nonce = alice.getNonceThenIncrement(); + + // sign the transaction // md-as-comment + transaction.signature = await alice.signTransaction(transaction); + + // sending the transaction // md-as-comment + 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 // md-as-comment + const entrypoint = new DevnetEntrypoint(); + const controller = entrypoint.createMultisigController(abi); + + const contract = Address.newFromBech32("erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf8llllswuedva"); + + const boardMembers = await controller.getAllBoardMembers({ multisigAddress: contract.toBech32() }); + } + // ``` +})().catch((e) => { + console.log({ e }); +});