Hello readers, writing this article while reading and understanding the Safe Protocol Developer documentation.
Purpose:
How to create a safe account & how to send transactions from it.
Safe Protocol Kit:
The protocol kit allows to interact with safe Contract using a typescript interface
- Create a new, safe account.
- Update the configuration of existing safes.
- Propose & execute transactions.
Requirements:
- Signers — to trigger transactions.
- Providers — to connect to the blockchain.
- RPC URL — RPC to connect on a node
you can use ethers.js to do this
import { ethers } from 'ethers'
import { EthersAdapter } from '@safe-global/protocol-kit'
const provider = new ethers.BrowserProvider(window.ethereum);
const ethAdapterOwner = new EthersAdapter({
ethers,
signerOrProvider: provider.getSigner(0)
})
Safe API Kit:
Safe API Kit uses “Safe transaction service API.” — which keeps track of transactions sent via a safe contract. It uses events and tracing to index the transactions.
I assume this API kit will help us read the transaction records.
import SafeApiKit from '@safe-global/api-kit'
// or using a custom service
const apiKit = new SafeApiKit({
chainId: 1n, // set the correct chainId
txServiceUrl: 'https://url-to-your-custom-service' // optional
})
we are using Sepolia for now, however, you can also get service URLs for different networks.
Initialize the protocol kit:
Sepolia is a supported network so you don’t need to specify the contract addresses, however, to see how to create a safe on a local or unsupported network, see Instantiate an EthAdapter(opens in a new tab).
- Safe Factory — is to create safes.
- Safe Class represents an instance of a specific safe accounts.
import { SafeFactory } from '@safe-global/protocol-kit'
const safeFactory = await SafeFactory.create({ ethAdapter: ethAdapterOwner })
Deploy a safe:
Calling a .deploySafe() on a safeFactory will deploy the desire safe and return a protocol kit initialized instance
import { SafeAccountConfig } from '@safe-global/protocol-kit'
const safeAccountConfig: SafeAccountConfig = {
owners: [address1,address2,address3],
threshold: 2,
// ... (Optional params)
}
/* This Safe is tied to owner 1 because the factory was initialized with
an adapter that had owner 1 as the signer. */
const protocolKitOwner = await safeFactory.deploySafe({ safeAccountConfig })
const safeAddress = await protocolKitOwner.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 safe:
You will send some ETH to this Safe.
const safeAmount = ethers.parseUnits('0.01', 'ether').toHexString()
const tx = await signer.sendTransaction({
to: safeAddress,
value: safeAmount,
});
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.
At a high level, making a transaction from the Safe requires the following steps:
The high-level overview of a multi-sig transaction is PCE: Propose. Confirm. Execute.
- First signer proposes a transaction
- Second signer confirms the transaction
- Anyone executes the transaction
Create a transaction:
required : receiver address, amount
import { MetaTransactionData } from '@safe-global/safe-core-sdk-types'
// Any address can be used. In this example you will use vitalik.eth
const destination = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
const amount = ethers.parseUnits('0.005', 'ether').toString()
const safeTransactionData: MetaTransactionData = {
to: destination,
data: '0x',
value: amount
}
// Create a Safe transaction with the provided parameters
const safeTransaction = await protocolKitOwner.createTransaction({ transactions: [safeTransactionData] })
Propose the transaction
To propose a transaction to the Safe Transaction Service we need to call the proposeTransaction method from the API Kit instance.
// Deterministic hash based on transaction parameters
const safeTxHash = await protocolKitOwner.getTransactionHash(safeTransaction)
// Sign transaction to verify that the transaction is coming from owner 1
const senderSignature = await protocolKitOwner.signHash(safeTxHash)
await apiKit.proposeTransaction({
safeAddress,
safeTransactionData: safeTransaction.data,
safeTxHash,
senderAddress: await owner1Signer.getAddress(),
senderSignature: senderSignature.data,
})
Get pending transactions:
const pendingTransactions = await apiKit.getPendingTransactions(safeAddress).results
Confirm the transaction: Second confirmation
assuming different device
// Assumes that the first pending transaction is the transaction you want to confirm
const transaction = pendingTransactions[0] // fetched transaction above
const safeTxHash = transaction.safeTxHash
// already created ethAdapterOwner, protocolKitOwner but this time it should create for 2nd owoner
// to confirm the transaction
const signature = await protocolKitOwner.signHash(safeTxHash)
const response = await apiKit.confirmTransaction(safeTxHash, signature.data)
since we set the threshold 2/3 confirmation now we can now execute this transaction. and anyone can execute this transaction have to pay for gas.
const safeTransaction = await apiKit.getTransaction(safeTxHash)
const executeTxResponse = await protocolKit.executeTransaction(safeTransaction)
const receipt = await executeTxResponse.transactionResponse?.wait()
console.log('Transaction executed:')
console.log(`https://sepolia.etherscan.io/tx/${receipt.transactionHash}`)
Great — let’s Confirm that the transaction was executed 🥁
You know that the transaction was executed if the balance in your Safe changes.
const afterBalance = await protocolKit.getBalance()
console.log(`The final balance of the Safe: ${ethers.formatUnits(afterBalance, 'ether')} ETH`)
Conclusion
Now you know how to create and deploy a Safe and to propose and then execute a transaction for the Safe.