Getting Started
Broadcast batched ERC2771 meta transactions for EVM Security Token (v4 and later) via a custom forwarder contract.
Feature Overview
Non-Sequential Nonces
The forwarder contract doesn't require sequential nonces, enforcing only nonce uniqueness. This allows for high-scale parallelization of transaction signing.
Batching
Incoming meta transactions are dynamically batched (based on a time window or a maximum batch size) and sent to the forwarder contract in a single transaction. This further increases the throughput of the system.
Status Callbacks
The API accepts a callback URL which is used to deliver status updates for transaction execution.
Usage
See Tooling for snippets and tools to generate key pairs, signatures, and meta transactions.
Authentication
Each API request must include an ed25519 public client key and a signature generated by the corresponding private key. The public key must be shared with Upside to whitelist the client, and should be rotated periodically. Please refer to the Test Utils, API Reference, and examples below for details on how to generate and use the keys.
Example Transaction
Step 1
Generate an ed25519 key pair for the API client and share the public key with Upside.
- Typescript
- CLI
import { createKeypair } from './createKeypair'
const keypair = createKeypair()
console.log(
`Public Key: ${Buffer.from(keypair.publicKey).toString('base64')}`,
)
console.log(
`Secret Key: ${Buffer.from(keypair.secretKey).toString('base64')}`,
)
ts-node test/utils/api/createKeypairCLI.ts
Step 2
Create signed meta transaction.
- Typescript
- CLI
import { Wallet } from 'ethers'
import { createMetaTx } from './createMetaTx'
import { createTxDataForSetAddressPermissions } from './createTxData'
const functionName = 'setAddressPermissions'
const functionArgs = ['0x42D00fC2Efdace4859187DE4865Df9BaA320D5dB', '1', 'false']
const contractAddress = '0x42D00fC2Efdace4859187DE4865Df9BaA320D5dB'
const forwarderAddr = '0xF4258B3415Cab41Fc9cE5f9b159Ab21ede0501B1'
const forwarderChainId = 1
const signerSk = 'SIGNER_PRIVATE_EVM_KEY'
const signer = new Wallet(signerSk)
const metaTxWithSig = await createMetaTx(
{
from: signer.address,
to: contractAddress,
value: 0,
gas: 1000000,
nonce: 0,
deadline: 0,
data: createTxDataForSetAddressPermissions({
destination: functionArgs[0],
regGroup: parseInt(functionArgs[1]),
frozen: functionArgs[2] === 'true',
}),
signature: '',
},
forwarderAddr,
forwarderChainId,
signer,
)
console.log(metaTxWithSig)
ts-node test/utils/cmkv4/transactions/createMetaTxCLI.ts setAddressPermissions 0x42D00fC2Efdace4859187DE4865Df9BaA320D5dB,1,false 0x42D00fC2Efdace4859187DE4865Df9BaA320D5dB 0xF4258B3415Cab41Fc9cE5f9b159Ab21ede0501B1 1 SIGNER_PRIVATE_EVM_KEY
Step 3
Sign and send the API request.
- Typescript
- CLI
import { createReqSignature } from './createReqSignature'
import { sign } from 'tweetnacl'
const callbackUrl = 'https://example.org/tx-updates/1/recieve'
const timestamp = Date.now()
const requestBody = {
metaTx: metaTxWithSig,
callback: {
url: callbackUrl,
},
timestamp,
}
const keyPair = sign.keyPair.fromSecretKey(
Buffer.from('ED_25519_SECRET_IN_BASE64', 'base64'),
)
const signature = createReqSignature(requestBody, keyPair.secretKey)
const sigB64 = Buffer.from(signature).toString('base64')
const pkB64 = Buffer.from(keyPair.publicKey).toString('base64')
const resp = await fetch('https://relayer.upside.gg/api/v1/relay_meta_tx', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Request-Pk-B64': pkB64,
'X-Request-Sig-B64': sigB64,
},
body: JSON.stringify(requestBody),
})
if (resp.status !== 201) {
throw new Error(`Failed to submit meta tx: ${await resp.text()}`)
}
ts-node test/utils/api/createReqSignatureCLI.ts REQUEST_JSON ED_25519_SECRET_IN_BASE64
curl -X POST \
https://relayer.upside.gg/api/v1/relay_meta_tx \
-H 'Content-Type: application/json' \
-H 'X-Request-Pk-B64: REQUEST_PK' \
-H 'X-Request-Sig-B64: REQUEST_SIGNATURE' \
-d REQUEST_JSON
Step 4
Listen for the callback on the provided URL. The callback will include status, transaction hash, and X-Request-Sig-B64
from the original request for authentication.
- Typescript
app.post('/tx-updates/:id/recieve', (req, res) => {
const { status, message, txHash, reqSignature } = req.body
// Verify that received signature:
// 1. Exists
// 2. Belongs to the correct meta-tx
// 3. Hasn't been received before
if (verifySignature(reqSignature, req.params.id)) {
console.log({ status, message, txHash })
res.status(200).send('OK')
} else {
res.status(401).send('Unauthorized')
}
})