Safe-Gnosis Backend Client

Alejo Lovallo
Think and Dev
Published in
4 min readMay 15, 2023

— Multisig Transactions from the backend

Last month, we in Think and Dev were requested to create a Safe backend to handle and administrate transactions.

Also, for the server, it would be an excellent opportunity to use the TRPC library and Zod package to build and create a typesafe API.

Using Safe Core SDK public repository, let´s put hands on!

Stack

  • Node js v18
  • Typescript
  • TRPC
  • Zod

The public repository can be found here: https://github.com/Think-and-Dev/safe-client

What we will build

At the end of this article, you will have a fully functional TRPC API which could also be used as a REST API with the following endpoints:

  • [GET] /owners: Get a list of the Safe owner's addresses.
  • [GET] /pending-transactions: Get a list of all Safe pending transactions.
  • [GET] /transaction?hash=${txHash}: Get information about a particular transaction.
  • [POST] /transaction: Propose a transaction to the Safe.
  • [POST] /transaction/confirm: Confirm (from one of the Safe owners' view) a proposed transaction.

TRPC Server

First of all, we will start by setting up our TRPC server. If you have never worked with TRPC, you have to know just a few things:

  • A query is equal to sending a GET request by query params.
  • A mutation is equal to sending a POST request with data encoded in your body request.
  • A context is like a general middleware for all your requests.
  • You could define any amount of middleware functions you may like, but keep in mind that your request will go first through your context function.

Zod validations

Zod is a typescript schema validation library that could be used when we define our trcp endpoints. Defining a schema is as simple as creating a zod primitive and then just creating a zod type. For example, for our get transaction endpoint, we have to require a transaction hash that has to be a string, so we will do the following:

export const GetTransactionSchema = z.object({
hash: z.string()
})

export type GetTransactionInput = z.infer<typeof GetTransactionSchema>

And then, in our trpc server we explicitly tell the server to validate the input automatically via zod:

  getTransaction: publicProcedure
.meta({
openapi: {
path: '/transaction',
method: 'GET',
description: 'Get transaction',
tags: ['SAFE'],
protect: false,
summary: 'Get transaction by tx hash'
}
})
.input(GetTransactionSchema)
......

Safe client setup

Steps:

  1. Ethers adapter

An EthAdapter, which contains all the required utilities for the SDKs to interact with the blockchain. It acts as a wrapper for web3.js or ethers.js Ethereum libraries.

  return new EthersAdapter({
ethers,
signerOrProvider: safeOwner
})
}

Notice the provider we have chosen to connect with the adapter is one of the Safe owners because it will perform more than just read operations.

2. Safe API kit: For this step, you must first decide in which network your service will be running, for a full list of Safe transactions services please check here

const txServiceUrl = getTxServiceURL()
const safeService = new SafeApiKit({ txServiceUrl, ethAdapter: adapter })

3. Initialize Safe Service: Here we will be creating a SafeFactory service.

const safeFactory = await SafeFactory.create({ ethAdapter: adapter })

4. Initialize Safe SDK: For this step, you will need to provide your Safe address.

sdk = await Safe.create({
ethAdapter: this.adapter,
safeAddress: address
})

And that´s it, you are already good to go and start using Safe automatically from your backend!

Get Safe owners

const owners: string[] | undefined = await this.sdk.getOwners()

Propose transaction

For this endpoint, you will have to:

  • Create a transaction: The only mandatory parameters to create a transaction are:
export interface MetaTransactionData {
readonly to: string;
readonly value: string;
readonly data: string;
}
  • Get the transaction hash:
const txHash = await this.sdk.getTransactionHash(transaction)
  • Sign the transaction:
const senderSignature = await this.sdk.signTransactionHash(txHash)
  • Propose transaction:
 await safeService.proposeTransaction({
safeAddress: this.safeAddress,
safeTransactionData: transaction.data,
safeTxHash: txHash,
senderAddress: sender,
senderSignature: senderSignature.data
})

It is important to clarify that both the sender's address and the sender's signature have to match with a valid Safe owner. Otherwise, the proposed transaction operation will fail.

See pending transactions

const pendingTxs = await safeService.getPendingTransactions(this.safeAddress!)

const processedPendingTxs = pendingTxs.results.map((pendingTx) => {
const { safe, to, value, data, safeTxHash } = pendingTx
return { safe, to, value, data, safeTxHash }
})

Get transaction info

Here you will only have to provide the hash of a valid transaction

      const {
safe,
to,
value,
data,
operation,
gasToken,
safeTxGas,
baseGas,
gasPrice,
nonce,
submissionDate,
modified,
transactionHash,
safeTxHash,
isExecuted,
origin,
confirmationsRequired
} = await safeService.getTransaction(txHash)

Accept transaction

const transaction = await safeService.getTransaction(hash)
const response = await this.sdk!.executeTransaction(transaction)

--

--

Alejo Lovallo
Think and Dev

Sr. Blockchain Developer || WIP Software engineer || DevOps Consultant.