|
| 1 | +# JavaScript Client |
| 2 | + |
| 3 | +`@evstack/evnode-viem` is a Viem-based client for EvNode transaction type `0x76`. It builds, signs, sponsors, serializes, and sends sponsored batch transactions to ev-reth. |
| 4 | + |
| 5 | +Use it when your application needs: |
| 6 | + |
| 7 | +- Sponsored gas, where an application account pays transaction fees. |
| 8 | +- Batched calls, where several EVM operations execute atomically. |
| 9 | +- Direct encoding and decoding of `0x76` raw transactions. |
| 10 | + |
| 11 | +## Install |
| 12 | + |
| 13 | +```bash |
| 14 | +pnpm add @evstack/evnode-viem viem |
| 15 | +``` |
| 16 | + |
| 17 | +## Create a Client |
| 18 | + |
| 19 | +`createEvnodeClient` wraps a Viem client. The signer must implement `signHash(hash)`, and that function must sign the raw 32-byte digest without adding an EIP-191 prefix. |
| 20 | + |
| 21 | +```typescript |
| 22 | +import { createClient, http } from 'viem' |
| 23 | +import { privateKeyToAccount, sign } from 'viem/accounts' |
| 24 | +import { createEvnodeClient } from '@evstack/evnode-viem' |
| 25 | + |
| 26 | +const rpcUrl = 'http://localhost:8545' |
| 27 | +const privateKey = '0x...' as const |
| 28 | + |
| 29 | +const client = createClient({ |
| 30 | + transport: http(rpcUrl), |
| 31 | +}) |
| 32 | + |
| 33 | +const account = privateKeyToAccount(privateKey) |
| 34 | + |
| 35 | +const evnode = createEvnodeClient({ |
| 36 | + client, |
| 37 | + executor: { |
| 38 | + address: account.address, |
| 39 | + signHash: async (hash) => sign({ hash, privateKey }), |
| 40 | + }, |
| 41 | +}) |
| 42 | +``` |
| 43 | + |
| 44 | +## Send a Batch |
| 45 | + |
| 46 | +Each call has `to`, `value`, and `data`. Use `to: null` only for the first call when deploying a contract. |
| 47 | + |
| 48 | +```typescript |
| 49 | +const txHash = await evnode.send({ |
| 50 | + calls: [ |
| 51 | + { to: recipient1, value: 1_000_000_000_000_000n, data: '0x' }, |
| 52 | + { to: recipient2, value: 1_000_000_000_000_000n, data: '0x' }, |
| 53 | + ], |
| 54 | +}) |
| 55 | +``` |
| 56 | + |
| 57 | +If either transfer fails, ev-reth reverts the whole transaction. |
| 58 | + |
| 59 | +## Sponsor a Transaction |
| 60 | + |
| 61 | +Use `createIntent` when the executor signs first and a sponsor signs later. |
| 62 | + |
| 63 | +```typescript |
| 64 | +import { createClient, http } from 'viem' |
| 65 | +import { privateKeyToAccount, sign } from 'viem/accounts' |
| 66 | +import { createEvnodeClient } from '@evstack/evnode-viem' |
| 67 | + |
| 68 | +const client = createClient({ transport: http('http://localhost:8545') }) |
| 69 | + |
| 70 | +const executorKey = '0x...' as const |
| 71 | +const sponsorKey = '0x...' as const |
| 72 | +const executor = privateKeyToAccount(executorKey) |
| 73 | +const sponsor = privateKeyToAccount(sponsorKey) |
| 74 | + |
| 75 | +const evnode = createEvnodeClient({ |
| 76 | + client, |
| 77 | + executor: { |
| 78 | + address: executor.address, |
| 79 | + signHash: async (hash) => sign({ hash, privateKey: executorKey }), |
| 80 | + }, |
| 81 | + sponsor: { |
| 82 | + address: sponsor.address, |
| 83 | + signHash: async (hash) => sign({ hash, privateKey: sponsorKey }), |
| 84 | + }, |
| 85 | +}) |
| 86 | + |
| 87 | +const intent = await evnode.createIntent({ |
| 88 | + calls: [ |
| 89 | + { to: executor.address, value: 0n, data: '0x' }, |
| 90 | + ], |
| 91 | +}) |
| 92 | + |
| 93 | +const txHash = await evnode.sponsorAndSend({ intent }) |
| 94 | +``` |
| 95 | + |
| 96 | +The executor remains the transaction sender. The sponsor pays gas and receives gas refunds. |
| 97 | + |
| 98 | +## Manual Parameters |
| 99 | + |
| 100 | +If omitted, the client resolves `chainId`, `nonce`, `maxFeePerGas`, `maxPriorityFeePerGas`, `gasLimit`, and `accessList` from the RPC endpoint or local defaults. |
| 101 | + |
| 102 | +Override them when your application already has fee estimates or nonce management: |
| 103 | + |
| 104 | +```typescript |
| 105 | +await evnode.send({ |
| 106 | + calls: [ |
| 107 | + { to: account.address, value: 0n, data: '0x' }, |
| 108 | + ], |
| 109 | + nonce: 12n, |
| 110 | + gasLimit: 100_000n, |
| 111 | + maxFeePerGas: 1_000_000_000n, |
| 112 | + maxPriorityFeePerGas: 0n, |
| 113 | + accessList: [], |
| 114 | +}) |
| 115 | +``` |
| 116 | + |
| 117 | +## Application Sponsor Service |
| 118 | + |
| 119 | +Applications can hide sponsor keys behind a JSON-RPC proxy. The proxy receives `eth_sendRawTransaction`, detects unsigned type `0x76` transactions, validates the app policy, adds `feePayerSignature`, and forwards the sponsored raw transaction to ev-reth. |
| 120 | + |
| 121 | +The ev-reth repository includes this pattern under `bin/sponsor-service`. |
| 122 | + |
| 123 | +Expected proxy behavior: |
| 124 | + |
| 125 | +- Forward ordinary JSON-RPC requests unchanged. |
| 126 | +- Intercept `eth_sendRawTransaction` only when the raw transaction starts with type byte `0x76`. |
| 127 | +- Forward already-sponsored transactions unchanged. |
| 128 | +- Reject intents that fail policy checks, such as wrong chain ID, high gas limit, high max fee, or low sponsor balance. |
| 129 | + |
| 130 | +Client-side code can then point Viem at the sponsor service instead of the node: |
| 131 | + |
| 132 | +```typescript |
| 133 | +const client = createClient({ |
| 134 | + transport: http('http://localhost:3000'), |
| 135 | +}) |
| 136 | + |
| 137 | +const evnode = createEvnodeClient({ |
| 138 | + client, |
| 139 | + executor: { |
| 140 | + address: executor.address, |
| 141 | + signHash: async (hash) => sign({ hash, privateKey: executorKey }), |
| 142 | + }, |
| 143 | +}) |
| 144 | + |
| 145 | +const txHash = await evnode.send({ |
| 146 | + calls: [ |
| 147 | + { to, value, data }, |
| 148 | + ], |
| 149 | +}) |
| 150 | +``` |
| 151 | + |
| 152 | +In this setup the executor signs the intent, while the sponsor service adds the sponsor signature before forwarding to ev-reth. |
| 153 | + |
| 154 | +## Utilities |
| 155 | + |
| 156 | +The package also exports lower-level helpers: |
| 157 | + |
| 158 | +| Helper | Use | |
| 159 | +|--------|-----| |
| 160 | +| `encodeSignedTransaction` | Serialize an EvNode signed transaction | |
| 161 | +| `decodeEvNodeTransaction` | Decode a type `0x76` raw transaction | |
| 162 | +| `computeExecutorSigningHash` | Compute the executor digest | |
| 163 | +| `computeSponsorSigningHash` | Compute the sponsor digest | |
| 164 | +| `computeTxHash` | Compute the transaction hash | |
| 165 | +| `recoverExecutor` | Recover the executor address | |
| 166 | +| `recoverSponsor` | Recover the sponsor address | |
| 167 | +| `estimateIntrinsicGas` | Estimate the minimum intrinsic gas for calls | |
| 168 | +| `validateEvNodeTx` | Validate local call-list constraints | |
| 169 | + |
| 170 | +## Related Docs |
| 171 | + |
| 172 | +- [Sponsored Batch Transactions](/ev-reth/features/sponsored-transactions) |
| 173 | +- [ev-reth Overview](/ev-reth/overview) |
| 174 | +- [EVM Quickstart](/getting-started/evm/quickstart) |
0 commit comments