push-backend

star 27

Use when writing Node.js scripts, bots, or server-side code with @pushchain/core - covers PushChain.initialize, wrapping ethers/viem/Solana keypairs into a UniversalSigner, all three routes, multichain cascades, transaction tracking, and utility functions. Not for browser or React code. Triggers on: 'initialize PushChain client in Node.js', 'send transaction from backend script', 'wrap ethers signer with toUniversal', 'track transaction by hash'.

pushchain By pushchain schedule Updated 6/8/2026

name: push-backend description: "Use when writing Node.js scripts, bots, or server-side code with @pushchain/core - covers PushChain.initialize, wrapping ethers/viem/Solana keypairs into a UniversalSigner, all three routes, multichain cascades, transaction tracking, and utility functions. Not for browser or React code. Triggers on: 'initialize PushChain client in Node.js', 'send transaction from backend script', 'wrap ethers signer with toUniversal', 'track transaction by hash'." id: push-backend intent: Execute universal transactions from server-side code, scripts, bots, and automation package: '@pushchain/core' package_version: 6.0.16 current_sdk_version: 6.0.16 entry: 'PushChain.initialize' resources: 'https://push.org/agents/resources/push-backend/index.json' references: - references/signer-options.md - references/initialize-client.md - references/send-universal-transaction.md - ../../workflows/send-multichain-transaction.md

Skill: Universal Transactions - Backend (Node.js / Scripts)

Intent: Execute universal transactions from server-side code, scripts, bots, and automation pipelines. Package: @pushchain/core - no other library (ethers.js, viem, wagmi) can replace sendTransaction, signMessage, prepareTransaction, or executeTransactions.

Full agent layer: push.org/llms.txt indexes every skill, workflow, example, error code, constant, and routing decision in the Push Chain agent layer. Pull it when this skill points outside its domain — cross-skill context, unknown progress-hook IDs, error recovery, or constants lookups.

PUSD stablecoin? For minting, redeeming, or integrating PUSD (par-backed) and PUSD+ (yield-bearing) — both native on Push Chain Donut — see the dedicated push-pusd skill (or pusd.push.org/llms.txt for the full PUSD agent-layer index: ABIs, deployment addresses, examples). Covers Node.js mint/redeem flows, the multicall sentinel pattern for one-signature deposits, and the on-chain Solidity interfaces for protocols holding PUSD/PUSD+.

Install

npm install @pushchain/core

Push Chain - EVM Compatible

Push Chain is 100% EVM-compatible. Any ethers.js or viem code that targets Ethereum works on Push Chain by pointing at the Push Chain RPC - same API, same tooling, no changes needed.

Value
HTTP RPC https://evm.donut.rpc.push.org/
WebSocket RPC wss://evm.donut.rpc.push.org
Chain ID 42101
Block Explorer https://donut.push.network

For read-only queries (transactions, blocks, balances, contract view calls) - no SDK needed. Use ethers.JsonRpcProvider or viem.createPublicClient directly. See Read Blockchain State.

For sending transactions - use @pushchain/core (PushChain.initialize + sendTransaction). Standard ethers/viem cannot cross chains or route through UEAs.


Read Blockchain State

For read-only queries - transactions, blocks, balances, contract view calls, WebSocket subscriptions - no Push Chain SDK needed. Use standard ethers or viem pointed at the Push Chain RPC above.

Initialize (ethers)

import { ethers } from 'ethers';

const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/');
const tx = await provider.getTransaction('0x...');
const block = await provider.getBlock('latest');
const bal = await provider.getBalance('0xAddress');

Initialize (viem)

import { createPublicClient, http } from 'viem';

const client = createPublicClient({
  transport: http('https://evm.donut.rpc.push.org/'),
});
const block = await client.getBlock();
const bal = await client.getBalance({ address: '0xAddress' });

WebSocket (real-time block subscription)

// ethers
const ws = new ethers.WebSocketProvider('wss://evm.donut.rpc.push.org');
ws.on('block', (n) => console.log('New block:', n));

// viem
import { createPublicClient, webSocket } from 'viem';
const wsClient = createPublicClient({
  transport: webSocket('wss://evm.donut.rpc.push.org'),
});
const stop = wsClient.watchBlocks({
  onBlock: (b) => console.log('New block:', b.number),
});

Full reference: https://push.org/agents/workflows/read-blockchain-state.md


Universal Origin - Send from Any Chain

Universal transactions can originate from any supported chain - Push Chain, Ethereum, Solana, BNB, Arbitrum, Base, or any supported chain. The user's wallet stays on their home chain; Push Chain routes execution transparently.

The primary pattern for all EVM chains (ethers.js or viem) is identical - the RPC URL determines the origin chain, not a separate parameter:

import { PushChain } from '@pushchain/core';
import { ethers } from 'ethers';

// Origin chain = Ethereum Sepolia (Sepolia RPC)
const provider = new ethers.JsonRpcProvider(
  'https://sepolia.gateway.tenderly.co'
);
// Origin chain = Push Chain (Donut RPC)
// const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/');
// Origin chain = BNB Testnet (BNB RPC)
// const provider = new ethers.JsonRpcProvider('https://data-seed-prebsc-1-s1.binance.org:8545/');

// Never hardcode - load from env; never log the key
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const universalSigner = await PushChain.utils.signer.toUniversal(wallet);

For Solana, explicitly pass the chain and library:

import { Keypair } from '@solana/web3.js';

// Never hardcode - load from env; never log the key or the raw buffer
const solKeypair = Keypair.fromSecretKey(
  Uint8Array.from(JSON.parse(process.env.SOLANA_KEY!))
);
const universalSigner = await PushChain.utils.signer.toUniversalFromKeypair(
  solKeypair,
  {
    chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET,
    library: PushChain.CONSTANTS.LIBRARY.SOLANA_WEB3JS, // Available LIBRARY values: SOLANA_WEB3JS (more may be added - see https://push.org/agents/workflows/constants-reference.md)
  }
);

After creating any signer, call PushChain.initialize to get the client:

const client = await PushChain.initialize(universalSigner, {
  network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, // optional, defaults to TESTNET
});
// Returns PushChainClient - use client.universal.* for all operations

Frontend / React? Use @pushchain/ui-kit instead - PushUniversalWalletProvider handles signer creation and PushChain.initialize automatically. Access the ready client via usePushChainClient(). See push-frontend skill.

Quick reference (backend / scripts):

Always two steps - create a chain-native signer, then convert it to a UniversalSigner:

Origin Step 1 - Create native signer Step 2 - Create Universal Signer
Ethereum / EVM (ethers.js) new ethers.Wallet(key, provider) ← RPC picks chain PushChain.utils.signer.toUniversal(ethersSigner)
Ethereum / EVM (viem) createWalletClient({ account, transport: http(rpc) }) ← RPC picks chain PushChain.utils.signer.toUniversal(viemClient)
Solana Keypair.fromSecretKey(...) PushChain.utils.signer.toUniversalFromKeypair(keypair, { chain, library })
Custom / any implement signMessage, signAndSendTransaction, signTypedData PushChain.utils.signer.construct(account, signingMethods)

See all supported chains: PushChain.utils.chains.getSupportedChainsByName(PushChain.CONSTANTS.PUSH_NETWORK.TESTNET)

Initialize Client

PushChain.initialize(signerOrAccount, options?)Promise<PushChainClient>

Argument Type Default Description
signerOrAccount UniversalSigner | UniversalAccount - UniversalSigner for full write/sign; UniversalAccount for read-only
options.network PushChain.CONSTANTS.PUSH_NETWORK TESTNET Push Chain network to connect to
options.rpcUrls Partial<Record<CHAIN, string[]>> {} Custom RPC URLs per chain

Full write/sign (pass a UniversalSigner):

const client = await PushChain.initialize(universalSigner, {
  network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET,
});

⚠️ Passing a UniversalAccount creates a read-only client. Calling sendTransaction, signMessage, prepareTransaction, or executeTransactions will throw. Use UniversalSigner for any write operation.

Read-only - pass a UniversalAccount (no private key). universal.account, universal.origin, and explorer.* still work:

const account = PushChain.utils.account.toUniversal('0xAddress', {
  chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA,
});
const client = await PushChain.initialize(account, {
  network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET,
});

Reinitialize - swap signer/account or update options without creating a fresh instance:

const newClient = await client.reinitialize(newSignerOrAccount, {
  // override options as needed (rpcUrls, blockExplorers, etc.)
});
// reinitialize always returns a new client - update your reference

Access account info after initialization:

client.universal.origin;  // { address: string, chain: CHAIN } — source chain wallet (object)
client.universal.account; // `0x${string}` — the Push Chain execution account address (UEA for
                          // cross-chain users, native EOA for Push-native users). Plain string,
                          // NOT an object.

⚠️ Shape difference. origin is an object ({ address, chain }); account is a plain address string — no .address field. Reading client.universal.account.address returns undefined and silently breaks dependent guards. Use client.universal.account directly.

Verify initialization succeeded:

console.log('origin:', client.universal.origin);   // { address, chain } - object, matches your signer
console.log('account:', client.universal.account); // '0x...' string - UEA (cross-chain) or EOA (Push-native)

Account status - UEA deployment and version (SDK handles upgrades automatically in most cases):

const status = await client.getAccountStatus();
// { mode: 'signer' | 'read-only', uea: { deployed, version, minRequiredVersion, requiresUpgrade } }

UI Kit (frontend): PushChain.initialize is called automatically by PushUniversalWalletProvider. Use usePushChainClient() to access the ready client - no manual initialization needed.

client.orchestrator is reserved for internal SDK use (RPC resolution, UEA management, gas orchestration) - do not call methods on it directly.

Reading blockchain state does NOT require @pushchain/core. Use ethers.js or viem directly with the Push Chain RPC https://evm.donut.rpc.push.org/. The PushChainClient is for sending and signing universal transactions - not general-purpose EVM reads.

Send Universal Transaction

client.universal.sendTransaction(tx)Promise<TxResponse>

Routing - determined by tx.to and tx.from

Route tx.to tx.from Executes on Via
Route 1 '0x...' plain address omitted Push Chain UEA
Route 2 { address, chain } omitted External chain CEA on target chain
Route 3 '0x...' plain address { chain } Push Chain CEA on from.chain as origin

Arguments

Argument Type Description
tx.to string | { address: string; chain: CHAIN } Execution target. Plain address → Route 1. { address, chain } → Route 2.
tx.from { chain: CHAIN } (optional) Forces CEA on the specified external chain as execution origin → Route 3.
tx.value bigint (optional) Native value in smallest unit - uPC on Push Chain; native asset on external routes.
tx.data string | Array<{ to: string; value: bigint; data: string }> (optional) Encoded calldata for a single call or multicall array. EVM: encodeTxData({ abi, functionName, args }). Solana: encodeTxData({ idl, functionName, args }). Multicall requires tx.to: '0x000...0'.
tx.funds { amount: bigint; token?: MOVEABLE.TOKEN } (optional) Move supported assets as part of the tx. For Route 1: external origin only (Push-native users use ERC-20 transfer directly).
tx.progressHook (progress: ProgressHookType) => void (optional) Callback for per-step lifecycle events. Event IDs are route-prefixed (SEND-TX-1xx Route 1, SEND-TX-2xx Route 2, SEND-TX-3xx Route 3). See ProgressHook Events below.
tx.payGasWith { token?: PAYABLE.TOKEN; slippageBps?: number; minAmountOut?: bigint | string } (optional) Pay universal gas fees with a supported ERC-20 instead of native.
tx.gasLimit bigint (optional, SDK estimated) Override gas limit.
tx.maxFeePerGas bigint (optional, SDK estimated) Override max fee per gas.
tx.maxPriorityFeePerGas bigint (optional, SDK estimated) Override priority fee.
tx.deadline bigint (optional) Execution deadline.
tx.options.enforceGasCheck boolean (optional, defaults to false) Pre-flight gas / balance check mode. false (default): emit a WARNING progress event on shortfall and proceed (the SDK's fee-locking / refill paths usually recover). true: emit an ERROR progress event and throw InsufficientUEABalanceError before broadcast. Use true when you want pre-flight guarantees over best-effort retries. Same option is accepted by prepareTransaction; setting it on any single hop opts the entire cascade into strict mode.

ProgressHook Events

Each progress object passed to tx.progressHook has this shape:

{
  id: string; // e.g. 'SEND-TX-101'
  title: string;
  message: string;
  level: 'INFO' | 'SUCCESS' | 'ERROR';
  response: object | null; // extra data: chain, address, txHash, etc.
  timestamp: string; // ISO-8601
}

Event IDs are prefixed by route. Key milestones per route:

Route Prefix First event Success Failure
Route 1 → Push Chain SEND-TX-1xx SEND-TX-101 SEND-TX-199-01 SEND-TX-199-02
Route 2 → External chain SEND-TX-2xx SEND-TX-201 SEND-TX-299-01 SEND-TX-299-02
Route 3 CEA → Push Chain SEND-TX-3xx SEND-TX-301 SEND-TX-399-01 SEND-TX-399-02
Multichain cascade SEND-TX-0xx/9xx SEND-TX-001 SEND-TX-999-01 SEND-TX-999-02

Full event list (all routes, all IDs, response shapes): https://push.org/agents/workflows/progress-hook-events.md


Route 1 - Any Origin → Push Chain (via UEA)

tx.to is a plain address. The user's UEA executes the call on Push Chain.

const tx = await client.universal.sendTransaction({
  to: '0xContractOnPushChain',
  value: PushChain.utils.helpers.parseUnits('0.01', 18),
  data: PushChain.utils.helpers.encodeTxData({
    abi,
    functionName: 'increment',
  }),
});
const receipt = await tx.wait();
if (receipt.status !== 1) throw new Error(`tx failed: ${receipt.hash}`);
console.log('status:', receipt.status === 1 ? 'success' : 'failed');

funds is only needed on Route 1 when the origin is an external chain and you want to move assets to Push Chain atomically. Push-native users call ERC-20 transfer directly instead.


Route 2 - Any Origin → External Chain (via CEA)

tx.to is { address, chain }. Push Chain coordinates execution through the user's CEA on the target chain.

const tx = await client.universal.sendTransaction({
  to: {
    address: '0xContractOnBnb',
    chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET,
  },
  data: PushChain.utils.helpers.encodeTxData({
    abi,
    functionName: 'increment',
  }),
  funds: {
    amount: PushChain.utils.helpers.parseUnits('1', 6),
    token: PushChain.CONSTANTS.MOVEABLE.TOKEN.BNB_TESTNET.USDT,
  },
});
const receipt = await tx.wait();
console.log('external hash:', receipt.externalTxHash);
console.log('explorer:', receipt.externalExplorerUrl);

Route 2 receipt includes additional fields: externalTxHash, externalChain, externalExplorerUrl.

Solana target variant - same Route 2 shape; pass the Anchor IDL via encodeTxData and the SDK resolves all accounts, PDAs, and the CEA automatically:

import testCounterIdl from './target/idl/test_counter.json';

const data = PushChain.utils.helpers.encodeTxData({
  idl: testCounterIdl, // Anchor IDL from target/idl/*.json
  functionName: 'receive_sol', // snake_case or camelCase both accepted
  args: [BigInt(0)], // use BigInt for u64/u128 args
});
const tx = await client.universal.sendTransaction({
  to: {
    address: '8yNqjrMnFiFbVTVQcKij8tNWWTMdFkrDf9abCGgc2sgx',
    chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET,
  },
  value: BigInt(0),
  data,
});
const receipt = await tx.wait();
console.log('Solana tx hash:', receipt.externalTxHash);
console.log('Solana explorer:', receipt.externalExplorerUrl);

Route 3 - CEA Origin → Push Chain

Add from: { chain } to use your CEA on an external chain as the execution origin on Push Chain. msg.sender inside the target contract will be the CEA, not the UEA.

Why CEAs exist. When your Push Chain account first interacts with an external chain (e.g. calling Aave on Ethereum), the protocol deterministically deploys a Chain Executor Account (CEA) for you on that chain. This CEA:

  1. Preserves identity - your actions on Ethereum are traceable to a stable, deterministic address derived from your Push Chain account.
  2. Isolates risk - the CEA is a dedicated smart account, separate from your home wallet. External-chain actions can't affect funds outside the CEA.
  3. Enables payload execution - the CEA is what actually holds assets and executes calldata on the external chain.

When to use Route 3. Use it when you need to bring state or assets back to Push Chain from a CEA you've already deployed - because only the CEA can speak for what happened on that external chain.

Example flow - a universal vault:

  1. Route 2: vault calls Aave on Ethereum via your Ethereum CEA to withdraw USDC.
  2. Route 3: the vault moves the withdrawn USDC from your Ethereum CEA back to Push Chain (from: { chain: ETHEREUM_SEPOLIA }) - msg.sender on Push Chain is your Ethereum CEA, which holds the tokens.
  3. Route 2 again: Push Chain forwards to a Solana lending protocol via your Solana CEA.
// Bring assets/state back from the Ethereum CEA to Push Chain
const tx = await client.universal.sendTransaction({
  from: { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA }, // your Ethereum CEA as origin
  to: '0xVaultOnPushChain',
  data: PushChain.utils.helpers.encodeTxData({
    abi,
    functionName: 'depositFromEthereum',
  }),
});
await tx.wait();
// Inside 0xVaultOnPushChain: msg.sender === CEA(Ethereum Sepolia, userAddress)
// The CEA is the only address that can prove "these assets came from your Ethereum side".
User → Push vault
  Route 2 → Ethereum CEA → Aave (withdraw USDC)
  Route 3 ← Ethereum CEA → Push vault (receive USDC, msg.sender = Ethereum CEA)
  Route 2 → Solana CEA → Solana lending (deposit)

Route 3 isn't for new outbound flows - use Route 2 for those. Route 3 is the return path from a CEA you've already deployed via prior Route 2 activity.

Route 3 funding pattern (what the dev actually has to send). Depends on whether an asset is moving off the external chain:

Sub-pattern Fund Why
Plain contract call / multicall on Push Chain (no funds, no value on the call) Source-chain UOA only (e.g. ~0.01 Sepolia ETH) The SDK fee-locks source-chain native, mints PC into the UEA, then UEA → UGPC swaps PC into the destination-chain native to cover CEA gas + first-time CEA deployment. CEA does not need a manual top-up.
Bridge native back (from: { chain } + value + to: client.universal.account) UOA on source chain AND the native asset on the CEA on the named external chain (amount = value) The native asset being swept back has to physically sit on the CEA before the call. CEA gas still comes from the source-chain fee-lock.
Bridge funds back (from: { chain } + funds: { amount, token: MOVEABLE.<chain>.<TOKEN> } + to: client.universal.account) UOA on source chain AND the SDK-registered PRC-20 source token on the CEA on the named external chain (amount = funds.amount) ERC-20 variant of the above. Mint the SDK-registered token at PushChain.CONSTANTS.MOVEABLE.TOKEN.<CHAIN>.<TOKEN>.address (e.g. 0xE935d9c9C24D02E61186c640cc01d713C876d40F for USDT on BNB Testnet) and transfer to the printed CEA address.
Funds-with-payload (bridge an asset back AND atomically call a Push Chain contract) Same as bridge-funds-back The payload runs on Push Chain via the UEA after the bridge settles.

Anti-pattern: telling devs to send 0.02 BNB / SOL to the CEA "for gas + fee-lock deposit" for a plain Route 3 call. The CEA gas is automatic; only ask for the CEA-side asset when an asset is actually being bridged back. Even then, ask for the burn amount itself (e.g. 0.001 BNB, 0.01 USDT), not a 10x buffer.

Solana CEA as origin - set from: { chain: SOLANA_DEVNET } to use the user's Solana CEA as the origin. msg.sender on Push Chain will be the Solana CEA address. Use deriveExecutorAccount beforehand if you need to fund it.

// Route 3 - Solana CEA origin → Push Chain contract
const tx = await client.universal.sendTransaction({
  from: { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET }, // Solana CEA as origin
  to: '0xContractOnPushChain',
  data: PushChain.utils.helpers.encodeTxData({
    abi,
    functionName: 'increment',
  }),
});
await tx.wait();
// Inside 0xContractOnPushChain: msg.sender === CEA(Solana Devnet, userAddress)

Multicall - Batch Multiple Calls

Pass an array to tx.data and set tx.to to the zero address. The zero-address target signals multicall mode to the SDK; individual call targets are in the data array.

External-chain origin only. Push-native senders cannot use multicall and the SDK will throw. Ensure the signer's origin is an external chain when using this pattern.

await client.universal.sendTransaction({
  to: '0x0000000000000000000000000000000000000000', // required for multicall
  data: [
    { to: '0xContract1', value: 0n, data: call1Data },
    { to: '0xContract2', value: 0n, data: call2Data },
  ],
});

TxResponse

Field Type Description
hash string Push Chain transaction hash
origin string CAIP-10 origin: 'eip155:chainId:address'
from string UEA address that executed on Push Chain
to string | null Target address
value bigint Value transferred (in smallest unit)
data string Calldata
chainId string Push Chain ID ('42101' on testnet)
blockNumber bigint Block number
nonce number UEA nonce
gasLimit bigint Gas limit
gasPrice bigint Gas price (wei)
maxFeePerGas bigint EIP-1559 max fee
maxPriorityFeePerGas bigint EIP-1559 priority fee
wait() (confirmations?) => Promise<TxReceipt> Wait for on-chain confirmation

External chain fields (externalTxHash, externalChain, externalExplorerUrl) are only available on TxReceipt after tx.wait() - not on the initial TxResponse.

TxReceipt (from await tx.wait())

Field Type Description
status number 1 = success, 0 = failure
hash string Transaction hash
blockNumber bigint Confirmed block number
from string Executor address (UEA)
to string | null Target address
gasUsed bigint Actual gas consumed
gasPrice bigint Gas price used
logs array Emitted event logs
contractAddress string | null Deployed contract address (deploy txs only)
raw { from, to, nonce, data, value } Raw on-chain data
externalTxHash string (Route 2 only) Hash on the external chain
externalChain string (Route 2 only) External chain identifier
externalExplorerUrl string (Route 2 only) External explorer URL

Prepare + Execute - Multichain Cascade

Use prepareTransaction + executeTransactions when you need multiple ordered hops across chains under a single user signature. Each hop is prepared independently, then all are submitted together.

prepareTransaction

client.universal.prepareTransaction(tx)Promise<PreparedUniversalTx>

Accepts the same arguments as sendTransaction (same routing rules: Route 1 / Route 2 / Route 3 via tx.to and tx.from). Does not submit - returns a prepared object.

// Hop 0: call a contract on Push Chain (Route 1)
const hop0 = await client.universal.prepareTransaction({
  to: '0xContractOnPushChain',
  data: PushChain.utils.helpers.encodeTxData({
    abi: MyABI,
    functionName: 'increment',
  }),
});

// Hop 1: call a contract on BNB Testnet (Route 2)
const hop1 = await client.universal.prepareTransaction({
  to: {
    address: '0xContractOnBnb',
    chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET,
  },
  data: PushChain.utils.helpers.encodeTxData({
    abi: MyABI,
    functionName: 'increment',
  }),
});

PreparedUniversalTx shape:

Property Type Description
route 'UOA_TO_PUSH' | 'UOA_TO_CEA' | 'CEA_TO_PUSH' | 'CEA_TO_CEA' Detected routing mode
estimatedGas bigint Estimated gas units
nonce bigint Nonce for submission
deadline bigint Signature expiry deadline
payload string Encoded payload ready for executeTransactions

executeTransactions

client.universal.executeTransactions(txs)Promise<CascadedTxResponse>

Submits all prepared hops as a single Push Chain transaction. The SDK coordinates downstream execution across chains.

const cascade = await client.universal.executeTransactions([hop0, hop1]);
console.log('Push Chain tx:', cascade.initialTxHash);
console.log('Hops:', cascade.hopCount);

// Wait for all hops to confirm
const result = await cascade.wait({
  progressHook: (e) =>
    console.log(`[Hop ${e.hopIndex}] ${e.status} on ${e.chain}`),
  pollingIntervalMs: 3000,
  timeout: 300_000, // 5 min
});
console.log('All complete:', result.success);

CascadedTxResponse shape:

Property Type Description
initialTxHash string Hash of the user-signed Push Chain transaction; use this to reference it downstream
initialTxResponse UniversalTxResponse Full response for the coordinating Push Chain tx; use this when you need nonce, gas, or block metadata
hops CascadeHopInfo[] All hops with routing and status
hopCount number Total hop count
finalTxHash string (optional) Final tx hash resolved by waitForAll() / wait() once cascade tracking completes
wait(opts?) Promise<CascadeCompletionResult> Wait for all hops to confirm
waitForAll(opts?) Promise<CascadeCompletionResult> Alias for wait

CascadeHopInfo per hop:

Property Type Description
hopIndex number 0-indexed position
route TransactionRouteType 'UOA_TO_PUSH', 'UOA_TO_CEA', 'CEA_TO_PUSH', or 'CEA_TO_CEA'
executionChain CHAIN Chain where this hop executes
expectedSubTxId string (optional) Expected universalSubTxId, computed deterministically from the parent; available before txHash resolves
status 'pending' | 'submitted' | 'confirmed' | 'failed' Current status
txHash string (optional) Resolved transaction hash
outboundDetails OutboundTxDetails (optional) Outbound hops only. Fields: externalTxHash, destinationChain (CHAIN), explorerUrl, recipient, amount, assetAddr (address(0) for native)

CascadeCompletionResult (from wait() / waitForAll()):

Property Type Description
success boolean True if all hops confirmed
hops CascadeHopInfo[] Final state of all hops
finalTxHash string (optional) Final tx hash for the last confirmed hop
finalTxResponse CascadedTxResponse (optional) Original cascade response, for consumers that need the full context
failedAt number (optional) Index of first failed hop, if any

CascadeTrackOptions (wait / waitForAll options):

Option Type Default Description
pollingIntervalMs number 3000 Poll interval (ms)
timeout number 300000 Total timeout (ms), default 5 min
progressHook (event: CascadeProgressEvent) => void - Per-hop callback: { hopIndex, route, chain, status, txHash, elapsed }
eventHook (event: ProgressEvent) => void - Unified ProgressEvent stream for the cascade marker set (001, 002-xx, 003-xx, 203-xx, 204-xx, 209-xx, 299-01, 999-xx, plus per-route awaiting/polling/success/failed/timeout). Cascade markers also fan out to the init-time progressHook on PushChain.initialize. Both channels are deduped if wired.

No atomicity across hops - if a downstream hop fails, earlier hops are already on-chain. Design contracts to handle partial execution.

Single signature - executeTransactions submits one transaction to Push Chain; the SDK coordinates all downstream hops automatically.


Track Transaction

trackTransaction - resume tracking any tx by hash

client.universal.trackTransaction(txHash, options)Promise<UniversalTxResponse>

Use this to re-check progress of a previously submitted transaction - after a page refresh, from a backend poller, or for any tx hash retrieved from storage. Works for transactions that originated on Push Chain or any external chain.

Argument Type Default Description
txHash string - Hash/signature of the transaction on its origin chain
options.chain CHAIN PushChain.CONSTANTS.CHAIN.PUSH_TESTNET_DONUT Chain where the tx was originally submitted
options.progressHook (event) => void undefined Progress callback (same shape as sendTransaction)
options.waitForCompletion boolean true true = wait for confirmation; false = return after first status check
options.advanced.pollingIntervalMs number 2000 Poll interval in ms (min 500)
options.advanced.timeout number 60000 Max wait ms before timeout error
options.advanced.rpcUrls Partial<Record<CHAIN, string[]>> {} Custom RPC URLs for status queries

Returns the same UniversalTxResponse shape as sendTransaction.

// Track a Push Chain tx
const response = await client.universal.trackTransaction(
  '0xbd765a6b60da077eaa89a382cd59c0469a4eaabcaca2707d3e6dcdeafc497a39',
  {
    progressHook: (p) => console.log(`${p.id}: ${p.message}`),
    advanced: { timeout: 30_000 },
  }
);

// Track an Ethereum Sepolia-origin tx
const response2 = await client.universal.trackTransaction(
  '0x9b4743376689eb6f90f3aeb9eea58381b3bcc033e1de4709281fd58a77b85098',
  { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA }
);

tx.wait() - wait for confirmation inline

After sendTransaction, call .wait() on the response:

try {
  const tx = await client.universal.sendTransaction({
    to: '0xRecipient',
    value: 1n,
  });
  const receipt = await tx.wait();
  if (receipt.status !== 1) throw new Error(`tx failed: ${receipt.hash}`);
  console.log('success');
} catch (e) {
  // Error codes and recovery actions: https://push.org/agents/errors.json
  console.error(e);
}

Account & CEA Maintenance

migrateCEA(chain) - upgrade a CEA to the latest version

client.universal.migrateCEA(chain)Promise<UniversalTxResponse>

Migrates the Chain Executor Account (the contract that runs your Push-originated calls on an external chain) to the latest implementation. Sends a MIGRATION_SELECTOR payload via Route 2. Pass the external chain whose CEA should be upgraded.

const tx = await client.universal.migrateCEA(
  PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA
);
await tx.wait();

rescueFunds({ universalTxId, prc20 }) - recover funds stuck in the source Vault

client.universal.rescueFunds(params)Promise<UniversalTxResponse>

When a CEA-to-Push (Route 3) inbound transaction fails, the bridged tokens stay locked in the source-chain Vault. rescueFunds triggers a manual TSS revert that releases them back to you.

Arg Type Description
universalTxId 0x${string} bytes32 hash of the failed inbound transaction
prc20 0x${string} Push Chain PRC-20 token address whose source-chain counterpart is locked
const tx = await client.universal.rescueFunds({
  universalTxId: '0x...', // the failed inbound tx hash
  prc20: '0x...',         // PRC-20 whose locked source funds to release
});
await tx.wait();

Sign a Message

const message = new TextEncoder().encode('Hello Push Chain'); // string also accepted; Uint8Array recommended
const signature = await client.universal.signMessage(message); // returns Uint8Array
// To get a hex string: Buffer.from(signature).toString('hex')

Utility Functions

All utilities are available as PushChain.utils.* - no separate import needed.

parseUnits(value, exponent)bigint

Converts a human-readable token amount to its smallest unit.

Argument Type Description
value string Human-readable amount, e.g. '1.5'
exponent number | { decimals: number } Decimal places - 18 for PC/ETH, 6 for USDC, 8 for BTC

Returns: bigint - e.g. 1500000000000000000n

formatUnits(value, decimals)string

Converts a raw smallest-unit amount back to a human-readable string.

Argument Type Description
value bigint | string Raw amount in smallest units
decimals number | { decimals: number; precision?: number } Decimal places; pass an object to also round to precision places

Returns: string - e.g. '1.5', '100.50'

encodeTxData({ abi | idl, functionName, args })string

Encodes smart contract calldata without needing viem or ethers.js. Works for both EVM (ABI) and Solana (Anchor IDL) targets.

Argument Type Description
abi | idl any[] EVM ABI array (abi) or Anchor IDL object (idl). Determines encoding: ABI → 4-byte selector + ABI-encoded args; IDL → 8-byte Anchor discriminator + Borsh-encoded args.
functionName string Function (EVM) or instruction (Solana) name. Both snake_case and camelCase accepted for Solana.
args any[] (optional) Function arguments, default []. Use bigint for uint256/u64/u128.

Returns: string - hex-encoded calldata, e.g. '0xd09de08a'

// EVM target - use `abi` key
PushChain.utils.helpers.encodeTxData({
  abi,
  functionName: 'transfer',
  args: ['0xabc...', 1000n],
});
// args follow ABI types - use bigint for uint256 (passing 1000 instead of 1000n will fail strict TypeScript)
// Solana target - use `idl` key (not `abi`)
PushChain.utils.helpers.encodeTxData({
  idl: testCounterIdl, // Anchor IDL from target/idl/*.json
  functionName: 'receive_sol', // snake_case or camelCase both accepted
  args: [BigInt(0)], // use BigInt for u64/u128
});

getSupportedChains(pushNetwork){ chains: CHAIN[] }

Argument Type Description
pushNetwork PushChain.CONSTANTS.PUSH_NETWORK Network to query

Returns: { chains } - array of PushChain.CONSTANTS.CHAIN.* constant values

getSupportedChainsByName(pushNetwork){ chains: string[] }

Argument Type Description
pushNetwork PushChain.CONSTANTS.PUSH_NETWORK Network to query

Returns: { chains } - human-readable names, e.g. ['PUSH_TESTNET_DONUT', 'ETHEREUM_SEPOLIA', 'SOLANA_DEVNET', ...]

getChainNamespace(chainName)string

Argument Type Description
chainName string e.g. 'ETHEREUM_SEPOLIA'

Returns: CAIP-2 namespace string, e.g. 'eip155:11155111' - undefined if unsupported

getChainName(chainNamespace)string

Argument Type Description
chainNamespace string CAIP-2 namespace, e.g. 'eip155:11155111'

Returns: Chain name string, e.g. 'ETHEREUM_SEPOLIA' - undefined if unsupported


getMoveableTokens(chainOrClient?){ tokens: MoveableToken[] }

Returns supported assets that can be moved across chains (used in tx.funds.token).

Argument Type Description
chainOrClient CHAIN | PushChainClient (optional) Filter tokens for a specific chain

Returns: { tokens: Array<{ chain, symbol, decimals, address }> }

getPayableTokens(chainOrClient?){ tokens: PayableToken[] }

Returns supported assets that can be used to pay gas or fund token movement (used in tx.payGasWith.token).

Argument Type Description
chainOrClient CHAIN | PushChainClient (optional) Filter tokens for a specific chain

Returns: { tokens: Array<{ chain, symbol, decimals, address }> }

getPRC20Address(token, options?){ address, chain, symbol, decimals, network }

Resolves the Push Chain synthetic PRC20 address for a supported origin-chain token. Accepts either a MoveableToken (e.g., from getMoveableTokens()) or an object containing the origin chain and token address.

Argument Type Description
token MoveableToken | { chain: string; address: string } Origin token - from getMoveableTokens() or explicit chain + address
options.network PushChain.CONSTANTS.PUSH_NETWORK Override the Push network. Defaults to client's initialized network. For example: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET

Returns: { address: 0x${string}, chain: CHAIN, symbol: string, decimals: number, network: PUSH_NETWORK }

// Using { chain, address }
const prc20Alt = PushChain.utils.tokens.getPRC20Address({
  chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA,
  address: '0x97F477B7f970D47a87B42869ceeace218106152a',
});
console.log('USDC.eth:', JSON.stringify(prc20Alt));

// Using a MoveableToken directly
const { tokens: moveable } = PushChain.utils.tokens.getMoveableTokens(
  PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA
);
const ethMoveable = moveable.find((t) => t.symbol === 'ETH');
const {
  address: prc20Addr,
  symbol,
  decimals,
  network,
} = PushChain.utils.tokens.getPRC20Address(ethMoveable);

toUniversal(address, { chain })UniversalAccount

Wraps an address and chain into a UniversalAccount object.

Argument Type Description
address string Wallet address
options.chain CHAIN PushChain.CONSTANTS.CHAIN.*

Returns: { chain: string, address: string } - e.g. { chain: 'eip155:11155111', address: '0xAbc...' }

toChainAgnostic(address, { chain })string

Produces a fully-qualified chain-agnostic address string.

Argument Type Description
address string Wallet address
options.chain CHAIN PushChain.CONSTANTS.CHAIN.*

Returns: string - e.g. 'eip155:11155111:0xAbc...'

fromChainAgnostic(chainAgnosticAddress)UniversalAccount

Parses a chain-agnostic address string back into a UniversalAccount.

Argument Type Description
chainAgnosticAddress string Full chain-agnostic string, e.g. 'eip155:11155111:0xAbc...'

Returns: { chain: string, address: string }

deriveExecutorAccount(universalAccount, options?)Promise<{ address, deployed? }>

Derives a UEA on Push Chain from any origin account, or a CEA on an external chain from a Push/UOA account.

Argument Type Description
universalAccount UniversalAccount Created via toUniversal()
options.chain CHAIN (optional) When provided, derives a CEA on that external chain instead of a UEA on Push Chain
options.skipNetworkCheck boolean (optional) Deterministic derivation only, skip deployment check. Default false

Returns: { address: string, deployed?: boolean } - deployed is included when skipNetworkCheck is false

resolveControllerAccount(account, options?)Promise<{ accounts }>

Reverse-maps any executor account (UEA or CEA) back to its origin controlling wallet. Complement of deriveExecutorAccount - forward is UOA→executor, this is executor→UOA. Prefer this over the older convertExecutorToOriginAccount (still exported but deprecated since 6.0.0): resolveControllerAccount handles UEA and CEA in one call, returns the full controller chain with type / role metadata, and supports skipNetworkCheck for deterministic-only resolution.

Argument Type Description
account string Executor address - can be a UEA, CEA, or Push Chain account address
options.chain CHAIN (optional) Required for CEA context - specifies the external chain the CEA is deployed on
options.skipNetworkCheck boolean (optional) Deterministic resolution only, skip existence check. Default false

Returns: Promise<{ accounts: Array<{ chain, chainName, address, type, exists, role? }> }> - type: 'uea' | 'uoa' | 'cea'; role: 'controller' marks the root controlling account.

// Resolve a UEA back to its controlling UOA
const { accounts } =
  await PushChain.utils.account.resolveControllerAccount('0xUEAAddress');
const controller = accounts.find((a) => a.role === 'controller');

// Resolve a CEA - chain is required
const { accounts: bnbAccounts } =
  await PushChain.utils.account.resolveControllerAccount('0xCEAAddress', {
    chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET,
  });

slippageToMinAmount(amount, { slippageBps })string

Calculates the minimum acceptable output amount given a slippage tolerance. Used when constructing tx.payGasWith.minAmountOut or validating swap quotes.

Argument Type Description
amount string Input amount in smallest units, e.g. '100000000' for 100 USDC
options.slippageBps number Slippage in basis points - 100 = 1%, 50 = 0.5%

Returns: string - minimum out amount in smallest units, e.g. '99000000'

Full reference: https://push.org/agents/workflows/use-utility-functions.md


Explorer Utilities

Available on any initialized client instance as pushChainClient.explorer.*. Works in both read-only and signing modes.

explorer.getTransactionUrl(txHash, options?)string

Returns a block explorer URL for any transaction hash.

Argument Type Description
txHash string Transaction hash
options.chain CHAIN (optional) Specific chain explorer to use. Defaults to Push Chain (PUSH_TESTNET_DONUT).

Returns: string - a full explorer URL, e.g. 'https://donut.push.network/tx/0x...'

// Push Chain explorer URL (default)
const url = pushChainClient.explorer.getTransactionUrl(txHash);

// External chain explorer URL
const sepoliaUrl = pushChainClient.explorer.getTransactionUrl(txHash, {
  chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA,
});

explorer.listUrls(options?){ explorers }

Lists block explorer URLs for one chain.

Argument Type Description
options.chain CHAIN (optional) Chain to list explorers for. Defaults to client's origin chain.

Returns: { explorers: Array<{ chain: string; chainName: string; urls: string[] }> }

explorer.listAllUrls(){ explorers }

Lists block explorer URLs for all supported chains at once.

Returns: { explorers: Array<{ chain: string; chainName: string; urls: string[] }> }

const { explorers } = pushChainClient.explorer.listAllUrls();
for (const { chainName, urls } of explorers) {
  console.log(chainName, urls[0]);
}

Contract Helpers

For off-chain UEA/CEA derivation from TypeScript, use PushChain.utils.account.deriveExecutorAccount() - see Utility Functions above. The UEAFactory below is for on-chain Solidity identity resolution. For the full Solidity contract pattern, see the push-contracts skill.

UEAFactory - Identity Resolution On-Chain

The Universal Executor Account Factory is deployed at a fixed address on Push Chain and lets your smart contract identify callers from external chains.

Address: 0x00000000000000000000000000000000000000eA

import "push-chain-core-contracts/src/Interfaces/IUEAFactory.sol";
// or define the interface manually - see docs

getOriginForUEA - Who called me?

// Inside your contract - identify if msg.sender is a UEA (external chain user) or a native Push account
(UniversalAccountId memory origin, bool isUEA) =
    IUEAFactory(0x00000000000000000000000000000000000000eA).getOriginForUEA(msg.sender);

// origin.chainNamespace  → "eip155" | "solana" | ...
// origin.chainId         → e.g. "11155111" (Sepolia)
// origin.owner           → caller's original address in bytes (hex-encode for EVM, base58 for Solana)
// isUEA                  → false if msg.sender is a native Push Chain EOA

getUEAForOrigin - What is a user's UEA address?

(address uea, bool isDeployed) = IUEAFactory(0x00000000000000000000000000000000000000eA).getUEAForOrigin(
    UniversalAccountId({
        chainNamespace: "eip155",
        chainId: "11155111",
        owner: abi.encodePacked(userAddress)
    })
);
// uea - deterministic UEA address (CREATE2), usable even before deployment
// isDeployed - true once the UEA has been deployed (first tx from that user)

Off-chain equivalent (SDK): PushChain.utils.account.deriveExecutorAccount(universalAccount)

Full reference: https://push.org/agents/workflows/use-contract-helpers.md


Common Mistakes

Symptom / Mistake Fix
receipt.hash is undefined / tx has no .hash sendTransaction returns a TxResponse, not a receipt - call await tx.wait() to get the receipt
wallet.sendTransaction() used - tx reverts or is malformed ethers/viem cannot produce a valid universal tx - replace with client.universal.sendTransaction()
PushChain.initialize() rejects the signer Raw ethers/viem wallet passed without wrapping - call await PushChain.utils.signer.toUniversal(wallet) first
signMessage return treated as a string It returns Uint8Array - use Buffer.from(sig).toString('hex') if you need a hex string
Silent tx failure (no throw, no logs) tx.wait() resolves even on reverts - always check receipt.status === 1
Private key in source code Use process.env.PRIVATE_KEY - never hardcode keys in scripts or commit them to version control
Treating client.universal.account as { address } object — account.address returns undefined account is a plain address string, not an object. Only origin has the { address, chain } shape. Read it directly: const me = client.universal.account; // `0x${string}` .

For Solana targets, use encodeTxData({ idl, functionName, args }) and pass the result as tx.data - same { to, value, data } shape as EVM. The SDK resolves program accounts, PDAs, and the sender's CEA automatically from the IDL.

For read-only queries, use ethers.js or viem directly with RPC https://evm.donut.rpc.push.org/ - @pushchain/core is not required.

Downloadable Resources

Copy these files into your project - self-contained and ready to run:

File Purpose
package.json Dependencies: @pushchain/core, ethers, viem, @solana/web3.js, tsx
client-ethers.ts ethers.js signer - Route 1 + Route 2
client-viem.ts viem signer - Route 1, Route 2, cascade
client-solana.ts Solana Keypair signer - Route 1 + Route 2 (Solana program via encodeTxData({ idl }))

Resource index - machine-readable file list

Extended Reference

Install via CLI
npx skills add https://github.com/pushchain/push-chain-website --skill push-backend
Repository Details
star Stars 27
call_split Forks 46
navigation Branch main
article Path SKILL.md
More from Creator