Send regular and sponsored transactions with viem
This guide shows how to send a standard Eden transaction with viem, then how to send an
Eden-sponsored transaction with the @evstack/evnode-viem client.
Use Eden testnet while building the flow. For mainnet, switch the RPC URL, chain ID, and keys after the testnet path is working.
Verified Eden testnet transactions
These transactions were produced from the Eden testnet flow in this guide:
-
Send a regular transaction: type
0x02, chain ID3735928814, with a0.001TIA recipient delta verified bycast. -
Send sponsored transactions:
0x4776ece6dcfdaf35f7bc46e79ab3321055c73f761d8232afaa62565829f487210x2b6e28053be6423e05a957b954990cc35971b48eb20d280fea148357fb2d09c0
Eden RPC and
castshow receipt status1, type0x76, an unfunded executor asfrom, and the sponsor asfeePayer. The second sponsored run is included in canonical block184343012; the executor nonce advanced from1to2, the executor balance stayed0, and the sponsor paid147000wei of gas.
Prerequisites
- Node.js 20 or newer.
- Foundry's
castCLI for key generation and transaction inspection. - A funded Eden testnet sender for the regular transaction.
- A funded Eden testnet sponsor for the sponsored transaction.
npm init -y
npm install viem @evstack/evnode-viem
npm install --save-dev tsxConfigure Eden
Use Eden testnet by default:
export RPC_URL=https://rpc.testnet.eden.gateway.fm/
export CHAIN_ID=3735928814For local regular-transaction testing, you can run Anvil with Eden's mainnet chain ID:
anvil --hardfork prague --chain-id 714
export RPC_URL=http://127.0.0.1:8545
export CHAIN_ID=714Create src/eden.ts:
// src/eden.ts
import { defineChain, http } from 'viem'
const rpcUrl = process.env.RPC_URL ?? 'https://rpc.testnet.eden.gateway.fm/'
const chainId = Number(process.env.CHAIN_ID ?? '3735928814')
export const eden = defineChain({
id: chainId,
name: chainId === 714 ? 'Eden' : 'Eden testnet',
nativeCurrency: { decimals: 18, name: 'TIA', symbol: 'TIA' },
rpcUrls: {
default: { http: [rpcUrl] }
},
blockExplorers: {
default: {
name: 'Blockscout',
url: chainId === 714 ? 'https://eden.blockscout.com/' : 'https://eden-testnet.blockscout.com/'
}
}
})
export const transport = http(rpcUrl)Send a regular transaction
A regular transaction is the standard EVM flow: the signer submits the transaction and pays gas.
Set a sender key and recipient address:
export SENDER_PK=<funded-sender-private-key>
export RECIPIENT=<0x...recipient-address>Create src/send-regular.ts:
// src/send-regular.ts
import {
createPublicClient,
createWalletClient,
formatEther,
parseEther,
type Address,
type Hex
} from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { eden, transport } from './eden.js'
function required(name: string): string {
const value = process.env[name]
if (!value) throw new Error(`Missing ${name}`)
return value
}
const account = privateKeyToAccount(required('SENDER_PK') as Hex)
const recipient = required('RECIPIENT') as Address
const amount = parseEther(process.env.AMOUNT_TIA ?? '0.001')
const publicClient = createPublicClient({ chain: eden, transport })
const walletClient = createWalletClient({ account, chain: eden, transport })
const before = await publicClient.getBalance({ address: recipient })
const hash = await walletClient.sendTransaction({
chain: eden,
to: recipient,
value: amount
})
const receipt = await publicClient.waitForTransactionReceipt({ hash })
const after = await publicClient.getBalance({ address: recipient })
console.log(`sender=${account.address}`)
console.log(`recipient=${recipient}`)
console.log(`tx=${hash}`)
console.log(`status=${receipt.status}`)
console.log(`recipientDeltaTIA=${formatEther(after - before)}`)Run it:
npx tsx src/send-regular.tsSave the printed hash and verify it with cast:
export REGULAR_TX=<0x...printed-hash>
cast tx $REGULAR_TX --rpc-url $RPC_URL
cast receipt $REGULAR_TX --rpc-url $RPC_URL
cast balance $RECIPIENT --rpc-url $RPC_URL --etherThe transaction should be a normal EVM transaction from SENDER_PK, and the recipient balance
should increase by the value sent.
Send a sponsored transaction
Eden sponsorship uses Evolve's 0x76 transaction type. The executor signs the call intent, and the
sponsor signs as the fee payer. The sponsor pays gas; the executor still pays any native TIA value
that the call transfers.
This example sends a zero-value call so the executor does not need TIA. Replace TARGET and data
with your app contract address and encoded calldata for a real app action.
Set an executor key, a funded sponsor key, and a target address:
export EXECUTOR_PK=<executor-private-key>
export SPONSOR_PK=<funded-sponsor-private-key>
export TARGET=<0x...target-address>
export EXECUTOR=$(cast wallet address --private-key $EXECUTOR_PK)
export SPONSOR=$(cast wallet address --private-key $SPONSOR_PK)
cast balance $EXECUTOR --rpc-url $RPC_URL --ether
cast balance $SPONSOR --rpc-url $RPC_URL --ether
cast nonce $EXECUTOR --rpc-url $RPC_URL
cast nonce $SPONSOR --rpc-url $RPC_URLCreate src/send-sponsored.ts:
// src/send-sponsored.ts
import { createClient, createPublicClient, type Address, type Hex } from 'viem'
import { privateKeyToAccount, sign } from 'viem/accounts'
import { createEvnodeClient } from '@evstack/evnode-viem'
import { eden, transport } from './eden.js'
function required(name: string): string {
const value = process.env[name]
if (!value) throw new Error(`Missing ${name}`)
return value
}
const executorPk = required('EXECUTOR_PK') as Hex
const sponsorPk = required('SPONSOR_PK') as Hex
const target = required('TARGET') as Address
const executor = privateKeyToAccount(executorPk)
const sponsor = privateKeyToAccount(sponsorPk)
const client = createClient({ transport })
const publicClient = createPublicClient({ chain: eden, transport })
const evnode = createEvnodeClient({
client,
executor: {
address: executor.address,
signHash: async (hash) => sign({ hash, privateKey: executorPk })
},
sponsor: {
address: sponsor.address,
signHash: async (hash) => sign({ hash, privateKey: sponsorPk })
}
})
const intent = await evnode.createIntent({
calls: [{ to: target, value: 0n, data: '0x' }]
})
const hash = await evnode.sponsorAndSend({ intent })
const receipt = await publicClient.waitForTransactionReceipt({ hash })
console.log(`executor=${executor.address}`)
console.log(`sponsor=${sponsor.address}`)
console.log(`tx=${hash}`)
console.log(`status=${receipt.status}`)Run it:
npx tsx src/send-sponsored.tsSave the printed hash and inspect it with cast:
export SPONSORED_TX=<0x...printed-hash>
cast tx $SPONSORED_TX --rpc-url $RPC_URL
cast receipt $SPONSORED_TX --rpc-url $RPC_URL
cast balance $EXECUTOR --rpc-url $RPC_URL --ether
cast balance $SPONSOR --rpc-url $RPC_URL --etherThe transaction should show type 0x76, the executor as the sender, and the sponsor as the fee
payer. The executor balance should not be charged gas for this zero-value call; the sponsor balance
should pay the fee.
Troubleshooting
Missing SENDER_PK,Missing EXECUTOR_PK, orMissing SPONSOR_PK: export the required private key before running the script.insufficient funds: fund the sender for regular transactions, or fund the sponsor for sponsored gas. If the sponsored call transfers native TIA value, fund the executor for that value too.invalid chain id: make sureCHAIN_IDmatches the RPC. Eden mainnet is714; Eden testnet is3735928814.- Stuck or rate-limited requests: the public Gateway.fm RPC is rate limited. Retry later or use a registered Gateway.fm endpoint.