From d7b40b73eeb0a9933441c324535b80bf5a70e9e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADnez?= Date: Fri, 20 Dec 2024 20:39:00 +0100 Subject: [PATCH] Update the Protocol Kit guide to execute transactions --- .../guides/execute-transactions.mdx | 353 ++++++++---------- 1 file changed, 155 insertions(+), 198 deletions(-) diff --git a/pages/sdk/protocol-kit/guides/execute-transactions.mdx b/pages/sdk/protocol-kit/guides/execute-transactions.mdx index d118cb43..4c2e9fac 100644 --- a/pages/sdk/protocol-kit/guides/execute-transactions.mdx +++ b/pages/sdk/protocol-kit/guides/execute-transactions.mdx @@ -1,284 +1,241 @@ +import { Steps, Tabs } from 'nextra/components' + # Execute transactions -In this quickstart guide, you will create a 2 of 3 multi-sig Safe and propose and execute a transaction to send some ETH out of this Safe. +In this guide, you will learn how to create Safe transactions, sign them, collect the signatures from the different owners, and execute them. + +See the [Protocol Kit reference](../../../reference-sdk-protocol-kit/overview.mdx) to find more details and configuration options. -To find more details and configuration options for available methods, see the [Protocol Kit reference](../../../reference-sdk-protocol-kit/overview.mdx). +## Prerequisites -### Prerequisites +- [Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). +- An existing Safe with several signers. -1. [Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) -2. Three externally-owned accounts with Testnet ETH in at least one account +## Install dependencies -### Install dependencies +First, you need to install some dependencies. -First, we need to install some dependencies. +{/* */} ```bash -yarn add @safe-global/protocol-kit \ - @safe-global/api-kit \ - @safe-global/safe-core-sdk-types +pnpm add @safe-global/api-kit \ + @safe-global/protocol-kit \ + @safe-global/types-kit ``` -### Initialize Signers and Providers - -The signers trigger transactions to the Ethereum blockchain or off-chain transactions. The provider connects to the Ethereum blockchain. - -You can get a public RPC URL from [Chainlist](https://chainlist.org), however, public RPC URLs can be unreliable so you can also try a dedicated provider like Infura or Alchemy. - -For this guide, we will be creating a Safe on the Sepolia Testnet. - -```tsx -// https://chainlist.org/?search=sepolia&testnets=true -const RPC_URL = 'https://eth-sepolia.public.blastapi.io' +{/* */} -// Initialize signers -const OWNER_1_ADDRESS = // ... -const OWNER_1_PRIVATE_KEY = // ... +## Steps -const OWNER_2_ADDRESS = // ... -const OWNER_2_PRIVATE_KEY = // ... + -const OWNER_3_ADDRESS = // ... + ### Imports -const provider = new ethers.JsonRpcProvider(RPC_URL) -const owner1Signer = new ethers.Wallet(OWNER_1_PRIVATE_KEY, provider) -``` + Here are all the necessary imports for this guide. -### Initialize the API Kit + {/* */} -The [API Kit](https://github.com/safe-global/safe-core-sdk/tree/main/packages/api-kit) consumes the [Safe Transaction Service API](https://github.com/safe-global/safe-transaction-service). To use this library, create a new instance of the `SafeApiKit` class, imported from `@safe-global/api-kit`. In chains where Safe provides a Transaction Service, it's enough to specify the `chainId.` You can specify your own service using the optional `txServiceUrl` parameter. + ```typescript + import SafeApiKit from '@safe-global/api-kit' + import Safe from '@safe-global/protocol-kit' + import { + MetaTransactionData, + OperationType + } from '@safe-global/types-types' + ``` -You will be using Sepolia for this tutorial, however, you can also get [service URLs for different networks](../../../advanced/smart-account-supported-networks.mdx?service=Transaction+Service&service=Safe{Core}+SDK). + {/* */} -```tsx -import SafeApiKit from '@safe-global/api-kit' + ### Setup -const apiKit = new SafeApiKit({ - chainId: 1n -}) + You need a Safe account setup with two or more signers and threshold two, so at least multiple signatures have to be collected when executing a transaction. -// Or using a custom service -const apiKit = new SafeApiKit({ - chainId: 1n, // set the correct chainId - txServiceUrl: 'https://url-to-your-custom-service' -}) -``` + This example uses private keys, but any EIP-1193 compatible signers can be used. -### Initialize the Protocol Kit + {/* */} -The `SafeFactory` class allows the deployment of new Safe accounts while the `Safe` class represents an instance of a specific one. + ```typescript + const SAFE_ADDRESS = // ... -```tsx -import { SafeFactory } from '@safe-global/protocol-kit' + const OWNER_1_ADDRESS = // ... + const OWNER_1_PRIVATE_KEY = // ... -const safeFactory = await SafeFactory.init({ - provider: RPC_URL, - signer: OWNER_1_PRIVATE_KEY -}) -``` + const OWNER_2_PRIVATE_KEY = // ... -### Deploy a Safe + const RPC_URL = 'https://eth-sepolia.public.blastapi.io' + ``` -Calling the `deploySafe` method will deploy the desired Safe and return a Protocol Kit initialized instance ready to be used. Check the [method reference](./protocol-kit/reference/safe-factory.mdx#deploysafe) for more details on additional configuration parameters and callbacks. + {/* */} -```tsx -import { SafeAccountConfig } from '@safe-global/protocol-kit' + This guide uses Sepolia, but you can use any chain from the Safe Transaction Service [supported networks](../../../advanced/smart-account-supported-networks.mdx?service=Transaction+Service&service=Safe{Core}+SDK). -const safeAccountConfig: SafeAccountConfig = { - owners: [ - await OWNER_1_ADDRESS, - await OWNER_2_ADDRESS, - await OWNER_3_ADDRESS - ], - threshold: 2, - // Optional params -} + ### Initialize the Protocol Kit -/* This Safe is tied to owner 1 because the factory was initialized with the owner 1 as the signer. */ -const protocolKitOwner1 = await safeFactory.deploySafe({ safeAccountConfig }) + To handle transactions and signatures, you need to create an instance of the Protocol Kit with the `provider`, `signer` and `safeAddress`. -const safeAddress = await protocolKitOwner1.getAddress() + {/* */} -console.log('Your Safe has been deployed:') -console.log(`https://sepolia.etherscan.io/address/${safeAddress}`) -console.log(`https://app.safe.global/sep:${safeAddress}`) -``` - -### Send ETH to the Safe + ```typescript + const protocolKitOwner1 = await Safe.init({ + provider: RPC_URL, + signer: OWNER_1_PRIVATE_KEY, + safeAddress: SAFE_ADDRESS + }) + ``` -You will send some ETH to this Safe. + {/* */} -```tsx -const safeAddress = protocolKit.getAddress() + ### Create a transaction -const safeAmount = ethers.parseUnits('0.01', 'ether').toHexString() + {/* */} -const transactionParameters = { - to: safeAddress, - value: safeAmount -} + Create a `safeTransactionData` object with the properties of the transaction, add it to an array of transactions you want to execute, and pass it to the `createTransaction` method. -const tx = await owner1Signer.sendTransaction(transactionParameters) + ```typescript + const safeTransactionData: MetaTransactionData = { + to: '0x', + value: '1', // 1 wei + data: '0x', + operation: OperationType.Call + } -console.log('Fundraising.') -console.log(`Deposit Transaction: https://sepolia.etherscan.io/tx/${tx.hash}`) -``` + const safeTransaction = await protocolKitOwner1.createTransaction({ + transactions: [safeTransactionData] + }) + ``` -## Making a transaction from a Safe + {/* */} -The first signer will sign and propose a transaction to send 0.005 ETH out of the Safe. Then, the second signer will add their own proposal and execute the transaction since it meets the 2 of 3 thresholds. + For more details on what to include in a transaction, see the [`createTransaction`](../reference/safe.mdx#createtransaction) method in the reference. -At a high level, making a transaction from the Safe requires the following steps: + ### Track the Safe transaction -### Overview + Optionally, you can track all your Safe transactions on-chain by attaching an on-chain identifier to the `data` property. -The high-level overview of a multi-sig transaction is PCE: Propose. Confirm. Execute. + This identifier must be unique for every project and has a length of 16 bytes. You can create a random one or derive it from a text string, maybe from your project name: -1. **First signer proposes a transaction** - 1. Create transaction: define the amount, destination, and any additional data - 2. Perform an off-chain signature of the transaction before proposing - 3. Submit the transaction and signature to the Safe Transaction Service -2. **Second signer confirms the transaction** - 1. Get pending transactions from the Safe service - 2. Perform an off-chain signature of the transaction - 3. Submit the signature to the service -3. **Anyone executes the transaction** - 1. In this example, the first signer executes the transaction - 2. Anyone can get the pending transaction from the Safe service - 3. Account executing the transaction pays the gas fee + {/* */} -### Create a transaction + ```typescript + const onchainIdentifier = toHex( + 'TEXT_TO_DERIVE_THE_IDENTIFIER', // It could be your project name + { size: 16 } + ) + ``` -For more details on what to include in a transaction see [`createTransaction`](../../../reference-sdk-protocol-kit/transactions/createtransaction.mdx) method. + {/* */} -```tsx -import { MetaTransactionData } from '@safe-global/safe-core-sdk-types' + Once generated, fill out the [Ecosystem On-chain Tracking Form](https://docs.google.com/forms/d/e/1FAIpQLSfHWSPbSQwmo0mbtuFFewfLvDEOvTxfuvEl7AHOyrFE_dqpwQ/viewform) and provide the value of your `onchainIdentifier`. -// Any address can be used. In this example you will use vitalik.eth -const destination = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' -const amount = ethers.parseUnits('0.005', 'ether').toString() + Add the `onchainIdentifier` at the end of the Safe transaction `data`. -const safeTransactionData: MetaTransactionData = { - to: destination, - data: '0x', - value: amount -} -// Create a Safe transaction with the provided parameters -const safeTransaction = await protocolKitOwner1.createTransaction({ transactions: [safeTransactionData] }) -``` + {/* */} -### Track the Safe transaction + ```typescript + safeTransaction.data.data = concat([ + safeOperation.data.data as `0x{string}`, + onchainIdentifier + ]).toString() + ``` -Optionally, you can track all your Safe transactions on-chain by attaching an on-chain identifier to the `data` property. + {/* */} -This identifier must be unique for every project and has a length of 16 bytes. You can create a random one or derive it from a text string, maybe from your project name: + ### Propose the transaction -{/* */} + Before a transaction can be executed, the signer who creates it needs to send it to the Safe Transaction Service so that it is accessible by the other owners, who can then give their approval and sign the transaction. -```typescript -const onchainIdentifier = toHex( - 'TEXT_TO_DERIVE_THE_IDENTIFIER', // It could be your project name - { size: 16 } -) -``` + Firstly, you need to create an instance of the API Kit. In chains where the [Safe Transaction Service](../../../core-api/transaction-service-overview.mdx) is supported, it's enough to specify the `chainId` property. -{/* */} + {/* */} -Once generated, fill the [Ecosystem On-chain Tracking Form](https://docs.google.com/forms/d/e/1FAIpQLSfHWSPbSQwmo0mbtuFFewfLvDEOvTxfuvEl7AHOyrFE_dqpwQ/viewform) and provide the value of your `onchainIdentifier`. + ```typescript + const apiKit = new SafeApiKit({ + chainId: 11155111n + }) + ``` -Add the `onchainIdentifier` at the end of the Safe transaction `data`. + {/* */} -{/* */} + You need to calculate the Safe transaction hash, sign the transaction hash, and call the `proposeTransaction` method from the API Kit instance to propose a transaction. -```typescript -safeTransaction.data.data = concat([ - safeOperation.data.data as `0x{string}`, - onchainIdentifier -]).toString() -``` + For a full list and description of the properties see [`proposeTransaction`](../../../reference-sdk-api-kit/proposetransaction.mdx) in the API Kit reference. -{/* */} + {/* */} -### Propose the transaction + ```typescript + // Deterministic hash based on transaction parameters + const safeTxHash = await protocolKitOwner1.getTransactionHash(safeTransaction) -To propose a transaction to the Safe Transaction Service we need to call the `proposeTransaction` method from the API Kit instance. + // Sign transaction to verify that the transaction is coming from owner 1 + const senderSignature = await protocolKitOwner1.signHash(safeTxHash) -For a full list and description of the properties see [`proposeTransaction`](../../../reference-sdk-api-kit/proposetransaction.mdx) in the API Kit reference. + await apiKit.proposeTransaction({ + safeAddress, + safeTransactionData: safeTransaction.data, + safeTxHash, + senderAddress: OWNER_1_ADDRESS, + senderSignature: senderSignature.data + }) + ``` -```tsx -// Deterministic hash based on transaction parameters -const safeTxHash = await protocolKitOwner1.getTransactionHash(safeTransaction) + {/* */} -// Sign transaction to verify that the transaction is coming from owner 1 -const senderSignature = await protocolKitOwner1.signHash(safeTxHash) + ### Retrieve the pending transactions -await apiKit.proposeTransaction({ - safeAddress, - safeTransactionData: safeTransaction.data, - safeTxHash, - senderAddress: OWNER_1_ADDRESS, - senderSignature: senderSignature.data -}) -``` + The other signers need to retrieve the pending transactions from the Safe Transaction Service. Depending on the situation, different methods in the API Kit are available. -### Get pending transactions + Call the [`getPendingTransactions`](../../../reference-sdk-api-kit/getpendingtransactions.mdx) method to retrieve all the pending transactions of a Safe account. -```tsx -const pendingTransactions = (await apiKit.getPendingTransactions(safeAddress)).results -``` + {/* */} -### Confirm the transaction: Second confirmation + ```typescript + const pendingTransactions = (await apiKit.getPendingTransactions(safeAddress)).results + ``` -When owner 2 is connected to the application, the Protocol Kit should be initialized again with the existing Safe address the address of the owner 2 instead of the owner 1. + {/* */} -```tsx -// Assumes that the first pending transaction is the transaction you want to confirm -const transaction = pendingTransactions[0] -const safeTxHash = transaction.safeTxHash + ### Confirm the transaction -const protocolKitOwner2 = await Safe.init({ - provider: RPC_URL, - signer: OWNER_2_PRIVATE_KEY, - safeAddress -}) + Once a signer has the pending transaction, they need to sign it with the Protocol Kit and submit the signature to the service using the [`confirmTransaction`](../../../reference-sdk-api-kit/confirmtransaction.mdx) method. -const signature = await protocolKitOwner2.signHash(safeTxHash) -const response = await apiKit.confirmTransaction(safeTxHash, signature.data) -``` + {/* */} -### Execute the transaction + ```typescript + const protocolKitOwner2 = await Safe.init({ + provider: RPC_URL, + signer: OWNER_2_PRIVATE_KEY, + safeAddress: SAFE_ADDRESS + }) -Anyone can execute the Safe transaction once it has the required number of signatures. In this example, owner 1 will execute the transaction and pay for the gas fees. + const safeTxHash = transaction.transactionHash + const signature = await protocolKitOwner2.signHash(safeTxHash) -```tsx -const safeTransaction = await apiKit.getTransaction(safeTxHash) -const executeTxResponse = await protocolKit.executeTransaction(safeTransaction) -const receipt = await executeTxResponse.transactionResponse?.wait() + // Confirm the Safe transaction + const signatureResponse = await apiKit.confirmTransaction( + safeTxHash, + signature.data + ) + ``` -console.log('Transaction executed:') -console.log(`https://sepolia.etherscan.io/tx/${receipt.transactionHash}`) -``` + {/* */} -### Confirm that the transaction was executed + ### Execute the transaction -You know that the transaction was executed if the balance in your Safe changes. + The Safe transaction is now ready to be executed. This can be done using the [Safe\{Wallet\}](https://app.safe.global) web interface, the [Protocol Kit](../../../reference-sdk-protocol-kit/transactions/executetransaction.mdx), the [Safe CLI](../../../advanced/cli-reference/tx-service-commands.mdx#execute-pending-transaction) or any other tool that's available. -```tsx -const afterBalance = await protocolKit.getBalance() + In this guide, the first signer will get the transaction from the service by calling the [`getTransaction`](../../../reference-sdk-api-kit/gettransaction.mdx) method and execute it by passing the transaction with all the signatures to the [`executeTransaction`](../../../reference-sdk-protocol-kit/transactions/executetransaction.mdx) method. -console.log(`The final balance of the Safe: ${ethers.formatUnits(afterBalance, 'ether')} ETH`) -``` + {/* */} -```bash -$ node index.js + ```typescript + const safeTransaction = await apiKit.getTransaction(safeTxHash) + const executeTxResponse = await protocolKitOwner1.executeTransaction(safeTransaction) + ``` -Fundraising. + {/* */} -Initial balance of Safe: 0.01 ETH -Buying a car. -The final balance of the Safe: 0.005 ETH -``` + -### Conclusion +## Recap and further reading -In this quickstart, you learned how to create and deploy a new Safe account and to propose and then execute a transaction from it. +After following this guide, you are able to create, sign, and execute Safe transactions with the Protocol Kit and share the signatures with the different signers using the API Kit.