UNPKG

@hazbase/relayer

Version:

The relayer SDK that provides gasless execution (meta-transactions) for the hazBase stack.

265 lines (202 loc) 9.44 kB
# @hazbase/relayer [![npm version](https://badge.fury.io/js/@hazbase%2Frelayer.svg)](https://badge.fury.io/js/@hazbase%2Frelayer) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ## Overview `@hazbase/relayer` is the **relayer SDK** that provides **gasless execution (meta-transactions)** for the hazBase stack. It is **designed to be used together with `@hazbase/kit`’s `ContractBuilder`**: build **calldata & gas estimates** with the builder, then **sign and relay** the request with this package. - Recommended composition: **`ContractBuilder` (call assembly)** + **`@hazbase/relayer` (EIP‑712 signing & relay)** + **`@hazbase/auth` (JWT/client‑key/audit logging)** - Typical use cases: frictionless first‑time UX, sponsored transactions for KYC‑verified users, better mobile UX - Security: one‑time **Auth Code** + `ForwardRequest.nonce` for replay protection, strict **EIP‑712 domain separation** --- ## Flow (architecture) 1) Build calldata & (optionally) gas with **ContractBuilder** (`encode`, `estimate`) 2) Build **ForwardRequest** (`buildForwardRequest`) 3) Produce **EIP‑712 signature** (`signForwardRequest`) 4) Obtain **one‑time Auth Code** (`generateRelayerAuthCode`) 5) **Send** to relayer (`sendRelayerTransaction`) returns **tx hash** > Prefer **`forwardCall`** to do all steps in one go (ABI/method/args relay). When you also use `ContractBuilder`, you can pre‑compute **data/gas** more precisely. --- ## Requirements - Node.js **>= 18** (ESM recommended) - TypeScript **>= 5** - `ethers` **v6** - `@hazbase/auth` (API endpoint & client key configuration, audit logging) - `@hazbase/kit` (`ContractBuilder`) --- ## Installation ```bash npm i @hazbase/relayer @hazbase/kit @hazbase/auth ethers # or pnpm add @hazbase/relayer @hazbase/kit @hazbase/auth ethers ``` --- ## Pre‑setup (`@hazbase/auth`) ```ts // All comments in English per project rule import { setClientKey } from '@hazbase/auth'; setClientKey(process.env.HAZBASE_CLIENT_KEY!); // required for validation & logging ``` --- ## Quick start (with ContractBuilder) ### A) Builder + `forwardCall` (fastest path) ```ts // Use ContractBuilder to centralize address/abi, then relay with forwardCall import { ethers } from 'ethers'; import { signInWithWallet } from '@hazbase/auth'; import { ContractBuilder } from '@hazbase/kit'; import { forwardCall } from '@hazbase/relayer'; import erc20Abi from './abi/MyToken.json' assert { type: 'json' }; const CHAIN_ID = "11155111"; // repolia testnet const provider = new ethers.JsonRpcProvider(process.env.RPC_URL!); const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); /** 1) Build a contract helper (address/abi binding) */ const tokenAddress = '0xToken...'; const signer = await provider.getSigner(); const { accessToken } = await signInWithWallet({ signer }); const contract = ContractBuilder .create({ address: tokenAddress, abi: erc20Abi, chainId: CHAIN_ID, signer }) .withRelayer({ accessToken }) .build(); const decimals = await contract.decimals(); const amountWei = ethers.parseUnits(amount.toString(), decimals); await contract.transfer(toAddress, amountWei); ``` ### B) Builder + low‑level control (explicit `data` / `gas`) ```ts // Use ContractBuilder to encode calldata and estimate gas, then sign & relay import { ethers } from 'ethers'; import { ContractBuilder } from '@hazbase/kit'; import { buildForwardRequest, makeDomain, signForwardRequest, generateRelayerAuthCode, sendRelayerTransaction } from '@hazbase/relayer'; import erc20Abi from './abi/MyToken.json' assert { type: 'json' }; const provider = new ethers.JsonRpcProvider(process.env.RPC_URL!); const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); const chainId = Number((await provider.getNetwork()).chainId); /** 1) Prepare builder */ const tokenAddress = '0xToken...'; const builder = ContractBuilder .create({ address: tokenAddress, abi: erc20Abi, chainId: CHAIN_ID, signer }) .build(); /** 2) Encode calldata via builder */ const to = '0xRecipient...'; const amount = 1_000n; const data = builder.encode('transfer', [to, amount]); // or builder.interface.encodeFunctionData(...) /** 3) Build ForwardRequest */ const req = await buildForwardRequest({ signer, tokenAddress, // maps to request.to chainId }); /** 4) (Optional) refine gas with builder */ const gasEst = await builder.estimate('transfer', [to, amount]); // typical helper name req.gas = BigInt(Math.ceil(Number(gasEst) * 1.2)); // add safety margin /** 5) Inject calldata and sign typed data */ req.data = data; const domain = makeDomain(chainId); const signature = await signForwardRequest({ signer, domain, request: req }); /** 6) Obtain one-time Auth Code and send */ const code = await generateRelayerAuthCode({ accessToken: 'JWT-from-signInWithWallet', chainId, contractAddress: tokenAddress }); const txHash = await sendRelayerTransaction({ chainId, code, request: req, signature }); console.log('relayed tx =', txHash); ``` > The exact API names on `ContractBuilder` (e.g., `encode`, `estimate`) can vary by version. Please consult the **type definitions (`.d.ts`) or your editor’s IntelliSense**. --- ## Function reference ### `forwardCall(params: ForwardCallParams) => Promise<string>` **Purpose**: from ABI/method/args, do ForwardRequest build sign get Auth Code send, and return the **tx hash**. **Params** - `signer: ethers.JsonRpcSigner` - `chainId: number` - `accessToken: string` JWT obtained from `@hazbase/auth.signInWithWallet` - `contractAddress: string` - `abi: ethers.InterfaceAbi` - `method: string` - `args: unknown[]` **Returns**: `string` **transaction hash** (relayer response) --- ### `generateRelayerAuthCode({ accessToken, chainId, contractAddress, type? }) => Promise<string>` **Purpose**: fetch a **one‑time code** required to execute via relayer. **Details** - Internally calls `ensureClientKeyActive(70)` `POST /api/app/code/generate-code` - On success, logs `createRequestTransaction(functionId=70, status='succeeded')` **Params** - `accessToken: string` - `chainId: number` - `contractAddress: string` - `type?: string` (default `'relayer_auth'`) **Returns**: `string` one‑time code --- ### `buildForwardRequest({ signer, tokenAddress, chainId }) => Promise<ForwardRequest>` **Purpose**: build a Minimal‑Forwarder compatible **ForwardRequest** (`from,to,value,gas,nonce,data`). **Details** - Reads `nonce` from the chain’s **Forwarder** (`getForwarderAddress(chainId)`) - Defaults: `value = 0`, `gas` is a coarse estimate (override with builder’s estimate if available) **Params** - `signer: ethers.JsonRpcSigner` - `tokenAddress: string` (mapped to request `to`) - `chainId: number` **Returns**: `ForwardRequest` --- ### `makeDomain(chainId: number) => Domain` **Purpose**: build the EIP‑712 **domain** (`name, version, chainId, verifyingContract`). **Note**: `verifyingContract` is the chain’s **Forwarder address**. --- ### `signForwardRequest({ signer, domain, request }) => Promise<string>` **Purpose**: sign the `ForwardRequest` using EIP‑712 and return the `signature`. --- ### `sendRelayerTransaction({ chainId, code, request, signature }) => Promise<string>` **Purpose**: send the signed `ForwardRequest` to the **relayer API** and get the **tx hash**. **Returns**: `string` transaction hash (or relayer’s canonical identifier) --- ## Types (primary) ```ts export interface ForwardRequest { from: string; to: string; value: bigint; // usually 0n gas: bigint; // estimation; override with builder's estimate nonce: bigint; // forwarder.getNonce(from) data: string; // calldata from ContractBuilder.encode(...) } export interface Domain { name: string; version: string; chainId: number; verifyingContract: string; } export interface ForwardCallParams { signer: ethers.JsonRpcSigner; chainId: number; accessToken: string; contractAddress: string; abi: ethers.InterfaceAbi; method: string; args: unknown[]; } ``` --- ## Utilities / constants (excerpt) - `getRelayerUrl(chainId): string` resolve relayer API base URL - `getForwarderAddress(chainId): string` resolve Minimal‑Forwarder address - `bigintReplacer` `JSON.stringify` replacer to stringify `bigint` > The chain forwarder mapping depends on your environment. See your `utils.ts` / `constants.ts`. --- ## Best practices - **Normalize with the builder**: centralize calldata generation & gas estimation in `ContractBuilder`. - **Domain separation**: always derive domain via `makeDomain()`; pin `chainId` and forwarder per env. - **Re‑issue Auth Code on retry**: it’s one‑time; also re‑read `nonce` on each retry. --- ## Troubleshooting - **`Client key not set`** call `@hazbase/auth.setClientKey()` first. - **`Relayer error`** check `result` in the response (expired Auth Code / forwarder mismatch / encoding issues). - **`invalid signature`** mismatch between `domain` and `request`. Ensure `from` matches the signer’s address. - **`nonce too low/high`** refresh `forwarder.getNonce(from)` and update `request.nonce`. - **`execution reverted`** check roles/paused state on target contract, arguments, and balances. --- ## License Apache-2.0