Relay

The Defender Relay service allows you to send transactions via a regular HTTP API, and takes care of private key secure storage, transaction signing, nonce management, gas price estimation, and resubmissions. This way you don’t need to worry about securing your private keys in your backend scripts, or by monitoring your transactions to ensure they get mined.

Use cases

Use a Defender Relayer every time you find yourself using a hot wallet in your code. Whenever you need to send a transaction from a script or your backend, you can connect to a Relayer instead to simplify key management and transaction submissions.

  • Poking your contracts to trigger a state transition

  • Update an on-chain oracle with external data

  • Sending meta-transactions to build a gasless experience

  • React to sign-ups in your app by airdropping tokens to your new users

  • Sweeping funds from protocol contracts to secure wallets

  • Building bots for interacting with smart contract protocols

What’s in a Relayer?

Each Defender Relayer is an Ethereum account assigned exclusively to your team. Every time you create a new Relayer, Defender will create a new private key in a secure vault. Whenever you request Defender to send a transaction through that Relayer, the corresponding private key will be used for signing.

You can think of each Relayer as a queue for sending transactions, where all transactions sent through the same Relayer will be sent in order and from the same Ethereum account, controlled exclusively by your team. Skip ahead to the Under the hood section if you want more info on how Relayers work behind the scenes!

Relayers on the Goerli and Rinkeby networks will be automatically funded with test ETH upon creation, and will be topped up when they run low on funds. On other networks, you’ll need to fund each Relayer individually with ETH to ensure they have enough funds to pay for the gas of the transactions you send. In these cases, Defender will send you an email notification if a Relayer’s funds drop below 0.1 ETH.
Defender Relayers are implemented as Externally Owned Accounts. We are working to implement Smart Accounts in the future as well, to support batched transactions, gas tokens, and cold keys for on-chain management.

API Keys

Each Relayer has one or more API keys associated with it. In order to send a transaction through a Relayer, you will need to authenticate the request with one an API key/secret pair. You can create or delete API keys as you see fit, and this will not change the sending address or Relayer balance.

When creating a new API key, keep in mind that the API secret is only visible once during the creation — if you don’t write it down, it’s lost forever. Luckily, creating a new API key is as easy as clicking a button.

Sending transactions

The easiest way to send a transaction via a Relayer is using the defender-relay-client npm package. The client is initialized with an API key/secret and exposes a simple API for sending transactions through the corresponding Relayer.

import { Relayer } from 'defender-relay-client';
const relayer = new Relayer({apiKey: YOUR_API_KEY, apiSecret: YOUR_API_SECRET});

const tx = await relayer.sendTransaction({
  to, value, data, gasLimit, speed: 'fast'
});
You don’t need to enter a private key when initializing a Relayer client, since the private key is kept secure in the Defender vault.

Using ethers.js

To avoid rewriting existing scripts, the Relayer client also integrates with ethers.js via a custom signer. This allows you to switch to Defender Relayer for sending transactions with minimal changes in your codebase.

const { DefenderRelaySigner } = require('defender-relay-client/lib/ethers');
const { ethers } = require('ethers');

const provider = ethers.getDefaultProvider(NETWORK);
const credentials = { apiKey: YOUR_API_KEY, apiSecret: YOUR_API_SECRET };
const signer = new DefenderRelaySigner(credentials, provider, { speed: 'fast' });

const erc20 = new ethers.Contract(ERC20_ADDRESS, ERC20_ABI, signer);
const tx = await erc20.transfer(beneficiary, 1e18.toString());
const mined = await tx.wait();

Speed

Instead of the usual gasPrice, the Relayer accepts a speed parameter, which can be safeLow, average, fast, or fastest. These values are mapped to actual gas prices when the transaction is sent or resubmitted and vary depending on the state of the network.

Today, mainnet gas prices for each speed are obtained from ethgasstation, falling back to etherchain if the former is not available. We are experimenting with other gas price oracles in the meantime. Testnet gas prices are obtained from a call to eth_gasPrice to the network.

Transaction IDs

Since the Relayer may resubmit a transaction with an updated gas price if it does not get mined in the expected time frame, the hash of a given transaction may change over time. To track the status of a given transaction, the Relayer API returns a transactionId identifier you can use to query it.

const latestTx = await relayer.query(tx.transactionId);
The query endpoint will return the latest view of the transaction from the Defender service, which gets updated every few minutes.

Signing

In addition to sending transactions, the Relayer can also sign arbitrary messages using its private key. You can access this feature via the sign method of the client or the equivalent ethers.js method.

const signResponse = await relayer.sign({ message });

Relayer Info

A relayer’s address can be retrieved using the getAddress method of the DefenderRelaySigner class.

const addr = await signer.getAddress();

If you need more info about a relayer then checkout the getRelayer method of the client. It returns the following data:

const info = await relayer.getRelayer();
console.log('Relayer info', info);

export interface RelayerModel {
  relayerId: string;
  name: string;
  address: string;
  network: string;
  paused: boolean;
  createdAt: string;
  pendingTxCost: string;
}

Autotasks integration

A Relayer can be attached to an Autotask. When doing so, the Autotask code will have direct access to the attached Relayer methods without requiring you to specify an API key. Instead, Defender will inject short-lived credentials for your Relayer in your Autotask handler function.

const { Relayer } = require('defender-relay-client');

// The credentials object is injected by the Defender Autotasks engine
exports.handler = async function(credentials) {
  const relayer = new Relayer(credentials);
  // ... use relayer as usual
}

Pausing

You can pause a Relayer from the Defender website, to quickly respond to an emergency. This will cause the Relayer to reject any incoming requests to send a transaction, whether it is from an Autotask or via the API. However, keep in mind that any transactions already sent will not be cancelled. When you are ready to resume operations, just hit the Unpause button and your Relayer will go back to normal.

Meta-transactions

Defender Relayers are general-purpose relayers, in the sense that you can use them to send any transaction you want to your contracts. In particular, they can also be used for relaying meta-transactions on behalf of your users. A simple setup for this requires setting up a server-side function that decides whether or not to relay a given meta-transaction, and then calls the Defender Relayer for effectively sending it.

If you would like to use Autotasks to host the server-side function to decide whether to pay for a meta-transaction request, let us know! We are working on a webhooks integration that can be used for this purpose.

EIP2771 GSNv2-compatible meta-transactions

We have created a demo application for Defender-powered meta-txs here. This application relies on an EIP-2771 Forwarder contract. This contract’s sole responsibility is to receive a signed meta-tx request, verify its signature, and forward the request to a recipient contract by appending the signer address to the call.

This setup is compatible with GSNv2, meaning that you can use a Defender Relayer for sending your meta-transactions, and at any point in the future, you can switch to the decentralized network of GSN relayers without any changes to your contracts.

You can explore the key parts of the code for the application here.

More meta-transaction patterns

The pattern described above is just one variant among several meta-transaction flavors available. Given that Defender Relayers are general-purpose, you can also use them for any other type of meta-transactions, such as relaying gasless ERC20 transfers using EIP2612 or EIP3009.

We are also working on adding support for sidechains and L2s, starting with xDai. This way, you can offer a full gasless experience to your users in other networks. Let us know if you have any preferred network you’d like to see in Defender!

Through the UI

You can also send transactions via a Relayer through the UI. To do so, go to the Relayer page, open the cog menu, and click on Send transaction:

Defender Relay Cog Menu

At the Send transaction screen, enter the address of the contract you want to interact with, select a function to execute, and enter its arguments.

At the time of writing, Defender only supports sending transactions to source verified contracts, so make sure the target contract has been verified either on Etherscan or Sourcify.
Defender Relay Send Transaction

When you click on Confirm transaction, the transaction will be sent through the relayer. We suggest that you wait for the transaction to be confirmed before leaving this screen, or to monitor it on your block explorer of choice, to ensure that it is confirmed and there is no further action required from you.

Defender sets speed to Fast for relayer transactions created through the UI.

Withdrawing funds

You can withdraw funds from a relayer by clicking on Withdraw funds in the Relayer page.

Defender Relay Withdraw Funds

At the Withdraw funds screen, you can choose to send funds in ETH or pick from a built-in list of ERC20 tokens.

Defender Relay Withdraw Funds Screen

Under the hood

Each Relayer is associated to a private key. When a request to send a transaction is received, the Relayer validates the request, atomically assigns it a nonce, reserves balance for paying for its gas fees, resolves its speed to a gas price, signs it with its private key, and enqueues it for submission to the blockchain. The response is sent back to the client only after this process has finished. Then, the transaction is broadcasted through both Infura and Alchemy for redundancy and retried up to three times in case both APIs are down.

Every five minutes, all in-flight transactions are checked by the system. If they have not been mined and more than a certain time has passed (which depends on the transaction speed), they are resubmitted with a 10% increase in their gas price (or the latest gas price for their speed, if it’s greater), which could be up to a 150% of the reported gas price for their speed. This process causes the transaction hash to change, but their ID is preserved. On the other hand, if the transaction has been mined, it is still monitored for several blocks until we consider it to be confirmed.

Security considerations

All private keys are stored in the AWS Key Management Service. Keys are generated within the KMS and never leave it, i.e., all sign operations are executed within the KMS. Furthermore, we rely on dynamically generated AWS Identity and Access Management policies to isolate access to the private keys among tenants.

As for API secrets, these are only kept in memory during creation when they are sent to the client. After that, they are hashed and stored securely in AWS Cognito, which is used behind the scenes for authenticating Relayer requests. This makes API keys easy to rotate while preserving the same private key on the KMS.

Relay API Reference

The Relay API exposes two endpoints txs and sign. The txs endpoint allows to send transactions to Ethereum blockchain and query their status. The sign endpoint allows to sign arbitrary data with relayer’s private key.

Authentication

A JWT Token is required to make requests to the Relay API. For security reasons authentication uses SRP protocol and it is not possible to retrieve a JWT Token via one HTTP request. Which is why it is advised to use the Amazon Cognito User Pool SDK to retrieve a JWT token.

Python Authentication

The official AWS SDK for python doesn’t support SRP authentication, but it is possible to retrieve a JWT token using the warrant library.

Here is an example:

import boto3
from warrant.aws_srp import AWSSRP

client = boto3.client('cognito-idp')
aws = AWSSRP(username='API_KEY', password='API_SECRET', pool_id='us-west-2_iLmIggsiy', client_id='1bpd19lcr33qvg5cr3oi79rdap', client=client)
tokens = aws.authenticate_user()
print('Access Token', tokens['AuthenticationResult']['AccessToken'])

Refreshing JWT Token

A JWT token will expire in 60 minutes. If your code requires sessions longer than 60 minutes consider recreating a JWT token or using a refresh token.

Making Requests

Once you get a JWT Token you can make requests to the Defender API. A request requires an API key, a JWT Token, optionally a payload, and an API URL. Set $KEY, $TOKEN to the values of API key and JWT Token acquired before. $END_POINT can be either txs or sign.

API_URL='http://api.defender.openzeppelin.com/'

curl \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
    "$API_URL/$END_POINT"

Txs Endpoint

Send Transaction

To send a transaction to the Ethereum blockchain submit a POST request with the desired payload. The payload format is as follows:

export type Address = string;
export type BigUInt = string | number;
export type Hex = string;
export type Speed = 'safeLow' | 'average' | 'fast' | 'fastest';

export interface SendTransactionRequest {
  to: Address;
  value?: BigUInt; // optional
  data?: Hex; // optional
  speed?: Speed; // optional
  gasLimit: BigUInt;
}

An example of the request:

DATA='{ "to": "0x179810822f56b0e79469189741a3fa5f2f9a7631", "value": "1", "speed": "fast", "gasLimit": "21000" }'


curl \
  -X POST \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
  -d "$DATA" \
    "$API_URL/txs" # http://api.defender.openzeppelin.com/txs

You would receive a response in the following format:

export type Address = string;
export type BigUInt = string | number;
export type Hex = string;
export type Speed = 'safeLow' | 'average' | 'fast' | 'fastest';
export type Status = 'pending' | 'sent' | 'submitted' | 'inmempool' | 'mined' | 'confirmed';

export interface TransactionResponse {
  transactionId: string; // Defender tx id
  hash: string; // Ethereum hash
  to: Address;
  from: Address;
  value: string;
  data: string;
  speed: Speed;
  gasPrice: number;
  gasLimit: number;
  nonce: number;
  status: Status;
  chainId: number;
}

Query Transaction

To retrieve a transaction status and data make a GET request to the txs endpoint with the Defender transactionId, not with the transaction hash.

An example of the request:

curl \
  -X GET \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
    "$API_URL/txs/$ID" # http://api.defender.openzeppelin.com/txs/affba150-e563-441e-ae49-04bd6050979a

You would receive a TransactionResponse object, e.g.:

{
   "chainId":4,
   "hash":"0xcef95469a9f02757f0968ec8c11449ae5e7486073075381dcd62bacec9e5d627",
   "transactionId":"affba150-e563-441e-ae49-04bd6050979a",
   "value":"0x1",
   "gasPrice":1000000000,
   "gasLimit":21000,
   "to":"0x179810822f56b0e79469189741a3fa5f2f9a7631",
   "from":"0xbce0b5b71668e42d908e387b68dba91789c932b8",
   "data":"0x",
   "nonce":160,
   "status":"mined",
   "speed":"fast"
}

Sign Endpoint

To sign arbitrary data with the Relay API make a POST request with a payload containing the hex string to sign. The payload format is:

export interface SignMessagePayload {
  message: Hex;
}

An example of the request:

DATA='{ "message": "0x0123456789abcdef" }'

curl \
  -X POST \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
  -d "$DATA" \
    "$API_URL/sign" # http://api.defender.openzeppelin.com/sign

You would receive a response in the following format:

export interface SignedMessagePayload {
  sig: Hex;
  r: Hex;
  s: Hex;
  v: number;
}

An example of the response:

{
   "r":"0x819b2645a0b73494724dac355e6ecfc983d94597b533d34fe3ecd0277046a1eb",
   "s":"0x3b73c695b47dd275d17246d86bbfe35f112a7bdb5bf4a5a1a8e22fe37dfd005a",
   "v":44,
   "sig":"0x819b2645a0b73494724dac355e6ecfc983d94597b533d34fe3ecd0277046a1eb3b73c695b47dd275d17246d86bbfe35f112a7bdb5bf4a5a1a8e22fe37dfd005a2c"
}

Relayer Endpoint

To retrieve a relayer’s data with the Relay API make a GET request to the relayer endpoint.

An example of the request:

curl \
  -X GET \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
    "$API_URL/relayer" # http://api.defender.openzeppelin.com/relayer

You would receive a response in the following format:

export interface RelayerModel {
  relayerId: string;
  name: string;
  address: string;
  network: string;
  paused: boolean;
  createdAt: string;
  pendingTxCost: string;
}

An example of the response:

{
   "relayerId":"d5484fb1-df83-4659-9903-16d57d41f188",
   "name":"Rinkeby",
   "address":"0x71764d6450c2b710fc3e4ee5b7a038d1e7e4fc29",
   "network":"rinkeby",
   "createdAt":"2020-11-02T18:00:00.212Z",
   "paused":false,
   "pendingTxCost":"0"
}

Coming up…​

We are working on new features, such as automatic top-ups of testnet Relayers, xDai support, and Relayer policies to restrict maximum expenditure. Stay tuned, and let us know if you have any requests!