Understanding Safe protocol development kit

Rebel
4 min readFeb 20, 2024

--

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:

  1. Signers — to trigger transactions.
  2. Providers — to connect to the blockchain.
  3. 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).

  1. Safe Factory — is to create safes.
  2. 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.

  1. First signer proposes a transaction
  2. Second signer confirms the transaction
  3. 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.

--

--

Responses (1)