# Push Chain β€” Full Documentation This file contains the full text of all Push Chain documentation pages. --- # Recommended Practices URL: https://push.org/docs/chain/build/recommended-practices/ Recommended Practices | Build | Push Chain Docs Push Chain enables developers to instantly **10x their userbase** with the same codebase. This is possible because Push Chain is purpose built for true interoperability between chains. Since Push Chain is the first true universal blockchain, it's recommended to read through the best practices for building on Push Chain. ## Recommended Practices for Developers on Push Chain Push Chain is a fully EVM-compatible blockchain, meaning that developers can deploy their existing Ethereum smart contracts to Push Chain without any code changes. If your contract is already built for Ethereum (e.g., tested on Sepolia or Mainnet), you can deploy it directly to Push Chain using the same deployment scripts and tooling, including Hardhat, Foundry, or Remix. This compatibility makes onboarding to Push Chain seamless and efficient for teams familiar with the Ethereum development ecosystem. ## Backend SDK: `@pushchain/core` If you're building backend services, automation scripts, bots, or analytics pipelines, Push Chain offers an official SDK: [@pushchain/core](https://npmjs.com/package/@pushchain/core). `@pushchain/core` is ideal for: - Server-side integrations - Backend logic for dApps - Indexing or monitoring tools that need to interact with the Push Chain network reliably and efficiently. ## UI Kit SDK: `@pushchain/ui-kit` Push Chain also offers [@pushchain/ui-kit](https://npmjs.com/package/@pushchain/ui-kit) which is a collection of React components that completely abstract away the complexity of wallet connections and user authentication. `@pushchain/ui-kit` is ideal for: - Building user interfaces for dApps - Abstracting away the complexity of wallet connections and user authentication (abstracted initialization of pushChainClient) - Multi-chain connections: Users can sign in and connect using wallets from other blockchains - Email login: For non-crypto native users, Push Wallet supports email login and onboarding, enabling apps to attract wider audiences ## Smart Contract Helper Functions To understand where your users are coming fromβ€”whether directly on Push Chain or via another chain like Sepolia or Solana Devnetβ€”Push Chain provides helper smart contracts. These helpers make it easy to track and categorize users/protocol usage depending on their origin chain. This is especially useful if you want to: - Tailor app behavior depending on user origin - Monitor multichain adoption - Incentivize or reward activity coming from specific chains These helpers are already deployed and maintained, so you can easily integrate them into your logic with minimal effort. ## Speed run {` // customPropHighlightRegexStart=PushChain\.initialize // customPropHighlightRegexEnd=}\\) // customPropGTagEvent=initialize_pushchain_client import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; async function main() { console.log('Creating Universal Signer - Ethers V6'); // Create random wallet const wallet = ethers.Wallet.createRandom(); // Set up provider connected to Ethereum Sepolia Testnet const provider = new ethers.JsonRpcProvider('https://gateway.tenderly.co/public/sepolia'); const signer = wallet.connect(provider); // Convert ethers signer to Universal Signer const universalSigner = await PushChain.utils.signer.toUniversal(signer); console.log('πŸ”‘ Got universal signer'); // Initialize Push Chain SDK const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); console.log('πŸš€ Got push chain client'); console.log(JSON.stringify(pushChainClient)); } await main().catch(console.error) `} ## Next Steps - Create [Universal Signer](/docs/chain/build/create-universal-signer) from existing signer - Abstract away creation of the Universal Signer using [UI Kit](/docs/chain/ui-kit) - Revist [Important Concepts](/docs/chain/important-concepts) --- # Custom Universal Signer URL: https://push.org/docs/chain/build/advanced/custom-universal-signer/ Custom Universal Signer | Build | Advanced | Push Chain Docs ## Overview If you don't have a supported library signer or want to create a custom implementation, you can construct a Universal Signer manually. ## Custom Universal Signer **_`PushChain.utils.signer.construct(account, {options}): Promise`_** ```typescript const account = { address: '', chain: ' }; const customSignAndSendTransaction = async (unsignedTx) => { return new Uint8Array(''); }; const customSignMessage = async (data) => { return new Uint8Array(''); }; const customSignTypedData = async (typedDataArgs) => { return new Uint8Array(''); }; const skeleton = PushChain.utils.signer.construct(account, { signMessage: customSignMessage, signTransaction: customSignTransaction, signTypedData: customSignTypedData }); const universalSigner = await PushChain.utils.signer.toUniversal(skeleton); ``` | **Arguments** | **Type** | **Description** | | ---------------------------------- | ------------------------------------------------- | ------------------------------------------------------ | | _`account`_ | `UniversalAccount` | Account information containing address and chain | | _`options`_ | `Object` | Object containing the signing function implementations | | _`options.signAndSendTransaction`_ | `(unsignedTx: Uint8Array) => Promise` | Function to sign transaction data | | _`options.signMessage`_ | `(data: Uint8Array) => Promise` | Function to sign raw message data | | `options.signTypedData` | `(params) => Promise` | Function to sign typed data (EIP-712) | " className="alert alert--fn-args"> ```typescript // UniversalSignerSkeleton object { signerId: 'CustomGeneratedSigner', account: { chain: 'eip155:42101', address: '0x98cA97d2FB78B3C0597E2F78cd11868cACF423C5' }, signMessage: [AsyncFunction: customSignMessage], signAndSendTransaction: [AsyncFunction: customSignAndSendTransaction], signTypedData: [AsyncFunction: customSignTypedData] } ``` **Live Playground**: Creating Custom Universal Signer from Ethers.js πŸ‘‡. {` // customPropHighlightRegexStart== PushChain\.utils\.signer\.construct // customPropHighlightRegexEnd=\\); // customPropGTagEvent=advanced_custom_universal_signer // Import Push Chain Core // Import if you are using ethers import { ethers } from 'ethers'; import readline from 'readline'; async function main() { // We need to pass the following to PushChain.utils.signer.construct(account, {options}) // 1. account which is a universal account // 2. options which is an object with the following properties // 2.1 signAndSendTransaction // 2.2 signMessage // 2.3 signTypedData // 1. account to universal account // Create random wallet const wallet = ethers.Wallet.createRandom(); // Convert wallet.address to Universal Account const universalAccount = PushChain.utils.account.toUniversal(wallet.address, { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, }); console.log('Created Universal Account', JSON.stringify(universalAccount)) // 2. options to construct // 2.1 signAndSendTransaction // create custom Sign and Send Transaction const customSignAndSendTransaction = async (unsignedTx) => { // Sign the transaction using ethers wallet const signedTx = await wallet.signTransaction(unsignedTx); const sendTx = await wallet.sendTransaction(signedTx); // Always a Uint8Array return Uint8Array.from(sendTx); }; // 2.2 signMessage const customSignMessage = async (message) => { // Sign message using ethers wallet const signature = await wallet.signMessage(message); // Always a Uint8Array return Uint8Array.from(signature); }; // 2.3 signMessage const customSignTypedData = async (domain, types, value) => { // Sign typed data using ethers wallet const signature = await wallet._signTypedData(domain, types, value); // Always a Uint8Array return Uint8Array.from(signature); }; // * Construct the universal signer skeleton with custom signing functions const universalSignerSkeleton = PushChain.utils.signer.construct(universalAccount, { signAndSendTransaction: customSignAndSendTransaction, signMessage: customSignMessage, signTypedData: customSignTypedData }); console.log('Created Universal Signer Skeleton', JSON.stringify(universalSignerSkeleton)); // ** Pass constructed universal signer skeleton to create universal signer ** const universalSigner = await PushChain.utils.signer.toUniversal(universalSignerSkeleton); console.log('Created Universal Signer', JSON.stringify(universalSigner)); } await main().catch(console.error); `} ## Next Steps - [Initialize Push Chain Client](/docs/chain/build/initialize-push-chain-client) with the Universal Signer - Abstract away creation of the Universal Signer using [UI Kit](/docs/chain/ui-kit) --- # Inbound to Push Chain URL: https://push.org/docs/chain/build/contract-initiated-examples/inbound-to-push-chain/ Inbound to Push Chain | Contract-Initiated Examples | Build | Push Chain Docs A pair of contracts that demonstrate the inbound direction: a contract on Sepolia (or any supported external chain) calls the per-chain Universal Gateway, and the TSS network relays the call to a contract on Push Chain. The Push contract sees the dispatching contract's UEA as **msg.sender**. For the conceptual background (UEAs, the Universal Gateway, the wire format), see [Contract-Initiated Multichain Execution](/docs/chain/build/contract-initiated-multichain-execution). ## What this example shows | Aspect | Details | |---|---| | **Direction** | External chain (Sepolia) to Push Chain. One-way. | | **Trigger** | A regular EOA on Sepolia calls `triggerOnPush(...)` on the Sepolia dispatcher, paying the gateway fee in ETH. | | **Identity on Push** | `msg.sender` on the Push target equals `UEA(SepoliaDispatcher)`. Use `IUEAFactory.getOriginForUEA` to recover the origin chain and the contract's Sepolia address. | | **Funds movement** | None. The example dispatches a payload only. The same surface supports bridging native ETH (`token = 0`, `amount > 0`); see the [Advanced Patterns](/docs/chain/build/contract-initiated-examples/advanced-patterns) for funds variants. | | **Verified on** | Sepolia + Donut Testnet. | ## Identity model When an external-chain contract calls the per-chain Universal Gateway, the TSS validators relay the call to Push Chain and execute it from the contract's UEA on Push. The UEA is always deterministic, derived from `(chainNamespace, chainId, contractAddress)`. From the Push target's perspective the UEA is a normal address. ```mermaid flowchart LR EOA([Sepolia EOA]) EXT[Sepolia ContractEthereumInboundDispatcher] UG[UniversalGatewaySepolia] UEA[Caller's UEAPush Chain] PC[PushCounterPush Chain] EOA -->|"triggerOnPush()"| EXT EXT -->|"sendUniversalTx{value}()"| UG UG -->|"TSS relay"| UEA UEA -->|"increment()"| PC style EXT fill:#1e3a8a,stroke:#60a5fa,color:#fff style UG fill:#1e3a8a,stroke:#60a5fa,color:#fff style UEA fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff style PC fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff ``` The Sepolia contract's UEA on Push is computable off-chain via [deriveExecutorAccount](/docs/chain/build/utility-functions/#derive-executor-account), so a Push-side target can pre-authorize that UEA before the first cross-chain call. ## Solidity Code (Sepolia side) ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; struct Multicall { address to; uint256 value; bytes data; } struct UniversalPayload { address to; uint256 value; bytes data; uint256 gasLimit; uint256 maxFeePerGas; uint256 maxPriorityFeePerGas; uint256 nonce; uint256 deadline; uint8 vType; } struct UniversalTxRequest { address recipient; address token; uint256 amount; bytes payload; address revertRecipient; bytes signatureData; } interface IUniversalGateway { function sendUniversalTx(UniversalTxRequest calldata req) external payable; } contract EthereumInboundDispatcher { /// @notice Per-chain UniversalGateway (Sepolia, BNB Testnet, etc.). Set via constructor. address public immutable gateway; /// @notice 4-byte marker the destination UEA looks for to decode multicall. bytes4 internal constant UEA_MULTICALL_SELECTOR = 0x2cc2842d; event InboundDispatched(address indexed pushTarget, bytes pushCalldata, uint256 nonce, uint256 fee); error ZeroAddress(); constructor(address _gateway) { if (_gateway == address(0)) revert ZeroAddress(); gateway = _gateway; } /// @notice Trigger an action on Push Chain. The contract's UEA on Push /// (deterministically derived from this contract's address) becomes the /// `msg.sender` that calls `pushTarget` with `pushCalldata`. function triggerOnPush( address pushTarget, bytes calldata pushCalldata, uint256 nonce, address revertRecipient ) external payable { if (pushTarget == address(0) || revertRecipient == address(0)) revert ZeroAddress(); // 1) Wrap (target, calldata) into the UEA's multicall format. Multicall[] memory calls = new Multicall[](1); calls[0] = Multicall({to: pushTarget, value: 0, data: pushCalldata}); bytes memory multicallData = abi.encodePacked(UEA_MULTICALL_SELECTOR, abi.encode(calls)); // 2) Wrap the multicall in the UniversalPayload struct the UEA expects. // Because `data` is multicall-wrapped (starts with UEA_MULTICALL_SELECTOR), // the UEA branches into _handleMulticall and IGNORES `to`. Conventionally // set to address(0). If you instead pass raw single-call calldata (no // selector prefix), the UEA does `to.call(data)` and `to` MUST be the // real target. bytes memory universalPayload = abi.encode( address(0), // to: ignored when data is multicall-wrapped (this example) uint256(0), // value multicallData, // data uint256(1e7), // gasLimit (matches SDK default) uint256(1e10), // maxFeePerGas (10 gwei) uint256(0), // maxPriorityFeePerGas nonce, // nonce: UEA nonce on Push uint256(9999999999),// deadline uint8(0) // vType = universalTxVerification ); // 3) Build the gateway request and dispatch. recipient is always zero // on the gateway request; the real Push-side target is inside payload. UniversalTxRequest memory req = UniversalTxRequest({ recipient: address(0), token: address(0), amount: 0, payload: universalPayload, revertRecipient: revertRecipient, signatureData: "" }); IUniversalGateway(gateway).sendUniversalTx{value: msg.value}(req); emit InboundDispatched(pushTarget, pushCalldata, nonce, msg.value); } receive() external payable {} } ``` The wire format is rigid. The Universal Gateway expects a specific encoding (UniversalPayload with `vType = 0`, multicall data prefixed with the `0x2cc2842d` sentinel, gateway request `recipient` always zero). Deviations cause TSS to silently drop the relay. ## Solidity Code (Push side) ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; contract PushCounter { uint256 public count; address public lastCaller; event Incremented(address indexed caller, uint256 newCount); function increment() external { count += 1; lastCaller = msg.sender; emit Incremented(msg.sender, count); } } ``` The Push-side counter is intentionally trivial. After the inbound lands, `lastCaller` will be the Sepolia dispatcher's UEA address, not the Sepolia EOA that started the chain. :::info Replay protection is handled by the UEA, not by your target The Sepolia caller's UEA on Push is a smart account with its own internal nonce. The UEA increments that nonce before forwarding the call to your target, so the same `(payload, nonce)` cannot be relayed twice. **Your target contract does NOT need to validate the caller or guard against replay.** It can be a plain Solidity function like the counter above. The `executeUniversalTx` + `txId` + `UNIVERSAL_EXECUTOR_MODULE` pattern you may have seen elsewhere is for a **different** path: the round-trip back-leg, where TSS delivers an inbound from your own contract's CEA back to your Push contract's `executeUniversalTx` callback. That handler is the one that needs guards. See [Round-Trip with Auto Back-Leg](/docs/chain/build/contract-initiated-examples/round-trip-auto-back-leg). ::: ## Source Code ### Run The runner deploys both contracts (Sepolia dispatcher + Push counter), encodes `increment()` calldata, calls `triggerOnPush()` on Sepolia, and watches the Push counter advance. ```bash git clone https://github.com/pushchain/push-chain-examples.git cd push-chain-examples/core-sdk-functions/contract-initiated-inbound-execution forge build npm install cp .env.sample .env # Edit .env: set ETH_PRIVATE_KEY (funded Sepolia EOA) and PUSH_PRIVATE_KEY # (a Push native wallet for the destination counter deploy). npm start ``` ### Prerequisites - Foundry and Node.js v18+. - A Sepolia EOA with at least 0.05 ETH (deploy + gateway fee + headroom). [Sepolia faucet](https://www.alchemy.com/faucets/ethereum-sepolia). - A Push native wallet with at least 1 PC for the Push-side counter deploy. [Push faucet](https://faucet.push.org). ## What can go wrong | Symptom | Cause | Fix | |---|---|---| | Sepolia tx reverts at the gateway | Wire format mismatch (missing `UEA_MULTICALL_SELECTOR` prefix, wrong `vType`, non-zero `recipient` on the gateway request) | Use the encoder in this example as a template. Do not improvise the encoding. | | Sepolia tx succeeds but Push counter never advances | `msg.value` was below the gateway's relay fee | Pass enough ETH as `msg.value`. The runner uses 0.001 ETH by default; bump if the relay does not pick up. | | `lastCaller` on Push is the Sepolia EOA, not the contract's UEA | The user called Push directly, bypassing the Sepolia dispatcher | This is expected when you skip the gateway path. To get a UEA-based identity, call from a contract that goes through the gateway. | ## Related - [Contract-Initiated Multichain Execution](/docs/chain/build/contract-initiated-multichain-execution) β†’ The conceptual reference for everything related to contract-initiated execution. - Other Basic Examples β†’ [Outbound from Push Chain](/docs/chain/build/contract-initiated-examples/outbound-from-push-chain) and the [Round-Trip with Auto Back-Leg](/docs/chain/build/contract-initiated-examples/round-trip-auto-back-leg). - [Advanced Patterns](/docs/chain/build/contract-initiated-examples/advanced-patterns) β†’ Harder variants: deposit-and-execute, recipient bridge, FIFO state machine, three-chain cascade. - [How CEA Works](/docs/chain/deep-dives/how-cea-works) β†’ The identity model behind UEA `msg.sender` attribution on Push. --- # Upgrade Universal Account URL: https://push.org/docs/chain/build/advanced/upgrade-universal-account/ Upgrade Universal Account | Advanced | Build | Push Chain Docs ## Overview Upgrades the UEA (Universal Executor Account) to the latest required implementation version. This is a gasless, signature-based operation and is done **automatically** via the SDK without requiring gas. You only need to call this when `getAccountStatus()` reports `uea.requiresUpgrade === true`. Sending universal transactions on an outdated UEA will fail. > **Note**: `upgradeAccount()` is a no-op if the UEA is already at or above the minimum required version. ## Upgrade Universal Account **_`pushChainClient.upgradeAccount({options?}): Promise`_** ```typescript await pushChainClient.upgradeAccount({ progressHook: (progress) => { console.log(`${progress.id}: ${progress.message}`); }, }); ``` | **Arguments** | **Type** | **Default** | **Description** | | ------------- | -------- | ----------- | --------------- | | `options.progressHook` | `(event: ProgressEvent) => void` | `undefined` | Callback invoked at each upgrade step showing progress. | Progress Hook Type and Response | Field | Type | Description | | ----- | ---- | ----------- | | `progress` | `Object` | The progress of the upgrade operation. | | `progress.id` | `string` | Unique identifier for the progress event. | | `progress.title` | `string` | Brief title of the progress event. | | `progress.message` | `string` | Detailed message describing the event. | | `progress.level` | `INFO` \| `SUCCESS` \| `ERROR` | Severity level of the event. | | `progress.response` | `object` \| `null` | Additional data object for the event, or `null` if not applicable. | | `progress.timestamp` | `string` | ISO-8601 timestamp when the event occurred. | | ID | Title | Message | Level | Response | | -- | ----- | ------- | ----- | -------- | | `UEA-MIG-01` | Checking UEA | Checking status for migration. | INFO | null | | `UEA-MIG-02` | Awaiting Migration Signature | Awaiting wallet signature for upgrading account. | INFO | null | | `UEA-MIG-03` | Broadcasting Migration TX | Broadcasting upgrade transaction to Push Chain... | INFO | null | | `UEA-MIG-9901` | UEA Migration Successful | UEA migration is successful. UEA is now version ``. | SUCCESS | `{ version }` | | `UEA-MIG-9902` | UEA Migration Failed | UEA migration failed. Check transaction on explorer. | ERROR | `{ error }` | | `UEA-MIG-9903` | UEA Migration Skipped | UEA migration skipped. | INFO | null | ## Recommended Usage Pattern Always check `getAccountStatus()` before calling `upgradeAccount()` to avoid unnecessary prompts: ```typescript const status = await pushChainClient.getAccountStatus(); if (status.uea.requiresUpgrade) { await pushChainClient.upgradeAccount({ progressHook: (progress) => { console.log(`${progress.id}: ${progress.message}`); }, }); } ``` ## Live Playground {` // customPropHighlightRegexStart=pushChainClient\.upgradeAccount // customPropHighlightRegexEnd=\\}\\); // customPropGTagEvent=upgrade_account_uea import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; async function main() { console.log('Initializing Push Chain Client'); const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider('https://ethereum-sepolia-rpc.publicnode.com'); const signer = wallet.connect(provider); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); console.log('πŸ”‘ Push Chain client initialized'); console.log('Origin account:', JSON.stringify(pushChainClient.universal.origin)); console.log('Execution account:', JSON.stringify(pushChainClient.universal.account)); // Check account status before attempting upgrade const status = await pushChainClient.getAccountStatus({ forceRefresh: true }); console.log('Account status:', JSON.stringify(status)); if (!status.uea.loaded) { console.log('⚠️ Account status could not be resolved yet.'); return; } if (!status.uea.deployed) { console.log('ℹ️ Universal Account is not deployed yet, so no upgrade is needed.'); return; } if (!status.uea.requiresUpgrade) { console.log('βœ… Universal Account is already up to date (version: ' + status.uea.version + ')'); return; } console.log( '⬆️ Upgrading Universal Account from ' + status.uea.version + ' to ' + status.uea.minRequiredVersion + '...' ); await pushChainClient.upgradeAccount({ progressHook: (progress) => { console.log('[' + progress.id + '] ' + progress.title + ': ' + progress.message); }, }); const updated = await pushChainClient.getAccountStatus({ forceRefresh: true }); console.log('Updated account status:', JSON.stringify(updated)); console.log('βœ… Upgrade complete. New version: ' + updated.uea.version); } await main().catch(console.error); `} ## Next Steps - Check UEA state before upgrading with [Get Account Status](/docs/chain/build/initialize-push-chain-client#get-account-status) - Send your first universal transaction after upgrading with [Send Universal Transaction](/docs/chain/build/send-universal-transaction) --- # Create Universal Signer URL: https://push.org/docs/chain/build/create-universal-signer/ Create Universal Signer | Build | Push Chain Docs ## Overview Wrap any EVM or non-EVM signer (ethers, viem, Solana, etc.) into a `UniversalSigner` so you can send cross-chain transactions on Push Chain without touching your on-chain code. > **Prerequisite** > Remember to install and import required libraries. See [Quickstart](/docs/chain/quickstart) for install steps. ## Create Universal Signer **_`PushChain.utils.signer.toUniversal(signer): Promise`_** The most common way to create a Universal Signer is by converting an existing signer from supported libraries (Ethers, Viem, Solana). ```typescript // Derive Ethers Signer const provider = new ethers.providers.JsonRpcProvider(''); const ethersSigner = new ethers.Wallet('', provider); // Convert to Universal Signer const universalSigner = await PushChain.utils.signer.toUniversal(ethersSigner); ``` :::note Provider determines the chain of the account - RPC URL picks the chain – Sepolia RPC β†’ Ethereum Sepolia, Donut RPC β†’ Push Chain Testnet ::: ```typescript // Derive Wallet Client const account = privateKeyToAccount(''); const viemClient = createWalletClient({ transport: http(''), // or your preferred RPC URL account, }); // Convert to Universal Signer const universalSigner = await PushChain.utils.signer.toUniversal(viemClient); ```` :::note Provider determines the chain of the account - RPC URL picks the chain – Sepolia RPC β†’ Ethereum Sepolia, Donut RPC β†’ Push Chain Testnet ::: ```typescript // Derive Solana Keypair const solKeypair = Keypair.generate(); // Convert Keypair to Universal Signer const universalSigner = await PushChain.utils.signer.toUniversalFromKeypair( solKeypair, { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, library: PushChain.CONSTANTS.LIBRARY.SOLANA_WEB3JS, } ); ``` | **Arguments** | **Type** | **Description** | | ------------- | ------------------------------------------------------------------- | ------------------------------------------------- | | _`signer`_ | `viem.WalletClient` \| `ethers.Wallet` \| `UniversalSignerSkeleton` | The signer to convert to Universal Signer format. | " className="alert alert--fn-args"> ```typescript // UniversalSigner object { account: { address: '0x32DE7d63C654d18F1382f5a30Ef69CB86b399ac7', chain: 'eip155:11155111' }, signMessage: [Function: signMessage], signAndSendTransaction: [Function: signAndSendTransaction], signTypedData: [Function: signTypedData] } ``` **Ready to dive in?** Try the code in live playground πŸ‘‡. {` // customPropHighlightRegexStart=PushChain\.utils\.signer\.toUniversal // Import Push Chain Core import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; async function main() { console.log('Creating Universal Signer - Ethers V6'); // Create random wallet const wallet = ethers.Wallet.createRandom(); // Set up provider // Replace it with different JsonRpcProvider to target Ethereum Account, BNB Account, etc // const provider = new ethers.JsonRpcProvider('https://gateway.tenderly.co/public/sepolia'); const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/'); const signer = wallet.connect(provider); // Convert ethers signer to Universal Signer const universalSigner = await PushChain.utils.signer.toUniversal(signer); console.log('πŸ”‘ Got universal signer - Ethers'); console.log(JSON.stringify(universalSigner)); } await main().catch(console.error); `} {` // customPropHighlightRegexStart=PushChain\.utils\.signer\.toUniversal // customPropHighlightRegexEnd=\; // customPropGTagEvent=convert_viem_to_universal_signer // Import Push Chain Core import { PushChain } from '@pushchain/core'; // Import Viem import { createWalletClient, http } from 'viem'; import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { sepolia } from 'viem/chains'; async function main() { // Create random wallet const account = privateKeyToAccount(generatePrivateKey()); // set chain to sepolia const walletClient = createWalletClient({ account, transport: http('https://evm.donut.rpc.push.org/'), }); // Convert viem signer to Universal Signer const universalSigner = await PushChain.utils.signer.toUniversal(walletClient); console.log('πŸ”‘ Got universal signer - Viem'); console.log(JSON.stringify(universalSigner)); } await main().catch(console.error); `} {` // customPropHighlightRegexStart=PushChain\.utils\.signer\.toUniversalFromKeypair // customPropHighlightRegexEnd=\\) // customPropGTagEvent=convert_solana_keypair_to_universal_signer // Import Push Chain Core import { PushChain } from '@pushchain/core'; // Import Solana Web3 JS import { Keypair } from '@solana/web3.js'; async function main() { // Create random wallet const solKeypair = Keypair.generate(); // Convert solana signer to Universal Signer const universalSigner = await PushChain.utils.signer.toUniversalFromKeypair( solKeypair, { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, library: PushChain.CONSTANTS.LIBRARY.SOLANA_WEB3JS, } ); console.log('πŸ”‘ Got universal signer'); console.log(JSON.stringify(universalSigner)); } await main().catch(console.error); `} ```jsx live // customPropHighlightRegexStart= // customPropGTagEvent=ui_kit_universal_wallet_provider_setup // customPropMinimized='false' // Import necessary components from @pushchain/ui-kit import { PushUniversalWalletProvider, PushUniversalAccountButton, usePushWalletContext, usePushChainClient, PushUI, } from '@pushchain/ui-kit'; function App() { // Define Wallet Config const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, }; function Component() { const { connectionStatus } = usePushWalletContext(); const { pushChainClient } = usePushChainClient(); return ( {connectionStatus == PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && Push Chain Client Initialized with Universal Signer: ${JSON.stringify(pushChainClient)} } ); } return ( ); } ``` ## Next Steps - [Initialize Push Chain Client](/docs/chain/build/initialize-push-chain-client) with the Universal Signer - Abstract away creation of the Universal Signer using [UI Kit](/docs/chain/ui-kit) - Learn how to create [Universal Signer from public / private keypair](/docs/chain/build/utility-functions#create-universal-signer-from-keypair) - Create [custom implementation](/docs/chain/build/advanced/custom-universal-signer) of universal signer (Advanced) --- # Outbound from Push Chain URL: https://push.org/docs/chain/build/contract-initiated-examples/outbound-from-push-chain/ Outbound from Push Chain | Contract-Initiated Examples | Build | Push Chain Docs A minimal Push Chain contract that demonstrates an outbound cross-chain call. The contract calls **UniversalGatewayPC (UGPC)**, the gateway contract responsible for handling outbound calls, and runs increment on a BNB Testnet counter via the contract's CEA. This is the smallest contract-initiated outbound that runs end to end on Donut. **Note**: For the conceptual background (UGPC, CEAs, fee model), see [Contract-Initiated Multichain Execution](/docs/chain/build/contract-initiated-multichain-execution). ## What this example shows | Aspect | Details | |---|---| | **Direction** | Push Chain to BNB Testnet. One-way. No back-leg. | | **Trigger** | A regular EOA calls `dispatchOutbound(...)` on the Push contract. | | **Identity on BNB** | The destination contract sees `msg.sender` equal to the Push contract's deterministic CEA on BNB. | | **Funds movement** | None. The example dispatches a payload only. The same surface supports bridging PRC20 (`token` + `amount`); see the [Advanced Patterns](/docs/chain/build/contract-initiated-examples/advanced-patterns) for funds variants. | | **Verified on** | Donut Testnet. | ## Identity model When a Push Chain contract dispatches through UGPC, the TSS validators relay the call to the destination chain and execute it from the contract's CEA on that chain. The CEA is always deterministic, derived from the contract's Push Chain address. From the destination contract's perspective the CEA is a **normal address**, and there is no Push-specific code at the destination. ```mermaid flowchart LR EOA([User EOA]) PC[Push ContractMinimalContractInitiatedExecutor] UGPC[UGPC0x...C1] CEA[Contract CEA on BNB] BNB[BNB Counter] EOA -->|"dispatchOutbound()"| PC PC -->|"sendUniversalTxOutbound{value}()"| UGPC UGPC -->|"TSS relay"| CEA CEA -->|"increment()"| BNB style PC fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style UGPC fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style CEA fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff style BNB fill:#1e3a8a,stroke:#60a5fa,color:#fff ``` The Push contract's CEA on the destination chain is computable off-chain via [deriveExecutorAccount](/docs/chain/build/utility-functions/#derive-executor-account), so destination protocols can whitelist or pre-fund the CEA before the first cross-chain activity has happened. ## Solidity Code ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; /// @notice Outbound request shape consumed by UGPC. struct UniversalOutboundTxRequest { bytes recipient; // CEA or target address on the external chain (bytes-encoded) address token; // PRC20 on Push Chain to bridge (address(0) for none) uint256 amount; // Amount of PRC20 to bridge uint256 gasLimit; // Gas limit for external execution uint256 gasPrice; // 0 = per-chain default from UniversalCore (new in SDK v6) uint256 maxPCForGas; // 0 = no cap on PC the AMM may spend on the gas swap (new in SDK v6) bytes payload; // ABI-encoded calldata for the CEA to execute address revertRecipient;// Address to receive bridged funds if external tx reverts } interface IUniversalGatewayPC { function sendUniversalTxOutbound(UniversalOutboundTxRequest calldata req) external payable; } interface IPRC20 { function approve(address spender, uint256 amount) external returns (bool); } contract MinimalContractInitiatedExecutor { /// @notice UGPC predeploy on Push Chain Donut Testnet. address public immutable ugpc; event OutboundDispatched( bytes indexed recipient, address indexed token, uint256 amount, bytes payload, address revertRecipient ); error ZeroAddress(); constructor(address _ugpc) { if (_ugpc == address(0)) revert ZeroAddress(); ugpc = _ugpc; } /// @notice Dispatch an outbound cross-chain execution from this contract. /// @dev `msg.value` must cover the UGPC protocol fee. If bridging PRC20 /// tokens, this function approves UGPC for the amount before calling. function dispatchOutbound( address token, uint256 amount, bytes calldata recipient, uint256 gasLimit, bytes calldata payload, address revertRecipient ) external payable { if (revertRecipient == address(0)) revert ZeroAddress(); if (amount > 0) { if (token == address(0)) revert ZeroAddress(); IPRC20(token).approve(ugpc, amount); } IUniversalGatewayPC(ugpc).sendUniversalTxOutbound{value: msg.value}( UniversalOutboundTxRequest({ recipient: recipient, token: token, amount: amount, gasLimit: gasLimit, gasPrice: 0, // per-chain default from UniversalCore maxPCForGas: 0, // no cap on PC for the gas swap payload: payload, revertRecipient: revertRecipient }) ); emit OutboundDispatched(recipient, token, amount, payload, revertRecipient); } receive() external payable {} } ``` The contract is intentionally tiny. It owns no business logic. It exists to forward an outbound request through UGPC. Real production contracts wrap `dispatchOutbound` behind their own access control, request-ID tracking, and event correlation; layer those on top. ## Source Code ### Run The runner deploys the contract on first run, derives the BNB CEA address, encodes `increment()` for the BNB counter, dispatches with PC value covering the UGPC protocol fee, and prints both Push and BNB explorer URLs. ```bash git clone https://github.com/pushchain/push-chain-examples.git cd push-chain-examples/core-sdk-functions/contract-initiated-outbound-execution forge build npm install cp .env.sample .env # Edit .env: set PUSH_PRIVATE_KEY (Push native wallet with at least 10 PC). npm start ``` ### Prerequisites - Foundry and Node.js v18+. - A Push native wallet on Donut Testnet with at least 10 PC (deploy + dispatch + headroom). To get funds, visit the [Push faucet](https://faucet.push.org). - No funding needed on the BNB CEA. This is a one-way example; the destination tx's gas comes from the UGPC fee paid in PC and converted internally. ## What can go wrong | Symptom | Cause | Fix | |---|---|---| | `dispatchOutbound` reverts immediately on Push | `msg.value` is zero or below the UGPC protocol fee | Pass enough PC as `msg.value`. The runner uses 5 PC by default. | | Push tx succeeds but no BNB tx fires | `gasLimit` was 0 or under the auto-floor (~500k) and the destination tx ran out of gas | Pass `gasLimit: 2_000_000` on the outbound. UGPC charges only for actual gas used and refunds the surplus, so over-provisioning is essentially free. See [Operational Knobs](/docs/chain/build/contract-initiated-multichain-execution#operational-knobs). | | BNB tx fires but the destination contract reverts | The destination contract restricts callers (whitelist or EOA-only guard) and does not recognise the CEA | Whitelist the deterministic CEA address on the destination contract. Derive it off-chain via `PushChain.utils.account.deriveExecutorAccount`. | ## Related - [Contract-Initiated Multichain Execution](/docs/chain/build/contract-initiated-multichain-execution) β†’ The conceptual reference for everything related to contract-initiated execution. - Other Basic Examples β†’ [Inbound to Push Chain](/docs/chain/build/contract-initiated-examples/inbound-to-push-chain) and the [Round-Trip with Auto Back-Leg](/docs/chain/build/contract-initiated-examples/round-trip-auto-back-leg). - [Advanced Patterns](/docs/chain/build/contract-initiated-examples/advanced-patterns) β†’ Harder variants: bridging PRC-20 alongside the call, recipient bridge, FIFO state machine, three-chain cascade. - [How CEA Works](/docs/chain/deep-dives/how-cea-works) β†’ The identity model behind deterministic CEA addresses on the destination chain. --- # Round-Trip with Auto Back-Leg URL: https://push.org/docs/chain/build/contract-initiated-examples/round-trip-auto-back-leg/ Round-Trip with Auto Back-Leg | Examples | Build | Push Chain Docs A single Push contract that dispatches an outbound to BNB Testnet and **automatically receives an inbound callback** when the destination chain finishes. One user signature on Push, two coordinated transactions across two chains. This is a **loaded pattern example** where the operational knobs that the basics gloss over (**gasLimit floor**, destination **CEA prefunding**, the 6-arg **executeUniversalTx dispatch** path, refund accumulation) all become concrete here. For the conceptual background, see [Contract-Initiated Multichain Execution](/docs/chain/build/contract-initiated-multichain-execution) (especially the [Round-Trip Wire Format](/docs/chain/build/contract-initiated-multichain-execution#round-trip-wire-format) and [Operational Knobs](/docs/chain/build/contract-initiated-multichain-execution#operational-knobs) sections). ## What this example shows | Aspect | Details | |---|---| | **Direction** | Push -> BNB -> Push, all from one user signature on Push. | | **Trigger** | EOA calls `kickOff(...)`. The contract dispatches outbound; TSS auto-fires the inbound callback when the BNB tx settles. | | **Wire format** | The destination CEA's outer multicall self-calls `sendUniversalTxToUEA` on the CEA itself. The CEA wraps the inner payload and submits it through BNB's gateway. | | **Inbound dispatch path** | The 6-arg `executeUniversalTx(string, bytes, bytes, uint256, address, bytes32)`. For Push-native contracts, TSS dispatches this signature; the 2-arg `executeUniversalTx(UniversalPayload, bytes)` overload in the codebase is reserved for UEA proxy accounts and is not invoked here. | | **Verified on** | Donut Testnet. End-to-end run with `outboundCount` and `callbacks` both advancing. | ## The wire format This is the part most newcomers get wrong. The destination CEA's outer multicall must include a step that **self-calls `sendUniversalTxToUEA` on the CEA itself**. The CEA wraps and routes the inner payload through its gateway internally; TSS reads the gateway event and fires the inbound back to Push. The full layered structure: ``` Layer 4 - Outer multicall (what TSS submits to the destination CEA) β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ 0x2cc2842d || abi.encode(Multicall[] { β”‚ β”‚ { to: destCEA, value: 0, data: } β”‚ β”‚ }) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό Layer 3 - CEA self-call to sendUniversalTxToUEA β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ sendUniversalTxToUEA(token=0, amount=0, , refundTo) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό Layer 2 - Encoded UniversalPayload (vType = 1, inbound) β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ abi.encode( β”‚ β”‚ address(0), uint256(0), , β”‚ β”‚ gasLimit, maxFeePerGas, 0, ueaNonce + 1, deadline, vType: 1 β”‚ β”‚ ) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό Layer 1 - Inner multicall (what the Push UEA executes after the back-leg) β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ 0x2cc2842d || abi.encode(Multicall[] { /* no-op or your call */ })β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` The destination CEA receives Layer 4, executes the multicall step, which invokes Layer 3 (the CEA self-call), which submits Layer 2 to the BNB gateway. TSS picks up the gateway event and delivers Layer 1 to the original Push contract via the 6-arg `executeUniversalTx`. > The four-layer encoding looks like a lot. In practice you write it once, encapsulate it in a helper function, and call that helper from every kickoff path. The runner in this example shows the helper. ## Solidity Code ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; struct UniversalOutboundTxRequest { bytes recipient; address token; uint256 amount; uint256 gasLimit; uint256 gasPrice; // 0 = per-chain default from UniversalCore (new in SDK v6) uint256 maxPCForGas; // 0 = no cap on PC the AMM may spend on the gas swap (new in SDK v6) bytes payload; address revertRecipient; } interface IUniversalGatewayPC { function sendUniversalTxOutbound(UniversalOutboundTxRequest calldata req) external payable; } contract RoundtripDispatcher { address public immutable ugpc; address public immutable universalExecutorModule; bytes4 internal constant UEA_MULTICALL_SELECTOR = 0x2cc2842d; uint256 public outboundCount; uint256 public callbacks; mapping(bytes32 => bool) public seenTxIds; bytes32 public lastTxId; event OutboundKicked(uint256 outboundCount, bytes payload); event Callback(uint256 sequence, bytes32 indexed txId, string sourceChainNamespace, bytes ceaAddress, address prc20, uint256 amount); error NotUniversalExecutor(); error TxAlreadyExecuted(); error InsufficientPC(uint256 required, uint256 available); struct Multicall { address to; uint256 value; bytes data; } constructor(address _ugpc, address _module) { ugpc = _ugpc; universalExecutorModule = _module; } function fund() external payable {} /// @notice Trigger the round-trip: dispatch outbound to BNB; TSS auto-fires the inbound callback. /// @param destinationCEAAddr This contract's CEA on the destination chain. /// @param tokenForRouting PRC-20 on Push that selects the destination chain (e.g. pBNB). /// @param protocolFeePc PC the contract forwards to UGPC for the outbound fee. /// @param ueaNonce Current Push UEA nonce. 0 for fresh. function kickOff( address destinationCEAAddr, address tokenForRouting, uint256 protocolFeePc, uint256 ueaNonce ) external { if (address(this).balance `kickOff` is the only entrypoint a caller needs. `executeUniversalTx` is the back-leg handler TSS invokes when the destination CEA's multicall completes. After running the example end to end on Donut, `outboundCount` and `callbacks` both advance to 1. ## Required configuration Two knobs determine whether the back-leg lands. Wrong values cause TSS to silently drop the relay. | Knob | Value | Why | |---|---|---| | `gasLimit` on the UGPC outbound | **`2_000_000`** | UGPC's auto-floor for `gasLimit = 0` is 500k. Below ~1.5M the destination tx runs out of gas during the nested gateway call and TSS does not retry. UGPC charges only for actual gas used and refunds the surplus, so over-provisioning is essentially free. | | Push contract `$PC` balance | covers `protocolFee + inbound execution fee` | Inbound execution on Push pays gas in `$PC`, charged to the dispatching contract. UGPC refunds surplus into `address(this)` via `receive()`. | The destination CEA does not need pre-funding. When TSS submits the destination tx it forwards the converted gas value to the CEA as `msg.value`, so the CEA has the native balance it needs for the nested `gateway.sendUniversalTxFromCEA(...)` call during the duration of that tx. ## Source Code ### Run The runner deploys the contract, funds it with PC if needed, derives the destination CEA, fires `kickOff()`, and watches both `outboundCount` and `callbacks` advance. ```bash git clone https://github.com/pushchain/push-chain-examples.git cd push-chain-examples/core-sdk-functions/contract-initiated-roundtrip-execution forge build npm install cp .env.sample .env # Edit .env: set PUSH_PRIVATE_KEY (Push native wallet with at least 12 PC). npm start ``` ### Prerequisites - Foundry and Node.js v18+. - A Push native wallet on Donut Testnet with at least 12 PC (deploy + 8 PC funded into the contract + protocol fees + headroom). [Push faucet](https://faucet.push.org). The first run deploys `RoundtripDispatcher`, funds it with 8 PC, fires `kickOff`, and watches the cascade unfold. After ~30 to 90 seconds you should see the outbound on BNB and the inbound callback land on Push. ## What can go wrong | Symptom | Cause | Fix | |---|---|---| | Push tx succeeds but no BNB tx fires | `gasLimit` was 0 or under the auto-floor (~500k) | Pass `gasLimit: 2_000_000`. The example hardcodes this; do not lower it. | | Both legs land but the back-leg never reaches `executeUniversalTx` | The outer multicall is missing the destination CEA's self-call to `sendUniversalTxToUEA` | Use the layered encoding above. Plain multicalls without that self-call step do not trigger a back-leg. | | Inbound callback never lands on `executeUniversalTx` | Implementing the 2-arg `executeUniversalTx(UniversalPayload, bytes)` overload as your primary handler | TSS dispatches only the 6-arg overload `(string, bytes, bytes, uint256, address, bytes32)` to Push-native contracts. The 2-arg signature is reserved for UEA proxies. Implement only the 6-arg version. | | Solana destination outbound reverts with `STF` | `msg.value` to UGPC under-sized the on-chain $PC -> pSOL Uniswap V3 swap | Off-chain compute the right value via the [Solana sizing snippet](/docs/chain/build/contract-initiated-multichain-execution#solana-outbound-value-sizing) and store it on the contract via a setter. Never use a flat `balance/2`. | | EOA wallet drains over many runs even though the contract is funded | UGPC routes its surplus refund to `address(this)`, not back to the EOA that called `kickOff()` | Plan a `withdraw()` path or treasury sweep. Refunds accumulate on the contract over time. | ## Related - [Contract-Initiated Multichain Execution](/docs/chain/build/contract-initiated-multichain-execution) β†’ The conceptual reference for everything related to contract-initiated execution. - Other Basic Examples β†’ [Inbound to Push Chain](/docs/chain/build/contract-initiated-examples/inbound-to-push-chain) and [Outbound from Push Chain](/docs/chain/build/contract-initiated-examples/outbound-from-push-chain). - [Advanced Patterns](/docs/chain/build/contract-initiated-examples/advanced-patterns) β†’ Harder variants: FIFO state machine, deposit-and-execute, recipient bridge, three-chain cascade. - [How CEA Works](/docs/chain/deep-dives/how-cea-works) β†’ The identity model that makes the round-trip back-leg land on the same Push contract. --- # Initialize Push Chain Client URL: https://push.org/docs/chain/build/initialize-push-chain-client/ Initialize Push Chain Client | Build | Push Chain Docs ## Overview Initializing the SDK client gives you: - **Chain-agnostic** `PushChainClient` for submitting on-chain calls - Automatic **RPC & block-explorer resolution** (with optional overrides) - Built-in **UEA** (Universal Executor Account) and **origin-account** getters - End-to-end **Universal fee abstraction**, signature orchestration & debug traces Just pass your universal signer and network, and you’re ready to write and transact on Push Chain from any wallet. ## Initialize Push Chain Client **_`PushChain.initialize(signer, {options}): Promise`_** ```typescript // Import @pushchain/core and ethers // Ensure you have created Universal Signer. // If not done, then check out Create Universal Signer. // Initialize Push Chain Client const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); ``` | Arguments | Type | Default | Description | | --------------------- | -------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | _`signer`_ | `UniversalSigner \| UniversalAccount` | - | Pass a `UniversalSigner` to enable full write/sign capabilities, or a `UniversalAccount` to initialize the client in read-only mode (no signing or transaction sending). | | _`options.network`_ | `PushChain.CONSTANTS.PUSH_NETWORK` | - | Push Chain network to connect to. For example: `PushChain.CONSTANTS.PUSH_NETWORK.TESTNET` | | `options.rpcUrls` | `Partial>` | `{}` | Custom RPC URLs mapped by chain IDs. If not provided, the default RPC URLs for the network will be used. Example: `rpcUrls: {[CHAIN.ETHEREUM_SEPOLIA]: ['https://sepolia.infura.io/v3/your-api-key'], [CHAIN.SOLANA_DEVNET]: ['https://api.devnet.solana.com']}` | | Arguments | Type | Default | Description | | --------------------- | -------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `options.blockExplorers` | `Partial>` | `{[CHAIN.PUSH_TESTNET_DONUT]: ['https://donut.push.network']}` | Custom block explorer URLs mapped by chain IDs. If not provided, the default block explorer URLs for the network will be used. | | `options.printTraces` | `boolean` | `false` | When true, console logs the internal trace logs for debugging requests to nodes | | `options.progressHook` | `(progress: ProgressEvent) => void` | `undefined` | Optional callback to receive progress events from long-running operations. | " className="alert alert--fn-args"> ```typescript // PushChainClient object { orchestrator: Orchestrator { universalSigner: { account: [Object], signMessage: [Function: signMessage], signAndSendTransaction: [Function: signAndSendTransaction], signTypedData: [Function: signTypedData] }, pushNetwork: 'TESTNET_DONUT', rpcUrls: {}, printTraces: false, progressHook: undefined, pushClient: PushClient { publicClient: [Object], pushChainInfo: [Object], ephemeralKey: '...' } }, universalSigner: { account: { address: '0xC8AE31cF444CAB447921277c4DcF65128d5B25a8', chain: 'eip155:11155111' }, signMessage: [Function: signMessage], signAndSendTransaction: [Function: signAndSendTransaction], signTypedData: [Function: signTypedData] }, blockExplorers: { 'eip155:42101': [ 'https://donut.push.network' ] }, universal: { origin: [Getter], account: [Getter], sendTransaction: [Function: bound execute], signMessage: [Function: signMessage], signTypedData: [Function: signTypedData] }, explorer: { getTransactionUrl: [Function: getTransactionUrl], listUrls: [Function: listUrls] } } ``` **Let's create your first Push Chain client!** Try the code in live playground πŸ‘‡. {` // customPropHighlightRegexStart=PushChain\.initialize // customPropHighlightRegexEnd=}\\); // customPropGTagEvent=initialize_client_ethers import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; async function main() { console.log('Creating Universal Signer - Ethers V6'); // Create random wallet const wallet = ethers.Wallet.createRandom(); // Set up provider connected to Ethereum Sepolia Testnet const provider = new ethers.JsonRpcProvider('https://gateway.tenderly.co/public/sepolia'); const signer = wallet.connect(provider); // Convert ethers signer to Universal Signer const universalSigner = await PushChain.utils.signer.toUniversal(signer); console.log('πŸ”‘ Got universal signer'); // Initialize Push Chain SDK const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); console.log('πŸ”‘ Got push chain client'); console.log(JSON.stringify(pushChainClient)); } await main().catch(console.error); `} {` // customPropHighlightRegexStart=PushChain\.initialize // customPropHighlightRegexEnd=}\\); // customPropGTagEvent=initialize_client_viem import { PushChain } from '@pushchain/core'; import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'; import { createWalletClient, http } from 'viem'; import { sepolia } from 'viem/chains'; async function main() { console.log('Creating Universal Signer - Viem'); // Create random wallet const account = privateKeyToAccount(generatePrivateKey()); const walletClient = createWalletClient({ account, chain: sepolia, transport: http(), }); // Convert viem wallet client to Universal Signer const universalSigner = await PushChain.utils.signer.toUniversal(walletClient); console.log('πŸ”‘ Got universal signer'); // Initialize Push Chain SDK const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); console.log('πŸ”‘ Got push chain client'); console.log(JSON.stringify(pushChainClient)); } await main().catch(console.error); `} {` // customPropHighlightRegexStart=PushChain\.initialize // customPropHighlightRegexEnd=}\\); // customPropGTagEvent=initialize_client_solana import { PushChain } from '@pushchain/core'; import { Keypair } from '@solana/web3.js'; async function main() { console.log('Creating Universal Signer - Solana Web3.js'); const keyPair = Keypair.generate(); const universalSigner = await PushChain.utils.signer.toUniversalFromKeypair(keyPair, { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, library: PushChain.CONSTANTS.LIBRARY.SOLANA_WEB3JS, }); console.log('πŸ”‘ Got universal signer'); // Initialize Push Chain SDK const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); console.log('πŸ”‘ Got push chain client'); console.log(JSON.stringify(pushChainClient)); } await main().catch(console.error) `} ```jsx live // customPropHighlightRegexStart= // customPropGTagEvent=initialize_client_ui_kit // customPropMinimized='false' // Import necessary components from @pushchain/ui-kit import { PushUniversalWalletProvider, PushUniversalAccountButton, usePushWalletContext, usePushChainClient, PushUI, } from '@pushchain/ui-kit'; function App() { // Define Wallet Config const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, }; function Component() { const { connectionStatus } = usePushWalletContext(); const { pushChainClient } = usePushChainClient(); return ( {connectionStatus == PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && Push Chain Client Initialized: {JSON.stringify(pushChainClient, (key, value) => typeof value === 'bigint' ? value.toString() : value )} } ); } return ( ); } ``` ## Read-only Mode **_`PushChain.initialize(account, {options}): Promise`_** You can initialize a **read-only** `PushChainClient` by passing a `UniversalAccount` (address + chain) instead of a `UniversalSigner`. This is useful when you want to inspect account state, resolve explorer URLs, or read metadata without signing or sending transactions. ```typescript // Import @pushchain/core and ethers // Ensure you have created Universal Account. // If not done, then check out Create Universal Account under Utility Functions. // Initialize Push Chain Client const pushChainClient = await PushChain.initialize(universalAccount, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); ``` {` // customPropHighlightRegexStart=PushChain\.initialize // customPropHighlightRegexEnd=}\\); // customPropGTagEvent=initialize_client_custom_signer import { PushChain } from '@pushchain/core'; async function main() { console.log('Initializing Read-only Push Chain Client'); // Create a UniversalAccount (read-only) const universalAccount = { address: '0x1234567890123456789012345678901234567890', chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, }; // Initialize read-only client const readOnlyClient = await PushChain.initialize(universalAccount, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); // Allowed operations (prettified) const origin = readOnlyClient.universal.origin; const account = readOnlyClient.universal.account; const txUrl = readOnlyClient.explorer.getTransactionUrl('0x123'); console.log('βœ… Initialized (read-only)'); console.log('Origin:', 'address=' + origin.address + ', chain=' + origin.chain); console.log('Account:', account); console.log('Tx URL:', txUrl); // Restricted operations (will throw/reject) try { const message = new TextEncoder().encode('Hello, Push Chain!'); await readOnlyClient.universal.signMessage(message); } catch (e) { console.log('πŸ›‘οΈ Restricted call blocked: ' + e.message); } try { await readOnlyClient.universal.sendTransaction({ to: '0x0000000000000000000000000000000000042101', value: 1n, }); } catch (e) { console.log('πŸ›‘οΈ Restricted call blocked: ' + e.message); } } await main().catch(console.error); `} ## Reinitialize Client **_`pushChainClient.reinitialize(signerOrAccount, {options}): Promise`_** You can reinitialize a `PushChainClient` with a different signer/account and/or updated configuration (RPCs, explorers, traces, progress hook). - Reinitialize will take the same parameters that you have passed for initializing the previous client. - To update the parameters, simply pass new ones in options object. **Parameters list** is same as [initialize(...)](#initialize-push-chain-client). - Reinitialize always returns a new client instance; swap references accordingly. - Changing the signer/account updates `universal.origin` and `universal.account`. ```typescript // Import @pushchain/core and ethers // Ensure you have created Universal Account. // If not done, then check out Create Universal Account under Utility Functions. // Initialize Push Chain Client const pushChainClient2 = await pushChainClient1.reinitialize(newSignerOrAccount, { // pass new parameters if needed }); ``` {` // customPropHighlightRegexStart=client1\.reinitialize // customPropHighlightRegexEnd=\\); // customPropGTagEvent=reinitialize_client import { PushChain } from '@pushchain/core'; import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'; import { createWalletClient, http } from 'viem'; import { sepolia } from 'viem/chains'; async function main() { console.log('Reinitialize Client Demo'); // Single EVM signer const account = privateKeyToAccount(generatePrivateKey()); const walletClient = createWalletClient({ account, chain: sepolia, transport: http() }); const signer = await PushChain.utils.signer.toUniversal(walletClient); // Initialize with default options const client1 = await PushChain.initialize(signer, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); console.log('Before (explorer URLs):', JSON.stringify(client1.explorer.listUrls())); // Reinitialize with SAME signer, change only ONE option (blockExplorers) const client2 = await client1.reinitialize(signer, { blockExplorers: { [PushChain.CONSTANTS.CHAIN.PUSH_TESTNET_DONUT]: ['https://custom-explorer.push.network'] }, } ); console.log('After (explorer URLs):', JSON.stringify(client2.explorer.listUrls())); } await main().catch(console.error); `} ## Manage Account ### Access Account Information Once initialized, your `PushChainClient` exposes: | Property | Description | | ----------------------------- | ----------------------------------------------------------------------------------------------------------- | | `pushChainClient.universal.account` | Push Chain **execution account**: for native Push Chain wallets this is your EOA or smart account; for cross-chain wallets this is your UEA (Universal Executor Account) that holds gas and executes transactions. | | `pushChainClient.universal.origin` | **Origin account** on the source chain (e.g. `eip155:1`), representing your wallet’s native address. | ```typescript // execution vs. origin accounts const execAccount = pushChainClient.universal.account; // Account that writes on Push Chain const originAccount = pushChainClient.universal.origin; // Source chain account that is mapped to the execution account ``` ### Get Account Status **_`pushChainClient.getAccountStatus({options?}): Promise`_** Returns the current UEA deployment and version information for the initialized account. **Note**: In most cases, UEA deployment and upgrades are handled automatically by the SDK. Most apps do not need to call this directly unless they are debugging account state or checking upgrade requirements. For advanced account management flows, read [Upgrade Universal Account](/docs/chain/build/advanced/upgrade-universal-account). ```typescript const status = await pushChainClient.getAccountStatus(); ``` | **Arguments** | **Type** | **Default** | **Description** | | ------------- | -------- | ----------- | --------------- | | `options.forceRefresh` | `boolean` | `false` | When `true`, re-fetches status from chain. | " className="alert alert--fn-args"> ```typescript { mode: 'read-only' | 'signer'; uea: { loaded: boolean; // Whether status has been fetched from chain deployed: boolean; // Whether the UEA proxy is deployed on Push Chain version: string; // Current UEA implementation version, e.g. "1.0.0" minRequiredVersion: string; // Latest required version from UEAFactory, e.g. "1.0.2" requiresUpgrade: boolean; // true when deployed && version {` // customPropHighlightRegexStart=pushChainClient\.getAccountStatus // customPropHighlightRegexEnd=\\); // customPropGTagEvent=get_account_status import { PushChain } from '@pushchain/core'; async function main() { // Create a UniversalAccount (read-only) const universalAccount = { address: '0x6d66cc8cbea02496735a9eb89ac6c2e0fc3b6689', chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, }; const pushChainClient = await PushChain.initialize(universalAccount, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); console.log('πŸ”‘ Push Chain client initialized'); // Fetch account status const status = await pushChainClient.getAccountStatus(); console.log('Account status:', JSON.stringify(status)); if (!status.uea.deployed) { console.log('ℹ️ UEA not deployed yet for this account'); } else { console.log('UEA version:', status.uea.version); console.log('Min required version:', status.uea.minRequiredVersion); console.log('Requires upgrade:', status.uea.requiresUpgrade); } } await main().catch(console.error); `} ## Next Steps - Initialize your EVM client with [Initialize EVM Client](/docs/chain/build/initialize-evm-client) - Send your first Universal Transaction with [Send Universal Transaction](/docs/chain/build/send-universal-transaction) - Explore on-chain helper contracts in [Contract Helpers](/docs/chain/build/contract-helpers) - Build wallet flows and abstract core SDK with the [UI Kit](/docs/chain/ui-kit) --- # Advanced Patterns URL: https://push.org/docs/chain/build/contract-initiated-examples/advanced-patterns/ Advanced Patterns | Contract-Initiated Examples | Build | Push Chain Docs The three preceding pages cover the basics: [plain inbound](/docs/chain/build/contract-initiated-examples/inbound-to-push-chain), [plain outbound](/docs/chain/build/contract-initiated-examples/outbound-from-push-chain), and the [round-trip with auto back-leg](/docs/chain/build/contract-initiated-examples/round-trip-auto-back-leg). This page is the full reference for every contract-initiated pattern we have built, including five advanced ones that combine funds bridging, state machines, and multi-chain cascades. Each entry links to a focused, single-purpose example in the [Push Chain Examples Repository](https://github.com/pushchain/push-chain-examples/tree/main/core-sdk-functions/) under `contract-initiated-*`. Each example is self-contained: contract, runner, README, .env.sample, and a verified end-to-end run on Donut Testnet. ## All patterns | Pattern | What it shows | |---|---| | **1. Plain Inbound** | Trigger a state change on Push Chain from a smart contract on an external chain. The Push target sees the dispatching contract's UEA as `msg.sender`, so on-Push logic can attribute the call to a real external identity. [Inbound to Push Chain](/docs/chain/build/contract-initiated-examples/inbound-to-push-chain) | | **2. Plain Outbound** | Run code on an external chain from a Push Chain contract without any live user driving the transaction. The destination sees the contract's CEA as `msg.sender`, so destination protocols can whitelist or pre-fund that CEA. [Outbound from Push Chain](/docs/chain/build/contract-initiated-examples/outbound-from-push-chain) | | **3. Round-Trip with Auto Back-Leg** | Dispatch to an external chain and automatically receive the result back on Push from a single user signature. No off-chain orchestration, no separate inbound trigger. [Round-Trip with Auto Back-Leg](/docs/chain/build/contract-initiated-examples/round-trip-auto-back-leg) BNB -> Push with auto back-leg via 6-arg executeUniversalTx" /> | | **4. Recipient Bridge** | Move native funds from an external chain to a Push wallet straight from contract logic. No payload, no contract call on Push, just a bridged balance bump for the recipient. | | **5. Inbound With Funds** | Bridge funds AND run a contract call on Push in the same transaction. The classic deposit-and-act pattern: a vault credits the depositor and runs business logic atomically with the funds arriving. | | **6. Outbound With Funds** | Bridge funds AND run a contract call on the destination chain in the same transaction. Symmetric to inbound-with-funds: pay an external protocol and trigger its action in one atomic move. | | **7. Round-Trip with Result** | Model a round-trip as a request-response on Push. A user opens a request; the destination chain performs the work; the result lands back on Push and resolves the original request. Useful for oracle-style flows where Push waits on external execution. | | **8. Cross-Chain Cascade** | One Push transaction fans out across multiple external chains in sequence. State changes land on two different external networks from a single user signature, with no off-chain glue between hops. BNB -> Push -> Solana" /> | ## Related - [Contract-Initiated Multichain Execution](/docs/chain/build/contract-initiated-multichain-execution) β†’ The conceptual reference for everything related to contract-initiated execution. - Basic Examples β†’ [Inbound to Push Chain](/docs/chain/build/contract-initiated-examples/inbound-to-push-chain), [Outbound from Push Chain](/docs/chain/build/contract-initiated-examples/outbound-from-push-chain), and the [Round-Trip with Auto Back-Leg](/docs/chain/build/contract-initiated-examples/round-trip-auto-back-leg). - [How CEA Works](/docs/chain/deep-dives/how-cea-works) β†’ The identity model that makes the round-trip guarantees possible. - [Derive Chain Executor Accounts (CEAs)](/docs/chain/tutorials/power-features/tutorial-derive-chain-executor-account) β†’ To pre-compute a contract's destination-chain CEA off-chain or on-chain. --- # Initialize EVM Client URL: https://push.org/docs/chain/build/initialize-evm-client/ Initialize EVM Client | Build | Push Chain Docs ## Overview Push Chain is fully EVM-compatible, so you can plug in your favorite Ethereum toolingβ€”whether that’s Ethers.js or Viem. For more details on each library, check out: - [ethers.js documentation](https://docs.ethers.org/) - [viem documentation](https://viem.sh/) ## Initialize EVM Client {` // customPropHighlightRegexStart=ethers\.JsonRpcProvider // customPropHighlightRegexEnd=\\); // customPropGTagEvent=initialize_evm_client_ethers import { ethers } from 'ethers'; // β€”β€”β€” CONFIG β€”β€”β€” const RPC_URL = 'https://evm.donut.rpc.push.org/'; function initEthers() { const provider = new ethers.JsonRpcProvider(RPC_URL); console.log('Got Ethers.js provider methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(provider))); } initEthers(); `} {` // customPropHighlightRegexStart=createPublicClient\\( // customPropHighlightRegexEnd=\\); // customPropGTagEvent=initialize_evm_client_viem import { createPublicClient, http } from 'viem'; function initViem() { const publicClient = createPublicClient({ transport: http('https://evm.donut.rpc.push.org/') }); console.log('Viem publicClient:', JSON.stringify(publicClient, null, 2)); } initViem(); `} {` // customPropHighlightRegexStart=provider\.getTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=initialize_evm_client_readonly import { ethers } from 'ethers'; async function fetchTxEthers() { const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/'); const txHash = '0x04ee80f072ab06ec88092701e7ba223451d0a1376e26755085271bc6de45a6a1'; const tx = await provider.getTransaction(txHash); console.log('Transaction:', JSON.stringify(tx, null, 2)); } console.log('Fetching transaction...'); await fetchTxEthers().catch(console.error); `} {` // customPropHighlightRegexStart=client\.getTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=initialize_evm_client_viem_fetch_tx import { createPublicClient, http } from 'viem'; async function fetchTxViem() { const client = createPublicClient({ transport: http('https://evm.donut.rpc.push.org/') }); const txHash = '0x04ee80f072ab06ec88092701e7ba223451d0a1376e26755085271bc6de45a6a1'; const tx = await client.getTransaction({ hash: txHash }); console.log('Transaction:', JSON.stringify(tx, null, 2)); } console.log('Fetching transaction...'); await fetchTxViem().catch(console.error); `} ## Next Steps - Send your first universal transaction with [Send Universal Transaction](/docs/chain/build/send-universal-transaction) - Learn about popular utilities in [Utility Functions](/docs/chain/build/utility-functions) - Skip core and directly jump to [UI Kit](/docs/chain/ui-kit) that provides complete abstraction --- # Understanding Universal Transactions URL: https://push.org/docs/chain/build/understanding-universal-transactions/ Understanding Universal Transactions | Build | Push Chain Docs ## Overview In most blockchain apps today, if a user on Ethereum wants to call a contract on another chain, they need to manually bridge tokens, switch wallets, pay gas on multiple networks, and hope nothing goes wrong between steps. **Universal transactions eliminate all of that.** You write one transaction. The SDK handles origin detection, gas orchestration, proof replay, and final execution regardless of which chain the user is on. **Push Chain turns all chains into universal execution environments behind a single transaction interface.** :::info Summary A universal transaction is a single transaction that executes across chains through Push Chain, without requiring manual bridging, network switching, or multi-step coordination. ::: ## Key Account Types Before diving into routes and lifecycle, it helps to understand the three account types that power this system. ### Universal Origin Account (UOA) The UOA is the user's **actual wallet**. It can be an Ethereum address, Solana public key, Push address or any chain-native identity. This is where transactions originate and the entity (controller) that authorizes execution. It never changes and requires no setup. ### Universal Executor Account (UEA) The UEA is a **smart contract account on Push Chain**, deterministically derived from the UOA. It is the entity that actually executes transactions on Push Chain on behalf of the user. ```mermaid flowchart LR UOA["UOAUser WalletAny Chain"] UEA["UEASmart Contract AccountPush Chain"] UOA -->|"deterministically derives"| UEA style UOA fill:#627eea,stroke:#fff,stroke-width:2px,color:#fff style UEA fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff ``` Key properties: - **Deterministic**: the same UOA always maps to the same UEA address, across all chains - **Non-custodial**: only a valid proof from the UOA can authorize UEA actions - **Lazy-deployed**: the UEA is deployed automatically on first use, no setup required ### Chain Executor Account (CEA) The CEA is an **executor account deployed on a supported external chain** (e.g., Ethereum, BNB Chain). It is deterministically mapped to a user or contract and allows execution on external chains while preserving identity across environments. CEAs are not limited to users with a UEA. They can also exist for native Push Chain EOAs or smart contracts. Depending on the target chain, a CEA may be implemented as an EOA or a smart contract, while remaining logically bound to its originating account. ```mermaid flowchart LR O["Origin AccountUEA / Push EOA / Contract"] CEA["CEAExecution AccountExternal Chain"] O -->|"maps to / controls"| CEA style O fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style CEA fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff ``` CEAs are only used when execution must happen on or originate from an external chain. ### Mental Model - UEA executes transactions on Push Chain for users interacting from external chains - CEA executes on external chains Together, they form a unified execution layer across all chains ## Transaction Routes Universal transactions support three routing modes. The route is determined automatically by the `tx.to` and `tx.from` fields you supply. | Route | Flow | Description | |------|------|------------| | Route 1 | Any Origin β†’ Push | Execute on Push Chain via UEA | | Route 2 | Any Origin β†’ External via CEA | Execute on external chain via CEA | | Route 3 | External (via CEA) β†’ Push | Execute on Push Chain with external origin | ```mermaid flowchart TD UOA["User WalletUniversal Origin Account"] PC["Push ChainSettlement Layer"] R1["Route 1Execute on Push Chaintx.to = address"] R2["Route 2Execute on External Chaintx.to = { address, chain }"] R3["Route 3CEA-Origin to Push Chaintx.from = { chain }"] UOA -->|"submits tx via SDK"| PC PC --> R1 PC --> R2 PC --> R3 style UOA fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style PC fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style R1 fill:#166534,stroke:#4ade80,color:#fff style R2 fill:#1e3a8a,stroke:#60a5fa,color:#fff style R3 fill:#7c2d12,stroke:#fb923c,color:#fff ``` ### Route 1: Any Origin to Push Chain The most common route. The user signs from any supported chain and the transaction executes on Push Chain via their UEA. **When to use**: Contract calls, token transfers, multicall batches β€” anything targeting Push Chain. ```mermaid flowchart LR O["Any Origin\nEthereum / Solana / Push / ..."] UEA["UEA\nPush Chain"] T["Target\nContract or EOA\non Push Chain"] O -->|"tx.to = address"| UEA --> T style O fill:#1e293b,stroke:#475569,color:#94a3b8 style UEA fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style T fill:#166534,stroke:#4ade80,color:#fff ``` ```typescript // Route 1: plain address target triggers Push Chain execution await pushChainClient.universal.sendTransaction({ to: '0xContractOnPushChain', data: encodedCalldata, value: BigInt(0), }); ``` ### Route 2: Any Origin to External Chain via CEA The user signs from any chain, but execution happens on an **external chain** through their CEA. Push Chain acts as the coordination layer. **When to use:** Calling a contract on Ethereum, BNB Chain, or any supported external chain without the user needing to switch networks. ```mermaid flowchart LR O["Any Origin"] PC["Push Chain\nCoordination Layer"] CEA["CEA\nExternal Chain"] T["Target\nContract on\nExternal Chain"] O -->|"tx.to = { address, chain }"| PC --> CEA --> T style O fill:#1e293b,stroke:#475569,color:#94a3b8 style PC fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style CEA fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff style T fill:#1e3a8a,stroke:#60a5fa,color:#fff ``` ```typescript // Route 2: { address, chain } target routes through CEA on that chain await pushChainClient.universal.sendTransaction({ to: { address: '0xContractOnEthereum', chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, }, data: encodedCalldata, }); ``` ### Route 3: External CEA Origin to Push Chain The execution *originates* from a CEA on an external chain and targets Push Chain. This route is used when a transaction returns from an external chain and needs to preserve the identity established there. For example, a user can move from Solana to Push Chain via a UEA, interact with Ethereum using a CEA (such as depositing into Aave), and then return to Push Chain. Route 3 ensures the execution reflects their Ethereum identity. **When to use:** When execution on Push Chain must reflect an external chain identity, typically after interacting with contracts on that chain. ```mermaid flowchart LR O["Any Origin"] CEA["CEA\nExternal Chain\nas origin"] PC["Push Chain\nExecution"] O -->|"tx.from = { chain }"| CEA --> PC style O fill:#1e293b,stroke:#475569,color:#94a3b8 style CEA fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff style PC fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff ``` ```typescript // Route 3: tx.from forces CEA on specified chain to be the execution origin await pushChainClient.universal.sendTransaction({ from: { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA }, to: '0xContractOnPushChain', data: encodedCalldata, }); ``` ## Transaction Lifecycle Every call to `sendTransaction` follows the same execution pipeline. These steps are handled automatically by the SDK. | Step | Stage | Description | |------|------|------------| | 1 | Origin Detection | Identifies the UOA and source chain | | 2 | Gas Estimation | Estimates total execution cost across chains | | 3 | UEA Resolution | Resolves or deploys the user's UEA on Push Chain | | 4 | User Authorization | Collects signature or verifies on-chain proof | | 5 | Gas Funding | Funds the UEA if required | | 6 | Asset Movement | Moves assets if `tx.funds` is set | | 7 | Broadcast | Submits the transaction to Push Chain | | 8 | Confirmation | Returns transaction hash and execution receipt | Asset Movement (step 6) only runs when `tx.funds` is set. For plain Push Chain transactions it is skipped entirely. Each step emits a `SEND-TX-*` progress event. You can subscribe via `progressHook` at the client level or per-call to show live status in your UI. ## Why Universal Transactions Matter Universal transactions fundamentally change how apps are built and used across chains. - **No per-chain deployments** Developers deploy once and reach users across all supported chains - **No bridging or network switching** Users interact from their native chain without managing infrastructure - **Unified user identity** The same user can execute across chains while preserving identity - **Composable cross-chain flows** Complex multi-chain interactions happen within a single transaction This enables a new class of applications that are truly chain-agnostic and universal. ## Next Steps - Send your first universal transaction with [Send Universal Transaction](/docs/chain/build/send-universal-transaction) - Track and monitor execution with [Track Universal Transaction](/docs/chain/build/track-universal-transaction) - Build advanced cross-chain flows with [Send Multichain Transactions](/docs/chain/build/send-multichain-transactions) --- # Send Universal Transaction URL: https://push.org/docs/chain/build/send-universal-transaction/ Send Universal Transaction | Build | Push Chain Docs ## Overview Universal transactions let you execute transfers, contract calls, asset movement, and batched transactions across **Push Chain** and **supported external chains** through one unified interface. You do not need separate transactions, wrapping, manual bridging, or extra tooling. To understand this concept in detail, please see [Understanding Universal Transactions](/docs/chain/build/understanding-universal-transactions/). ### How Routing Works `sendTransaction` automatically selects the execution route based on the `to` and `from` fields. | Route | Input Shape | Executes On | Notes | | ------- | ----------------------------------- | -------------- | ------------------------------------------------------- | | Route 1 | `to: "0x..."` | Push Chain | Default route for Push Chain targets. | | Route 2 | `to: { address, chain }` | External chain | Executes on the specified external chain. | | Route 3 | `to: "0x..."` and `from: { chain }` | Push Chain | Uses your CEA on the specified external chain as the execution origin. | ## Send a Universal Transaction **_`pushChainClient.universal.sendTransaction({tx}): Promise`_** ```typescript const txResponse = await pushChainClient.universal.sendTransaction({ to: '0xa54E96d3fB93BD9f6cCEf87c2170aEdB1D47E1cF', value: PushChain.utils.helpers.parseUnits('0.1', 18), // 0.1 PC in uPC // value: BigInt('100000000000000000') is equivalent here }); ``` | **Arguments** | **Type** | **Description** | | ----------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | _`tx.to`_ | `string` \| `{ address: string; chain: CHAIN }` | Defines the execution target. Passing a plain address triggers Push Chain execution. Passing `{ address, chain }` targets an external chain. | | `tx.from` | `{ chain: PushChain.CONSTANTS.CHAIN }` | Optional. When set to an external chain, execution uses the Chain Executor Account (CEA) for that chain as the transaction origin.This is primarily used for external origin or CEA-based execution flows. | | `tx.value` | `BigInt` | Native value to send, expressed in the smallest unit of the execution context.On Push Chain this is uPC (the smallest unit, like wei in ETH). On external execution routes, this maps to the native asset amount of that route. | | `tx.data` | `string` \| `Array` | Encoded calldata for a single call `string` or batched multicall `Array`. Use `encodeTxData` to produce the correct bytes for EVM (ABI) or Solana (Anchor IDL) targets. | | `tx.funds` | `{ amount: BigInt; token?: PushChain.CONSTANTS.MOVEABLE.TOKEN }` | Moves supported assets as part of the transaction flow.Depending on the route, assets may be moved into Push Chain or between Push Chain and a supported external chain.If `tx.data` is provided, asset movement and execution happen atomically. | | `tx.progressHook` | `(progress: ProgressHookType) => void` | A callback function to receive progress updates during transaction lifecycle, especially useful for tracking cross-chain transactions. | Progress Hook Type and Response | Field | Type | Description | | -------------------- | ------------------------------ | ------------------------------------------------------------------------------ | | `progress` | `Object` | The progress of the transaction. | | `progress.id` | `string` | Unique identifier for the progress event. | | `progress.title` | `string` | Brief title of the progress event. | | `progress.message` | `string` | Detailed message describing the event. | | `progress.level` | `INFO` \| `SUCCESS` \| `ERROR` | Severity level of the event. | | `progress.response` | `object` \| `null` | Additional data object for the event, or `null` if not applicable. | | `progress.timestamp` | `string` | ISO-8601 timestamp when the event occurred (e.g. `2025-06-26T15:04:05.000Z`). | **Route 1 β†’ Execute on Push Chain** | ID | Title | Message | Level | Response | | -- | ----- | ------- | ----- | -------- | | `SEND-TX-101` | Origin Chain Detected | Origin chain: `` β€” Address: `` | INFO | `{ chain, address }` | | `SEND-TX-102-01` | Estimating Gas | Estimating and fetching gas limit, gas price for TX | INFO | `{ stage: 'estimating-gas' }` | | `SEND-TX-102-02` | Gas Estimated | Total execution cost: `` UPC | SUCCESS | `{ totalCost, currency }` | | `SEND-TX-103-01` | Resolving Universal Execution Account | Resolving UEA – computing address, checking deployment and balance | INFO | `{ stage: 'resolving-uea' }` | | `SEND-TX-103-02` | Universal Execution Account Resolved | UEA: ``, Deployed: `` | SUCCESS | `{ uea, deployed }` | | `SEND-TX-103-03` | Calculating Prepaid Deposit | Calculating required prepaid deposit (one-time >$1; refilled only when gas nears exhaustion) | INFO | null | | `SEND-TX-103-03-01` | Adjusting Prepaid Deposit to be >$1 | Required deposit below $1 minimum β€” padding to $1 floor | INFO | `{ gasRequired, extraDepositPC, totalDepositUSD }` | | `SEND-TX-103-03-02` | Prepaid Deposit in range (>=$1 and <$10) | Required deposit `${x}` within $1–$10 range β€” depositing as required | INFO | `{ gasRequired, extraDepositPC, totalDepositUSD }` | | `SEND-TX-103-03-03` | Prepaid Deposit Exceeds $10 Cap, splitting Gas and Funds | Required deposit exceeds $10 cap β€” splitting: $10 gas leg + `${overflow}` UPC overflow bridged as funds | INFO | `{ gasRequired, extraDepositPC, totalDepositUSD }` | | `SEND-TX-103-04` | Prepaid Deposit Estimated | Prepaid deposit estimated | SUCCESS | `{ totalPCDeposit, totalDepositUSD }` | | `SEND-TX-104-01` | Awaiting Transaction | Awaiting user transaction on origin chain | INFO | `{ stage: 'awaiting-transaction' }` | | `SEND-TX-104-02` | Awaiting Signature | Awaiting user signature for universal payload | INFO | `{ stage: 'awaiting-signature' }` | | `SEND-TX-104-03` | Verification Success | Verification completed via Transaction or Signature | SUCCESS | `{ stage: 'verified' }` | | `SEND-TX-104-04` | Verification Declined / Signature Failed | Verification declined by user \| `` | ERROR | `{ error, isUserDecline }` | | `SEND-TX-105-01` | Gas Funding In Progress | Gas funding tx sent: `` | INFO | `{ txHash, originChainTx }` | | `SEND-TX-105-02` | Gas Funding Confirmed | Gas funding confirmed on origin chain | SUCCESS | `{ stage: 'gas-funded', txHash }` | | `SEND-TX-106-01` | Preparing Funds Transfer | Preparing to move ` ` from origin chain | INFO | `{ amount, symbol }` | | `SEND-TX-106-02` | Funds Lock Submitted | Locking ` ` β€” Tx: `` | INFO | `{ txHash, amount, symbol, originChainTx }` | | `SEND-TX-106-03` | Awaiting Confirmations | Waiting for `` confirmations | INFO | `{ current: 0, required }` | | `SEND-TX-106-03-01` | Confirmation `/` Received | `/` confirmations received | INFO | `{ current, required }` | | `SEND-TX-106-03-02` | Confirmation `/` Received | `/` confirmations received | SUCCESS | `{ current, required }` | | `SEND-TX-106-04` | Funds Confirmed | Origin chain lock confirmed | SUCCESS | `{ stage: 'funds-confirmed', txHash }` | | `SEND-TX-106-05` | Syncing with Push Chain | Waiting for transaction to appear on Push Chain | INFO | `{ stage: 'syncing-push-chain' }` | | `SEND-TX-106-06` | Funds Credited on Push Chain | Funds credited: ` ` | SUCCESS | `{ amount, symbol }` | | `SEND-TX-107` | Broadcasting to Push Chain | Sending tx to Push Chain... | INFO | `{ stage: 'broadcasting', destination: 'push-chain' }` | | `SEND-TX-199-01` | Push Chain Tx Success | Tx confirmed: `` | SUCCESS | `{ txHash, response, receipt }` | | `SEND-TX-199-02` | Push Chain Tx Failed | `` | ERROR | `{ error }` | **Route 2 β†’ Execute on External Chain** | ID | Title | Message | Level | Response | | -- | ----- | ------- | ----- | -------- | | `SEND-TX-201` | `` Detected | External chain: `` β€” Target address: `` | INFO | `{ chain, address }` | | `SEND-TX-202-01` | Estimating `` Chain Gas | Querying Push Chain gas and UGPC relay fee | INFO | `{ stage: 'estimating-gas', chain }` | | `SEND-TX-202-02` | `` Chain Gas Estimated | Push gas: `` UPC + UGPC relay: `` UPC = `` UPC | SUCCESS | `{ gasEstimate, relayFee, totalCost, currency }` | | `SEND-TX-203-01` | Resolving `` Execution Account | Resolving UEA on Push Chain and CEA on `` | INFO | `{ stage: 'resolving-cea', chain }` | | `SEND-TX-203-02` | `` Execution Account Ready | UEA: ``. CEA: `` on ``. Deployed: `` | SUCCESS | `{ uea, cea, chain, deployed }` | | `SEND-TX-204-01` | Awaiting Signature | Awaiting user signature for universal payload | INFO | `{ stage: 'awaiting-signature' }` | | `SEND-TX-204-02` | Signature Received | Universal payload signed β€” preparing broadcast | SUCCESS | `{ stage: 'signed' }` | | `SEND-TX-204-03` | Verification Success | Verification completed | SUCCESS | `{ stage: 'verified' }` | | `SEND-TX-204-04` | Verification Declined / Signature Failed | Verification declined by user \| `` | ERROR | `{ error, isUserDecline }` | | `SEND-TX-207` | Broadcasting from Push Chain β†’ `` | Sending tx to Push Chain... | INFO | `{ chain }` | | `SEND-TX-209-01` | Awaiting Push Chain Relay | Waiting for UGPC to relay execution to `` | INFO | `{ chain }` | | `SEND-TX-209-02` | Syncing State with `` | Polling `` for CEA execution | INFO | `{ chain, elapsedMs }` | | `SEND-TX-299-01` | `` Tx Success | CEA executed on `` - tx: `` | SUCCESS | `{ txHash, ...details }` | | `SEND-TX-299-02` | `` Tx Failed | `` | ERROR | `{ error, chain }` | | `SEND-TX-299-03` | Syncing State with `` Timeout | Timed out waiting for UGPC relay to `` | ERROR | `{ error: 'relay timeout', chain, elapsedMs }` | | `SEND-TX-299-99` | `` Tx Completed | Intermediate `` tx confirmed: ``, progressing to next phase | INFO | `{ chain, txHash }` | **Route 3 β†’ Execute on Push Chain from CEA** | ID | Title | Message | Level | Response | | -- | ----- | ------- | ----- | -------- | | `SEND-TX-199-99-99` | Push Chain TX Completed | Intermediate Push Chain tx confirmed: ``, progressing to next phase | INFO | `{ txHash }` | | `SEND-TX-301` | ``'s Executor Account Detected | Source chain: `` β€” CEA: `` | INFO | `{ chain, address }` | | `SEND-TX-302-01` | Estimating `` Gas | Querying Push Chain gas and UGPC relay fee | INFO | `{ stage: 'estimating-gas', chain }` | | `SEND-TX-302-02` | `` Gas Estimated | Push gas: `` UPC + UGPC relay: `` UPC = `` UPC | SUCCESS | `{ gasEstimate, relayFee, totalCost, currency }` | | `SEND-TX-302-03` | Calculating Prepaid Deposit | Calculating required prepaid deposit (one-time >$1; refilled only when gas nears exhaustion) | INFO | null | | `SEND-TX-302-03-01` | Adjusting Prepaid Deposit to be >$1 | Required deposit below $1 minimum β€” padding to $1 floor | INFO | `{ gasRequired, extraDepositPC, totalDepositUSD }` | | `SEND-TX-302-03-02` | Prepaid Deposit in range (>=$1 and <$10) | Required deposit `${x}` within $1–$10 range β€” depositing as required | INFO | `{ gasRequired, extraDepositPC, totalDepositUSD }` | | `SEND-TX-302-03-03` | Prepaid Deposit Exceeds $10 Cap, splitting Gas and Funds | Required deposit exceeds $10 cap β€” splitting: $10 gas leg + `${overflow}` UPC overflow bridged as funds | INFO | `{ gasRequired, extraDepositPC, totalDepositUSD }` | | `SEND-TX-302-04` | Prepaid Deposit Estimated | Prepaid deposit estimated | SUCCESS | `{ totalPCDeposit, totalDepositUSD }` | | `SEND-TX-303-01` | Resolving Execution Accounts on Chains | Resolving UEA on Push Chain and CEA on `` | INFO | `{ stage: 'resolving-cea-uea', chain }` | | `SEND-TX-303-02` | Execution Accounts Resolved | UEA: ``. CEA: `` on ``. Deployed: true | SUCCESS | `{ uea, cea, chain, deployed }` | | `SEND-TX-304-01` | Awaiting Signature | Awaiting user signature for universal payload | INFO | `{ stage: 'awaiting-signature' }` | | `SEND-TX-304-02` | Signature Received | Universal payload signed β€” preparing broadcast | SUCCESS | `{ stage: 'signed' }` | | `SEND-TX-304-03` | Verification Success | Verification completed | SUCCESS | `{ stage: 'verified' }` | | `SEND-TX-304-04` | Verification Declined / Signature Failed | Verification declined by user \| `` | ERROR | `{ error, isUserDecline }` | | `SEND-TX-307` | Broadcasting from Push Chain β†’ `` | Sending tx from Push Chain... | INFO | `{ chain }` | | `SEND-TX-309-01` | Awaiting `` Relay | Waiting for UGPC to relay to CEA on `` | INFO | `{ chain }` | | `SEND-TX-309-02` | Syncing State with `` | Polling `` for CEA execution | INFO | `{ chain, elapsedMs }` | | `SEND-TX-309-03` | `` Tx Confirmed | CEA executed on ``: `` β€” return inbound initiated | INFO | `{ chain, txHash }` | | `SEND-TX-310-01` | `` β†’ Push Chain Inbound Tx Submitted | CEA initiated return β€” waiting for Push Chain inbound from `` | INFO | `{ chain }` | | `SEND-TX-310-02` | Syncing State with Push Chain for Inbound Tx | Polling Push Chain for inbound from `` | INFO | `{ chain, elapsedMs }` | | `SEND-TX-399-01` | Push Chain Inbound Tx Success | Inbound from `` confirmed Β· Push tx: `` | SUCCESS | `{ chain, txHash, receipt }` | | `SEND-TX-399-02` | `` Tx Failed / Push Chain Tx Failed / Push Chain Inbound Tx Failed | `` | ERROR | `{ error, phase, chain }` | | `SEND-TX-399-03` | `` Timeout / Push Chain Timeout / Push Chain Inbound Timeout | Timed out waiting for... | ERROR | `{ error, phase, chain, elapsedMs }` | **Executing multiple transactions (Send Multichain Transations)** | ID | Title | Message | Level | Response | | -- | ----- | ------- | ----- | -------- | | `SEND-TX-001` | Multichain Transactions Initiated | ``-hop transaction β€” `` | INFO | `{ hopCount, chains }` | | `SEND-TX-002-01` | Starting Intermediate Transaction #``/`` | Starting tx `` of ``: `` β†’ `` | INFO | `{ n, total, fromChain, toChain }` | | `SEND-TX-002-99-99` | Intermediate Transaction #``/`` Complete | Tx `` of `` confirmed β€” proceeding to tx `` | INFO | `{ n, total }` | | `SEND-TX-999-01` | All Multichain Transactions Successful | ``-hop transaction confirmed across all chains | SUCCESS | `{ hopCount }` | | `SEND-TX-999-02` | Multichain Transactions Failed | Cascade failed at hop `` of ``: `` | ERROR | `{ failedAt, total, error }` | | `SEND-TX-999-03` | Multichain Transactions Timeout | Cascade timed out at hop `` of `` | ERROR | `{ failedAt, total, error: 'cascade timeout' }` | | Arguments | Type | Default | Description | | ------------------------- | ------------------------------------------------------------------------------------------------------ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `tx.gasLimit` | `BigInt` | `SDK Estimated` | Optional override for transaction gas limit. If omitted, the SDK estimates it. | | `tx.maxFeePerGas` | `BigInt` | `SDK Estimated` | Optional override for max fee per gas. If omitted, the SDK estimates it when applicable. | | `tx.maxPriorityFeePerGas` | `BigInt` | `SDK Estimated` | Optional override for priority fee. If omitted, the SDK estimates it when applicable. | | `tx.payGasWith` | `{ token?: PushChain.CONSTANTS.PAYABLE.TOKEN; slippageBps?: number; minAmountOut?: bigint \| string }` | - | Pay universal transaction fees using a supported token (e.g., `PushChain.CONSTANTS.PAYABLE.TOKEN.ETHEREUM_SEPOLIA.USDT`). Optional `slippageBps` (e.g., `100` = 1%) and `minAmountOut` (wei) let you control on-chain swap execution. | | `tx.deadline` | `BigInt` | - | Optional execution deadline for the transaction. | Returns TxResponse " className="alert alert--fn-args"> ```typescript { hash: '0xe2302bd21ab0902f37cb605d491ce5f95ee35ce4083405dddf3657d782acae35', origin: 'eip155:42101:0xFd6C2fE69bE13d8bE379CCB6c9306e74193EC1A9', blockNumber: 0n, blockHash: '', transactionIndex: 0, chainId: '42101', from: '0xFd6C2fE69bE13d8bE379CCB6c9306e74193EC1A9', to: '0x35B84d6848D16415177c64D64504663b998A6ab4', nonce: 341, data: '0x', value: 1000n, gasLimit: 21000n, gasPrice: 1325000000n, maxFeePerGas: 1325000000n, maxPriorityFeePerGas: 125000000n, accessList: [], wait: [Function: wait], type: '2', typeVerbose: 'eip1559', signature: { r: '0x556566ba1304bf8e93025fc82daff32eb24b7ee9804a76d0baa0098dfa7237de', s: '0x4495d7811d3dcb1beac16f29261903b542b0b65f51aa5942f65dbaf67e735724', v: 1, yParity: 1 }, raw: { from: '0xFd6C2fE69bE13d8bE379CCB6c9306e74193EC1A9', to: '0x35B84d6848D16415177c64D64504663b998A6ab4', nonce: 341, data: '0x', value: 1000n } } ``` | Property | Type | Description | | ---------------------- | ---------- | -------------------------------------------------------------------------------- | | `hash` | `string` | Unique transaction hash identifier | | `origin` | `string` | Origin identifier in format "eip155:chainId:address" or "solana:chainId:address" | | `blockNumber` | `BigInt` | Block number where transaction was included | | `blockHash` | `string` | Hash of the block containing this transaction | | `transactionIndex` | `number` | Position/index of transaction within the block | | `chainId` | `string` | Chain identifier (e.g. Push Chain = `42101`) | | `from` | `string` | UEA (Universal Executor Account) that executed the transaction | | `to` | `string` | Target address the UEA executed against | | `nonce` | `number` | Derived nonce for the UEA | | `data` | `string` | Perceived calldata (transaction input data) | | `value` | `BigInt` | Amount of native tokens transferred (in wei) | | `gasLimit` | `BigInt` | Maximum gas units allocated for transaction | | `gasPrice` | `BigInt` | Gas price for legacy transactions (in wei) | | `maxFeePerGas` | `BigInt` | Maximum fee per gas for EIP-1559 transactions | | `maxPriorityFeePerGas` | `BigInt` | Maximum priority fee (tip) per gas for EIP-1559 | | `accessList` | `array` | EIP-2930 access list for optimized storage access | | `type` | `string` | Transaction type identifier | | `typeVerbose` | `string` | Human-readable transaction type | | `signature` | `object` | ECDSA signature components | | `signature.r` | `string` | R component of ECDSA signature | | `signature.s` | `string` | S component of ECDSA signature | | `signature.v` | `number` | Recovery ID (legacy format) | | `signature.yParity` | `number` | Y-parity for EIP-1559 (0 or 1) | | `raw` | `object` | Original on-chain transaction data | | `raw.from` | `string` | Actual from address that went on chain | | `raw.to` | `string` | Actual to address that went on chain | | `raw.nonce` | `number` | Actual raw nonce used on chain | | `raw.data` | `string` | Actual raw data that went on chain | | `raw.value` | `BigInt` | Actual derived value that went on chain | | `wait` | `function` | Async function that returns a Promise resolving to UniversalTxReceipt | from `txResponse` "> Calling the `wait()` function from `txResponse` object will give you a `Promise` once the transaction is confirmed on-chain. ```typescript const txReceipt = await txResponse.wait(1); // number of blocks confirmations to wait for ``` ```typescript { hash: '0xb52706db4116dd6bbea87be5142ac2c69b17fe8ccf8e2b88ac176adb30b90dd6', blockNumber: 3413247n, blockHash: '0x5a7b6e2716f7d4450b6ca08aebfe74cea3d876367a8afe6f603196ba8c346a2d', transactionIndex: 0, from: '0xFd6C2fE69bE13d8bE379CCB6c9306e74193EC1A9', to: '0x35B84d6848D16415177c64D64504663b998A6ab4', contractAddress: null, gasPrice: 1325000000n, gasUsed: 21000n, cumulativeGasUsed: 21000n, logs: [], logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', status: 1, raw: { from: '0xFd6C2fE69bE13d8bE379CCB6c9306e74193EC1A9', to: '0x35B84d6848D16415177c64D64504663b998A6ab4', nonce: 342, data: '0x', value: 1000n } } ``` | Property | Type | Description | | ------------------- | ------------------ | -------------------------------------------------------------- | | `hash` | `string` | Transaction hash (same as in transaction response) | | `blockNumber` | `BigInt` | Block number where transaction was confirmed | | `blockHash` | `string` | Hash of the block containing the transaction | | `transactionIndex` | `number` | Position/index of transaction within the block | | `from` | `string` | Executor account address (UEA on Push Chain) | | `to` | `string` | Actual intended target address of the transaction | | `contractAddress` | `string` \| `null` | Address of deployed contract (null for regular transfers) | | `gasPrice` | `BigInt` | Gas price used for the transaction (in wei) | | `gasUsed` | `BigInt` | Actual gas consumed by the transaction | | `cumulativeGasUsed` | `BigInt` | Total gas used by all transactions in the block up to this one | | `logs` | `array` | Array of log objects emitted by the transaction | | `logsBloom` | `string` | Bloom filter for efficient log searching | | `status` | `number` | Transaction status (1 = success, 0 = failure) | | `raw` | `object` | Raw on-chain transaction data | | `raw.from` | `string` | Actual from address that executed on chain | | `raw.to` | `string` | Actual to address that was called on chain | | `raw.nonce` | `number` | Actual nonce used on chain | | `raw.data` | `string` | Actual calldata sent on chain | | `raw.value` | `BigInt` | Actual value transferred on chain | ## Send Transaction with Contract Interaction When calling a smart contract method via sendTransaction, supply the ABI-encoded function call as a **hex string in the data field**. You can choose `ethers` or `viem` or any of your favorite libraries to encode the function data. Or, use our utility function `PushChain.utils.helpers.encodeTxData` to encode the function data. ```typescript // Define the ABI for the ERC20 transfer function const erc20Abi = [ 'function transfer(address to, uint256 amount) returns (bool)', ]; // Generate the encoded function data using viem const data = PushChain.utils.helpers.encodeTxData({ abi: erc20Abi, functionName: 'transfer', // Transfer 10 tokens, converted to 18 decimal places args: ['0xRecipientAddress', PushChain.utils.helpers.parseUnits('10', 18)], }); // Send the transaction using Push Chain SDK const txResponse = await pushChainClient.universal.sendTransaction({ to: '0xTokenContractAddress', // The smart contract address on Push Chain value: BigInt('0'), // No $PC being sent, just contract interaction data: data, // The encoded function call }); ``` ## Send Transaction with Asset Movement You can move supported assets (e.g., USDT, USDC, or other tokens) from your origin chain to Push Chain and execute your call in a single transaction. Use the funds field to specify the amount of assets to move, and _optionally_ the data field to specify the function call to execute on Push Chain. > **Note**: funds transactions are only supported from external origin chains. > Native Push Chain users should call ERC-20 `transfer` or `transferFrom` directly (instead of using funds). ```typescript // Send 1 USDT to the recipient address const txResponse = await pushChainClient.universal.sendTransaction({ to: '0xRecipientAddress', // The recipient address on Push Chain data: data, // pass this if you want to execute a function on Push Chain as well funds: { amount: PushChain.utils.helpers.parseUnits('1', 6), // 1 USDT token: PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_SEPOLIA.USDT, // MoveableToken accessor from client }, }); ``` ## Send Batch Transactions (Multicall) You can batch multiple calls into a single transaction. This pattern is commonly referred to as **Multicall** in EVM ecosystems. To do so, instead of passing a single `data` field, supply an array of calls (each with `to`, `value`, and `data`) to sendTransaction. > **Note:** Batch transactions are only supported from external origin chains. > Native Push Chain users cannot use batch mode on Push but can use it on other chains (ie: when doing Route 2 or Route 3 universal transactions). ```typescript // Execute two increment() calls atomically const incrementData = PushChain.utils.helpers.encodeTxData({ abi: CounterABI, functionName: 'increment', }); await client.universal.sendTransaction({ // Must be '0x0000000000000000000000000000000000000000' for multicall to: '0x0000000000000000000000000000000000000000', data: [ { to: '0xCounterContract1', value: 0n, data: incrementData }, { to: '0xCounterContract2', value: 0n, data: incrementData }, ], }); ``` :::warning Multicall requirements For multicall, the `to` should always be zero address (`0x0000000000000000000000000000000000000000`). The SDK will `console.warn` if you pass any other address. This will become mandatory in a future release. ::: ## Live Playground {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_ethers_basic const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); async function main() { // Create a fresh wallet on Push Chain Testnet (Donut) const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/'); const signer = wallet.connect(provider); console.log('πŸ”‘ Push Chain account:', wallet.address); // Initialize Push Chain SDK const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); // Fund the wallet before sending await rl.question(':::prompt:::Send at least 1 PC to: ' + wallet.address + ' on Push Chain Testnet (Donut), then press Enter.'); // Send a simple value transfer on Push Chain const tx = await client.universal.sendTransaction({ to: '0x0000000000000000000000000000000000042101', value: PushChain.utils.helpers.parseUnits('0.001', 18), // 0.001 PC progressHook: (p) => console.log('πŸ”„ Progress:', p.title || p.id), }); console.log('βœ… TX Hash:', tx.hash); console.log('⛓️ Chain ID:', tx.chainId); } await main().catch(console.error); `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_ethers_with_prompt // β€”β€”β€” CONFIG β€”β€”β€” // RPC URL OF DIFFERENT CHAINS const RPC_PUSH = 'https://evm.donut.rpc.push.org/'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; // Dummy Address const RECIPIENT = '0x0000000000000000000000000000000000042101'; // Enable User Input const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // ⭐️ MAIN FUNCTION ⭐️ async function main() { console.log('πŸš€ Initializing Universal Transaction Example'); // Choose chain from which to send transaction const chainMeta = await returnUserChainSelection(); // 1) Create a wallet (in production, you'd use your own wallet) const wallet = ethers.Wallet.createRandom(); console.log('πŸ“ Created wallet:', wallet.address); // 2) Set up provider and connect wallet const provider = new ethers.JsonRpcProvider(chainMeta.id === '1' ? RPC_PUSH : RPC_SEPOLIA); const signer = wallet.connect(provider); // 3) Convert to Universal Signer console.log('πŸ”„ Converting to Universal Signer...'); const universalSigner = await PushChain.utils.signer.toUniversal(signer); // 4) Initialize Push Chain Client console.log('πŸ”— Initializing Push Chain Client...'); const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET }); // 5) Prepare transaction parameters const txParams = { to: RECIPIENT, value: PushChain.utils.helpers.parseUnits('0.001', 18), // 0.001 PC in uPC (wei) }; // wait for user to send funds first const fundingAmount = chainMeta.id === '1' ? '5 PC' : '0.005 ETH'; const faucetHint = chainMeta.id === '1' ? '' : 'Sepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia'; await rl.question(':::prompt:::Send at least ' + fundingAmount + ' to: ' + wallet.address + ' on ' + chainMeta.name + ', then press Enter.' + faucetHint); // 6) Send universal transaction console.log('πŸ“€ Sending transaction to:', RECIPIENT); try { // Note: This would fail in playground without funds // In production, ensure wallet has funds const txResponse = await pushChainClient.universal.sendTransaction({ ...txParams, progressHook: (p) => console.log('πŸ”„ Progress:', p.title || p.id), }); console.log('βœ… Transaction sent! Tx:', JSON.stringify(txResponse)); } catch (error) { console.error('❌ Transaction failed:', error.message); // In playground, this will fail without funds console.log('Note: In playground, this might fail without funds. Ensure your wallet has PC tokens.'); } } await main().catch(console.error); // --- HELPER FUNCTIONS --- async function returnUserChainSelection() { const selection = await rl.question('Please select the chain(1 for Push Testnet Donut, 2 for Ethereum Sepolia): '); if (selection !== '1' && selection !== '2') { console.log('Invalid selection. Please select 1 or 2.'); process.exit(0); } const name = selection === '1' ? 'PUSH_TESTNET_DONUT' : 'ETHEREUM_SEPOLIA'; return {id: selection, name: name}; } `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_viem_basic // Dummy Address const RECIPIENT = '0x0000000000000000000000000000000000042101'; const RPC_PUSH = 'https://evm.donut.rpc.push.org/'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; // Enable User Input const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); // ⭐️ MAIN FUNCTION ⭐️ async function main() { console.log('πŸš€ Initializing Universal Transaction Example'); // Choose chain from which to send transaction const chainMeta = await returnUserChainSelection(); // 1) Create a wallet (in production, you'd use your own wallet) const privateKey = generatePrivateKey(); const account = privateKeyToAccount(privateKey); console.log('πŸ”‘ Got account: ', account.address); // 2) Create viem client const client = createWalletClient({ account, transport: http(chainMeta.id === '1' ? RPC_PUSH : RPC_SEPOLIA), }); // 3) Convert to Universal Signer console.log('πŸ”„ Converting to Universal Signer...'); const universalSigner = await PushChain.utils.signer.toUniversal(client); // 4) Initialize Push Chain Client console.log('πŸ”— Initializing Push Chain Client...'); const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); // 5) Prepare transaction parameters const txParams = { to: RECIPIENT, value: PushChain.utils.helpers.parseUnits('0.001', 18), // 0.001 PC in uPC // data: '0x...', // For contract interactions - hex encoded }; // wait for user to send funds first const fundingAmount = chainMeta.id === '1' ? '5 PC' : '0.005 ETH'; const faucetHint = chainMeta.id === '1' ? '' : 'Sepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia'; await rl.question(':::prompt:::Send at least ' + fundingAmount + ' to: ' + account.address + ' on ' + chainMeta.name + ', then press Enter.' + faucetHint); // 6) Send universal transaction console.log('πŸ“€ Sending transaction to:', RECIPIENT); try { // Note: This would fail in playground without funds // In production, ensure wallet has funds const txResponse = await pushChainClient.universal.sendTransaction({ ...txParams, progressHook: (p) => console.log('πŸ”„ Progress:', p.title || p.id), }); console.log('βœ… Transaction sent! Tx Response:', JSON.stringify(txResponse)); } catch (error) { console.error('❌ Transaction failed:', error.message); // In playground, this will fail without funds console.log('Note: In playground, this might fail without funds. Ensure your wallet has PC tokens.'); } } await main().catch(console.error); // --- HELPER FUNCTIONS --- async function returnUserChainSelection() { const selection = await rl.question('Please select the chain(1 for Push Testnet Donut, 2 for Ethereum Sepolia): '); if (selection !== '1' && selection !== '2') { console.log('Invalid selection. Please select 1 or 2.'); process.exit(0); } const name = selection === '1' ? 'PUSH_TESTNET_DONUT' : 'ETHEREUM_SEPOLIA'; return { id: selection, name: name }; } `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_solana_basic // β€”β€”β€” CONFIG β€”β€”β€” // Dummy Address const RECIPIENT = '0x0000000000000000000000000000000000042101'; // Enable User Input const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // ⭐️ MAIN FUNCTION ⭐️ async function main() { console.log('πŸš€ Initializing Universal Transaction Example'); // 1) Create a keypair const keypair = Keypair.generate(); console.log('πŸ”‘ Got keypair: ', keypair.publicKey.toBase58()); // 2) Convert to Universal Signer from Keypair console.log('πŸ”„ Converting to Universal Signer from Keypair...'); const universalSigner = await PushChain.utils.signer.toUniversalFromKeypair(keypair, { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, library: PushChain.CONSTANTS.LIBRARY.SOLANA_WEB3JS, }); // 3) Initialize Push Chain Client console.log('πŸ”— Initializing Push Chain Client...'); const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET }); // 4) Prepare transaction parameters const txParams = { to: RECIPIENT, value: PushChain.utils.helpers.parseUnits('0.001', 18), // 0.001 PC in uPC // data: '0x...', // For contract interactions - hex encoded }; // wait for user to send funds first await rl.question(':::prompt:::Send at least 0.02 SOL to: ' + keypair.publicKey.toBase58() + ' on Solana Devnet, then press Enter. Solana devnet faucet: https://faucet.solana.com'); // 5) Send universal transaction console.log('πŸ“€ Sending transaction to:', RECIPIENT); try { // Note: This would fail in playground without funds // In production, ensure wallet has funds const txResponse = await pushChainClient.universal.sendTransaction({ ...txParams, progressHook: (p) => console.log('πŸ”„ Progress:', p.title || p.id), }); console.log('βœ… Transaction sent! Hash:', JSON.stringify(txResponse)); } catch (error) { console.error('❌ Transaction failed:', error.message); // In playground, this will fail without funds console.log('Note: In playground, this might fail without funds. Ensure your wallet has PC tokens.'); } } await main().catch(console.error); `} ```jsx live // customPropHighlightRegexStart= // customPropGTagEvent=send_transaction_ui_kit // customPropMinimized='false' // Import necessary components from @pushchain/ui-kit import { PushUniversalWalletProvider, PushUniversalAccountButton, usePushWalletContext, usePushChainClient, PushUI, } from '@pushchain/ui-kit'; function App() { // Define Wallet Config const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, }; function Component() { const [txnHash, setTxnHash] = useState(null); const [isLoading, setIsLoading] = useState(false); const { connectionStatus } = usePushWalletContext(); const { pushChainClient } = usePushChainClient(); const handleSendTransaction = async () => { if (pushChainClient) { setIsLoading(true); try { const res = await pushChainClient.universal.sendTransaction({ to: '0xFaE3594C68EDFc2A61b7527164BDAe80bC302108', value: PushChain.utils.helpers.parseUnits('0.001', 18), // 0.001 PC in uPC data: '0x', }); setTxnHash(res.hash); } catch (err) { console.log(err); } finally { setIsLoading(false); } } }; return ( {connectionStatus == PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && Send Transaction } {txnHash && ( <> Txn Hash: {txnHash} View in Explorer )} ); } return ( ); } ``` ## Next Steps - Explore end-to-end examples for each route with [Universal Transaction Scenarios](/docs/chain/build/universal-transaction-scenarios) - Sequence multiple chain transactions in one go with [Send Multichain Transactions](/docs/chain/build/send-multichain-transactions) - Trigger cross-chain execution directly from a smart contract with [Contract Initiated Multichain Execution](/docs/chain/build/contract-initiated-multichain-execution) - Integrate transaction flows into your frontend app using the [UI Kit](/docs/chain/ui-kit) --- # Universal Transaction Scenarios URL: https://push.org/docs/chain/build/universal-transaction-scenarios/ Universal Transaction Scenarios | Build | Push Chain Docs ## Overview End-to-end playground examples for each universal transaction route. For API reference and argument details, see [Send Universal Transaction](/docs/chain/build/send-universal-transaction). ## Route 1 β†’ Execute on Push Chain {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_contract_call_from_push_chain import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // Counter contract deployed on Push Chain Testnet (Donut) // https://donut.push.network/address/0x5FbDB2315678afecb367f032d93F642f64180aa3?tab=contract const COUNTER_ADDRESS = '0x5FbDB2315678afecb367f032d93F642f64180aa3'; const COUNTER_ABI = [ { inputs: [], name: 'increment', outputs: [], stateMutability: 'nonpayable', type: 'function' }, { inputs: [], name: 'countPC', outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], stateMutability: 'view', type: 'function' }, ]; async function main() { // This example uses Push Chain as the origin. // The same Route 1 pattern also works from other supported origin chains. // Just swap the RPC and signer; sendTransaction stays the same. const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/'); const signer = wallet.connect(provider); console.log('πŸ”‘ Account:', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: (p) => console.log('TX Progress:', p.title || p.id), }); await rl.question(':::prompt:::Send at least 1 PC to: ' + wallet.address + ' on Push Chain Testnet (Donut), then press Enter.'); const data = PushChain.utils.helpers.encodeTxData({ abi: COUNTER_ABI, functionName: 'increment', }); try { const tx = await client.universal.sendTransaction({ to: COUNTER_ADDRESS, data, }); console.log('βœ… TX Hash:', tx.hash); await tx.wait(); const pushProvider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/'); const counter = new ethers.Contract(COUNTER_ADDRESS, COUNTER_ABI, pushProvider); console.log('πŸ“Š Counter value:', (await counter.countPC()).toString()); } catch (err) { console.error('❌ Failed:', err && err.message ? err.message : err); } } await main().catch(console.error); `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_move_funds_native_ethers import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('πŸ”‘ Sepolia account:', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: (progress) => console.log('TX Progress:', progress.title || progress.id) }); // Require user to fund gas in ETH await rl.question(':::prompt:::Send at least 0.005 ETH (for gas) to: ' + wallet.address + ' on Sepolia, then press Enter.\\\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia'); try { const res = await client.universal.sendTransaction({ to: client.universal.account, funds: { amount: BigInt(1), token: PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_SEPOLIA.ETH // for native, can omit token, ie: same as { amount: BigInt(1) } }, }); console.log('βœ… Sent. Tx:', JSON.stringify(res)); } catch (err) { console.error('❌ Failed:', err && err.message ? err.message : err); console.log('Note: Ensure adequate Sepolia ETH for gas.'); } } await main().catch(console.error); `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_funds_erc20 import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('πŸ”‘ Sepolia account:', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: (progress) => console.log('TX Progress:', progress.title || progress.id), } ); const usdt = PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_SEPOLIA.USDT; // Require user to fund gas and USDT await rl.question(':::prompt:::Send the following to ' + wallet.address + ' on Sepolia, then press Enter:\\\\n β€’ at least 0.005 ETH (gas)\\\\n β€’ at least 0.02 USDT (ERC-20)\\\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia\\\\nMint USDT: https://sepolia.etherscan.io/address/' + usdt.address + '#writeContract#F6'); const oneCents = PushChain.utils.helpers.parseUnits('0.01', { decimals: usdt.decimals }); try { const res = await client.universal.sendTransaction({ to: client.universal.account, funds: { amount: oneCents, token: usdt }, }); console.log('βœ… Sent. Tx:', JSON.stringify(res)); } catch (err) { console.error('❌ Failed:', err && err.message ? err.message : err); console.log('Note: Ensure Sepolia ETH for gas and USDT balance.'); } } await main().catch(console.error); `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_funds_payload import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; // RPC const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; // Enable User Input const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('πŸ”‘ Sepolia account:', wallet.address); // Convert to Universal signer and initialize SDK const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: (progress) => console.log('TX Progress:', progress.title || progress.id), }); // Prepare payload call const UCABI = [ { inputs: [], name: 'increment', outputs: [], stateMutability: 'nonpayable', type: 'function' }, ]; const COUNTER_ADDRESS = '0x9F95857e43d25Bb9DaFc6376055eFf63bC0887C1'; // simple counter to increment const data = PushChain.utils.helpers.encodeTxData({ abi: UCABI, functionName: 'increment' }); // prepare ERC-20 funds const usdt = PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_SEPOLIA.USDT; const oneCents = PushChain.utils.helpers.parseUnits('0.01', { decimals: usdt.decimals }); // Require user to fund gas in ETH and supply USDT to move to USDT to Push Chain await rl.question(':::prompt:::Send the following to ' + wallet.address + ' on Sepolia, then press Enter:\\\\n β€’ at least 0.005 ETH (gas)\\\\n β€’ at least 0.02 USDT (ERC-20)\\\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia\\\\nMint USDT: https://sepolia.etherscan.io/address/' + usdt.address + '#writeContract#F6'); try { const res = await client.universal.sendTransaction({ to: COUNTER_ADDRESS, value: BigInt(0), data, funds: { amount: oneCents, token: usdt }, }); console.log('βœ… Sent. Tx:', JSON.stringify(res)); } catch (err) { console.error('❌ Failed:', err && err.message ? err.message : err); console.log('Note: Ensure adequate Sepolia ETH for gas and USDT balance.'); } } await main().catch(console.error); `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_full_example // Full Documentation: https://push.org/docs/chain/build/send-universal-transaction // Import Push Chain Core import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; // Readline for input import * as readline from 'node:readline/promises'; // Enable User Input const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); // Counter ABI on Push Chain const CounterABI = [ { inputs: [], name: 'increment', outputs: [], stateMutability: 'nonpayable', type: 'function' }, { inputs: [], name: 'countPC', outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], stateMutability: 'view', type: 'function' }, ]; // Counter contract address used in examples/tests const COUNTER_ADDRESS = '0x5FbDB2315678afecb367f032d93F642f64180aa3'; // ⭐️ MAIN FUNCTION ⭐️ async function main() { console.log('🌟 Multicall Example - Sepolia Origin β†’ Push Chain Target'); // 1) Create a fresh Sepolia account const RPC_URL = 'https://ethereum-sepolia-rpc.publicnode.com'; const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_URL); const signer = wallet.connect(provider); console.log('πŸ”‘ Got account:', wallet.address); // Convert to Universal Signer and initialize Push Chain Client const universalSigner = await PushChain.utils.signer.toUniversal(signer); const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); console.log('πŸš€ Push Chain client initialized'); // 3) Prompt to fund the Sepolia account before sending (required to originate the universal tx) console.log('3. Fund the Sepolia account to cover the origin transaction'); await rl.question( ':::prompt:::Send at least 0.005 ETH (for gas) to ' + wallet.address + ' on Ethereum Sepolia, then press Enter.\\\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia' ); // 4) Build and send a Multicall universal transaction // We will perform two consecutive "increment" calls on the Counter contract. console.log('4. Build and Send Multicall Universal Transaction'); // Encode the function call data for Counter.increment() const incrementData = PushChain.utils.helpers.encodeTxData({ abi: CounterABI, functionName: 'increment', }); // Two calls executed atomically on Push Chain const calls = [ { to: COUNTER_ADDRESS, value: BigInt(0), data: incrementData }, { to: COUNTER_ADDRESS, value: BigInt(0), data: incrementData }, ]; try { // Read counter BEFORE via ethers const pushProvider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/'); const counterContract = new ethers.Contract(COUNTER_ADDRESS, CounterABI, pushProvider); const before = await counterContract.countPC(); const txResponse = await pushChainClient.universal.sendTransaction({ to: pushChainClient.universal.account, value: BigInt(0), data: calls, }); console.log('πŸ“€ Transaction hash:', txResponse.hash); await txResponse.wait(); // Read counter AFTER const after = await counterContract.countPC(); console.log('πŸŽ‰ Congrats! You just sent a universal multicall transaction!'); console.log('1️⃣ You sent a Sepolia-origin transaction to the Universal Gateway'); console.log('2️⃣ Push Chain Validators handled settlement and executed the calls on Push Chain'); console.log('3️⃣ Two increments were executed on the Counter contract, atomically'); console.log('πŸ“Š Counter on Push Chain β†’ before: ' + before.toString() + ' | after: ' + after.toString()); } catch (error) { console.error('❌ Error:', error.message); console.log('πŸ’‘ Note: This example requires Sepolia testnet funds to execute'); } } // Run main await main().catch(console.error); `} {` import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; // β€”β€”β€” CONFIG β€”β€”β€” // RPC URL OF DIFFERENT CHAINS const RPC_PUSH = 'https://evm.donut.rpc.push.org/'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; // Dummy Address const RECIPIENT = '0x0000000000000000000000000000000000042101'; // Enable User Input const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // ⭐️ MAIN FUNCTION ⭐️ async function main() { console.log('πŸš€ Initializing Universal Transaction Example'); // Choose chain from which to send transaction const chainMeta = await returnUserChainSelection(); // 1) Create a wallet (in production, you'd use your own wallet) const wallet = ethers.Wallet.createRandom(); console.log('πŸ“ Created wallet:', wallet.address); // 2) Set up provider and connect wallet const provider = new ethers.JsonRpcProvider(chainMeta.id === '1' ? RPC_PUSH : RPC_SEPOLIA); const signer = wallet.connect(provider); // 3) Convert to Universal Signer console.log('πŸ”„ Converting to Universal Signer...'); const universalSigner = await PushChain.utils.signer.toUniversal(signer); // 4) Initialize Push Chain Client console.log('πŸ”— Initializing Push Chain Client...'); const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: async (progress) => { console.log('TX Progress: ', progress.title, ' | Time:', progress.timestamp); } }); // 5) Prepare transaction parameters const txParams = { to: RECIPIENT, value: PushChain.utils.helpers.parseUnits(".001", 18) // 0.001 PC in uPC (wei) }; // wait for user to send funds first const fundingAmount = chainMeta.id === '1' ? '5 PC' : '0.005 ETH'; const faucetHint = chainMeta.id === '1' ? '' : '\\\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia'; await rl.question(':::prompt:::Send at least ' + fundingAmount + ' to: ' + wallet.address + ' on ' + chainMeta.name + ', then press Enter.' + faucetHint); // 6) Send universal transaction console.log('πŸ“€ Sending transaction to:', RECIPIENT); try { const txResponse = await pushChainClient.universal.sendTransaction(txParams); console.log('βœ… Transaction sent! Tx Hash:', txResponse.hash); } catch (error) { console.error('❌ Transaction failed:', error.message); console.log('Note: In playground, this might fail without funds. Ensure your wallet has PC tokens.'); } } await main().catch(console.error); // --- HELPER FUNCTIONS --- async function returnUserChainSelection() { const selection = await rl.question('Please select the chain(1 for Push Testnet Donut, 2 for Ethereum Sepolia): '); if (selection !== '1' && selection !== '2') { console.log('Invalid selection. Please select 1 or 2.'); process.exit(0); } const name = selection === '1' ? 'PUSH_TESTNET_DONUT' : 'ETHEREUM_SEPOLIA'; return {id: selection, name: name}; } `} ## Route 2 β†’ Execute on External Chain {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_route2_payload // Route 2: Execute a contract call on an external chain (e.g. BNB Testnet) via CEA import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // Counter contract on BNB Testnet const COUNTER_ADDRESS = '0x7f0936bb90e7dcf3edb47199c2005e7184e44cf8'; const COUNTER_ABI = [ { type: 'function', name: 'increment', inputs: [], outputs: [], stateMutability: 'nonpayable' }, { type: 'function', name: 'count', inputs: [], outputs: [{ name: '', type: 'uint256' }], stateMutability: 'view' }, ]; async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('Sepolia account:', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: (p) => console.log('TX Progress:', p.title || p.id), }); await rl.question(':::prompt:::Send at least 0.005 ETH (for gas) to: ' + wallet.address + ' on Sepolia, then press Enter.\\\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia'); // Encode counter.increment() call for BNB Testnet const data = PushChain.utils.helpers.encodeTxData({ abi: COUNTER_ABI, functionName: 'increment', }); try { // Route 2: to is { address, chain } β€” executes on external chain via CEA const tx = await client.universal.sendTransaction({ to: { address: COUNTER_ADDRESS, chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET, }, data, }); console.log('Push Chain TX Hash:', tx.hash); // Wait for CEA relay and get external chain receipt const receipt = await tx.wait(); console.log('External TX Hash:', receipt.externalTxHash); console.log('External Chain:', receipt.externalChain); console.log('Explorer:', receipt.externalExplorerUrl); } catch (err) { console.error('Failed:', err && err.message ? err.message : err); } } await main().catch(console.error); `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_route2_funds // Route 2: Transfer native value to an address on an external chain import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('Sepolia account:', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: (p) => console.log('TX Progress:', p.title || p.id), }); console.log('UEA on Push Chain:', client.universal.account); await rl.question(':::prompt:::Fund these accounts, then press Enter:\\\\n β€’ UOA ' + wallet.address + ' on Sepolia β€” at least 0.005 ETH (gas to sign)\\\\n β€’ UEA ' + client.universal.account + ' on Push Chain β€” at least 1 PC + 0.002 pETH (burned to release ETH on Sepolia)\\\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia'); const TARGET = '0x1234567890123456789012345678901234567890'; try { // Route 2: transfer ETH to TARGET on Sepolia // Burns pETH on Push Chain, releases native ETH to TARGET on Sepolia const tx = await client.universal.sendTransaction({ to: { address: TARGET, chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, }, value: PushChain.utils.helpers.parseUnits('0.0005', 18), }); console.log('Push Chain TX Hash:', tx.hash); const receipt = await tx.wait(); console.log('External TX Hash:', receipt.externalTxHash); console.log('External Chain:', receipt.externalChain); console.log('Explorer:', receipt.externalExplorerUrl); } catch (err) { console.error('Failed:', err && err.message ? err.message : err); } } await main().catch(console.error); `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_route2_funds_erc20 // Route 2: Move Assets to an address on an external chain via CEA import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('Sepolia account:', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: (p) => console.log('TX Progress:', p.title || p.id), }); const usdt = PushChain.CONSTANTS.MOVEABLE.TOKEN.BNB_TESTNET.USDT; console.log('UEA on Push Chain:', client.universal.account); await rl.question(':::prompt:::Fund these accounts, then press Enter:\\\\n β€’ UOA ' + wallet.address + ' on Sepolia β€” at least 0.005 ETH (gas to sign)\\\\n β€’ UEA ' + client.universal.account + ' on Push Chain β€” at least 1 PC + 0.02 pUSDT(BNB) (burned to release USDT on BNB)\\\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia'); const TARGET = '0x1234567890123456789012345678901234567890'; try { // Route 2: transfer USDT to TARGET on BNB Testnet // Burns pUSDT on Push Chain, releases USDT to TARGET on BNB Testnet const tx = await client.universal.sendTransaction({ to: { address: TARGET, chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET, }, funds: { amount: PushChain.utils.helpers.parseUnits('0.01', { decimals: usdt.decimals }), token: usdt, }, }); console.log('Push Chain TX Hash:', tx.hash); const receipt = await tx.wait(); console.log('External TX Hash:', receipt.externalTxHash); console.log('External Chain:', receipt.externalChain); console.log('Explorer:', receipt.externalExplorerUrl); } catch (err) { console.error('Failed:', err && err.message ? err.message : err); } } await main().catch(console.error); `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_route2_funds_with_payload // Route 2: Move Assets to external chain AND atomically call a contract import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // Counter contract on BNB Testnet const COUNTER_ADDRESS = '0x7f0936bb90e7dcf3edb47199c2005e7184e44cf8'; const COUNTER_ABI = [ { type: 'function', name: 'increment', inputs: [], outputs: [], stateMutability: 'nonpayable' }, ]; async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('Sepolia account:', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: (p) => console.log('TX Progress:', p.title || p.id), }); const usdt = PushChain.CONSTANTS.MOVEABLE.TOKEN.BNB_TESTNET.USDT; console.log('UEA on Push Chain:', client.universal.account); await rl.question(':::prompt:::Fund these accounts, then press Enter:\\\\n β€’ UOA ' + wallet.address + ' on Sepolia β€” at least 0.005 ETH (gas to sign)\\\\n β€’ UEA ' + client.universal.account + ' on Push Chain β€” at least 1 PC + 0.02 pUSDT(BNB) (burned to release USDT on BNB with payload)\\\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia'); const data = PushChain.utils.helpers.encodeTxData({ abi: COUNTER_ABI, functionName: 'increment', }); try { // Route 2: move USDT to BNB Testnet and call increment() atomically // Burns pUSDT on Push Chain, releases USDT and executes contract call on BNB Testnet const tx = await client.universal.sendTransaction({ to: { address: COUNTER_ADDRESS, chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET, }, data, funds: { amount: PushChain.utils.helpers.parseUnits('0.01', { decimals: usdt.decimals }), token: usdt, }, }); console.log('Push Chain TX Hash:', tx.hash); const receipt = await tx.wait(); console.log('External TX Hash:', receipt.externalTxHash); console.log('External Chain:', receipt.externalChain); console.log('Explorer:', receipt.externalExplorerUrl); } catch (err) { console.error('Failed:', err && err.message ? err.message : err); } } await main().catch(console.error); `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_route2_multicall // Route 2: Execute multiple contract calls atomically on an external chain via CEA import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // Counter contract on BNB Testnet const COUNTER_ADDRESS = '0x7f0936bb90e7dcf3edb47199c2005e7184e44cf8'; const COUNTER_ABI = [ { type: 'function', name: 'increment', inputs: [], outputs: [], stateMutability: 'nonpayable' }, ]; async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('Sepolia account:', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: (p) => console.log('TX Progress:', p.title || p.id), }); await rl.question(':::prompt:::Send at least 0.005 ETH (for gas) to: ' + wallet.address + ' on Sepolia, then press Enter.\\\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia'); const incrementData = PushChain.utils.helpers.encodeTxData({ abi: COUNTER_ABI, functionName: 'increment', }); try { // Route 2: batch two increment() calls on BNB Testnet atomically const tx = await client.universal.sendTransaction({ to: { address: '0x0000000000000000000000000000000000000000', chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET, }, data: [ { to: COUNTER_ADDRESS, value: BigInt(0), data: incrementData }, { to: COUNTER_ADDRESS, value: BigInt(0), data: incrementData }, ], }); console.log('Push Chain TX Hash:', tx.hash); const receipt = await tx.wait(); console.log('External TX Hash:', receipt.externalTxHash); console.log('External Chain:', receipt.externalChain); console.log('Explorer:', receipt.externalExplorerUrl); } catch (err) { console.error('Failed:', err && err.message ? err.message : err); } } await main().catch(console.error); `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_route2_solana // Route 2: UOA_TO_CEA β€” call a Solana program via your CEA on Solana Devnet. // Same shape as an EVM call (to, value, data). The SDK reads the IDL to resolve // account lists, PDAs, and your CEA authority. import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // test_counter program deployed on Solana Devnet (base58 β€” native Solana form). // The SDK also accepts 0x-prefixed 32-byte hex for callers who already have it normalized. const SOL_TEST_PROGRAM = '8yNqjrMnFiFbVTVQcKij8tNWWTMdFkrDf9abCGgc2sgx'; // Anchor IDL for the Solana target β€” trimmed to just the receive_sol instruction. // In your own app this comes from your Anchor program's target/idl/*.json. const testCounterIdl = { address: SOL_TEST_PROGRAM, metadata: { name: 'test_counter', version: '0.1.0', spec: '0.1.0' }, instructions: [ { name: 'receive_sol', discriminator: [121, 244, 250, 3, 8, 229, 225, 1], accounts: [ { name: 'counter', writable: true, pda: { seeds: [{ kind: 'const', value: [99, 111, 117, 110, 116, 101, 114] }] } }, { name: 'recipient', writable: true, address: '89q1AUFb7YREHtjc1aYaPywovPq6tb3GYNPyDUJ3rshi' }, { name: 'cea_authority', writable: true }, { name: 'system_program', address: '11111111111111111111111111111111' }, ], args: [{ name: 'amount', type: 'u64' }], }, ], }; async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('Sepolia account (UOA):', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: (p) => console.log('TX Progress:', p.title || p.id), }); console.log('UEA on Push Chain:', client.universal.account); await rl.question(':::prompt:::Fund these accounts, then press Enter:\\\\n β€’ UOA ' + wallet.address + ' on Sepolia β€” at least 0.005 ETH (gas to sign)\\\\n β€’ UEA ' + client.universal.account + ' on Push Chain β€” at least 5 PC (covers gas + gas-token swap for the Solana outbound)\\\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia'); const data = PushChain.utils.helpers.encodeTxData({ idl: testCounterIdl, functionName: 'receive_sol', args: [BigInt(0)], }); try { const tx = await client.universal.sendTransaction({ to: { address: SOL_TEST_PROGRAM, chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, }, value: BigInt(0), data, }); console.log('TX Hash (Push Chain):', tx.hash); const receipt = await tx.wait(); console.log('External TX Hash (Solana):', receipt.externalTxHash); console.log('Explorer:', receipt.externalExplorerUrl); } catch (err) { console.error('Failed:', err && err.message ? err.message : err); } } await main().catch(console.error); `} ## Route 3 β†’ Execute on Push Chain from CEA {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_route3_payload // Route 3: CEA_TO_PUSH β€” Trigger a contract call on Push Chain from an external chain. // Your CEA on the external chain sends the tx, which is relayed inbound to Push Chain. // Use from: { chain } to specify which external chain your CEA lives on. import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // Counter contract on Push Chain (Testnet Donut) const COUNTER_ADDRESS = '0x70d8f7a0fF8e493fb9cbEE19Eb780E40Aa872aaf'; const COUNTER_ABI = [ { type: 'function', name: 'increment', inputs: [], outputs: [], stateMutability: 'payable' }, { type: 'function', name: 'countPC', inputs: [], outputs: [{ name: '', type: 'uint256' }], stateMutability: 'view' }, ]; async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('Sepolia account:', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: (p) => console.log('TX Progress:', p.title || p.id), }); // Derive CEA address on BNB Testnet so the user knows where to fund. const bnbProvider = new ethers.JsonRpcProvider('https://bsc-testnet-rpc.publicnode.com'); const ceaFactory = new ethers.Contract('0xe2182dae2dc11cBF6AA6c8B1a7f9c8315A6B0719', ['function getCEAForPushAccount(address) view returns (address, bool)'], bnbProvider); const [ceaAddress] = await ceaFactory.getCEAForPushAccount(client.universal.account); console.log('CEA on BNB Testnet:', ceaAddress); await rl.question(':::prompt:::Fund these accounts, then press Enter:\\\\n β€’ UOA ' + wallet.address + ' on Sepolia β€” at least 0.005 ETH (gas to sign)\\\\n β€’ CEA ' + ceaAddress + ' on BNB Testnet β€” at least 0.02 BNB (CEA gas + ~$10 fee-lock deposit required for fresh UEA)\\\\nFaucets: Sepolia https://cloud.google.com/application/web3/faucet/ethereum/sepolia | BNB Testnet https://www.bnbchain.org/en/testnet-faucet'); const data = PushChain.utils.helpers.encodeTxData({ abi: COUNTER_ABI, functionName: 'increment', }); try { // Route 3: from: { chain } means "use my CEA on BNB_TESTNET to execute this on Push Chain" const tx = await client.universal.sendTransaction({ from: { chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET }, to: COUNTER_ADDRESS, data, }); console.log('βœ… Push Chain TX Hash:', tx.hash); // .wait() polls for the outbound relay to BSC then the inbound back to Push Chain const receipt = await tx.wait(); console.log('πŸ“¦ External TX Hash:', receipt.externalTxHash); console.log('🌐 External Chain:', receipt.externalChain); console.log('πŸ” Explorer:', receipt.externalExplorerUrl); } catch (err) { console.error('❌ Failed:', err && err.message ? err.message : err); } } await main().catch(console.error); `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_route3_native // Route 3: CEA_TO_PUSH β€” Bridge native value from CEA on external chain back to Push Chain. import { PushChain } from '@pushchain/core'; import { createWalletClient, createPublicClient, http } from 'viem'; import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { bscTestnet } from 'viem/chains'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); async function main() { const privateKey = generatePrivateKey(); const account = privateKeyToAccount(privateKey); console.log('πŸ”‘ Sepolia account:', account.address); const walletClient = createWalletClient({ account, transport: http(RPC_SEPOLIA) }); const signer = await PushChain.utils.signer.toUniversal(walletClient); const client = await PushChain.initialize(signer, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: (p) => console.log('TX Progress:', p.title || p.id), }); // Derive CEA address on BNB Testnet from the CEAFactory contract const bnbClient = createPublicClient({ chain: bscTestnet, transport: http() }); const [ceaAddress] = await bnbClient.readContract({ address: '0xe2182dae2dc11cBF6AA6c8B1a7f9c8315A6B0719', abi: [{ type: 'function', name: 'getCEAForPushAccount', inputs: [{ name: 'pushAccount', type: 'address' }], outputs: [{ name: '', type: 'address' }, { name: '', type: 'bool' }], stateMutability: 'view' }], functionName: 'getCEAForPushAccount', args: [client.universal.account], }); console.log('CEA on BNB Testnet:', ceaAddress); await rl.question(':::prompt:::Fund these accounts, then press Enter:\\\\n β€’ UOA ' + account.address + ' on Sepolia β€” at least 0.005 ETH (gas to sign)\\\\n β€’ CEA ' + ceaAddress + ' on BNB Testnet β€” at least 0.02 BNB (CEA gas + ~$10 fee-lock deposit; 0.00005 BNB is bridged to Push Chain)\\\\nFaucets: Sepolia https://cloud.google.com/application/web3/faucet/ethereum/sepolia | BNB Testnet https://www.bnbchain.org/en/testnet-faucet'); try { // Route 3: bridge native BNB from CEA on BNB_TESTNET back to Push Chain const tx = await client.universal.sendTransaction({ from: { chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET }, to: client.universal.account, value: PushChain.utils.helpers.parseUnits('0.00005', 18), }); console.log('βœ… Push Chain TX Hash:', tx.hash); const receipt = await tx.wait(); console.log('πŸ“¦ External TX Hash:', receipt.externalTxHash); console.log('🌐 External Chain:', receipt.externalChain); console.log('πŸ” Explorer:', receipt.externalExplorerUrl); } catch (err) { console.error('❌ Failed:', err && err.message ? err.message : err); } } await main().catch(console.error); `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_route3_funds // Route 3: CEA_TO_PUSH β€” Bridge tokens from your CEA back to Push Chain. // Useful when your CEA on an external chain holds tokens you want back on Push Chain. import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('Sepolia account:', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: (p) => console.log('TX Progress:', p.title || p.id), }); // Derive CEA address on BNB Testnet from the CEAFactory contract const bnbProvider = new ethers.JsonRpcProvider('https://bsc-testnet-rpc.publicnode.com'); const ceaFactory = new ethers.Contract('0xe2182dae2dc11cBF6AA6c8B1a7f9c8315A6B0719', ['function getCEAForPushAccount(address) view returns (address, bool)'], bnbProvider); const [ceaAddress] = await ceaFactory.getCEAForPushAccount(client.universal.account); console.log('CEA on BNB Testnet:', ceaAddress); await rl.question(':::prompt:::Fund these accounts, then press Enter:\\\\n β€’ UOA ' + wallet.address + ' on Sepolia β€” at least 0.005 ETH (gas to sign)\\\\n β€’ CEA ' + ceaAddress + ' on BNB Testnet β€” at least 0.02 BNB (gas + ~$10 fee-lock deposit) + 0.02 USDT (the asset being bridged)\\\\nFaucets: Sepolia https://cloud.google.com/application/web3/faucet/ethereum/sepolia | BNB Testnet https://www.bnbchain.org/en/testnet-faucet'); const usdt = PushChain.CONSTANTS.MOVEABLE.TOKEN.BNB_TESTNET.USDT; try { // Route 3: bridge USDT from CEA on BNB_TESTNET back to your account on Push Chain const tx = await client.universal.sendTransaction({ from: { chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET }, to: client.universal.account, // bridge back to self funds: { amount: BigInt(10000), // 0.01 USDT (6 decimals) token: usdt, }, }); console.log('βœ… Push Chain TX Hash:', tx.hash); const receipt = await tx.wait(); console.log('External TX Hash:', receipt.externalTxHash); console.log('External Chain:', receipt.externalChain); console.log('Explorer:', receipt.externalExplorerUrl); } catch (err) { console.error('❌ Failed:', err && err.message ? err.message : err); } } await main().catch(console.error); `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_route3_funds_with_payload // Route 3: CEA_TO_PUSH β€” Bridge Assets from external chain and call a contract on Push Chain atomically import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // Counter contract on Push Chain (Testnet Donut) const COUNTER_ADDRESS = '0x70d8f7a0fF8e493fb9cbEE19Eb780E40Aa872aaf'; const COUNTER_ABI = [ { type: 'function', name: 'increment', inputs: [], outputs: [], stateMutability: 'payable' }, ]; async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('Sepolia account:', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: (p) => console.log('TX Progress:', p.title || p.id), }); // Derive CEA address on BNB Testnet from the CEAFactory contract const bnbProvider = new ethers.JsonRpcProvider('https://bsc-testnet-rpc.publicnode.com'); const ceaFactory = new ethers.Contract('0xe2182dae2dc11cBF6AA6c8B1a7f9c8315A6B0719', ['function getCEAForPushAccount(address) view returns (address, bool)'], bnbProvider); const [ceaAddress] = await ceaFactory.getCEAForPushAccount(client.universal.account); console.log('CEA on BNB Testnet:', ceaAddress); const usdt = PushChain.CONSTANTS.MOVEABLE.TOKEN.BNB_TESTNET.USDT; await rl.question(':::prompt:::Fund these accounts, then press Enter:\\\\n β€’ UOA ' + wallet.address + ' on Sepolia β€” at least 0.005 ETH (gas to sign)\\\\n β€’ CEA ' + ceaAddress + ' on BNB Testnet β€” at least 0.02 BNB (gas + ~$10 fee-lock deposit) + 0.02 USDT (bridged + payload on Push Chain)\\\\nFaucets: Sepolia https://cloud.google.com/application/web3/faucet/ethereum/sepolia | BNB Testnet https://www.bnbchain.org/en/testnet-faucet'); const data = PushChain.utils.helpers.encodeTxData({ abi: COUNTER_ABI, functionName: 'increment', }); try { // Route 3: bridge USDT from CEA on BNB_TESTNET and call increment() on Push Chain const tx = await client.universal.sendTransaction({ from: { chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET }, to: COUNTER_ADDRESS, data, funds: { amount: BigInt(10000), // 0.01 USDT (6 decimals) token: usdt, }, }); console.log('Push Chain TX Hash:', tx.hash); const receipt = await tx.wait(); console.log('External TX Hash:', receipt.externalTxHash); console.log('External Chain:', receipt.externalChain); console.log('Explorer:', receipt.externalExplorerUrl); } catch (err) { console.error('Failed:', err && err.message ? err.message : err); } } await main().catch(console.error); `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_route3_multicall // Route 3: CEA_TO_PUSH β€” Batch multiple contract calls on Push Chain from a CEA origin import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // Counter contract on Push Chain (Testnet Donut) const COUNTER_ADDRESS = '0x70d8f7a0fF8e493fb9cbEE19Eb780E40Aa872aaf'; const COUNTER_ABI = [ { type: 'function', name: 'increment', inputs: [], outputs: [], stateMutability: 'payable' }, ]; async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('Sepolia account:', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: (p) => console.log('TX Progress:', p.title || p.id), }); // Derive CEA address on BNB Testnet so the user knows where to fund. const bnbProvider = new ethers.JsonRpcProvider('https://bsc-testnet-rpc.publicnode.com'); const ceaFactory = new ethers.Contract('0xe2182dae2dc11cBF6AA6c8B1a7f9c8315A6B0719', ['function getCEAForPushAccount(address) view returns (address, bool)'], bnbProvider); const [ceaAddress] = await ceaFactory.getCEAForPushAccount(client.universal.account); console.log('CEA on BNB Testnet:', ceaAddress); await rl.question(':::prompt:::Fund these accounts, then press Enter:\\\\n β€’ UOA ' + wallet.address + ' on Sepolia β€” at least 0.005 ETH (gas to sign)\\\\n β€’ CEA ' + ceaAddress + ' on BNB Testnet β€” at least 0.02 BNB (CEA gas + ~$10 fee-lock deposit required for fresh UEA)\\\\nFaucets: Sepolia https://cloud.google.com/application/web3/faucet/ethereum/sepolia | BNB Testnet https://www.bnbchain.org/en/testnet-faucet'); const incrementData = PushChain.utils.helpers.encodeTxData({ abi: COUNTER_ABI, functionName: 'increment', }); try { // Route 3: batch two increment() calls on Push Chain with CEA as origin const tx = await client.universal.sendTransaction({ from: { chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET }, to: '0x0000000000000000000000000000000000000000', data: [ { to: COUNTER_ADDRESS, value: BigInt(0), data: incrementData }, { to: COUNTER_ADDRESS, value: BigInt(0), data: incrementData }, ], }); console.log('Push Chain TX Hash:', tx.hash); const receipt = await tx.wait(); console.log('External TX Hash:', receipt.externalTxHash); console.log('External Chain:', receipt.externalChain); console.log('Explorer:', receipt.externalExplorerUrl); } catch (err) { console.error('Failed:', err && err.message ? err.message : err); } } await main().catch(console.error); `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_route3_solana // Route 3: CEA_TO_PUSH β€” your Solana CEA calls a contract on Push Chain. // from: { chain: SOLANA_DEVNET } pins the originating CEA to Solana. import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const COUNTER_ADDRESS = '0x70d8f7a0fF8e493fb9cbEE19Eb780E40Aa872aaf'; const COUNTER_ABI = [ { type: 'function', name: 'increment', inputs: [], outputs: [], stateMutability: 'payable' }, { type: 'function', name: 'countPC', inputs: [], outputs: [{ name: '', type: 'uint256' }], stateMutability: 'view' }, ]; async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('Sepolia account (UOA):', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, progressHook: (p) => console.log('TX Progress:', p.title || p.id), }); // Derive the Solana CEA address so the user knows where to fund SOL. const uoa = PushChain.utils.account.toUniversal(wallet.address, { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, }); const solanaCEA = await PushChain.utils.account.deriveExecutorAccount(uoa, { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, skipNetworkCheck: true, }); console.log('CEA on Solana Devnet:', solanaCEA.address); await rl.question(':::prompt:::Fund these accounts, then press Enter:\\\\n β€’ UOA ' + wallet.address + ' on Sepolia β€” at least 0.005 ETH (gas to sign)\\\\n β€’ CEA ' + solanaCEA.address + ' on Solana Devnet β€” at least 0.02 SOL (CEA gas)\\\\nFaucets: Sepolia https://cloud.google.com/application/web3/faucet/ethereum/sepolia | Solana https://faucet.solana.com'); const data = PushChain.utils.helpers.encodeTxData({ abi: COUNTER_ABI, functionName: 'increment', }); try { const tx = await client.universal.sendTransaction({ from: { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET }, to: COUNTER_ADDRESS, data, }); console.log('TX Hash (Push Chain):', tx.hash); const receipt = await tx.wait(); console.log('External TX Hash (Solana):', receipt.externalTxHash); console.log('Explorer:', receipt.externalExplorerUrl); } catch (err) { console.error('Failed:', err && err.message ? err.message : err); } } await main().catch(console.error); `} ## Next Steps - Sequence multiple transactions with [Send Multichain Transactions](/docs/chain/build/send-multichain-transactions) - Trigger cross-chain execution from contracts with [Contract Initiated Multichain Execution](/docs/chain/build/contract-initiated-multichain-execution) - Track and poll transaction state with [Track Universal Transaction](/docs/chain/build/track-universal-transaction) - Sign arbitrary data with [Sign Universal Message](/docs/chain/build/sign-universal-message) --- # Send Multichain Transactions URL: https://push.org/docs/chain/build/send-multichain-transactions/ Send Multichain Transactions | Build | Push Chain Docs ## Overview Multichain transactions let you compose multiple universal transactions into a single ordered flow across different routes. This allows you to submit a **single user-signed transaction** to Push Chain that coordinates execution across Push Chain, external chains, or both. **Prerequisite:** Familiarize yourself with [Send Universal Transaction](/docs/chain/build/send-universal-transaction) before reading this page. ### Mental Model 1. **Prepare** each transaction step with `prepareTransaction` 2. **Execute** all steps together with `executeTransactions` ## Prepare Transaction **_`pushChainClient.universal.prepareTransaction({tx}): Promise`_** Prepares a transaction without executing it. Returns a **PreparedUniversalTx** object that you pass to **executeTransactions**. ```typescript const preparedTx = await pushChainClient.universal.prepareTransaction({ to: '0xContractAddress', value: BigInt(0), data: PushChain.utils.helpers.encodeTxData({ abi: MyABI, functionName: 'myFunction', args: [arg1, arg2], }), }); ``` :::info **PreparedUniversalTx** is an intermediate object that you pass to `executeTransactions`. Most apps do not need to manually inspect or modify its fields. ::: | **Arguments** | **Type** | **Description** | | ------------- | -------- | --------------- | | _`tx.to`_ | `string` \| `{ address: string; chain: CHAIN }` | Target address on Push Chain (plain string), or `{ address, chain }` for an external chain via CEA. | | `tx.from` | `{ chain: CHAIN }` | Optional. When set, originates from the CEA on that chain. | | `tx.value` | `BigInt` | Native value to send in the smallest unit of the execution context. | | `tx.data` | `string` \| `Array` | Encoded calldata for a single call `string` or batched multicall `Array`. Use [`encodeTxData`](/docs/chain/build/utility-functions#encode-transaction-data) to produce the correct bytes for EVM (ABI) or Solana (Anchor IDL) targets. | | `tx.funds` | `{ amount: bigint; token?: MoveableToken }` | Move tokens from origin chain atomically with the call. Use `PushChain.CONSTANTS.MOVEABLE.TOKEN..`. | > `prepareTransaction` accepts the same transaction arguments as [Send Universal Transaction](/docs/chain/build/send-universal-transaction). " className="alert alert--fn-args"> | **Property** | **Type** | **Description** | | ------------ | -------- | --------------- | | `route` | `'UOA_TO_PUSH'` \| `'UOA_TO_CEA'` \| `'CEA_TO_PUSH'` \| `'CEA_TO_CEA'` | Detected routing mode for this transaction. | | `estimatedGas` | `bigint` | Estimated gas units for execution. | | `nonce` | `bigint` | Nonce to use for submission. | | `deadline` | `bigint` | Signature expiry deadline. | | `payload` | `string` | Encoded payload ready for submission. | | `gatewayRequest` | `object` | Gateway request object (inbound or outbound). | {` // customPropHighlightRegexStart=prepareTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=prepare_transaction // Inspect PreparedUniversalTx before sending import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const COUNTER_ABI = [ { inputs: [], name: 'increment', outputs: [], stateMutability: 'nonpayable', type: 'function' }, ]; const COUNTER_ADDRESS = '0x5FbDB2315678afecb367f032d93F642f64180aa3'; async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('πŸ”‘ Sepolia account:', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); // Route 1: prepare a Push Chain transaction without sending it const prepared = await client.universal.prepareTransaction({ to: COUNTER_ADDRESS, value: BigInt(0), data: PushChain.utils.helpers.encodeTxData({ abi: COUNTER_ABI, functionName: 'increment', }), }); console.log('πŸ“‹ route:', prepared.route); console.log('β›½ estimatedGas:', prepared.estimatedGas.toString()); console.log('πŸ”’ nonce:', prepared.nonce.toString()); console.log('⏱️ deadline:', prepared.deadline.toString()); console.log('πŸ“¦ Returned PreparedUniversalTx:', JSON.stringify(prepared)); // Route 2: prepare a cross-chain transaction const preparedCrossChain = await client.universal.prepareTransaction({ to: { address: COUNTER_ADDRESS, chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA }, value: BigInt(0), data: PushChain.utils.helpers.encodeTxData({ abi: COUNTER_ABI, functionName: 'increment', }), }); console.log('πŸ“‹ cross-chain route:', preparedCrossChain.route); console.log('β›½ estimatedGas:', preparedCrossChain.estimatedGas.toString()); console.log('πŸ“¦ Returned PreparedUniversalTx:', JSON.stringify(preparedCrossChain)); } await main().catch(console.error); `} ## Execute Transactions **_`pushChainClient.universal.executeTransactions(txs: PreparedUniversalTx[], options?: { progressHook? }): Promise`_** Executes an ordered array of prepared transactions as a multichain flow. This is submitted and handled as a **single transaction**. You sign once, and the SDK coordinates execution across Push Chain and supported external chains automatically. Each prepared transaction becomes one ordered step in the multichain flow. ```typescript const step1 = await pushChainClient.universal.prepareTransaction({...}); // Push Chain const step2 = await pushChainClient.universal.prepareTransaction({...}); // BNB // Live progress for pre-flight, broadcast, and cascade tracking. const result = await pushChainClient.universal.executeTransactions([step1, step2], { progressHook: (event) => { console.log('[' + event.id + '] ' + event.level + ' - ' + event.title); }, }); ``` | **Arguments** | **Type** | **Description** | | ------------- | -------- | --------------- | | `txs` | `PreparedUniversalTx[]` | Ordered array of prepared transactions from `prepareTransaction`. | | `options.progressHook` | `(event: ProgressEvent) => void` | Optional per-call progress callback. Progress hook follows the same structure as [Send Universal Transaction - Progress Hook](/docs/chain/build/send-universal-transaction#progress-hook-type-and-response). | > In the API response, each executed step is reported as a hop. " className="alert alert--fn-args"> | Property | Type | Description | |----------|------|-------------| | `initialTxHash` | `string` | Hash of the user-signed Push Chain transaction that kicked off the cascade. | | `initialTxResponse` | `UniversalTxResponse` | Full response object for the initial Push Chain transaction. | | `hops` | `CascadeHopInfo[]` | Ordered list of all hops with routing and status information. | | `hopCount` | `number` | Total number of hops in the cascade. | | `wait(opts?)` | `Promise` | Alias for `waitForAll`. Waits for all hops to complete. | | `waitForAll(opts?)` | `Promise` | Waits for all hops to complete across all chains. | **CascadeHopInfo** fields: | Property | Type | Description | |----------|------|-------------| | `hopIndex` | `number` | Position in the cascade (0-indexed). | | `route` | `string` | Routing mode for this hop (`UOA_TO_PUSH`, `UOA_TO_CEA`, etc.). | | `executionChain` | `CHAIN` | Chain where this hop executes. | | `status` | `'pending'` \| `'submitted'` \| `'confirmed'` \| `'failed'` | Current hop status. | | `txHash` | `string` | Resolved transaction hash once available. | | `outboundDetails` | `object` | External chain tx details for outbound hops, including hash, explorer URL, recipient, and amount. | **CascadeCompletionResult** fields: | Property | Type | Description | |----------|------|-------------| | `success` | `boolean` | Whether all hops completed successfully. | | `hops` | `CascadeHopInfo[]` | Final state of all hops. | | `failedAt` | `number` | Index of first failed hop, if any. | **waitForAll** options: | Option | Type | Default | Description | |--------|------|---------|-------------| | `pollingIntervalMs` | `number` | `5000` | Polling interval in milliseconds. | | `timeout` | `number` | `600000` | Total timeout in milliseconds (10 min). | | `progressHook` | `(event: CascadeProgressEvent) => void` | - | Progress callback per hop. Reports `hopIndex`, `route`, `chain`, `status`, `txHash`, `elapsed`. | {` // customPropHighlightRegexStart=executeTransactions // customPropHighlightRegexEnd=\\); // customPropGTagEvent=execute_transactions_fund_and_call // 3-hop cascade from a Sepolia UOA: // Hop 0 (Route 1): send extra gas for UEA for Hop 2 // Hop 1 (Route 1): increment counter on Push Chain // Hop 2 (Route 2): increment counter on BNB Testnet via CEA import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const RPC_PUSH = 'https://evm.donut.rpc.push.org/'; const RPC_BNB = 'https://bsc-testnet-rpc.publicnode.com'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const COUNTER_ABI = [ { inputs: [], name: 'increment', outputs: [], stateMutability: 'nonpayable', type: 'function' }, { inputs: [], name: 'count', outputs: [{ name: '', type: 'uint256' }], stateMutability: 'view', type: 'function' }, { inputs: [], name: 'countPC', outputs: [{ name: '', type: 'uint256' }], stateMutability: 'view', type: 'function' }, ]; const COUNTER_PUSH = '0x5FbDB2315678afecb367f032d93F642f64180aa3'; // Push Chain Testnet (Donut) const COUNTER_BNB = '0x7f0936bb90e7dcf3edb47199c2005e7184e44cf8'; // BNB Testnet async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('πŸ”‘ Sepolia wallet (UOA):', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); console.log('UEA on Push Chain:', client.universal.account); await rl.question(':::prompt:::Fund the account, then press Enter:\\\\n β€’ UOA ' + wallet.address + ' on Sepolia (at least 0.005 ETH, to be used for gas for BNB).\\\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia'); // Read counters BEFORE const pushProvider = new ethers.JsonRpcProvider(RPC_PUSH); const bnbProvider = new ethers.JsonRpcProvider(RPC_BNB); const pushCounter = new ethers.Contract(COUNTER_PUSH, COUNTER_ABI, pushProvider); const bnbCounter = new ethers.Contract(COUNTER_BNB, COUNTER_ABI, bnbProvider); console.log('πŸ“Š Push Chain counter BEFORE:', (await pushCounter.countPC()).toString()); console.log('πŸ“Š BNB counter BEFORE:', (await bnbCounter.count()).toString()); // ── Hop 0 (UEA_TO_PUSH) ─ Bridge Gas from Sepolia EOA // value β†’ 5 PC at the destination; SDK fee-locks ETH on Sepolia // to mint this PC into the UEA, covering gas for BNB hop. const hop0 = await client.universal.prepareTransaction({ from: { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA }, to: client.universal.account, value: PushChain.utils.helpers.parseUnits('5', 18), }); console.log('βœ… hop0 prepared (Fund 5 PC as Gas) - route:', hop0.route); const calldata = PushChain.utils.helpers.encodeTxData({ abi: COUNTER_ABI, functionName: 'increment' }); // Hop 1 (Route 1): increment counter on Push Chain const hop1 = await client.universal.prepareTransaction({ to: COUNTER_PUSH, data: calldata, }); console.log('βœ… hop1 prepared - route:', hop1.route); // Hop 2 (Route 2): increment counter on BNB Testnet via CEA const hop2 = await client.universal.prepareTransaction({ to: { address: COUNTER_BNB, chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET }, data: calldata, }); console.log('βœ… hop2 prepared - route:', hop2.route); // executeTransactions's progressHook streams ProgressEvent across every // phase (pre-flight, broadcast, cascade tracking). Each event has id, // title, message, and level β€” combine them for a full status line. const cascade = await client.universal.executeTransactions([hop0, hop1, hop2], { progressHook: (event) => { const icon = { INFO: 'ℹ️', SUCCESS: 'βœ…', ERROR: '❌' }[event.level] || 'β€’'; console.log(icon + ' [' + event.id + '] ' + event.title + ' - ' + event.message); }, }); console.log('πŸš€ Cascade submitted - initialTxHash:', cascade.initialTxHash); console.log('πŸ“¦ hopCount:', cascade.hopCount); // executeTransactions's progressHook above already streams per-hop tracking // events (SEND-TX-309-* and SEND-TX-399-*), so cascade.wait() doesn't need // its own progressHook β€” just await for completion. const result = await cascade.wait(); console.log('🏁 All hops complete. Success:', result.success); if (result.success) { console.log('πŸ“Š Push Chain counter AFTER:', (await pushCounter.countPC()).toString()); console.log('πŸ“Š BNB counter AFTER:', (await bnbCounter.count()).toString()); } } await main().catch(console.error); `} ## More Examples ### Batch Contract Calls: Push Chain + BNB + Solana in One Signature Increment counters on Push Chain and BNB Testnet, then trigger a call on Solana Devnet. Three independent contract interactions across three chains, all composed into a single user signature. {` // customPropHighlightRegexStart=executeTransactions // customPropHighlightRegexEnd=\\); // customPropGTagEvent=execute_transactions_batch // Batch 3 contract calls across 3 chains - one user signature. import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const RPC_PUSH = 'https://evm.donut.rpc.push.org/'; const RPC_BNB = 'https://bsc-testnet-rpc.publicnode.com'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const COUNTER_ABI = [ { inputs: [], name: 'increment', outputs: [], stateMutability: 'nonpayable', type: 'function' }, { inputs: [], name: 'count', outputs: [{ name: '', type: 'uint256' }], stateMutability: 'view', type: 'function' }, { inputs: [], name: 'countPC', outputs: [{ name: '', type: 'uint256' }], stateMutability: 'view', type: 'function' }, ]; const COUNTER_PUSH = '0x5FbDB2315678afecb367f032d93F642f64180aa3'; // Push Chain Testnet (Donut) const COUNTER_BNB = '0x7f0936bb90e7dcf3edb47199c2005e7184e44cf8'; // BNB Testnet const SOL_TEST_PROGRAM = '8yNqjrMnFiFbVTVQcKij8tNWWTMdFkrDf9abCGgc2sgx'; // Solana Devnet, base58 // Anchor IDL for the Solana target β€” trimmed to just the receive_sol // instruction we call below. In a real app this comes from your Anchor // program's target/idl/*.json. const testCounterIdl = { address: SOL_TEST_PROGRAM, metadata: { name: 'test_counter', version: '0.1.0', spec: '0.1.0' }, instructions: [ { name: 'receive_sol', discriminator: [121, 244, 250, 3, 8, 229, 225, 1], accounts: [ { name: 'counter', writable: true, pda: { seeds: [{ kind: 'const', value: [99, 111, 117, 110, 116, 101, 114] }] } }, // 'counter' { name: 'recipient', writable: true, address: '89q1AUFb7YREHtjc1aYaPywovPq6tb3GYNPyDUJ3rshi' }, { name: 'cea_authority', writable: true }, // auto-populated with sender's CEA { name: 'system_program', address: '11111111111111111111111111111111' }, ], args: [{ name: 'amount', type: 'u64' }], }, ], }; async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('πŸ”‘ Sepolia wallet (UOA):', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); console.log('UEA on Push Chain:', client.universal.account); await rl.question(':::prompt:::Fund the account, then press Enter:\\\\n β€’ UOA ' + wallet.address + ' on Sepolia (at least 0.008 ETH for gas).\\\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia'); // Read counters BEFORE const pushProvider = new ethers.JsonRpcProvider(RPC_PUSH); const bnbProvider = new ethers.JsonRpcProvider(RPC_BNB); const pushCounter = new ethers.Contract(COUNTER_PUSH, COUNTER_ABI, pushProvider); const bnbCounter = new ethers.Contract(COUNTER_BNB, COUNTER_ABI, bnbProvider); console.log('πŸ“Š Push Chain counter BEFORE:', (await pushCounter.countPC()).toString()); console.log('πŸ“Š BNB counter BEFORE:', (await bnbCounter.count()).toString()); // ── Hop 0 (UEA_TO_PUSH) ─ Bridge Gas from Sepolia EOA // value β†’ 10 PC at the destination; SDK fee-locks ETH on Sepolia // to mint this PC into the UEA, covering gas for BNB hop, Solana hop. const hop0 = await client.universal.prepareTransaction({ from: { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA }, to: client.universal.account, value: PushChain.utils.helpers.parseUnits('10', 18), }); console.log('βœ… hop0 prepared (Fund 10 PC as Gas) - route:', hop0.route); const calldata = PushChain.utils.helpers.encodeTxData({ abi: COUNTER_ABI, functionName: 'increment' }); // Hop 1 (Route 1): increment counter on Push Chain const hop1 = await client.universal.prepareTransaction({ to: COUNTER_PUSH, data: calldata, }); console.log('βœ… hop1 prepared - route:', hop1.route); // Hop 2 (Route 2): increment counter on BNB Testnet via CEA const hop2 = await client.universal.prepareTransaction({ to: { address: COUNTER_BNB, chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET }, data: calldata, }); console.log('βœ… hop2 prepared - route:', hop2.route); // Hop 3 (Route 2): call test_counter on Solana Devnet via CEA // Same shape as EVM (to, value, data). Accounts, PDAs and CEA come from the IDL. const solCalldata = PushChain.utils.helpers.encodeTxData({ idl: testCounterIdl, functionName: 'receive_sol', args: [BigInt(0)], }); const hop3 = await client.universal.prepareTransaction({ to: { address: SOL_TEST_PROGRAM, chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, }, data: solCalldata, }); console.log('βœ… hop3 prepared - route:', hop3.route); // executeTransactions's progressHook streams ProgressEvent across every // phase (pre-flight, broadcast, cascade tracking). Each event has id, // title, message, and level β€” combine them for a full status line. const cascade = await client.universal.executeTransactions([hop0, hop1, hop2, hop3], { progressHook: (event) => { const icon = { INFO: 'ℹ️', SUCCESS: 'βœ…', ERROR: '❌' }[event.level] || 'β€’'; console.log(icon + ' [' + event.id + '] ' + event.title + ' - ' + event.message); }, }); console.log('πŸš€ Cascade submitted - initialTxHash:', cascade.initialTxHash); console.log('πŸ“¦ hopCount:', cascade.hopCount); // executeTransactions's progressHook above already streams per-hop tracking // events (SEND-TX-309-* and SEND-TX-399-*), so cascade.wait() doesn't need // its own progressHook β€” just await for completion. const result = await cascade.wait(); console.log('🏁 All hops complete. Success:', result.success); if (result.success) { console.log('πŸ“Š Push Chain counter AFTER:', (await pushCounter.countPC()).toString()); console.log('πŸ“Š BNB counter AFTER:', (await bnbCounter.count()).toString()); // Solana state isn't read via a public RPC contract call here β€” // surface the on-chain Solana hop's tx hash + explorer URL from // result.hops[].outboundDetails so devs can inspect it manually. const solanaHop = result.hops.find((h) => h.executionChain === PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET); if (solanaHop && solanaHop.outboundDetails) { console.log('🌞 Solana hop tx:', solanaHop.outboundDetails.externalTxHash); console.log('πŸ”— Solana explorer:', solanaHop.outboundDetails.explorerUrl); } } } await main().catch(console.error); `} ### Cross-Chain AMM Swap: ETH β†’ pSOL via Push Chain AMM Swap ETH on Sepolia for pSOL on Solana Devnet in a single user signature. - Hop 0 bridges 0.065 ETH from the Sepolia EOA into 0.005 gas and 0.001 pETH on the UEA on Push Chain, with `value: 25 PC` seeding the UEA's gas via SDK fee-locking. - Hops 1 and 2 approve the SwapRouter for pETH and WPC. - Hops 3 and 4 swap pETH β†’ WPC β†’ pSOL via two `exactInputSingle` calls on the Push Chain AMM (the AMM has no direct pETH/pSOL pool, so WPC is the intermediate). - Hop 5 bridges the pSOL out to the user's Solana Devnet CEA. {` // customPropHighlightRegexStart=executeTransactions // customPropHighlightRegexEnd=\\); // customPropGTagEvent=execute_transactions // 6-hop cross-chain cascade from a fresh Sepolia UOA: // Hop 0 (UOA_TO_PUSH) - Deposit 0.001 ETH from UOA β†’ pETH on UEA (Route 1 sendTxWithFunds); // value=25 PC triggers SDK fee-lock to seat the UEA's PC gas budget. // Hop 1 (UOA_TO_PUSH) - Approve SwapRouter to spend pETH // Hop 2 (UOA_TO_PUSH) - Approve SwapRouter to spend WPC // Hop 3 (UOA_TO_PUSH) - Swap pETH β†’ WPC on Push Chain AMM (Uniswap V3 fork) // Hop 4 (UOA_TO_PUSH) - Swap WPC β†’ pSOL on Push Chain AMM // Hop 5 (UOA_TO_CEA) - Bridge pSOL from UEA β†’ Solana Devnet CEA import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import * as readline from 'node:readline/promises'; const RPC_SEPOLIA = 'https://ethereum-sepolia-rpc.publicnode.com'; const RPC_PUSH = 'https://evm.donut.rpc.push.org/'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // Push Chain AMM (Uniswap V3 fork) on Donut Testnet const SWAP_ROUTER_ADDRESS = '0x5D548bB9E305AAe0d6dc6e6fdc3ab419f6aC0037'; const QUOTER_V2_ADDRESS = '0x83316275f7C2F79BC4E26f089333e88E89093037'; const WPC_ADDRESS = '0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9'; // wrapped PC, AMM intermediate const pETH_ADDRESS = '0x2971824Db68229D087931155C2b8bB820B275809'; const pSOL_ADDRESS = '0x5D525Df2bD99a6e7ec58b76aF2fd95F39874EBed'; const POOL_FEE = 500; // 0.05% fee tier of both pETH/WPC and pSOL/WPC pools on Donut const AMOUNT_IN = PushChain.utils.helpers.parseUnits('0.001', 18); const MAX_UINT256 = ethers.MaxUint256; const ERC20_APPROVE_ABI = [ { inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }], name: 'approve', outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', type: 'function' }, ]; const SWAP_ROUTER_ABI = [ { inputs: [{ components: [ { name: 'tokenIn', type: 'address' }, { name: 'tokenOut', type: 'address' }, { name: 'fee', type: 'uint24' }, { name: 'recipient', type: 'address' }, { name: 'amountIn', type: 'uint256' }, { name: 'amountOutMinimum', type: 'uint256' }, { name: 'sqrtPriceLimitX96', type: 'uint160' }, ], name: 'params', type: 'tuple' }], name: 'exactInputSingle', outputs: [{ name: 'amountOut', type: 'uint256' }], stateMutability: 'payable', type: 'function' }, ]; const QUOTER_ABI = [ { inputs: [{ components: [ { name: 'tokenIn', type: 'address' }, { name: 'tokenOut', type: 'address' }, { name: 'amountIn', type: 'uint256' }, { name: 'fee', type: 'uint24' }, { name: 'sqrtPriceLimitX96', type: 'uint160' }, ], name: 'params', type: 'tuple' }], name: 'quoteExactInputSingle', outputs: [ { name: 'amountOut', type: 'uint256' }, { name: 'sqrtPriceX96After', type: 'uint160' }, { name: 'initializedTicksCrossed', type: 'uint32' }, { name: 'gasEstimate', type: 'uint256' }, ], stateMutability: 'nonpayable', type: 'function' }, ]; async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('πŸ”‘ Sepolia wallet (UOA):', wallet.address); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); console.log('🏠 UEA on Push Chain:', client.universal.account); // Derive the Solana CEA destination for Hop 5 const uoa = PushChain.utils.account.toUniversal(wallet.address, { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, }); const solanaCEA = await PushChain.utils.account.deriveExecutorAccount(uoa, { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, skipNetworkCheck: true, }); console.log('πŸ“ Solana CEA (Hop 5 destination):', solanaCEA.address); await rl.question(':::prompt:::Fund both accounts, then press Enter:\\\\n β€’ UOA ' + wallet.address + ' on Sepolia (at least 0.005 ETH for signing + the 0.001 ETH that Hop 0 deposits as pETH).\\\\n β€’ UEA ' + client.universal.account + ' on Push Chain (at least 25 PC β€” covers UEA gas across hops 1-5 plus the SVM outbound gas-token swap).\\\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia\\\\nPush Chain faucet: https://faucet.push.org/'); // Quote both swap legs so we can size Hop 4 amountIn and Hop 5 bridge amount. // QuoterV2 is non-view, so call via staticCall to read return values. const pushProvider = new ethers.JsonRpcProvider(RPC_PUSH); const quoter = new ethers.Contract(QUOTER_V2_ADDRESS, QUOTER_ABI, pushProvider); const wpcQuote = await quoter.quoteExactInputSingle.staticCall({ tokenIn: pETH_ADDRESS, tokenOut: WPC_ADDRESS, amountIn: AMOUNT_IN, fee: POOL_FEE, sqrtPriceLimitX96: BigInt(0), }); const wpcAmount = (wpcQuote.amountOut * BigInt(99)) / BigInt(100); // 1% slippage buffer const pSolQuote = await quoter.quoteExactInputSingle.staticCall({ tokenIn: WPC_ADDRESS, tokenOut: pSOL_ADDRESS, amountIn: wpcAmount, fee: POOL_FEE, sqrtPriceLimitX96: BigInt(0), }); const pSolAmount = (pSolQuote.amountOut * BigInt(99)) / BigInt(100); // 1% slippage buffer console.log('πŸ’± Estimated WPC after Hop 3:', wpcAmount.toString()); console.log('πŸ’± Estimated pSOL after Hop 4:', pSolAmount.toString()); // ── Hop 0 (UOA_TO_PUSH) ─ Deposit ETH from UOA β†’ pETH on UEA ───────── // No 'from' field means Route 1 (UOA initiates). funds.amount tells the // SDK to attach 0.001 ETH as msg.value on the source-chain gateway tx, // which mints 0.001 pETH on the UEA. value=25 PC bumps the UEA's gas // budget β€” the SDK fee-locks enough ETH from the same UOA tx to seat // 25 PC on the UEA for hops 1-5 (2 approves, 2 swaps, 1 outbound). const hop0 = await client.universal.prepareTransaction({ to: client.universal.account, value: PushChain.utils.helpers.parseUnits('25', 18), funds: { amount: AMOUNT_IN, token: PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_SEPOLIA.ETH, }, }); console.log('βœ… hop0 prepared (deposit ETHβ†’pETH + 25 PC) - route:', hop0.route); // ── Hop 1 (UOA_TO_PUSH) ─ Approve SwapRouter for pETH ──────────────── const hop1 = await client.universal.prepareTransaction({ to: pETH_ADDRESS, data: PushChain.utils.helpers.encodeTxData({ abi: ERC20_APPROVE_ABI, functionName: 'approve', args: [SWAP_ROUTER_ADDRESS, MAX_UINT256], }), }); console.log('βœ… hop1 prepared (approve pETH) - route:', hop1.route); // ── Hop 2 (UOA_TO_PUSH) ─ Approve SwapRouter for WPC ───────────────── const hop2 = await client.universal.prepareTransaction({ to: WPC_ADDRESS, data: PushChain.utils.helpers.encodeTxData({ abi: ERC20_APPROVE_ABI, functionName: 'approve', args: [SWAP_ROUTER_ADDRESS, MAX_UINT256], }), }); console.log('βœ… hop2 prepared (approve WPC) - route:', hop2.route); // ── Hop 3 (UOA_TO_PUSH) ─ Swap pETH β†’ WPC on Push Chain AMM ────────── const hop3 = await client.universal.prepareTransaction({ to: SWAP_ROUTER_ADDRESS, data: PushChain.utils.helpers.encodeTxData({ abi: SWAP_ROUTER_ABI, functionName: 'exactInputSingle', args: [{ tokenIn: pETH_ADDRESS, tokenOut: WPC_ADDRESS, fee: POOL_FEE, recipient: client.universal.account, amountIn: AMOUNT_IN, amountOutMinimum: BigInt(0), sqrtPriceLimitX96: BigInt(0), }], }), }); console.log('βœ… hop3 prepared (pETH β†’ WPC) - route:', hop3.route); // ── Hop 4 (UOA_TO_PUSH) ─ Swap WPC β†’ pSOL on Push Chain AMM ────────── const hop4 = await client.universal.prepareTransaction({ to: SWAP_ROUTER_ADDRESS, data: PushChain.utils.helpers.encodeTxData({ abi: SWAP_ROUTER_ABI, functionName: 'exactInputSingle', args: [{ tokenIn: WPC_ADDRESS, tokenOut: pSOL_ADDRESS, fee: POOL_FEE, recipient: client.universal.account, amountIn: wpcAmount, amountOutMinimum: BigInt(0), sqrtPriceLimitX96: BigInt(0), }], }), }); console.log('βœ… hop4 prepared (WPC β†’ pSOL) - route:', hop4.route); // ── Hop 5 (UOA_TO_CEA) ─ Bridge pSOL out to Solana Devnet CEA ──────── const hop5 = await client.universal.prepareTransaction({ to: { address: solanaCEA.address, chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET }, funds: { amount: pSolAmount, token: PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.pSol, }, }); console.log('βœ… hop5 prepared (pSOL β†’ Solana CEA) - route:', hop5.route); // ── Execute all 6 hops as one user-signed transaction ──────────────── // executeTransactions's progressHook streams ProgressEvent across every // phase (pre-flight, broadcast, cascade tracking). Each event has id, // title, message, and level β€” combine them for a full status line. const cascade = await client.universal.executeTransactions([hop0, hop1, hop2, hop3, hop4, hop5], { progressHook: (event) => { const icon = { INFO: 'ℹ️', SUCCESS: 'βœ…', ERROR: '❌' }[event.level] || 'β€’'; console.log(icon + ' [' + event.id + '] ' + event.title + ' - ' + event.message); }, }); console.log('πŸš€ Cascade submitted - initialTxHash:', cascade.initialTxHash); console.log('πŸ“¦ hopCount:', cascade.hopCount); // executeTransactions's progressHook above already streams per-hop tracking // events (SEND-TX-309-* and SEND-TX-399-*), so cascade.wait() doesn't need // its own progressHook β€” just await for completion. const result = await cascade.wait(); console.log('🏁 All hops complete. Success:', result.success); // Solana outbound details from the cascade hops registry. const solanaHop = result.hops.find((h) => h.executionChain === PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET); if (solanaHop && solanaHop.outboundDetails) { console.log('🌞 Solana hop tx:', solanaHop.outboundDetails.externalTxHash); console.log('πŸ”— Solana explorer:', solanaHop.outboundDetails.explorerUrl); } } await main().catch(console.error); `} ## Key Considerations - **Single signature**: `executeTransactions` submits one transaction to Push Chain. You sign once, and the SDK coordinates the full multichain execution automatically. - **No atomicity guarantee**: If a downstream hop fails, earlier hops are already on-chain. Design contracts to handle partial execution. - **Gas per hop**: Each hop has its own estimated gas. Ensure gas is properly funded. - **Tracking**: Progress channel work side-by-side and can be wired too keep track of the execution flow: - `executeTransactions(txs, { progressHook })` streams every `ProgressEvent` (rich marker ids like `SEND-TX-101`, severity, structured payload) across pre-flight, broadcast, and cascade phases. Best for UI step indicators and granular telemetry. ## Next Steps - [Track Universal Transaction](/docs/chain/build/track-universal-transaction) to monitor individual transaction confirmation status - [Contract Initiated Multichain Execution](/docs/chain/build/contract-initiated-multichain-execution) to trigger multichain flows from on-chain contracts - [Sign Universal Message](/docs/chain/build/sign-universal-message) to sign typed messages across chains with a universal signer - [Utility Functions](/docs/chain/build/utility-functions) for `encodeTxData`, `parseUnits`, and other helpers used in this page --- # Contract-Initiated Multichain Execution URL: https://push.org/docs/chain/build/contract-initiated-multichain-execution/ Contract-Initiated Multichain Execution | Build | Push Chain Docs ## Overview Contract-Initiated Multichain Execution lets a **Push Chain smart contract trigger execution on an external chain**, or **receive a call originating from an external chain**, without any live user interaction at call time. This enables Push contracts to autonomously interact with external protocols, call contracts on Ethereum or BNB Chain, and optionally receive inbound payloads back on Push Chain, all driven by on-chain contract code. > All directions run through the same primitives (UEAs, CEAs, the gateway pair) but the wire format and the on-chain identity differ. For real examples, see the [Contract-Initiated Examples](/docs/chain/build/contract-initiated-examples/). ## How This Differs from Universal Transactions Universal transactions are initiated by users. Contract-initiated multichain execution is initiated by Push Chain smart contracts. Both use the same cross-chain infrastructure, but differ in execution model and integration surface. | Dimension | Universal Transaction | Contract-Initiated Multichain Execution | |-----------|----------------------|------------------------------| | **Who initiates** | A user wallet (UOA). | A Push Chain smart contract. | | **When it happens** | At user signature time. | During contract execution, triggered by any on-chain call. | | **Authorization** | User signature or proof. | Contract logic, no live user required. | | **Return handling** | SDK receives `TxResponse`. | Inbound `executeUniversalTx()` call on the originating contract. | | **Identity on external chain** | User's CEA. | Contract's CEA (bound to the contract address). | | **SDK involvement** | Required on client side. | Fully on-chain, no SDK required. | The key distinction is that contract-initiated multichain execution is **programmable and autonomous**. Any call into your Push contract can trigger execution on an external chain. Liquidation triggers, scheduled jobs, governance outcomes, and user actions that fan out across chains all fit this model. ## Key Concepts ### Contract CEA Every Push Chain smart contract has a deterministically derived **Chain Executor Account (CEA)** on each supported external chain. Same idea as user-initiated CEAs, but bound to the contract address instead of a user wallet. The contract CEA: - Is derived from the Push contract's address, not from any user. - Is lazily deployed on first use by the TSS network. - Acts as `msg.sender` on the external chain when the contract initiates execution there. - Gas is taken in **$PC** on Push Chain and converted to the native token of the external chain. - Is scoped to the contract, not to any user. ### UniversalGatewayPC (UGPC) UGPC is the on-chain gateway contract on Push Chain through which all outbound cross-chain calls are routed. Your contract calls `UGPC.sendUniversalTxOutbound()`, which relays the payload, optionally burns or locks PRC20 tokens, and emits the event the TSS network listens for. UGPC is a predeploy at `0x00000000000000000000000000000000000000C1` on every Push Chain network. ### Universal Executor Module The **UNIVERSAL_EXECUTOR_MODULE** (`0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7`) is the privileged address authorized to deliver round-trip back-leg payloads to Push-native contracts. When your contract's CEA on an external chain triggers a callback to Push, the module calls `executeUniversalTx()` on your contract. Regular inbound (an external contract calling the Universal Gateway) lands on your Push target via the caller's UEA, not via this module. :::warning Always validate inbound in your contract's `executeUniversalTx` handler If your contract implements `executeUniversalTx` (the round-trip back-leg handler), validate `msg.sender == UNIVERSAL_EXECUTOR_MODULE` and replay-protect on `txId`. Without these guards, anyone can call your handler with fabricated data. Regular inbound targets (called via the caller's UEA) do not need either guard. ::: ## Three Directions Contract-initiated execution flows in three directions. Pick the one that matches your use case. ### Inbound: External Chain β†’ Push Chain An external-chain contract calls the per-chain Universal Gateway. The TSS network relays the call to Push Chain, where the dispatching contract's UEA executes the payload on the target Push contract. ```mermaid flowchart LR EOA["External-chain EOA"] EXT["External Contract(Sepolia / BNB / etc.)"] UG["UniversalGatewayExternal Chain"] UEA["Caller's UEAPush Chain"] PC["Target ContractPush Chain"] EOA --> EXT --> UG -->|"TSS relay"| UEA --> PC style EXT fill:#1e3a8a,stroke:#60a5fa,color:#fff style UG fill:#1e3a8a,stroke:#60a5fa,color:#fff style UEA fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff style PC fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff ``` **Use this when** an external-chain contract needs to mutate state on Push (e.g. an Ethereum-side staking contract triggers a balance update on Push). For instance a cross-chain governance proposal lands on Push from a vote on Sepolia, or an external chain bridges a payload with funds that should land in a Push vault. ### Outbound: Push Chain β†’ External Chain A Push contract dispatches a call that runs on an external chain. The call executes on the destination as the Push contract's CEA on that chain. ```mermaid flowchart LR PC["Push ContractPush Chain"] UGPC["UGPC0x...C1"] CEA["Contract CEAExternal Chain"] EXT["Target ContractExternal Chain"] PC -->|"sendUniversalTxOutbound()"| UGPC -->|"TSS relay"| CEA --> EXT style PC fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style UGPC fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style CEA fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff style EXT fill:#1e3a8a,stroke:#60a5fa,color:#fff ``` **Use this when** you require liquidations on external DEXes, scheduled rebalances on Aave, fanning out a single Push tx to multiple destination chains, paying out winners on external networks. ### Round-Trip: Push Chain β†’ External Chain β†’ Push Chain A Push contract dispatches outbound; the destination CEA's multicall nests a gateway call that fires an inbound back to the original Push contract. One user signature, an external action, and an automatic back-leg into the contract's `executeUniversalTx` handler. ```mermaid flowchart LR EOA(["User EOA"]) PC["Push Contract(kickOff + executeUniversalTx)"] UGPC["UGPC"] CEA["Contract CEAExternal Chain"] EXT["External Target"] EOA --> PC -->|"sendUniversalTxOutbound()"| UGPC -->|"TSS"| CEA --> EXT CEA -.->|"back-leg via TSS"| PC style PC fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style UGPC fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style CEA fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff style EXT fill:#1e3a8a,stroke:#60a5fa,color:#fff ``` **Use this when** you need cross-chain state machines (request-and-fulfill), oracle-style flows where Push waits on external execution, or multi-chain cascades fired by a single user signature. ## Inbound Wire Format An inbound dispatch is a single call into the per-chain `IUniversalGateway` from an external chain. The TSS network relays the call to Push, where the dispatching contract's UEA forwards the payload to your target. Below is the full surface. ### IUniversalGateway **_`sendUniversalTx(UniversalTxRequest): void`_** is external payable **Deployed Address**: Per supported chain (Sepolia, BNB Testnet, Arbitrum Sepolia, Base Sepolia, Solana Devnet). See [Smart Contract Address Book - External Chain Gateway Contracts](/docs/chain/setup/smart-contract-address-book/#external-chain-gateway-contracts). ```solidity struct UniversalPayload { address to; // Real target on Push (or address(0) if data is multicall-wrapped) uint256 value; // Native value to forward to the target bytes data; // Raw calldata, or 0x2cc2842d-prefixed multicall encoding uint256 gasLimit; uint256 maxFeePerGas; uint256 maxPriorityFeePerGas; uint256 nonce; // Caller's UEA nonce on Push uint256 deadline; uint8 vType; // 0 = universalTxVerification (inbound to UEA) } struct UniversalTxRequest { address recipient; // Always address(0); real target is inside payload address token; // address(0) for native; ERC-20 address for bridged token uint256 amount; // Amount to bridge with this inbound bytes payload; // ABI-encoded UniversalPayload address revertRecipient; // Address to receive bridged funds on revert bytes signatureData; // Empty for contract-initiated inbound } interface IUniversalGateway { function sendUniversalTx(UniversalTxRequest calldata req) external payable; } ``` | Arguments | Type | Description | | --------- | ---- | ----------- | | _`req.recipient`_ | `address` | Always `address(0)`. The real Push-side target lives inside `payload`. | | _`req.token`_ | `address` | `address(0)` for native asset; ERC-20 address when bridging a token alongside the call. | | _`req.amount`_ | `uint256` | Amount to bridge with this inbound. Set to `0` for payload-only inbounds. | | _`req.payload`_ | `bytes` | ABI-encoded `UniversalPayload` describing the target call. | | _`req.revertRecipient`_ | `address` | Address that receives bridged funds if the Push-side execution reverts. | | _`req.signatureData`_ | `bytes` | Empty for contract-initiated inbound. | ### Single-call vs multicall payload The caller's UEA inspects the first 4 bytes of `payload.data`. - If they match the multicall selector **_0x2cc2842d_** (= `bytes4(keccak256("UEA_MULTICALL"))`) β†’ the UEA decodes the rest as `Multicall[]` and ignores `payload.to`. - **Otherwise** β†’ it treats `payload.data` as raw calldata and runs it once against `payload.to`. ```solidity // Single call: target is `to`, calldata is `data`. UniversalPayload memory payload = UniversalPayload({ to: pushTarget, value: 0, data: abi.encodeWithSignature("increment()"), gasLimit: 1e7, maxFeePerGas: 1e10, maxPriorityFeePerGas: 0, nonce: ueaNonce, deadline: 9999999999, vType: 0 }); // Multicall: prefix the sentinel, then encode an array of (to, value, data). // In multicall mode, payload.to is ignored. Convention is to leave it as address(0). Multicall[] memory calls = new Multicall[](1); calls[0] = Multicall({ to: pushTarget, value: 0, data: abi.encodeWithSignature("increment()") }); bytes memory multicallData = abi.encodePacked( bytes4(keccak256("UEA_MULTICALL")), // 0x2cc2842d abi.encode(calls) ); ``` ### Target identity and replay protection When the inbound lands on Push, the caller's UEA executes the payload. From your target's perspective, `msg.sender` is that UEA, a smart account with its own internal nonce. **The UEA increments its nonce before forwarding, so your target does NOT need replay protection** and does NOT need to validate `msg.sender` against any module. A plain Solidity function works as-is. To recover the origin chain and external wallet from `msg.sender`: ```solidity (string memory chainNamespace, bytes memory externalAddress) = IUEAFactory(UEA_FACTORY).getOriginForUEA(msg.sender); ``` ### Minimal dispatch ```solidity address constant GATEWAY = 0x...; // Per-chain UG (Sepolia, BNB Testnet, etc.) bytes4 constant UEA_MULTICALL_SELECTOR = 0x2cc2842d; function triggerOnPush( address pushTarget, bytes calldata pushCalldata, uint256 nonce ) external payable { // Wrap (target, calldata) into the UEA's multicall format. Multicall[] memory calls = new Multicall[](1); calls[0] = Multicall({ to: pushTarget, value: 0, data: pushCalldata }); bytes memory multicallData = abi.encodePacked(UEA_MULTICALL_SELECTOR, abi.encode(calls)); // Wrap multicall data in the UniversalPayload (vType = 0, inbound to UEA). bytes memory payload = abi.encode( address(0), uint256(0), multicallData, uint256(1e7), uint256(1e10), uint256(0), nonce, uint256(9999999999), uint8(0) ); // Dispatch through the per-chain Universal Gateway. IUniversalGateway(GATEWAY).sendUniversalTx{value: msg.value}( UniversalTxRequest({ recipient: address(0), token: address(0), amount: 0, payload: payload, revertRecipient: address(this), signatureData: "" }) ); } ``` A complete runnable version (Sepolia dispatcher plus Push target) is in the [Inbound to Push](/docs/chain/build/contract-initiated-examples/inbound-to-push-chain) example. ## Outbound Wire Format The outbound dispatch is a single call into UGPC. Below is the full surface. ### IUniversalGatewayPC **_`sendUniversalTxOutbound(UniversalOutboundTxRequest): void`_** is external payable **Deployed Address**: [**_`0x00000000000000000000000000000000000000C1`_**](/docs/chain/setup/smart-contract-address-book/#push-chain-core-functionalities) ```solidity struct UniversalOutboundTxRequest { bytes recipient; // CEA or target address on the external chain (bytes-encoded) address token; // PRC20 token on Push Chain to bridge (address(0) for none) uint256 amount; // Amount of PRC20 to bridge uint256 gasLimit; // Gas limit for external-chain execution (see Operational Knobs) uint256 gasPrice; // Gas price override (0 = per-chain default from UniversalCore; new in SDK v6) uint256 maxPCForGas; // Max native PC the AMM may consume for the gas swap (0 = no cap; new in SDK v6) bytes payload; // Calldata for the CEA to execute on the external chain address revertRecipient; // Address to receive funds if the tx reverts on the external chain } interface IUniversalGatewayPC { function sendUniversalTxOutbound(UniversalOutboundTxRequest calldata req) external payable; } ``` | Arguments | Type | Description | | --------- | ---- | ----------- | | _`req.recipient`_ | `bytes` | CEA or target address on the external chain, bytes-encoded. | | _`req.token`_ | `address` | PRC20 token address on Push Chain to bridge. Use `address(0)` if no token is being bridged. | | _`req.amount`_ | `uint256` | Amount of PRC20 to bridge. Set to `0` if not bridging. | | _`req.gasLimit`_ | `uint256` | Gas limit for external-chain execution. Default to `2_000_000` (see [Operational Knobs](#operational-knobs)); UGPC charges only for actual gas used and refunds the surplus. | | _`req.gasPrice`_ | `uint256` | Gas price override for the destination chain. Set to `0` to use the per-chain default quoted by UniversalCore (recommended). | | _`req.maxPCForGas`_ | `uint256` | Maximum native `$PC` the on-chain AMM may consume when swapping for destination gas. Set to `0` for no cap (recommended on testnet). | | _`req.payload`_ | `bytes` | ABI-encoded calldata for the CEA to execute on the external chain. | | _`req.revertRecipient`_ | `address` | Address to receive bridged funds if the external transaction reverts. | ### Single-call vs multicall payload The caller's UEA inspects the first 4 bytes of `payload.data`. - If they match the multicall selector **_0x2cc2842d_** (= `bytes4(keccak256("UEA_MULTICALL"))`) β†’ the UEA decodes the rest as `Multicall[]` and ignores `payload.to`. - **Otherwise** β†’ it treats `payload.data` as raw calldata and runs it once against `payload.to`. ```solidity // Single call (most common): payload is the ABI-encoded calldata for the target. bytes memory payload = abi.encodeCall(ICounter.increment, ()); // Multicall: prefix the sentinel, then encode an array of (to, value, data). bytes memory multicallData = abi.encodePacked( bytes4(keccak256("UEA_MULTICALL")), abi.encode(callsArray) ); ``` The multicall path is what enables [round-trip patterns](#round-trip-wire-format) further down. ### Minimal dispatch ```solidity address constant UGPC = 0x00000000000000000000000000000000000000C1; function dispatchToBNB(address bnbCounter, uint256 protocolFeePc) external payable { bytes memory payload = abi.encodeWithSignature("increment()"); IUniversalGatewayPC(UGPC).sendUniversalTxOutbound{value: protocolFeePc}( UniversalOutboundTxRequest({ recipient: abi.encodePacked(bnbCounter), token: address(0), amount: 0, gasLimit: 2_000_000, gasPrice: 0, // per-chain default from UniversalCore maxPCForGas: 0, // no cap on PC for the gas swap payload: payload, revertRecipient: address(this) }) ); } ``` A complete runnable version is in the [Plain Outbound](/docs/chain/build/contract-initiated-examples/outbound-from-push-chain) example. ## Round-Trip Wire Format A round-trip is a single outbound whose destination-chain payload **automatically fires** an inbound back to the originating Push contract. It reuses the outbound surface - [UniversalGatewayPC](#iuniversalgatewaypc), plus a back-leg handler on the dispatching contract - [executeUniversalTx()](#executeuniversaltx). Below is the full surface. > For a visual breakdown of how the four payload layers nest, see the [layered diagram in the Round-Trip example](/docs/chain/build/contract-initiated-examples/round-trip-auto-back-leg#the-wire-format). ### executeUniversalTx **_`executeUniversalTx(string, bytes, bytes, uint256, address, bytes32): void`_** is external payable **Caller**: **_`UNIVERSAL_EXECUTOR_MODULE`_** > Deployed Address [**_`0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7`_**](/docs/chain/setup/smart-contract-address-book/#push-chain-core-functionalities) ```solidity /** * @notice Back-leg handler. TSS invokes this on the originating Push contract * when the destination CEA's outer multicall completes. * @dev Only callable by UNIVERSAL_EXECUTOR_MODULE. Must validate msg.sender * and guard against replay via txId. */ function executeUniversalTx( string calldata sourceChainNamespace, // CAIP-2 namespace, e.g. "eip155:97" bytes calldata ceaAddress, // CEA address on source chain, bytes-encoded bytes calldata payload, // ABI-encoded action data uint256 amount, // PRC20 amount bridged in address prc20, // PRC20 token address on Push bytes32 txId // Unique cross-chain tx id; use for replay protection ) external payable; ``` | Arguments | Type | Description | | --------- | ---- | ----------- | | _`sourceChainNamespace`_ | `string` | CAIP-2 chain identifier of the originating chain, e.g. `"eip155:97"`. | | _`ceaAddress`_ | `bytes` | CEA address on the source chain, bytes-encoded. | | _`payload`_ | `bytes` | ABI-encoded action data. Decode inside your handler to determine the action. | | _`amount`_ | `uint256` | Amount of PRC20 tokens bridged with this back-leg. | | _`prc20`_ | `address` | PRC20 token address on Push corresponding to the bridged asset. | | _`txId`_ | `bytes32` | Unique cross-chain transaction identifier. Use this to prevent replay. | ### Required guards The back-leg handler is privileged. Validate the caller and replay-protect on `txId`. ```solidity mapping(bytes32 => bool) public executedTxIds; address public constant UNIVERSAL_EXECUTOR_MODULE = 0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7; modifier onlyUniversalExecutor() { if (msg.sender != UNIVERSAL_EXECUTOR_MODULE) revert NotExecutorModule(); _; } function executeUniversalTx( string calldata sourceChainNamespace, bytes calldata ceaAddress, bytes calldata payload, uint256 amount, address prc20, bytes32 txId ) external payable onlyUniversalExecutor { if (executedTxIds[txId]) revert TxAlreadyExecuted(); executedTxIds[txId] = true; // Decode payload and apply your application logic. (uint8 action, address user) = abi.decode(payload, (uint8, address)); if (action == 0) { stakedBalance[user][prc20] += amount; emit Staked(user, prc20, amount, txId); } } ``` ### Strict dispatch signature The dispatch signature in your push-side contract needs to match exactly the one shown below for the round trip to complete. | Signature | Used by | |---|---| | `executeUniversalTx(string, bytes, bytes, uint256, address, bytes32)` | Push-native contracts. **This is the path TSS dispatches to.** | ### Minimal round-trip dispatch A round-trip dispatch is just a regular UGPC outbound. The only extra thing you do is shape the outbound's **payload** so that, when the destination CEA executes it, **one step of the outer multicall is a self-call to `sendUniversalTxToUEA` on the CEA**. That self-call is what TSS reads as "fire the inbound back to the originating Push contract." Without it, only the outbound leg runs. > **Note**: You don't deploy or fund the destination CEA. TSS deploys it lazily on first use and forwards the converted gas value to it as `msg.value` when executing the destination tx (see [Operational Knobs](#operational-knobs)). ```solidity // Build the inner UniversalPayload (vType = 1, inbound to Push UEA). bytes memory innerMulticallData = abi.encodePacked( UEA_MULTICALL_SELECTOR, // 0x2cc2842d abi.encode(/* Multicall[] - what runs on the Push UEA after the back-leg */) ); bytes memory inboundUniversalPayload = abi.encode( address(0), uint256(0), innerMulticallData, uint256(1e7), uint256(1e10), uint256(0), ueaNonce + 1, uint256(9999999999), uint8(1) ); // The back-leg trigger: a CEA self-call wrapping the inner payload. bytes memory ceaSelfCallData = abi.encodeWithSelector( bytes4(keccak256("sendUniversalTxToUEA(address,uint256,bytes,address)")), address(0), uint256(0), inboundUniversalPayload, address(this) ); // Outer multicall delivered to the destination CEA: do the external action, then trigger the back-leg. Multicall[] memory outerCalls = new Multicall[](2); outerCalls[0] = Multicall({ to: targetOnExternalChain, value: 0, data: actionCalldata }); outerCalls[1] = Multicall({ to: destinationCEAAddr, value: 0, data: ceaSelfCallData }); bytes memory outerMulticallData = abi.encodePacked(UEA_MULTICALL_SELECTOR, abi.encode(outerCalls)); // Dispatch with gasLimit β‰₯ 2_000_000 (see Operational Knobs). UGPC.sendUniversalTxOutbound{value: protocolFeePc}(UniversalOutboundTxRequest({ recipient: abi.encodePacked(destinationCEAAddr), token: pBNB, amount: 0, gasLimit: 2_000_000, gasPrice: 0, // per-chain default from UniversalCore maxPCForGas: 0, // no cap on PC for the gas swap payload: outerMulticallData, revertRecipient: address(this) })); ``` A complete runnable version is in the [Round-Trip with Auto Back-Leg](/docs/chain/build/contract-initiated-examples/round-trip-auto-back-leg) example. ## Operational Knobs Two operational settings determine whether a round-trip lands. Verified on Donut Testnet; wrong values cause TSS to silently drop the back-leg. | Knob | Value | Why | |---|---|---| | `gasLimit` on the UGPC outbound | **`β‰₯ 2_000_000`** | UGPC's auto-floor for `gasLimit = 0` is 500k. Below ~1.5M, the destination tx runs out of gas during the nested gateway call and TSS does not retry.The Push tx still succeeds and UGPC emits its event, but no destination tx fires.**Note**: UGPC charges only for actual gas used and refunds the surplus into the calling contract, so over-provisioning is essentially free. | | Push contract `$PC` balance | covers `protocolFee + inbound execution fee` | Inbound execution on Push pays gas in `$PC`, charged to the dispatching contract. UGPC refunds surplus, so refunds **accumulate on the contract**, not the user EOA.Plan a `withdraw()` path or treasury sweep for long-running flows. | :::tip Destination CEA is auto funded The destination CEA does not need pre-funding. When TSS submits the destination tx it forwards the converted gas value to the CEA as **msg.value**, so the CEA has the native balance it needs for nested gateway calls during the duration of that tx. ::: ## Deterministic CEA Conversion To convert a contract address on Push to a deterministic CEA on another chain, either to whitelist or pre-fund with other assets, use this off-chain SDK code. ```ts const dispatcherAccount = PushChain.utils.account.toUniversal( contractAddressOnPush, { chain: PushChain.CONSTANTS.CHAIN.PUSH_TESTNET } ); const destinationCEA = await PushChain.utils.account.deriveExecutorAccount( dispatcherAccount, { chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET, skipNetworkCheck: true } ); console.log('CEA address:', destinationCEA.address); ``` ## Outbound Value Sizing Cross-chain gas is converted from **$PC** to the native token using the internal Universal V3 AMM. Either ensure that proper amount of **$PC** is sent as `msg.value` or over-size the value to account for potential slippage. Since unused gas is refunded, over-sizing is recommended. The [Cross-Chain Cascade](https://github.com/pushchain/push-chain-examples/tree/main/core-sdk-functions/contract-initiated-roundtrip-between-external-chains) example implements this end to end. ## Security Considerations - **Function _executeUniversalTx_ must validate the caller and guard against replay** If your contract implements the back-leg handler, gate it on `msg.sender == UNIVERSAL_EXECUTOR_MODULE` and maintain a `mapping(bytes32 => bool) executedTxIds` keyed by the incoming `txId`. Without these guards, anyone can call your handler with fabricated data, and a legitimate callback can be replayed. **Regular inbound targets (called via the caller's UEA) do not need these guards** because the UEA's nonce handles replay internally. - **CEA identity is contract-bound** The contract's CEA is derived from its Push Chain address. A different deployment, even identical bytecode at a new address, will have a different CEA. If you use a proxy pattern, the CEA is bound to the **proxy** address, not the implementation. Upgrades do not change the CEA. - **No cross-chain atomicity** The outbound dispatch and the external execution are not atomic. Push-side state changes commit independently of whether the external call succeeds. Defer critical state commits to the inbound handler, or use an explicit pending/failed state machine. - **Inbound timing is not predictable** Inbound delivery depends on external chain finality and TSS observation. Do not design contracts that require an inbound within a specific block window. ## Best Practices - **Emit an event at dispatch time** Include a request ID, target address, and operation type so inbound payloads can be correlated with the original outbound call. - **Use per-dispatch request IDs, or a FIFO queue** If multiple outbound calls can be in flight, you have two options. (a) Stamp a request ID into your event log and correlate from off-chain. (b) Maintain a `bytes32[] pendingQueue` plus `pendingHead` and pop on each callback. TSS preserves outbound-submission order, so the popped ID always matches the just-completed leg. The queue option avoids any payload-byte introspection and is more robust. - **Keep inbound handlers lean** The handler is called by an external module account; keep it tight and apply re-entrancy guards if it calls other contracts. - **Fund the Push contract before dispatching** Verify the contract has sufficient `$PC` to cover inbound execution fees. UGPC refunds surplus into the calling contract via `receive()`, so over-provisioning is safe; refunds **accumulate on the contract**, not on the EOA. Plan a `withdraw()` path for long-running flows. ## Limitations | Area | Constraint | |------|------------| | **No synchronous result** | Outbound and inbound are always separate transactions. There is no in-call return value. | | **CEA as `msg.sender`** | External contracts that restrict callers (whitelists, EOA-only guards) must explicitly whitelist the contract's CEA address. | | **Proxy upgrade safety** | CEA is bound to the proxy address. New deployments at different addresses have different CEAs. | | **Supported chains** | Target chains must be supported by the TSS network. | ## Troubleshooting Common failure modes when wiring contract-initiated flows. Every row links to the section that explains the underlying mechanic. | Symptom | Likely cause | Fix | |---|---|---| | Push tx succeeds but no destination tx fires | `gasLimit` was `0` or under the auto-floor (~500k) | Pass `gasLimit: 2_000_000` on the UGPC outbound. See [Operational Knobs](#operational-knobs). | | Destination tx fires but the target contract reverts | Destination contract restricts callers (whitelist or EOA-only guard) and does not recognise the CEA | Whitelist the contract's CEA on the destination. Derive it off-chain via [Deterministic CEA Conversion](#deterministic-cea-conversion). | | Outbound succeeds but the back-leg never reaches `executeUniversalTx` | Destination CEA's outer multicall is missing the self-call to `sendUniversalTxToUEA` | Include the self-call step inside the multicall. See [Minimal round-trip dispatch](#minimal-round-trip-dispatch). | | `executeUniversalTx` reverts with `NotExecutorModule` | Caller is not `UNIVERSAL_EXECUTOR_MODULE` | Validate `msg.sender == UNIVERSAL_EXECUTOR_MODULE` (`0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7`) in your handler. | | `executeUniversalTx` reverts with `TxAlreadyExecuted` | Replay protection rejected a duplicate `txId` | Expected behaviour. The same back-leg was delivered twice; your idempotency guard is working. | | Outbound to an external destination reverts with `STF` | `msg.value` under-sized the live $PC β†’ routing-token swap inside UGPC | Over-size `msg.value`. UGPC refunds the surplus to the calling contract. See [Outbound Value Sizing](#outbound-value-sizing). | | EOA balance drains across many runs even though the contract is funded | UGPC routes refunds to `address(this)`, not back to the EOA that called the dispatcher | Plan a `withdraw()` path or treasury sweep on the dispatching contract. See [Best Practices](#best-practices). | | Back-leg lands on Push but reverts with out-of-gas / insufficient `$PC` | Inbound execution on Push pays gas in `$PC`, charged to the dispatching contract. | Ensure your contract is funded with enough `$PC` for execution. | ## When to Use This Use this pattern when: - A Push Chain contract needs to call an external protocol (Aave, Uniswap, a custom contract on Ethereum) without requiring the user to be online at execution time. - A governance or automation contract needs to execute an external action after an on-chain condition is met. - Your app logic lives on Push Chain but state or liquidity lives on an external chain. - You are building a cross-chain keeper, liquidator, or staking coordinator. Do not use it when: - The user is online and can sign directly. User-initiated universal transactions are simpler. - Your logic requires atomic rollback across both chains. Partial failure must be handled explicitly. ## Next Steps - Explore [Inbound Contract example](/docs/chain/build/contract-initiated-examples/inbound-to-push-chain) to see External Chain β†’ Push Chain - Explore [Outbound Contract example](/docs/chain/build/contract-initiated-examples/outbound-from-push-chain) to see Push Chain β†’ External Chain - Explore [Round-Trip Contract example](/docs/chain/build/contract-initiated-examples/round-trip-auto-back-leg) to see Push Chain β†’ External Chain β†’ Push Chain - Checkout [advanced patterns in Contract-Initiated Examples](/docs/chain/build/contract-initiated-examples/advanced-patterns/) - Compare with user-initiated flows in [Send Universal Transaction](/docs/chain/build/send-universal-transaction) --- # Track Universal Transaction URL: https://push.org/docs/chain/build/track-universal-transaction/ Track Universal Transaction | Build | Push Chain Docs ## Overview Track the status of a universal transaction using the hash of the chain where it was originally submitted, whether that transaction started on Push Chain or an external chain. This is useful for re-checking transaction progress, restoring status after a page refresh, polling from a backend, or tracking a transaction created in a different session. > **Note**: `trackTransaction()` can be used independently of `sendTransaction()`. You can pass any previously stored transaction hash and origin chain to resume tracking. ## Track Universal Transaction **_`pushChainClient.universal.trackTransaction(txHash, {options}): Promise`_** ```typescript const response = await pushChainClient.universal.trackTransaction( '0xbd765a6b60da077eaa89a382cd59c0469a4eaabcaca2707d3e6dcdeafc497a39', { progressHook: (progress) => { console.log(`${progress.id}: ${progress.message}`); }, } ); ``` | **Arguments** | **Type** | **Default** | **Description** | | ------------- | -------- | ----------- | --------------- | | _`txHash`_ | `string` | - | Transaction hash or signature to track on the origin chain. Format depends on the chain where the transaction was originally submitted. | | `options.chain` | `CHAIN` | `CHAIN.PUSH_TESTNET_DONUT` | The chain on which the transaction was submitted. | | `options.progressHook` | `(event: ProgressEvent) => void` | `undefined` | Callback invoked at each tracking step showing progress.Progress hook follows the same structure as [Send Universal Transaction - Progress Hook](/docs/chain/build/send-universal-transaction#progress-hook-type-and-response). | | `options.waitForCompletion` | `boolean` | `true` | When `true`, waits for on-chain confirmation before resolving. When `false`, returns immediately after the first status check. | | Arguments | Type | Default | Description | | --------- | ---- | ------- | ----------- | | `options.advanced.pollingIntervalMs` | `number` | `2000` | Milliseconds between polling attempts. Minimum: `500`. | | `options.advanced.timeout` | `number` | `60000` | Maximum milliseconds to wait before throwing a timeout error. | | `options.advanced.rpcUrls` | `Partial>` | `{}` | Custom RPC URLs to use when querying status. | " className="alert alert--fn-args"> The returned `UniversalTxResponse` contains the latest resolved transaction state, including Push Chain execution details and external-chain details when applicable. For the full response shape, see [Send Universal Transaction - TxResponse object](/docs/chain/build/send-universal-transaction#returns-tx-response). ## Live Playground {` // customPropHighlightRegexStart=universal\.trackTransaction // customPropHighlightRegexEnd=\\}\\); // customPropGTagEvent=track_transaction_uea_to_cea import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; async function main() { console.log('Creating Universal Signer - Ethers V6'); // Transaction Hashes // Originating from Push Chain // https://explorer.push.org/tx/0x169929f61574baf62b84ce68b944e09faf566129d0175b2ee1e020c76ae7bd2f const UNIVERSAL_TX_FROM_PUSH_TX_HASH = '0x169929f61574baf62b84ce68b944e09faf566129d0175b2ee1e020c76ae7bd2f'; // Originating from Sepolia // https://sepolia.etherscan.io/tx/0x9b4743376689eb6f90f3aeb9eea58381b3bcc033e1de4709281fd58a77b85098 const UNIVERSAL_TX_FROM_ETH_SEPOLIA_TX_HASH = '0x9b4743376689eb6f90f3aeb9eea58381b3bcc033e1de4709281fd58a77b85098'; // Originating from Solana // https://explorer.solana.com/tx/22SirqSwhcSjgyb3wdrW9Zis19dxcLHD5yy3BtRbRoLmykrv8eCzKnPaRGxrrZ7a4A7yKGRMGMehqKpTcdF2ByFR?cluster=devnet const UNIVERSAL_TX_FROM_SOLANA_TX_HASH = '22SirqSwhcSjgyb3wdrW9Zis19dxcLHD5yy3BtRbRoLmykrv8eCzKnPaRGxrrZ7a4A7yKGRMGMehqKpTcdF2ByFR'; // Initialize client const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider("https://evm.donut.rpc.push.org/"); const signer = wallet.connect(provider); const universalSigner = await PushChain.utils.signer.toUniversal(signer); const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); console.log('πŸ”‘ Push Chain client initialized'); // Track transactions from different chains console.log('πŸ“‘ Tracking universal tx from push chain:', UNIVERSAL_TX_FROM_PUSH_TX_HASH); const tx1Response = await pushChainClient.universal.trackTransaction(UNIVERSAL_TX_FROM_PUSH_TX_HASH, { progressHook: (progress) => { console.log('TX 1 Progress: ', progress.title, ' | Time:', progress.timestamp); }, advanced: { timeout: 30000 }, }); console.log(JSON.stringify(tx1Response)); console.log('πŸ“‘ Tracking universal tx from eth sepolia:', UNIVERSAL_TX_FROM_ETH_SEPOLIA_TX_HASH); const tx2Response = await pushChainClient.universal.trackTransaction(UNIVERSAL_TX_FROM_ETH_SEPOLIA_TX_HASH, { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, progressHook: (progress) => { console.log('TX 2 Progress: ', progress.title, ' | Time:', progress.timestamp); }, advanced: { timeout: 30000 }, }); console.log(JSON.stringify(tx2Response)); console.log('πŸ“‘ Tracking universal tx from solana:', UNIVERSAL_TX_FROM_SOLANA_TX_HASH); const tx3Response = await pushChainClient.universal.trackTransaction(UNIVERSAL_TX_FROM_SOLANA_TX_HASH, { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, progressHook: (progress) => { console.log('TX 3 Progress: ', progress.title, ' | Time:', progress.timestamp); }, advanced: { timeout: 30000 }, }); console.log(JSON.stringify(tx3Response)); } await main().catch(console.error); `} ## Next Steps - Learn about signing messages with [Sign Universal Message](/docs/chain/build/sign-universal-message) - Explore helper functions in [Utility Functions](/docs/chain/build/utility-functions) - Build rich UIs with transaction tracking using the [UI Kit](/docs/chain/ui-kit) - Read blockchain state with [Reading Blockchain State](/docs/chain/build/reading-blockchain-state) --- # Sign Universal Message URL: https://push.org/docs/chain/build/sign-universal-message/ Sign Message | Build | Push Chain Docs ## Overview Sign arbitrary data with your universal signer, across EVM, Solana, or any supported chain. ## Sign Universal Message **_`pushChainClient.universal.signMessage(message): Promise`_** ```typescript // Create message data const message = new TextEncoder().encode('Hello, Push Chain!') // Sign the message const signature = await pushChainClient.universal.signMessage(message) ``` " className="alert alert--fn-args"> ```typescript // Signed Message '0xf10cabddd923cf05578dd253c0642009e7651286171a17b3d40f270f42e97aff56f8941ff9989333c23edb82ae1fad11b1e82b939b9e74a96ae6e3db9ae63e0b1c' ``` {` // customPropHighlightRegexStart=universal\.signMessage // customPropHighlightRegexEnd=\\); // customPropGTagEvent=sign_universal_message import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; async function main() { // Using ethers for this demo // Set up wallet, provider and signer const wallet = ethers.Wallet.createRandom(); // Setup provide and signer const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/'); const signer = wallet.connect(provider); // Convert to Universal Signer and Initialize Push Chain SDK const universalSigner = await PushChain.utils.signer.toUniversal(signer); const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); // Sign Message console.log('Signing Message...'); const messageToSign = new TextEncoder().encode('Hello, Push Chain!'); const messageSignature = await pushChainClient.universal.signMessage(messageToSign); console.log('Message signature:', messageSignature); } await main().catch(console.error); `} ## Sign Typed Data **_`pushChainClient.universal.signTypedData({typedData}): Promise`_** The `signTypedData` function signs structured data following the EIP-712 standard. This function is only supported when connected to an **EVM compatible chain**. ```typescript // Sign the message const signature = await pushChainClient.universal.signTypedData({}) ``` " className="alert alert--fn-args"> ```typescript // Signed Message '0x9356ffe552cf0bdaa624c5121b1da0598a65b6bba357ba33868f92c9dedd490e1b9757b64af7dd16d5797e0e151fe731858c49defcc04894f53a4ab10429499f1c' ``` {` // customPropHighlightRegexStart=universal\.signTypedData // customPropHighlightRegexEnd=\\); // customPropGTagEvent=sign_universal_typed_data import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; async function main() { // Using ethers for this demo // Set up wallet, provider and signer const wallet = ethers.Wallet.createRandom(); // Setup provide and signer const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/'); const signer = wallet.connect(provider); // Convert to Universal Signer and Initialize Push Chain SDK const universalSigner = await PushChain.utils.signer.toUniversal(signer); const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); // Sign Typed Data Example console.log('Signing Typed Data...') const domain = { name: 'Push Chain', version: '1', chainId: 42101, // Push testnet verifyingContract: '0x1234567890123456789012345678901234567890', }; const types = { Person: [ { name: 'name', type: 'string' }, { name: 'wallet', type: 'address' }, ], }; const message = { name: 'Alice', wallet: '0x9821655B609186a9296261638FA74e1DFBA4AC88', }; // Sign the typed data const signature = await pushChainClient.universal.signTypedData({ domain, types, primaryType: 'Person', message, }) console.log('Typed data signature:', signature) } await main().catch(console.error); `} ## Next Steps - Query on-chain data with our [Utility Functions](/docs/chain/build/utility-functions) - Read contract state using the [Blockchain State Reader](/docs/chain/build/reading-blockchain-state) - Build rich UIs around your signer using the [UI Kit](/docs/chain/ui-kit) --- # Utility Functions URL: https://push.org/docs/chain/build/utility-functions/ Utility Functions | Build | Push Chain Docs ## Overview This section covers the most commonly used helpers in the Push Chain Core SDK to simplify common workflows. ## Helper Utilities ### Parse Units **_`PushChain.utils.helpers.parseUnits(value, exponent): bigint`_** Converts a human-readable token amount into its smallest unit representation (bigint). It multiplies the given value by 10^decimals, ensuring amounts are safe for on-chain use. Commonly used when preparing transaction parameters (e.g., converting `1.5` into `1500000000000000000`, similar to how you convert ETH to wei, PC to uPC, or any other tokens to its smallest denominator). ```typescript const result = PushChain.utils.helpers.parseUnits('1.5', { decimals: 18 }); // variation: const result = PushChain.utils.helpers.parseUnits('1.5', 18); // Returns: 1500000000000000000n (1.5 PC in uPC) ``` | **Arguments** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | _`value`_ | `string` | The string representation of the number to parse. Can include decimals (e.g., `"1.5"`, `"420"`, `"0.1"`). | | _`exponent`_ | `number \| { decimals: number }` | Number of decimal places to scale by. Provide either a number (e.g., `18`) or an object with `decimals`. Must be a non-negative integer. Examples: `18` for PC/ETH, `6` for USDC, `8` for BTC. | " className="alert alert--fn-args"> ```typescript // bigint - the scaled integer value 1500000000000000000n ``` {` // customPropHighlightRegexStart=PushChain\.utils\.helpers\.parseUnits // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_parse_units function main() { console.log('=== Common Token Conversion Examples ==='); // ETH to Wei or PC to uPC (18 decimals) const ethToWei = PushChain.utils.helpers.parseUnits('1.5', { decimals: 18 }); console.log('1.5 ETH to Wei:', ethToWei.toString()); // USDC amount (6 decimals) const usdcAmount = PushChain.utils.helpers.parseUnits('100.50', { decimals: 6 }); console.log('100.50 USDC to smallest unit:', usdcAmount.toString()); // BTC to Satoshi (8 decimals) const btcToSatoshi = PushChain.utils.helpers.parseUnits('0.00000001', { decimals: 8 }); console.log('0.00000001 BTC to Satoshi:', btcToSatoshi.toString()); console.log('=== Basic Number Parsing ==='); // Integer values const integerResult = PushChain.utils.helpers.parseUnits('420', { decimals: 9 }); console.log('420 with 9 decimal places:', integerResult.toString()); // Decimal values const decimalResult = PushChain.utils.helpers.parseUnits('0.1', { decimals: 6 }); console.log('0.1 with 6 decimal places:', decimalResult.toString()); console.log('=== Variation Examples ==='); // PC token amount (18 decimals) const pushAmount = PushChain.utils.helpers.parseUnits('1000.5', 18); console.log('1000.5 PC tokens:', pushAmount.toString()); // Precise decimal matching const preciseAmount = PushChain.utils.helpers.parseUnits('1.123456', 6); console.log('Precise 6-decimal amount:', preciseAmount.toString()); } main(); `} ### Format Units **_`PushChain.utils.helpers.formatUnits(value, {options}): string`_** Converts a raw token amount in smallest units (bigint) into a human-readable decimal string. It divides the given value by 10^decimals, making it easy to display amounts for users. Commonly used in UI or logs (e.g., turning `1500000000000000000` into `1.5` or any token from smallest unit to its human-readable value). ```typescript const result = PushChain.utils.helpers.formatUnits('1500000000000000000', { decimals: 18 }); // variation: const result = PushChain.utils.helpers.formatUnits('1500000000000000000', 18); // Returns: 1.5 (1.5 PC in uPC) ``` | **Arguments** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | _`value`_ | `bigint \| string` | Raw amount in smallest units (e.g., `'1500000000000000000'` for 1.5 assuming `18` decimals). | | _`options.decimals`_ | `number` | The number of decimal places to scale by. Must be a non-negative integer. For example, use `18` for PC or ETH, `6` for USDC, `8` for BTC. | | `options.precision` | `number` | The number of precision to scale by, will round up the value. Must be a non-negative integer. For example, use `4` for returning 4 digits after the decimal. | " className="alert alert--fn-args"> ```typescript // string - human readable value 1.5 ``` {` // customPropHighlightRegexStart=PushChain\.utils\.helpers\.formatUnits // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_format_units function main() { console.log('=== Common Token Conversion Examples ==='); // Wei to ETH or uPC to PC (18 decimals) const ethToWei = PushChain.utils.helpers.formatUnits('1500000000000000000', { decimals: 18 }); console.log('1500000000000000000 Wei to ETH (1.5):', ethToWei); // USDC amount (6 decimals) const usdcAmount = PushChain.utils.helpers.formatUnits('100500000', { decimals: 6 }); console.log('100500000 unit of USDC to human readable USDC (100.5):', usdcAmount); console.log('=== Basic Number Formatting ==='); // Integer values const integerResult = PushChain.utils.helpers.formatUnits('420000000000', { decimals: 9, precision: 2 }); console.log('420000000000 with 9 decimals and 2 precision (420.00):', integerResult); // Decimal values const decimalResult = PushChain.utils.helpers.formatUnits('123456', { decimals: 5, precision: 4 }); console.log('123456 with 6 decimal places and 4 precision (1.2346):', decimalResult); console.log('=== Variation Examples ==='); // PC token amount (18 decimals) const pushAmount = PushChain.utils.helpers.formatUnits('1000500000000000000000', 18); console.log('1000500000000000000000 uPC tokens to PC tokens (1000.5):', pushAmount); } main(); `} ### Encode Transaction Data **_`PushChain.utils.helpers.encodeTxData({abi_or_idl, functionName, args}): string`_** ```typescript const encodedData = PushChain.utils.helpers.encodeTxData({ abi: 'smart_contract_abi', functionName: 'functionName', args: [] }); ``` ```typescript const encodedData = PushChain.utils.helpers.encodeTxData({ idl: 'smart_contract_idl', functionName: 'receive_sol', args: [BigInt(0)], }); ``` `encodeTxData` produces chain-appropriate calldata based on the shape of `abi` or `idl`. | **Arguments** | **Type** | **Default** | **Description** | | ----------------- | ----------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | _`abi`_ \| _`idl`_ | `any[]` | - | Either an EVM ABI array or an Anchor IDL object. The input shape determines which encoding is produced. | | _`functionName`_ | `string` | - | The function (EVM) or instruction (Solana) name to encode. Both `snake_case` and `camelCase` are accepted and matched against the IDL. | | `args` | `any[]` | `[]` | Positional arguments. Use `BigInt` for `u64`/`u128`; 0x-hex 32-byte strings are auto-converted to Solana `PublicKey` when the IDL declares a `pubkey` arg. | " className="alert alert--fn-args"> ```typescript // encodedData string - the encoded function call data '0xd09de08a'; ``` {` // customPropHighlightRegexStart=PushChain\.utils\.helpers\.encodeTxData // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_encode_tx_data function main() { // Example ABI for a simple counter contract const testAbi = [ { inputs: [], stateMutability: 'nonpayable', type: 'constructor', }, { inputs: [], name: 'increment', outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [], name: 'countPC', outputs: [ { internalType: 'uint256', name: '', type: 'uint256', }, ], stateMutability: 'view', type: 'function', }, ]; // Encode transaction data for the increment function const result = PushChain.utils.helpers.encodeTxData({ abi: testAbi, functionName: 'increment' }); console.log('Encoded transaction data:', result); } main(); `} {` // customPropHighlightRegexStart=PushChain\.utils\.helpers\.encodeTxData // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_encode_tx_data_solana function main() { // Anchor IDL for the Solana target β€” trimmed to just the receive_sol // instruction we call below. In a real app this comes from your Anchor // program's target/idl/*.json. const testCounterIdl = { address: '8yNqjrMnFiFbVTVQcKij8tNWWTMdFkrDf9abCGgc2sgx', // SOL_TEST_PROGRAM metadata: { name: 'test_counter', version: '0.1.0', spec: '0.1.0' }, instructions: [ { name: 'receive_sol', discriminator: [121, 244, 250, 3, 8, 229, 225, 1], accounts: [ { name: 'counter', writable: true, pda: { seeds: [{ kind: 'const', value: [99, 111, 117, 110, 116, 101, 114] }] } }, // 'counter' { name: 'recipient', writable: true, address: '89q1AUFb7YREHtjc1aYaPywovPq6tb3GYNPyDUJ3rshi' }, { name: 'cea_authority', writable: true }, // auto-populated with sender's CEA { name: 'system_program', address: '11111111111111111111111111111111' }, ], args: [{ name: 'amount', type: 'u64' }], }, ], }; const result = PushChain.utils.helpers.encodeTxData({ idl: testCounterIdl, functionName: 'receive_sol', args: [BigInt(0)] }); console.log('Encoded transaction data:', result); } main(); `} ## Chain Utilities ### Get Chain Namespace from Chain Name **_`PushChain.utils.chains.getChainNamespace(chainName): string`_** Every external chain is represented as a particular string on Push Chain. You can see the list of supported chains in the [Chain Configuration](/docs/chain/setup/chain-config#universal-chain-namespace) section. ```typescript const chainName = PushChain.utils.chains.getChainNamespace('PUSH_TESTNET_DONUT'); ``` | **Arguments** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | _`name`_ | `string` | The chain name to convert to chain namespace. Eg: `PUSH_TESTNET_DONUT` converts to `eip155:42101`, `ETHEREUM_SEPOLIA` converts to `eip155:11155111`. | " className="alert alert--fn-args"> ```typescript // chainNamespace string 'eip155:42101'; // NOTE: returns undefined if chainName is unsupported ``` {` // customPropHighlightRegexStart=PushChain\.utils\.chains\.getChainNamespace // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_get_chain_namespace function main() { const chainName = PushChain.utils.chains.getChainNamespace('PUSH_TESTNET_DONUT'); console.log(chainName); } main(); `} ### Get Chain Name from Chain Namespace **_`PushChain.utils.chains.getChainName(chainNamespace): string`_** Every external chain is represented as a particular string on Push Chain. You can see the list of supported chains in the [chain configuration](/docs/chain/setup/chain-config#universal-chain-namespace) section. ```typescript const chainName = PushChain.utils.chains.getChainName('eip155:42101'); ``` | **Arguments** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | _`namespace`_ | `string` | The chain namespace to convert to chain name. Eg: `eip155:42101` converts to `PUSH_TESTNET_DONUT`, `eip155:11155111` converts to `ETHEREUM_SEPOLIA`. | " className="alert alert--fn-args"> ```typescript // chainName string 'PUSH_TESTNET_DONUT'; // NOTE: returns undefined if chainNamespace is unsupported ``` {` // customPropHighlightRegexStart=PushChain\.utils\.chains\.getChainName // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_get_chain_name function main() { const chainName = PushChain.utils.chains.getChainName('eip155:42101'); console.log(chainName); } main(); `} ### Get Supported Chains By Name **_`PushChain.utils.chains.getSupportedChainsByName(pushNetwork): { chains: [] }`_** Returns the list of supported chain names (human-readable strings) for a given Push Network. ```typescript const chains = PushChain.utils.chains.getSupportedChainsByName(PushChain.CONSTANTS.PUSH_NETWORK.TESTNET); ``` | **Arguments** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | _`pushNetwork`_ | `PushChain.CONSTANTS.PUSH_NETWORK ` | Push Chain network to retrieve list of supported chain names from. For example: `PushChain.CONSTANTS.PUSH_NETWORK.TESTNET` | " className="alert alert--fn-args"> ```typescript // { chains } object - returns human-readable chain names as strings { chains: [ 'PUSH_TESTNET_DONUT', 'ETHEREUM_SEPOLIA', 'ARBITRUM_SEPOLIA', 'BASE_SEPOLIA', 'BNB_TESTNET', 'SOLANA_DEVNET', // ... ] } // NOTE: returns empty chains array if pushNetwork is unsupported ``` {` // customPropHighlightRegexStart=PushChain\\.utils\\.chains\\.getSupportedChainsByName // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_get_supported_chains_by_name function main() { const chains = PushChain.utils.chains.getSupportedChainsByName(PushChain.CONSTANTS.PUSH_NETWORK.TESTNET); console.log(JSON.stringify(chains)); } main(); `} ### Get Supported Chains **_`PushChain.utils.chains.getSupportedChains(pushNetwork): { chains: [] }`_** Returns the list of chains supported for a given Push Network. ```typescript const chains = PushChain.utils.chains.getSupportedChains(PushChain.CONSTANTS.PUSH_NETWORK.TESTNET); ``` | **Arguments** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | _`pushNetwork`_ | `PushChain.CONSTANTS.PUSH_NETWORK ` | Push Chain network to retrieve list of supported chains from. For example: `PushChain.CONSTANTS.PUSH_NETWORK.TESTNET` | " className="alert alert--fn-args"> ```typescript // { chains } object { chains: [ PushChain.CONSTANTS.CHAIN.PUSH_TESTNET, PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, PushChain.CONSTANTS.CHAIN.ARBITRUM_SEPOLIA, PushChain.CONSTANTS.CHAIN.BASE_SEPOLIA, PushChain.CONSTANTS.CHAIN.BNB_TESTNET, PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, // ... ] } // NOTE: returns empty chains array if pushNetwork is unsupported ``` {` // customPropHighlightRegexStart=PushChain\.utils\.chains\.getSupportedChains // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_get_supported_chains function main() { const chains = PushChain.utils.chains.getSupportedChains(PushChain.CONSTANTS.PUSH_NETWORK.TESTNET); console.log(JSON.stringify(chains)); } main(); `} ## Account Utilities ### Convert to Universal Account **_`PushChain.utils.account.toUniversal(address, {options}): `_** ```typescript const account = PushChain.utils.account.toUniversal(address, { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, }); ``` | **Arguments** | **Type** | **Description** | | ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | _`address`_ | `string` | An address string (e.g., `0xabc...`). | | _`options.chain`_ | `CHAIN` | The target chain for the signer. For example: `PushChain.CONSTANTS.CHAIN.PUSH_TESTNET_DONUT` | " className="alert alert--fn-args"> ```typescript // UniversalAccount object { chain: 'eip155:11155111', address: '0xD8d6aF611a17C236b13235B5318508FA61dE3Dba' } ``` {` // customPropHighlightRegexStart=PushChain\.utils\.account\.toUniversal // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_account_to_universal function main() { const account = PushChain.utils.account.toUniversal( '0xD8d6aF611a17C236b13235B5318508FA61dE3Dba', { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, } ); console.log(JSON.stringify(account, null, 2)); } main() `} ### Convert to Chain-Agnostic Address **_`PushChain.utils.account.toChainAgnostic(address, {options}): string`_** ```typescript const chainAgnosticAddress = PushChain.utils.account.toChainAgnostic(address, { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, }); ``` | **Arguments** | **Type** | **Description** | | ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | _`address`_ | `string` | An address string (e.g., `0xabc...`). | | _`options.chain`_ | `CHAIN` | The target chain for the signer. For example: `PushChain.CONSTANTS.CHAIN.PUSH_TESTNET_DONUT` | " className="alert alert--fn-args"> ```typescript // Chain Agnostic Address 'eip155:11155111:0xD8d6aF611a17C236b13235B5318508FA61dE3Dba'; ``` {` // customPropHighlightRegexStart=PushChain\.utils\.account\.toChainAgnostic // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_account_to_chain_agnostic import { PushChain } from '@pushchain/core'; function main() { const chainAgnosticAddress = PushChain.utils.account.toChainAgnostic( '0xD8d6aF611a17C236b13235B5318508FA61dE3Dba', { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, } ); console.log(JSON.stringify(chainAgnosticAddress, null, 2)); } main() `} ### Convert from Chain-Agnostic to Universal Account **_`PushChain.utils.account.fromChainAgnostic(chainAgnosticAddress): `_** ```typescript const account = PushChain.utils.account.fromChainAgnostic(chainAgnosticAddress); ``` | **Arguments** | **Type** | **Description** | | ------------------------ | -------- | ---------------------------------------------------------------------------------------------------------- | | _`chainAgnosticAddress`_ | `string` | A full chain agnostic address string (e.g., `eip155:11155111:0x35B84d6848D16415177c64D64504663b998A6ab4`). | " className="alert alert--fn-args"> ```typescript // UniversalAccount object: { chain: string, address: string } { chain: 'eip155:11155111', address: '0xD8d6aF611a17C236b13235B5318508FA61dE3Dba' } ``` {` // customPropHighlightRegexStart=PushChain\.utils\.account\.fromChainAgnostic // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_account_from_chain_agnostic function main() { const account = PushChain.utils.account.fromChainAgnostic( 'eip155:11155111:0xD8d6aF611a17C236b13235B5318508FA61dE3Dba' ); console.log(JSON.stringify(account, null, 2)); } main(); `} ### Derive Executor Account **_`PushChain.utils.account.deriveExecutorAccount(universalAccount, { options? }): Promise`_** Derives the execution account for a given input account. This function supports multiple derivation flows based on the input and options provided. **Use Cases:** - **UOA β†’ UEA**: Derive a Universal Executor Account on Push Chain from a Universal Origin Account - **Push account / UOA β†’ CEA**: Derive a Chain Executor Account on an external chain from a Push Chain account or UOA - **Push-native account**: Returns the same account if it's already a Push Chain native account ```typescript // Derive UEA from UOA const universalAccount = PushChain.utils.account.toUniversal( '0xD8d6aF611a17C236b13235B5318508FA61dE3Dba', { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA } ); const result = await PushChain.utils.account.deriveExecutorAccount(universalAccount); // Derive UEA from Solana account const solanaAccount = PushChain.utils.account.toUniversal( 'EUYcfSUScdFgKMbB3rRdgRZwXmcxY7QCRQa2JwrchP1Q', { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET } ); const ueaResult = await PushChain.utils.account.deriveExecutorAccount(solanaAccount); // Derive CEA from Push account const pushAccount = PushChain.utils.account.toUniversal( '0x98cA97d2FB78B3C0597E2F78cd11868cACF423C5', { chain: PushChain.CONSTANTS.CHAIN.PUSH_TESTNET } ); const ceaResult = await PushChain.utils.account.deriveExecutorAccount( pushAccount, { chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET } ); // Derive CEA from Solana account that will be there on BNB Testnet const ceaSolanaResult = await PushChain.utils.account.deriveExecutorAccount( solanaAccount, { chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET } ); ``` | **Arguments** | **Type** | **Default** | **Description** | | ---------------- | ------------------ | ----------- | ---------------------------------------------------------------------------------------------------------- | | _`universalAccount`_ | `UniversalAccount` | - | UniversalAccount object created via `toUniversal()`. Represents any blockchain account in a universal format. | | `options.chain` | `CHAIN` | `undefined` | Optional. When provided, derives a Chain Executor Account (CEA) on the specified external chain. Use `PushChain.CONSTANTS.CHAIN` values. | | `options.skipNetworkCheck` | `boolean` | `false` | When `true`, performs deterministic derivation only without checking deployment status. When `false`, includes deployment/existence check. | " className="alert alert--fn-args"> ```typescript // Response object { address: '0x98cA97d2FB78B3C0597E2F78cd11868cACF423C5', deployed: true // Only included when skipNetworkCheck is false } ``` {` // customPropHighlightRegexStart=PushChain\\.utils\\.account\\.deriveExecutorAccount // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_derive_executor_account async function main() { // Example 1: Derive UEA from Ethereum account const ethAccount = PushChain.utils.account.toUniversal( '0xe1ceea8efaf7fb973cb65653caa7dd3d59283f25', { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA } ); const ueaResult = await PushChain.utils.account.deriveExecutorAccount(ethAccount); console.log('UEA from Ethereum account:'); console.log(JSON.stringify(ueaResult, null, 2)); // Example 2: Derive UEA from Solana account const solanaAccount = PushChain.utils.account.toUniversal( '5BoLqCmrqbrqv2QwUnpccC62scUxDojpYw2UyM8aGpru', { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET } ); const ueaFromSolana = await PushChain.utils.account.deriveExecutorAccount(solanaAccount); console.log('UEA from Solana account:'); console.log(JSON.stringify(ueaFromSolana, null, 2)); // Example 3: Derive CEA from Push account const pushAccount = PushChain.utils.account.toUniversal( '0x3ee31c0C8b9888e267781b2FD73cDA1D7FfA46eE', { chain: PushChain.CONSTANTS.CHAIN.PUSH_TESTNET } ); const ceaResult = await PushChain.utils.account.deriveExecutorAccount( pushAccount, { chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET } ); console.log('CEA on BNB Testnet:'); console.log(JSON.stringify(ceaResult, null, 2)); // Example 4: Skip network check (deterministic only) const deterministicResult = await PushChain.utils.account.deriveExecutorAccount( ethAccount, { skipNetworkCheck: true } ); console.log('Deterministic derivation:'); console.log(JSON.stringify(deterministicResult, null, 2)); } await main().catch(console.error) `} ### Resolve Controller Account **_`PushChain.utils.account.resolveControllerAccount(account, { options? }): Promise }>`_** Resolves the controller identity behind an execution account. This function supports recursive resolution to trace back to the original Universal Origin Account (UOA). **Use Cases:** - **UEA β†’ UOA**: Resolve the Universal Origin Account from a Universal Executor Account - **CEA β†’ Push account β†’ UOA**: Resolve through Chain Executor Account to Push account, then to UOA if applicable - **Recursive resolution**: Automatically follows the chain of derivation back to the controller identity ```typescript // Resolve UOA from UEA const result = await PushChain.utils.account.resolveControllerAccount('0xUEA...'); // Resolve from CEA with chain context const ceaResult = await PushChain.utils.account.resolveControllerAccount( '0xCEA...', { chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET } ); ``` | **Arguments** | **Type** | **Default** | **Description** | | ---------------- | ------------------ | ----------- | ---------------------------------------------------------------------------------------------------------- | | _`account`_ | `string` | - | Executor account to resolve. Can be a UEA, CEA, or Push Chain account address. | | `options.chain` | `CHAIN` | `undefined` | Required for CEA context. Specifies which chain the CEA is deployed on. Use `PushChain.CONSTANTS.CHAIN` values. | | `options.skipNetworkCheck` | `boolean` | `false` | When `true`, performs deterministic resolution only without checking existence. When `false`, includes existence check. | " className="alert alert--fn-args"> ```typescript // Example 1: Resolving CEA that has UEA with UOA controller { accounts: [ { chain: 'eip155:42101', chainName: 'PUSH_TESTNET_DONUT', address: '0x2Fd904d6f2C0b34d58426C8Ae9c5267E845CE98f', type: 'uea', exists: true }, { chain: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', chainName: 'SOLANA_DEVNET', address: '72JBejJFXrRKpQ69Hmaqr7vWJr6pdZXFEL6jt3sadsXU', type: 'uoa', exists: true, role: 'controller' } ] } // Example 2: Resolving CEA from Push Account (EOA) or smart contract { accounts: [ { chain: 'eip155:42101', chainName: 'PUSH_TESTNET_DONUT', address: '0x2Fd904d6f2C0b34d58426C8Ae9c5267E845CE98f', type: 'uoa', exists: true, role: 'controller' } ] } ``` | Field | Type | Description | | ----------- | --------- | --------------------------------------------------------------------------- | | `accounts` | `Array` | Array of account objects in the resolution chain | | `chain` | `string` | Chain namespace identifier (e.g., `eip155:42101`, `solana:EtWTRABZaYq...`) | | `chainName` | `string` | Human-readable chain constant name (e.g., `PUSH_TESTNET_DONUT`, `SOLANA_DEVNET`) | | `address` | `string` | Account address on the chain | | `type` | `string` | Account type: `uea`, `uoa`, or `cea` | | `exists` | `boolean` | Whether the account exists on-chain | | `role` | `string` | `controller` indicates the controlling account in the resolution chain | {` // customPropHighlightRegexStart=PushChain\\.utils\\.account\\.resolveControllerAccount // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_resolve_controller_account async function main() { // Example 1: Resolve controller from UEA const ueaAddress = '0x98cA97d2FB78B3C0597E2F78cd11868cACF423C5'; const result1 = await PushChain.utils.account.resolveControllerAccount(ueaAddress); console.log('Resolution chain from UEA:'); console.log(JSON.stringify(result1, null, 2)); // Example 2: Resolve from CEA with chain context const ceaAddress = '0x5d71c70571789F0cd3bE84513523a9993740BDf6'; const result2 = await PushChain.utils.account.resolveControllerAccount( ceaAddress, { chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET } ); console.log('Resolution chain from CEA:'); console.log(JSON.stringify(result2, null, 2)); // Example 3: Skip network check (deterministic only) const result3 = await PushChain.utils.account.resolveControllerAccount( ueaAddress, { skipNetworkCheck: true } ); console.log('Deterministic resolution:'); console.log(JSON.stringify(result3, null, 2)); } await main().catch(console.error) `} ## Signer Utilities ### Create Universal Signer from Keypair **_`PushChain.utils.signer.toUniversalFromKeypair(keypair, {options}): Promise`_** ```typescript const universalSigner = await PushChain.utils.signer.toUniversalFromKeypair( keypair, { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, library: PushChain.CONSTANTS.LIBRARY.SOLANA_WEB3JS, } ); ``` | **Arguments** | **Type** | **Description** | | ------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | _`keypair`_ | `Keypair` | A keypair object from one of the supported libraries (ethers v5/v6, viem, or a custom UniversalSignerSkeleton) | | _`options.chain`_ | `CHAIN` | The target chain for the signer. For example: `PushChain.CONSTANTS.CHAIN.PUSH_TESTNET_DONUT` | | _`options.library`_ | `LIBRARY` | The library to use for the signer. For example: `PushChain.CONSTANTS.LIBRARY.ETHEREUM_ETHERSV6` | " className="alert alert--fn-args"> ```typescript // UniversalSigner object { account: { address: '0xD173b7f04D539A5794e14030c4E172B2E3df92f3', chain: 'eip155:11155111' }, signMessage: [Function: signMessage], signAndSendTransaction: [Function: signAndSendTransaction], signTypedData: [Function: signTypedData] } ``` {` // customPropHighlightRegexStart=PushChain\.utils\.signer\.toUniversalFromKeypair // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_signer_from_keypair_ethers import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; async function main() { const provider = new ethers.JsonRpcProvider('https://sepolia.gateway.tenderly.co'); const wallet = ethers.Wallet.createRandom(provider); const universalSigner = await PushChain.utils.signer.toUniversalFromKeypair(wallet, { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, library: PushChain.CONSTANTS.LIBRARY.ETHEREUM_ETHERSV6, }); console.log(JSON.stringify(universalSigner, null, 2)); } await main().catch(console.error); `} {` // customPropHighlightRegexStart=PushChain\.utils\.signer\.toUniversalFromKeypair // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_signer_from_keypair_viem import { PushChain } from '@pushchain/core' import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'; import { createWalletClient, http } from 'viem'; import { sepolia } from 'viem/chains'; async function main() { const account = privateKeyToAccount(generatePrivateKey()); const walletClient = createWalletClient({ account, chain: sepolia, transport: http(), }); const universalSigner = await PushChain.utils.signer.toUniversalFromKeypair(walletClient, { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, library: PushChain.CONSTANTS.LIBRARY.ETHEREUM_VIEM, }) console.log(JSON.stringify(universalSigner, null, 2)); } await main().catch(console.error); `} {` // customPropHighlightRegexStart=PushChain\.utils\.signer\.toUniversalFromKeypair // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_signer_from_keypair_solana async function main() { const keypair = Keypair.generate() const universalSigner = await PushChain.utils.signer.toUniversalFromKeypair(keypair, { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, library: PushChain.CONSTANTS.LIBRARY.SOLANA_WEB3JS, }); console.log(JSON.stringify(universalSigner, null, 2)); } await main().catch(console.error); `} ## Token Utilities ### Get Moveable Tokens **_`PushChain.utils.tokens.getMoveableTokens(chainOrClient?): { tokens: [] }`_** Commonly used to get list of supported assets that can be moved across chains. See [send universal transaction](/docs/chain/build/send-universal-transaction/#sending-universal-transaction) for more info. ```typescript // All supported moveable tokens across chains const { tokens: allMoveable } = PushChain.utils.tokens.getMoveableTokens(); // Filtered for a specific chain const { tokens: sepoliaMoveable } = PushChain.utils.tokens.getMoveableTokens( PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA ); ``` | **Arguments** | **Type** | **Description** | | ------------------ | ---------------------- | ------------------------------------------------------------------------------- | | _`chainOrClient`_ | `CHAIN \| PushChain` | Optional. A chain enum or an initialized client to filter tokens for that chain. | " className="alert alert--fn-args"> ```typescript // tokens object { tokens: [] } { tokens: [ { chain: 'eip155:11155111', symbol: 'ETH', decimals: 18, address: '0x...' }, { chain: 'eip155:11155111', symbol: 'USDC', decimals: 6, address: '0x...' }, // ... ] } ``` {` // customPropHighlightRegexStart=PushChain\.utils\.tokens\.getMoveableTokens // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_get_moveable_tokens function main() { const { tokens: sepolia } = PushChain.utils.tokens.getMoveableTokens( PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA ); console.log("Sepolia moveable supported tokens:", JSON.stringify(sepolia, null, 2)); } main(); `} ### Get Payable Tokens **_`PushChain.utils.tokens.getPayableTokens(chainOrClient?): { tokens: [] }`_** Commonly used to get list of supported assets to pay with (either for gas or token movement) across chains. See [send universal transaction](/docs/chain/build/send-universal-transaction/#sending-universal-transaction) for more info. ```typescript // All supported payable tokens across chains const { tokens: allPayable } = PushChain.utils.tokens.getPayableTokens(); // Filtered for a specific chain const { tokens: solanaDevnetPayable } = PushChain.utils.tokens.getPayableTokens( PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET ); ``` | **Arguments** | **Type** | **Description** | | ------------------ | ---------------------- | ------------------------------------------------------------------------------- | | _`chainOrClient`_ | `CHAIN \| PushChain` | Optional. A chain enum or an initialized client to filter tokens for that chain. | " className="alert alert--fn-args"> ```typescript // tokens object { tokens: [] } { tokens: [ { chain: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', symbol: 'SOL', decimals: 9, address: 'So11111111111111111111111111111111111111112' }, { chain: 'eip155:11155111', symbol: 'USDC', decimals: 6, address: '0x...' }, // ... ] } ``` {` // customPropHighlightRegexStart=PushChain\.utils\.tokens\.getPayableTokens // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_get_payable_tokens function main() { const { tokens: devnet } = PushChain.utils.tokens.getPayableTokens( PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET ); console.log("Solana supported payable tokens:", JSON.stringify(devnet, null, 2)); } main(); `} ### Get PRC20 Address **_`PushChain.utils.tokens.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` (for example from `getMoveableTokens`) or an object containing the origin `chain` and token `address`. ```typescript const { address, chain, symbol, decimals, network } = PushChain.utils.tokens.getPRC20Address(ethMoveableToken); // Or with explicit chain/address input const prc20Alt = PushChain.utils.tokens.getPRC20Address({ chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, address: "0x97F477B7f970D47a87B42869ceeace218106152a", }); ``` | **Arguments** | **Type** | **Description** | | ------------- | -------- | --------------- | | _`token`_ | `MoveableToken \| { chain: string; address: string }` | Origin token info. Either pass a `MoveableToken` (e.g., from `getMoveableTokens()`) or provide the origin chain plus token address. | | `options.network` | `PushChain.CONSTANTS.PUSH_NETWORK` | Override the Push network to resolve the PRC20 on. Defaults to the network the client was initialized with. For example: `PushChain.CONSTANTS.PUSH_NETWORK.TESTNET` | " className="alert alert--fn-args"> ```typescript { address: `0x${string}`; // PRC20 contract address on Push Chain chain: CHAIN; // Always CHAIN.PUSH_TESTNET_DONUT (or mainnet when live) symbol: string; // e.g. 'USDC.eth', 'pETH' decimals: number; // Token decimals on Push Chain network: PUSH_NETWORK; // The Push network this PRC20 belongs to } ``` {` // customPropHighlightRegexStart=PushChain\\.utils\\.tokens\\.getPRC20Address // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_get_prc20_address async function main() { // Using { chain, address } const prc20Alt = PushChain.utils.tokens.getPRC20Address({ chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, address: "0x97F477B7f970D47a87B42869ceeace218106152a", }); console.log('USDC.eth:', JSON.stringify(prc20Alt)); // Moveable token example (ETH on Sepolia) const { tokens: moveable } = PushChain.utils.tokens.getMoveableTokens( PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA ); const ethMoveable = moveable.find((t) => t.symbol === 'ETH'); if (!ethMoveable) { throw new Error('ETH moveable token not found'); } // Using MoveableToken directly const { address: prc20Addr, symbol, decimals, network } = PushChain.utils.tokens.getPRC20Address(ethMoveable); console.log('pETH address:', prc20Addr, '| symbol:', symbol, '| decimals:', decimals); } main(); `} ## Conversion Utilities ### Calculate Minimum Amount from Slippage **_`PushChain.utils.conversion.slippageToMinAmount(amount, { slippageBps }): string`_** ```typescript const minOut = PushChain.utils.conversion.slippageToMinAmount('100000000', { slippageBps: 100, // 1% }); // Returns: '99000000' ``` | **Arguments** | **Type** | **Description** | | --------------------- | -------------------- | ------------------------------------------------------------- | | _`amount`_ | `string` | Input amount in smallest units (e.g., '100000000' for 100 USDC as it has 6 decimals). | | _`options.slippageBps`_ | `number` (integer) | Slippage in basis points. `100 = 1%`, `50 = 0.5%`. | " className="alert alert--fn-args"> ```typescript // minOut `string` (in smallest units) '99000000' ``` {` // customPropHighlightRegexStart=PushChain\.utils\.conversion\.slippageToMinAmount // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_slippage_to_min_amount function main() { const minOut = PushChain.utils.conversion.slippageToMinAmount('100000000', { slippageBps: 100, }); console.log('Min out with 1% slippage:', minOut); } main(); `} ### Get Conversion Quote **_`pushChainClient.funds.getConversionQuote(amount, {options}): Promise`_** > **Note**: This function is available only after initializing the Push Chain client. The function is used to get conversion quote especially when you want to pay with (from) one token, move as (to) another token. Used in [send universal transaction](/docs/chain/build/send-universal-transaction/#sending-universal-transaction) for token movement across chains or to pay gas in other tokens instead of native token of the source chain. > **Convention:** from = the token you pay with (Payable), to = the token you move as (Moveable). ```typescript const quote = pushChainClient.funds.getConversionQuote('100000000', { from: PushChain.CONSTANTS.PAYABLE.TOKEN.ETHEREUM_SEPOLIA.WETH, to: PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_SEPOLIA.USDT, }); // Returns: { "amountIn": "5000000000000000", "amountOut": "11813463066488417", "rate": 2362692613297.683, "route": [ "WETH", "USDT" ], "timestamp": 1758582899267 } ``` | **Arguments** | **Type** | **Description** | | ------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | _`amount`_ | `string` | The string representation of the amount to parse. Can include decimals (e.g., "1.5", "420", "0.1"). | | _`options.from`_ | `PushChain.CONSTANTS.PAYABLE.TOKEN` | The token you pay with. | | _`options.to`_ | `PushChain.CONSTANTS.MOVEABLE.TOKEN` | The token you move as. | - `funds.getConversionQuote` currently works on Ethereum Mainnet and Sepolia. Other origins will throw an error. " className="alert alert--fn-args"> | Field | Type | Description | | --- | --- | --- | | `amountIn` | `string` | Input amount in smallest units | | `amountOut` | `string` | Output amount in smallest units | | `rate` | `number` | Normalized rate: tokenOut per tokenIn | | `route` | `string[]` | Optional swap path (e.g., `["WETH","USDT"]`) | | `timestamp` | `number` | Unix time (ms) | {` import { PushChain } from '@pushchain/core'; import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'; import { createWalletClient, http } from 'viem'; import { sepolia } from 'viem/chains'; async function main() { // Create a Sepolia wallet client const account = privateKeyToAccount(generatePrivateKey()); const walletClient = createWalletClient({ account, chain: sepolia, transport: http() }); // Convert to Universal Signer and initialize const universalSigner = await PushChain.utils.signer.toUniversal(walletClient); const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); // Amount: 0.005 WETH (18 decimals) const amountIn = PushChain.utils.helpers.parseUnits('0.005', 18); // Get quote: pay with WETH β†’ move as USDT const quote = await client.funds.getConversionQuote(amountIn, { from: PushChain.CONSTANTS.PAYABLE.TOKEN.ETHEREUM_SEPOLIA.WETH, to: PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_SEPOLIA.USDT, }); console.log('Quote:', JSON.stringify(quote, null, 2)); } await main().catch(console.error); `} ## Explorer Utilities ### Get Transaction URL **_`pushChainClient.explorer.getTransactionUrl(txHash, { options? }): string`_** > **Note**: This function is available only after initializing the Push Chain client. Returns the explorer URL for a given transaction hash. By default, uses the chain from the initialized `pushChainClient`. When `options.chain` is provided, generates the explorer URL for that specific chain instead. ```typescript // Default: Uses client's chain (Push Chain) const url = pushChainClient.explorer.getTransactionUrl(txHash); // Override: Generate URL for external chain explorer const sepoliaUrl = pushChainClient.explorer.getTransactionUrl(txHash, { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA }); ``` | **Arguments** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------ | | _`txHash`_ | `string` | The transaction hash to convert to explorer URL. | | `options.chain` | `CHAIN` | Optional. Override the chain for explorer URL generation. When provided, generates the URL for that chain's explorer instead of the client's chain. Use `PushChain.CONSTANTS.CHAIN` values. | " className="alert alert--fn-args"> ```typescript // Push Chain transaction URL (default) 'https://donut.push.network/tx/0x828911db033c65de8faab4906cfcb7d13ce225c3cd283534d110414a5b78cf87' // External chain transaction URL (when options.chain is provided) 'https://sepolia.etherscan.io/tx/0x828911db033c65de8faab4906cfcb7d13ce225c3cd283534d110414a5b78cf87' ``` {` // customPropHighlightRegexStart=pushChainClient\.explorer\.getTransactionUrl // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_explorer_get_transaction_url // Using ethers for example - You can use any library // ethers, viem, solana web3js, etc import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; async function main() { // Create random wallet const wallet = ethers.Wallet.createRandom() // Set up provider const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org') const signer = wallet.connect(provider) // Convert to Universal Signer const universalSigner = await PushChain.utils.signer.toUniversal(signer); // Initialize Push Chain Client const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); const txHash = '0x828911db033c65de8faab4906cfcb7d13ce225c3cd283534d110414a5b78cf87'; // Default: Push Chain explorer URL const pushChainUrl = pushChainClient.explorer.getTransactionUrl(txHash); console.log("Push Chain URL:", pushChainUrl); // Override: Ethereum Sepolia explorer URL const sepoliaUrl = pushChainClient.explorer.getTransactionUrl(txHash, { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA }); console.log("Sepolia URL:", sepoliaUrl); } await main().catch(console.error); `} ### List Explorer URLs **_`pushChainClient.explorer.listUrls({ options? }): { explorers: [] }`_** > **Note**: This function is available only after initializing the Push Chain client. Returns explorer URLs for a specific chain. By default, uses the chain from the initialized `pushChainClient`. When `options.chain` is provided, returns explorer URLs for that specific chain instead. ```typescript // Default: Uses client's chain const result = pushChainClient.explorer.listUrls(); // Override: Get explorer URLs for specific chain const sepoliaExplorers = pushChainClient.explorer.listUrls({ chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA }); ``` | **Arguments** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------ | | `options.chain` | `CHAIN` | Optional. Override the chain to get explorer URLs for. When provided, returns explorer URLs for that specific chain. Use `PushChain.CONSTANTS.CHAIN` values. | " className="alert alert--fn-args"> ```typescript // explorers object { explorers: [ { chain: 'eip155:42101', chainName: 'PUSH_TESTNET_DONUT', urls: ['https://donut.push.network', 'https://scan.push.org'] } ] } ``` {` // customPropHighlightRegexStart=pushChainClient\.explorer\.listUrls // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_explorer_list_urls // Using ethers for example - You can use any library // ethers, viem, solana web3js, etc import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; async function main() { // Create random wallet const wallet = ethers.Wallet.createRandom() // Set up provider const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org') const signer = wallet.connect(provider) // Convert to Universal Signer const universalSigner = await PushChain.utils.signer.toUniversal(signer); // Initialize Push Chain Client const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); // Default: Get explorer URLs for client's chain (Push Chain) const pushChainExplorers = pushChainClient.explorer.listUrls(); console.log('Push Chain explorers:', JSON.stringify(pushChainExplorers, null, 2)); // Override: Get explorer URLs for Ethereum Sepolia const sepoliaExplorers = pushChainClient.explorer.listUrls({ chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA }); console.log('Sepolia explorers:', JSON.stringify(sepoliaExplorers, null, 2)); } await main().catch(console.error); `} ### List All Explorer URLs **_`pushChainClient.explorer.listAllUrls(): { explorers: [] }`_** > **Note**: This function is available only after initializing the Push Chain client. Returns explorer URLs for all supported chains in the current Push Network. ```typescript // ... Initialize Push Chain Client const allExplorers = pushChainClient.explorer.listAllUrls(); ``` " className="alert alert--fn-args"> ```typescript // explorers object with all supported chains { explorers: [ { chain: 'eip155:42101', chainName: 'PUSH_TESTNET_DONUT', urls: ['https://donut.push.network', 'https://scan.push.org'] }, { chain: 'eip155:11155111', chainName: 'ETHEREUM_SEPOLIA', urls: ['https://sepolia.etherscan.io'] }, { chain: 'eip155:421614', chainName: 'ARBITRUM_SEPOLIA', urls: ['https://sepolia.arbiscan.io'] }, // ... more chains ] } ``` {` // customPropHighlightRegexStart=pushChainClient\.explorer\.listAllUrls // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_explorer_list_all_urls // Using ethers for example - You can use any library // ethers, viem, solana web3js, etc import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; async function main() { // Create random wallet const wallet = ethers.Wallet.createRandom() // Set up provider const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org') const signer = wallet.connect(provider) // Convert to Universal Signer const universalSigner = await PushChain.utils.signer.toUniversal(signer); // Initialize Push Chain Client const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); // Get all explorer URLs for all supported chains const allExplorers = pushChainClient.explorer.listAllUrls(); console.log('All explorers:', JSON.stringify(allExplorers, null, 2)); console.log('Total chains:', allExplorers.explorers.length); } await main().catch(console.error); `} ## Next Steps - Dive into [reading blockchain state](/docs/chain/build/reading-blockchain-state) - Harness our [on-chain contract helpers](/docs/chain/build/contract-helpers) to supercharge your app - Explore and abstract away wallet and any chain-related logic using [UI Kit](/docs/chain/ui-kit) --- # Reading Blockchain State URL: https://push.org/docs/chain/build/reading-blockchain-state/ Reading Blockchain State | Build | Push Chain Docs ## Overview Push Chain is an EVM-compatible blockchain, so you can use familiar Ethereum tools to fetch on-chain data. This guide shows you how to: - Initialize HTTP client for one-off requests - Fetch transactions and blocks - Initialize WebSocket client for real-time streaming - Subscribe to new blocks and filter for specific transactions For full reference on each library, see: - [ethers.js documentation](https://docs.ethers.org/) - [viem documentation](https://viem.sh/) ## Initialize HTTP Client {` // customPropHighlightRegexStart=ethers\.JsonRpcProvider // customPropHighlightRegexEnd=\\); // customPropGTagEvent=setup_ethers_http_provider // HTTP JSON-RPC provider const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/'); console.log('Ethers provider methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(provider))); `} {` // customPropHighlightRegexStart=createPublicClient\\( // customPropHighlightRegexEnd=\\); // customPropGTagEvent=setup_viem_http_client // HTTP client const viemClient = createPublicClient({ transport: http('https://evm.donut.rpc.push.org/') }); console.log('Viem client:', JSON.stringify(viemClient, null, 2)); `} ## Fetch a Transaction by Hash {` // customPropHighlightRegexStart=provider\.getTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=get_transaction_ethers const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/'); const txHash = '0x04ee80f072ab06ec88092701e7ba223451d0a1376e26755085271bc6de45a6a1'; const tx = await provider.getTransaction(txHash); console.log(JSON.stringify(tx, null, 2)); `} {` // customPropHighlightRegexStart=viemClient\.getTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=get_transaction_viem const viemClient = createPublicClient({ transport: http('https://evm.donut.rpc.push.org/') }); const txHash = '0x04ee80f072ab06ec88092701e7ba223451d0a1376e26755085271bc6de45a6a1'; const tx = await viemClient.getTransaction({ hash: txHash }); console.log(JSON.stringify(tx, null, 2)); `} ## Fetch Blocks ### Latest Block {` // customPropHighlightRegexStart=provider\.getBlock // customPropHighlightRegexEnd=\\); // customPropGTagEvent=get_latest_block_ethers import { ethers } from 'ethers'; const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/'); const latestBlock = await provider.getBlock('latest'); console.log(JSON.stringify(latestBlock, null, 2)); `} {` // customPropHighlightRegexStart=viemClient\.getBlock // customPropHighlightRegexEnd=\\); // customPropGTagEvent=get_latest_block_viem const viemClient = createPublicClient({ transport: http('https://evm.donut.rpc.push.org/') }); const latestBlock = await viemClient.getBlock(); console.log(JSON.stringify(latestBlock, null, 2)); `} ### Block by Hash {` // customPropHighlightRegexStart=provider\.getBlock // customPropHighlightRegexEnd=\\); // customPropGTagEvent=get_specific_block_ethers const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/'); const blockHash = '0xa1a69fa217d219f71d44f4707f8aa4ded2aec52a0c737e0dca0dbf9096252b89'; const block = await provider.getBlock(blockHash); console.log(JSON.stringify(block, null, 2)); `} {` // customPropHighlightRegexStart=viemClient\.getBlock // customPropHighlightRegexEnd=\\); // customPropGTagEvent=get_specific_block_viem const viemClient = createPublicClient({ transport: http('https://evm.donut.rpc.push.org/') }); const blockHash = '0xa1a69fa217d219f71d44f4707f8aa4ded2aec52a0c737e0dca0dbf9096252b89'; const block = await viemClient.getBlock({ blockHash }); console.log(JSON.stringify(block, null, 2)); `} ## Websocket Client ### Initialize WebSocket Client {` // customPropHighlightRegexStart=const ws = new ethers\.Web // customPropHighlightRegexEnd=\\); // customPropGTagEvent=setup_ethers_websocket_provider import { ethers } from 'ethers'; const ws = new ethers.WebSocketProvider('https://evm.donut.rpc.push.org/'); console.log('Connection Opened!'); ws.on('block', async (blockNumber) => { console.log("Block produced: ", blockNumber); }); // Stop after 10s setTimeout(() => { ws.removeAllListeners(); ws.destroy(); console.log('Connection closed!'); }, 10000); `} {` // customPropHighlightRegexStart=createPublicClient\\( // customPropHighlightRegexEnd=\\); // customPropGTagEvent=setup_viem_websocket_client import { createPublicClient, webSocket } from 'viem'; const client = createPublicClient({ transport: webSocket('wss://evm.donut.rpc.push.org') }); console.log('Connection Opened!'); const stop = client.watchBlocks({ onBlock: (b) => console.log("Block produced: ", b.number), onError: console.error, }); // Stop after 10s setTimeout(stop, 10000); `} ### Subscribing to New Blocks {` // customPropHighlightRegexStart=ws\.on // customPropHighlightRegexEnd=}\\); // customPropGTagEvent=subscribe_new_blocks_ethers import { ethers } from 'ethers'; const ws = new ethers.WebSocketProvider('https://evm.donut.rpc.push.org/'); console.log('Connection Opened!'); ws.on('block', async (blockNumber) => { console.log("Block produced: ", blockNumber); }); // Stop after 10s setTimeout(() => { ws.removeAllListeners(); ws.destroy(); console.log('Connection closed!'); }, 20000); `} {` // customPropHighlightRegexStart=client\.watchBlocks // customPropHighlightRegexEnd=}\\); // customPropGTagEvent=watch_new_blocks_viem import { createPublicClient, webSocket } from 'viem'; const client = createPublicClient({ transport: webSocket('wss://evm.donut.rpc.push.org') }); const stop = client.watchBlocks({ onBlock: (b) => console.log("Block produced: ", b.number), onError: console.error, }); // Stop after 10s setTimeout(stop, 10000); `} ### Filtering New Blocks for Specific Transactions {` // customPropHighlightRegexStart=ws\.on // customPropHighlightRegexEnd=}\\); // customPropGTagEvent=subscribe_pending_transactions_ethers const ws = new ethers.WebSocketProvider('https://evm.donut.rpc.push.org/'); const watchedAddress = '0x0000000000000000000000000000000000042101'; console.log('Listening for tx on:', watchedAddress, "Try sending some $PC here"); ws.on('block', async (blockNumber) => { const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org'); const block = await provider.getBlock(blockNumber, true); if (block && block.transactions) { const txs = await Promise.all(block.transactions.map(hash => block.getTransaction(hash))); txs .filter((tx) => tx.to === watchedAddress) .forEach((tx) => console.log('Tx detected β†’', tx.hash)); console.log("Block produced: ", blockNumber); } }); // Stop after 20s setTimeout(() => { ws.removeAllListeners(); ws.destroy(); console.log('Connection closed!'); }, 20000); `} {` // customPropHighlightRegexStart=viemClient\.watchBlocks // customPropHighlightRegexEnd=}\\); // customPropGTagEvent=watch_pending_transactions_viem import { createPublicClient, webSocket } from 'viem'; const client = createPublicClient({ transport: webSocket('wss://evm.donut.rpc.push.org') }); const watchedAddress = '0x0000000000000000000000000000000000042101'; console.log('Listening for tx on:', watchedAddress, "Try sending some $PC here"); const stop = client.watchBlocks({ onBlock: async (block) => { console.log('Block produced: ', block.number) for (const txHash of block.transactions) { try { const tx = await client.getTransaction({ hash: txHash }) // Check if transaction is from watched address if (tx.to?.toLowerCase() === watchedAddress.toLowerCase()) { console.log('Tx detected β†’', tx.hash) } } catch (error) { console.error('Error fetching transaction:', txHash, error) } } }, }); // Stop after 20s setTimeout(stop, 20000); `} ## Next Steps - Dive into our [Contract Helpers](/docs/chain/build/contract-helpers) to detect and work with both cross-chain accounts and native Push Chain smart accounts. - Use our [UI Kit](/docs/chain/ui-kit) to abstract away wallet and chain logic and accelerate your front-end development. --- # Contract Helpers URL: https://push.org/docs/chain/build/contract-helpers/ Contract Helpers | Build | Push Chain Docs ## Overview When building smart contract applications on Push Chain, you’ll at times need helper contracts to surface on-chain metadataβ€”like identifying external chain users or computing deterministic smart account addresses. Push Chain provides a set of helper interfaces under the hood to simplify these workflows. One primary helper is the Universal Executor Account Factory (UEAFactory), which underpins Push Chain’s multi‐chain smart account abstraction. ## Universal Executor Account Factory > As previously mentioned, [Universal Executor Accounts (UEAs)](/docs/chain/important-concepts#account-types-on-push-chain) are a type of executor smart accounts that represent external chain users on Push Chain, allowing them to interact with Push Chain applications without having to connect, bridge, or move to Push Chain. The [Universal Executor Account Factory](https://github.com/pushchain/push-chain-contracts/blob/main/src/Interfaces/IUEAFactory.sol) is the central contract responsible for deploying and managing Universal Executor Accounts (UEAs) for users from different blockchains. ### UEAFactory Features The [UEA Factory](https://github.com/pushchain/push-chain-contracts/blob/main/src/UEAFactoryV1.sol) serves these key features: - **Multi-Chain Support**: Register and manage UEAs for users from different blockchains - **Deterministic Addresses**: Uses `CREATE2` + minimal proxies for predictable UEA addresses - **Deployment Status**: Optionally check if a UEA is already deployed - **Owner ↔ UEA Mapping**: Bidirectional mapping between Universal Accounts and their UEAs, VM types and implementations. ### UEAFactory Interface **Deployed Address**: **_`0x00000000000000000000000000000000000000eA`_** This helper contract helps in fetching cross-chain information about an address. It also provides identity-mapping between source chain wallet address and Push Chain address and can determine if the address is native to Push Chain or is proxy for external chain user. In order to use the UEAFactory in your contract, you can either: #### 1. Import it directly from Push Chain Core Repository ```solidity import "push-chain-core-contracts/src/Interfaces/IUEAFactory.sol"; ``` Do the additional steps to enable the same in your Foundry: 1. Run forge install ```bash forge install pushchain/push-chain-core-contracts ``` 2. Add remappings to your **foundry.toml** file ```toml remappings = ["push-chain-core-contracts/=lib/push-chain-core-contracts/"] ``` #### Or 2. Define the interface manually in your solidity contract ```solidity pragma solidity ^0.8.0; struct UniversalAccountId { string chainNamespace; // Chain namespace identifier of the owner account (e.g., "eip155" or "solana") string chainId; // Chain ID of the source chain of the owner of this UEA. bytes owner; // Owner's public key or address in bytes format } /// @title Universal Executor Account Factory Interface /// @notice Helper interface for deploying and querying UEAs on Push Chain interface IUEAFactory { /** * @dev Returns the owner key (UOA) for a given UEA address * @param addr Any given address ( msg.sender ) on push chain * @return account The Universal Account identity information associated with this UEA * @return isUEA True if the address addr is a UEA contract. Else it is a native EOA of PUSH chain (i.e., isUEA = false) */ function getOriginForUEA(address addr) external view returns (UniversalAccountId memory account, bool isUEA); /** * @dev Returns the computed UEA address for a given Universal Account ID and deployment status * @param _id The Universal Account identity information * @return uea The address of the UEA (computed deterministically) * @return isDeployed True if the UEA has already been deployed */ function getUEAForOrigin(UniversalAccountId memory _id) external view returns (address uea, bool isDeployed); } ``` ### UEAFactory Methods ### UEAFactory β†’ getOriginForUEA **_`getOriginForUEA(address): (UniversalAccountId, bool)`_** is external view Returns the owner information and UEA status for a given address on Push Chain. **Commonly used for**: - Checking if a given address is a native account on Push Chain or is controlled by another chain user. - Determining the source chain of a given address on Push Chain. - Getting the Universal Account identity information associated with this address. > Note: The returned origin address will be encoded in Hex format. For example, for Solana addresses, a base58 conversion should be done to get the readable format. ```solidity /** * @dev Returns the owner key (UOA) for a given UEA address * @param addr Any given address ( msg.sender ) on push chain * @return account The Universal Account identity information associated with this UEA * @return isUEA True if the address addr is a UEA contract. False if it is a native account on PUSH chain (i.e., isUEA = false) */ function getOriginForUEA( address addr ) external view returns ( UniversalAccountId memory account, bool isUEA ); ``` | Arguments | Type | Description | | --------- | --------- | ------------------------------------------------- | | _`addr`_ | `address` | Any address on Push Chain (typically msg.sender). | and `bool`" className="alert alert--fn-args"> | Response | Type | Description | | -------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------- | | account | `UniversalAccountId` | The Universal Account identity information containing: - **chainNamespace**: Chain namespace identifier (e.g., "eip155" for EVM based chains, "solana" for Solana, etc.)- **chainId**: Chain ID of the source chain of the owner of this UEA.- **owner**: Owner's public key or address in bytes format. | | isUEA | `bool` | True if the address addr is a UEA contract. False if it is a native address on PUSH chain (i.e., isUEA = false). | ```solidity function checkCallerType() public view returns (bool isUEA) { (UniversalAccountId memory account, bool isUEA) = IUEAFactory(0x00000000000000000000000000000000000000eA).getOriginForUEA(msg.sender); if (isUEA) { // Do something with the UEA } else { // Do something with the native account } } ``` {` // customPropHighlightRegexStart=factory\.getOriginForUEA // customPropHighlightRegexEnd=\\); // customPropGTagEvent=get_origin_for_uea import { ethers } from 'ethers'; import { bs58 } from 'bs58'; // β€”β€”β€” CONFIG β€”β€”β€” const RPC_URL = 'https://evm.donut.rpc.push.org/'; const FACTORY_ADDRESS = '0x00000000000000000000000000000000000000eA'; // Corrected ABI const IUEAFactoryABI = [ // returns (UniversalAccountId account, bool isUEA) "function getOriginForUEA(address addr) view returns (tuple(string chainNamespace, string chainId, bytes owner) account, bool isUEA)" ] async function main() { // 1) set up const provider = new ethers.JsonRpcProvider(RPC_URL); const factory = new ethers.Contract(FACTORY_ADDRESS, IUEAFactoryABI, provider); // 2) const someAddress = '0xbCfaD05E5f19Ae46feAab2F72Ad9977BC239b395'; // 3) call getOriginForUEA console.log("Calling getOriginForUEA on PushChain"); const originResult = await factory.getOriginForUEA(someAddress); console.log("Note: The address returned are always in hex format even for non-EVM chains.") console.log("Result -", JSON.stringify(originResult)); // 4) optional: convert non-evm chain address according to their standards if (originResult[0][0] === "solana") { // Convert hex-encoded address to base58 address format const bytesAddress = ethers.getBytes(originResult[0][2]); const base58Address = bs58.encode(bytesAddress); console.log("Solana (Base58) Address -", base58Address); } // Note: If the origin is Solana (chainNamespace === "solana"), the owner address // will be in hex format and needs to be converted to base58 for readable format } await main().catch(console.error); `} ### UEAFactory β†’ getUEAForOrigin **_`getUEAForOrigin(UniversalAccountId): (address, bool)`_** is external view Returns the computed UEA address for a given Universal Account. Additionaly, it also returns the deployment status of the UEA. **Commonly used for**: - Get or compute the UEA address for a given Universal Account. - Check if a given Universal Account is deployed or not. > Note: UniversalAccountId is a struct that returns chainNamespace, chainId and owner. `chainNamespace` contains the [chain namespace](/docs/chain/setup/chain-config/#universal-chain-namespace) (e.g., "eip155" for EVM based chains, "solana" for Solana, etc.) and `chainId` contains the chain ID of the source chain of the owner of this UEA. `owner` contains the wallet address in bytes. ```solidity /** * @dev Returns the computed UEA address for a given Universal Account ID and deployment status * @param _id The Universal Account identity information * @return uea The address of the UEA (computed deterministically) * @return isDeployed True if the UEA has already been deployed */ function getUEAForOrigin( UniversalAccountId memory _id ) external view returns ( address uea, bool isDeployed ); ``` | Arguments | Type | Description | | --------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | _`_id`_ | UniversalAccountId | The Universal Account identity information containing: - **chainNamespace**: Chain namespace identifier (e.g., "eip155" for EVM based chains, "solana" for Solana, etc.)- **chainId**: Chain ID of the source chain of the owner of this UEA.- **owner**: Owner's public key or address in bytes format. | | Response | Type | Description | | ---------- | ------- | ---------------------------------------------------- | | uea | address | The address of the UEA (computed deterministically). | | isDeployed | bool | True if the UEA has already been deployed. | ```solidity function checkUEAType() public view returns (address uea, bool isDeployed) { (address uea, bool isDeployed) = IUEAFactory(0x00000000000000000000000000000000000000eA).getUEAForOrigin(account); if (isDeployed) { // Do something with the deployed UEA } else { // UEA is not deployed yet but you have deterministic address for the UEA. } } ``` {` // customPropHighlightRegexStart=factory\.getUEAForOrigin // customPropHighlightRegexEnd=\\); // customPropGTagEvent=get_uea_for_origin import { ethers } from 'ethers'; // β€”β€”β€” CONFIG β€”β€”β€” const RPC_URL = 'https://evm.donut.rpc.push.org/'; const FACTORY_ADDRESS = '0x00000000000000000000000000000000000000eA'; // Corrected ABI const IUEAFactoryABI = [ "function getUEAForOrigin(tuple(string chainNamespace, string chainId, bytes owner) _id) view returns (address uea, bool isDeployed)" ] async function main() { // 1) set up const provider = new ethers.JsonRpcProvider(RPC_URL); const factory = new ethers.Contract(FACTORY_ADDRESS, IUEAFactoryABI, provider); // 2) create UniversalAccountId struct const universalAccountId = { chainNamespace: 'eip155', // EVM chain chainId: '11155111', // Sepolia testnet (more likely to be registered on Push testnet) owner: '0xa96CaA79eb2312DbEb0B8E93c1Ce84C98b67bF11', // owner address in bytes format }; // 3) call getUEAForOrigin console.log('Calling getUEAForOrigin on PushChain'); const originResult = await factory.getUEAForOrigin(universalAccountId); console.log('Result -', JSON.stringify(originResult)); } await main().catch(console.error); `} ## Next Steps - Wire up your SDK in [Initialize Push Chain Client](/docs/chain/build/initialize-push-chain-client) - Simplify cross-chain workflows via [Utility Functions](/docs/chain/build/utility-functions) - Dive into on-chain reads with [Reading Blockchain State](/docs/chain/build/reading-blockchain-state) - Abstract wallet & UI flows with our [UI Kit](/docs/chain/ui-kit) - Go deeper into advanced patterns in [Deep Dives](/docs/chain/deep-dives) --- # Constants Reference URL: https://push.org/docs/chain/build/constants/ Constants Reference | Build | Push Chain Docs ## Overview This page provides a comprehensive reference for all constants available in the Push Chain Core SDK (`@pushchain/core`). These constants are used throughout the SDK to ensure type safety and consistency when working with chains, networks, libraries, and other configurations. All constants are accessed via the `PushChain.CONSTANTS` namespace. ## Push Network **_`PushChain.CONSTANTS.PUSH_NETWORK`_** Defines the Push Chain network environments available for initialization. | Constant | Value | Description | | -------- | ----- | ----------- | | `PushChain.CONSTANTS.PUSH_NETWORK.MAINNET` | `MAINNET` | Push Chain mainnet environment | | `PushChain.CONSTANTS.PUSH_NETWORK.TESTNET_DONUT` | `TESTNET_DONUT` | Push Chain testnet environment (Donut) | | `PushChain.CONSTANTS.PUSH_NETWORK.TESTNET` | `TESTNET` | Push Chain testnet environment (points to latest testnet) | | `PushChain.CONSTANTS.PUSH_NETWORK.LOCALNET` | `LOCALNET` | Local development environment | ### Usage Examples ```typescript // Get supported chains for a network // Note: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET always points to the latest version of the testnet const chains = PushChain.utils.chains.getSupportedChains( PushChain.CONSTANTS.PUSH_NETWORK.TESTNET ); // Initialize client with testnet (Donut) const client = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET_DONUT, }); // Initialize client with localnet const localClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.LOCALNET, }); ``` ## Chain Constants **_`PushChain.CONSTANTS.CHAIN`_** Defines all supported blockchain chains across the Push Chain ecosystem. These constants are used for chain-specific operations, account conversions, and cross-chain transactions. | Constant | Chain Namespace | Description | | -------- | --------------- | ----------- | | `PushChain.CONSTANTS.CHAIN.PUSH_TESTNET` | `eip155:42101` | Push Chain testnet, always points to latest version of testnet | | `PushChain.CONSTANTS.CHAIN.PUSH_TESTNET_DONUT` | `eip155:42101` | Push Chain testnet (Donut) | | `PushChain.CONSTANTS.CHAIN.PUSH_LOCALNET` | `eip155:9001` | Push Chain local development | | `PushChain.CONSTANTS.CHAIN.ETHEREUM_MAINNET` | `eip155:1` | Ethereum mainnet | | `PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA` | `eip155:11155111` | Ethereum Sepolia testnet | | `PushChain.CONSTANTS.CHAIN.ARBITRUM_SEPOLIA` | `eip155:421614` | Arbitrum Sepolia testnet | | `PushChain.CONSTANTS.CHAIN.BASE_SEPOLIA` | `eip155:84532` | Base Sepolia testnet | | `PushChain.CONSTANTS.CHAIN.BNB_TESTNET` | `eip155:97` | BNB Chain testnet | | `PushChain.CONSTANTS.CHAIN.SOLANA_MAINNET` | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | Solana mainnet-beta | | `PushChain.CONSTANTS.CHAIN.SOLANA_TESTNET` | `solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z` | Solana testnet | | `PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET` | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` | Solana devnet | ### Usage Examples ```typescript // Convert address to UniversalAccount const account = PushChain.utils.account.toUniversal(address, { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, }); // Resolve a CEA back to its controlling origin account (v6 replacement // for the removed convertExecutorToOrigin / convertExecutorToOriginAccount). const originInfo = await PushChain.utils.account.resolveControllerAccount( executorAddress, { chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET } ); // Get explorer URL for specific chain const explorerUrl = pushChainClient.explorer.getTransactionUrl(txHash, { chain: PushChain.CONSTANTS.CHAIN.ARBITRUM_SEPOLIA }); ``` ## Library Constants **_`PushChain.CONSTANTS.LIBRARY`_** Defines the supported blockchain libraries for creating Universal Signers from keypairs. | Constant | Value | Description | | -------- | ----- | ----------- | | `PushChain.CONSTANTS.LIBRARY.ETHEREUM_ETHERSV6` | `'ethers-v6'` | Ethers.js v6 library | | `PushChain.CONSTANTS.LIBRARY.ETHEREUM_VIEM` | `'viem'` | Viem library for Ethereum | | `PushChain.CONSTANTS.LIBRARY.SOLANA_WEB3JS` | `'solana-web3js'` | Solana Web3.js library | ### Usage Examples ```typescript // Create Universal Signer with Ethers v6 const wallet = ethers.Wallet.createRandom(); const universalSigner = await PushChain.utils.signer.toUniversalFromKeypair( wallet, { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, library: PushChain.CONSTANTS.LIBRARY.ETHEREUM_ETHERSV6, } ); ``` ```typescript // Create Universal Signer with Solana Web3.js const keypair = Keypair.generate(); const universalSigner = await PushChain.utils.signer.toUniversalFromKeypair( keypair, { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, library: PushChain.CONSTANTS.LIBRARY.SOLANA_WEB3JS, } ); ``` ## Moveable Token Constants **_`PushChain.CONSTANTS.MOVEABLE.TOKEN`_** Defines tokens that can be transferred across chains using Push Chain's universal transaction system. | Constant | Chain | Description | | -------- | ----- | ----------- | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_SEPOLIA.ETH` | Ethereum Sepolia | Native ETH token | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_SEPOLIA.USDT` | Ethereum Sepolia | Tether USD stablecoin | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_SEPOLIA.USDC` | Ethereum Sepolia | USD Coin stablecoin | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_SEPOLIA.WETH` | Ethereum Sepolia | Wrapped ETH | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_SEPOLIA.stETH` | Ethereum Sepolia | Staked ETH (Lido) | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.ARBITRUM_SEPOLIA.ETH` | Arbitrum Sepolia | Native ETH token | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.ARBITRUM_SEPOLIA.USDT` | Arbitrum Sepolia | Tether USD stablecoin | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.ARBITRUM_SEPOLIA.USDC` | Arbitrum Sepolia | USD Coin stablecoin | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.ARBITRUM_SEPOLIA.WETH` | Arbitrum Sepolia | Wrapped ETH | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.BASE_SEPOLIA.ETH` | Base Sepolia | Native ETH token | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.BASE_SEPOLIA.USDT` | Base Sepolia | Tether USD stablecoin | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.BASE_SEPOLIA.USDC` | Base Sepolia | USD Coin stablecoin | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.BASE_SEPOLIA.WETH` | Base Sepolia | Wrapped ETH | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.BNB_TESTNET.BNB` | BNB Testnet | Native BNB token | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.BNB_TESTNET.USDT` | BNB Testnet | Tether USD stablecoin | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.BNB_TESTNET.USDC` | BNB Testnet | USD Coin stablecoin | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.SOLANA_DEVNET.SOL` | Solana Devnet | Native SOL token | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.SOLANA_DEVNET.USDT` | Solana Devnet | Tether USD stablecoin | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.SOLANA_DEVNET.USDC` | Solana Devnet | USD Coin stablecoin | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.pEth` | Push Testnet Donut | Push-wrapped ETH from Ethereum | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.pEthArb` | Push Testnet Donut | Push-wrapped ETH from Arbitrum | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.pEthBase` | Push Testnet Donut | Push-wrapped ETH from Base | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.pBnb` | Push Testnet Donut | Push-wrapped BNB from BNB Chain | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.pSol` | Push Testnet Donut | Push-wrapped SOL from Solana | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.USDT.eth` | Push Testnet Donut | Push-wrapped USDT from Ethereum | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.USDT.arb` | Push Testnet Donut | Push-wrapped USDT from Arbitrum | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.USDT.base` | Push Testnet Donut | Push-wrapped USDT from Base | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.USDT.bnb` | Push Testnet Donut | Push-wrapped USDT from BNB Chain | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.USDT.sol` | Push Testnet Donut | Push-wrapped USDT from Solana | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.USDC.eth` | Push Testnet Donut | Push-wrapped USDC from Ethereum | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.USDC.arb` | Push Testnet Donut | Push-wrapped USDC from Arbitrum | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.USDC.base` | Push Testnet Donut | Push-wrapped USDC from Base | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.USDC.bnb` | Push Testnet Donut | Push-wrapped USDC from BNB Chain | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.USDC.sol` | Push Testnet Donut | Push-wrapped USDC from Solana | ### Usage Examples ```typescript // Move USDT from Ethereum Sepolia to Push Chain const txHash = await pushChainClient.universal.sendTransaction({ to: '0xa54E96d3fB93BD9f6cCEf87c2170aEdB1D47E1cF', funds: { amount: PushChain.utils.helpers.parseUnits('100', 6), // 100 USDT token: PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_SEPOLIA.USDT, }, }); // Move SOL from Solana to Push Chain const solTxHash = await pushChainClient.universal.sendTransaction({ to: 'FNDJWigdNWsmxXYGrFV2gCvioLYwXnsVxZ4stL33wFHf', funds: { amount: PushChain.utils.helpers.parseUnits('1', 9), // 1 SOL token: PushChain.CONSTANTS.MOVEABLE.TOKEN.SOLANA_DEVNET.SOL, }, }); ``` ## Payable Token Constants **_`PushChain.CONSTANTS.PAYABLE.TOKEN`_** Defines tokens that can be used to pay for gas fees on Push Chain transactions. | Constant | Chain | Description | | -------- | ----- | ----------- | | `PushChain.CONSTANTS.PAYABLE.TOKEN.ETHEREUM_SEPOLIA.ETH` | Ethereum Sepolia | Native ETH token | | `PushChain.CONSTANTS.PAYABLE.TOKEN.ETHEREUM_SEPOLIA.USDT` | Ethereum Sepolia | Tether USD stablecoin | | `PushChain.CONSTANTS.PAYABLE.TOKEN.ETHEREUM_SEPOLIA.USDC` | Ethereum Sepolia | USD Coin stablecoin | | `PushChain.CONSTANTS.PAYABLE.TOKEN.ETHEREUM_SEPOLIA.WETH` | Ethereum Sepolia | Wrapped ETH | | `PushChain.CONSTANTS.PAYABLE.TOKEN.ETHEREUM_SEPOLIA.stETH` | Ethereum Sepolia | Staked ETH (Lido) | | `PushChain.CONSTANTS.PAYABLE.TOKEN.ARBITRUM_SEPOLIA.ETH` | Arbitrum Sepolia | Native ETH token | | `PushChain.CONSTANTS.PAYABLE.TOKEN.ARBITRUM_SEPOLIA.USDT` | Arbitrum Sepolia | Tether USD stablecoin | | `PushChain.CONSTANTS.PAYABLE.TOKEN.ARBITRUM_SEPOLIA.USDC` | Arbitrum Sepolia | USD Coin stablecoin | | `PushChain.CONSTANTS.PAYABLE.TOKEN.BASE_SEPOLIA.ETH` | Base Sepolia | Native ETH token | | `PushChain.CONSTANTS.PAYABLE.TOKEN.BASE_SEPOLIA.USDT` | Base Sepolia | Tether USD stablecoin | | `PushChain.CONSTANTS.PAYABLE.TOKEN.BASE_SEPOLIA.USDC` | Base Sepolia | USD Coin stablecoin | | `PushChain.CONSTANTS.PAYABLE.TOKEN.BNB_TESTNET.BNB` | BNB Testnet | Native BNB token | | `PushChain.CONSTANTS.PAYABLE.TOKEN.BNB_TESTNET.USDT` | BNB Testnet | Tether USD stablecoin | | `PushChain.CONSTANTS.PAYABLE.TOKEN.BNB_TESTNET.USDC` | BNB Testnet | USD Coin stablecoin | | `PushChain.CONSTANTS.PAYABLE.TOKEN.SOLANA_DEVNET.SOL` | Solana Devnet | Native SOL token | | `PushChain.CONSTANTS.PAYABLE.TOKEN.SOLANA_DEVNET.USDT` | Solana Devnet | Tether USD stablecoin | | `PushChain.CONSTANTS.PAYABLE.TOKEN.SOLANA_DEVNET.USDC` | Solana Devnet | USD Coin stablecoin | ### Usage Examples ```typescript // Pay gas fees with USDT instead of native token const txHash = await pushChainClient.universal.sendTransaction({ to: '0xa54E96d3fB93BD9f6cCEf87c2170aEdB1D47E1cF', value: PushChain.utils.helpers.parseUnits('0.1', 18), payGasWith: { token: PushChain.CONSTANTS.PAYABLE.TOKEN.ETHEREUM_SEPOLIA.USDT, slippageBps: 100, // 1% slippage tolerance }, }); // Pay gas with USDC on Arbitrum const arbTxHash = await pushChainClient.universal.sendTransaction({ to: '0xa54E96d3fB93BD9f6cCEf87c2170aEdB1D47E1cF', value: PushChain.utils.helpers.parseUnits('0.05', 18), payGasWith: { token: PushChain.CONSTANTS.PAYABLE.TOKEN.ARBITRUM_SEPOLIA.USDC, slippageBps: 50, // 0.5% slippage tolerance }, }); ``` ## Common Patterns ### Chain Selection When working with multiple chains, use the CHAIN constants to ensure consistency: ```typescript // Define supported chains for your app const SUPPORTED_CHAINS = [ PushChain.CONSTANTS.CHAIN.PUSH_TESTNET_DONUT, PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, PushChain.CONSTANTS.CHAIN.BASE_SEPOLIA, PushChain.CONSTANTS.CHAIN.ARBITRUM_SEPOLIA, PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, ]; // Get chain-specific configuration function getChainConfig(chain: string) { switch (chain) { case PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA: return { rpc: 'https://sepolia.gateway.tenderly.co', explorer: 'https://sepolia.etherscan.io' }; case PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET: return { rpc: 'https://api.devnet.solana.com', explorer: 'https://explorer.solana.com' }; // ... more chains } } ``` ### Network-Specific Initialization ```typescript // Development environment if (process.env.NODE_ENV === 'development') { const client = await PushChain.initialize(signer, { network: PushChain.CONSTANTS.PUSH_NETWORK.LOCALNET, }); } // Production/Testnet environment if (process.env.NODE_ENV === 'production') { const client = await PushChain.initialize(signer, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET_DONUT, }); } ``` ### Library Detection ```typescript function detectLibrary(signer: any) { if (signer instanceof ethers.Wallet) { return PushChain.CONSTANTS.LIBRARY.ETHEREUM_ETHERSV6; } else if (signer.type === 'viem') { return PushChain.CONSTANTS.LIBRARY.ETHEREUM_VIEM; } // ... more detection logic } ``` ## Type Safety All constants are strongly typed in TypeScript. When using TypeScript, you'll get autocomplete and type checking: ```typescript // TypeScript will autocomplete available chains const chain: typeof PushChain.CONSTANTS.CHAIN = PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA; // TypeScript will catch invalid values const invalidChain = PushChain.CONSTANTS.CHAIN.INVALID; // ❌ Type error ``` ## Next Steps - Learn how to [initialize the Push Chain client](/docs/chain/build/initialize-push-chain-client) - Explore [utility functions](/docs/chain/build/utility-functions) that use these constants - Build [universal transactions](/docs/chain/build/send-universal-transaction) across chains --- # Advanced URL: https://push.org/docs/chain/build/advanced/ Advanced Section | Build | Push Chain Docs # Advanced Section This section covers the advanced setup of Push Chain Core SDK. --- # Contract-Initiated Examples URL: https://push.org/docs/chain/build/contract-initiated-examples/ Contract-Initiated Examples Section | Build | Push Chain Docs # Contract-Initiated Examples Section Reference implementations including examples of cross-chain smart contracts on Push Chain. Each example is a complete, deployable contract you can copy, adapt, and ship. --- # How Universal Executor Account (UEA) Works URL: https://push.org/docs/chain/deep-dives/how-uea-works/ How Universal Executor Account (UEA) Works | Deep Dives | Push Chain Docs The **Universal Executor Account (UEA)** is one of the key innovations behind **Push Chain’s Universal Execution Layer**. A system that lets any user, from *any origin chain*, execute transactions natively on Push Chain. In this deep dive, we’ll explore: - 🧠 What a UEA is - βš™οΈ How transactions execute under the hood - πŸ”— How it links identity across chains - 🧩 How it differs from EOAs and Smart Accounts ## What Is a Universal Executor Account (UEA)? A **Universal Executor Account** (UEA) is an *interoperable execution identity* that lives on Push Chain but can be *controlled from any origin chain* (such as Ethereum, Solana, Base, Polygon, etc.). It acts as your **on-chain agent** on Push Chain: - It holds balances and maintains on-chain state. - It executes transactions, including batched multicalls. - It validates cross-chain signatures from other networks. - It ensures your **source-chain wallet remains in control**. - It supports **universal fee abstraction**, meaning you don’t need $PC to transact. > Think of the UEA as your universal smart account that can be signed from anywhere. ## The UEA Architecture At a system level, every UEA transaction flows through **Routing β†’ Verification β†’ Execution**. | Component | Role | Description | |------------|------|-------------| | **Universal Gateway** | Routing | Handles inbound and outbound routing between origin chains and Push Chain. Locks user fees and relays payloads. | | **Universal Validators** | Verification / Security | Form consensus on the validity of cross-chain transactions before relaying to Push Chain. | | **Universal Verification Layer (UVL)** | Signature Verification | Verifies signatures from multiple chain types (EVM, Solana, etc.) via pluggable verifiers. | | **Universal Executor Account (UEA)** | Execution | Executes encoded transactions and manages account state on Push Chain. | ```mermaid sequenceDiagram participant Origin as Origin Wallet participant Gateway as Universal Gateway participant UValidators as Universal Validators participant UEA as UEA Contract participant UVL as UVL (Verification Layer) Origin->>Gateway: locks Fees +send Tx(payload + signature) Gateway->>UValidators: verifies transaction +origin proof UValidators-->>UEA: valid UValidators->>UEA: relay(payload + signature) UEA->>UVL: verify signature UVL-->>UEA: valid UEA-->>UEA: execute(payload) ``` ## How It Works When a user signs and submits a transaction from another chain, Push Chain routes and executes it through five stages: **1. Routing (Universal Gateway)** The Universal Gateway locks the required fee on the origin chain and emits the transaction payload to Push Chain. **2. Transaction Verification (Universal Validators)** Universal Validators form consensus on the validity of the transaction. Once verified, the payload is relayed to the user’s corresponding UEA (deterministically derived from their origin wallet). **3. Payload Reception (UEA)** The UEA receives the payload and signature bundle from the validators, which it passes to the Universal Verification Layer for validation. **4. Signature Verification (UVL)** The UVL verifies the origin signature according to that chain’s ruleset β€” for example, using ECDSA for EVM chains or Ed25519 for Solana. **5. Execution (UEA)** Upon successful verification, the UEA executes the payload on Push Chain β€” performing single or multiple contract calls atomically. This design guarantees that every cross-chain transaction is validated, deterministic, and secure before execution. ### Transaction Routing Optimizations Not all cross-chain transactions require full routing through the Universal Gateway. The SDK dynamically detects whether the user’s UEA already holds sufficient fees for execution. If fees exist on the UEA: - The transaction bypasses both the Gateway and Validators. - It’s sent directly to the UEA for immediate execution. - This removes source-chain confirmation latency, giving users a near-instant experience after their first funded transaction. Subsequent transactions feel instant because the UEA can self-fund execution once fees are already available. ### Fast Mode vs Standard Mode | Mode | When It Activates | Description | |------|------------------|-------------| | Fast Mode | When native asset value ≀ $10 | Relays the transaction to the UEA after a single confirmation on the source chain. Ideal for UX-critical low-value operations. | | Standard Mode | Default | Waits for multiple block confirmations (based on re-org probability) before relaying to Push Chain for execution. | ## How is the Identity Preserved and Linked? Each UEA is deterministically linked to the user’s origin wallet address. This linkage is achieved by using the origin wallet as the **seed for UEA address generation**. Whenever a new user performs their first transaction, a UEA is automatically deployed (always gasless) and a **mapping** is created between: - The origin wallet address, and - The derived UEA address on Push Chain. This mapping is stored on the [UEAFactory contract](/docs/chain/build/contract-helpers/#ueafactory-interface) and can be queried either **on-chain** or through the SDK. This ensures every user’s identity remains consistent across chains, and their UEA always maps back to the same origin wallet. This deterministic linkage also enables advanced cross-chain use cases such as β€” - Tracking activity per chain or per identity, - Linking multi-chain accounts for the same user, and - Enabling β€œchain-vs-chain” gameplay or logic. Examples β€” - πŸ•Ή [Ballsy App](https://ballsy.push.org) β€” demonstrates chain-based PvP logic through deterministic UEAs. - πŸ” [Universal Counter Tutorial](/docs/chain/tutorials/basics/tutorial-universal-counter/#live-playground) β€” showcases UEA persistence across origin chains. ## Comparison β€” EOAs vs Smart Accounts vs UEAs | Feature | **EOA** | **Smart Account** | **Universal Executor Account (UEA)** | |----------|----------|--------------------|--------------------------------------| | **Scope** | Single chain | Single chain | Multi-chain (universal) | | **Control** | Private key | Smart contract logic | Origin-chain wallet signature | | **Execution** | Local to chain | Local to chain | Routed through Gateway + Validators | | **Atomic Multicall** | ❌ | βœ… | βœ… | | **Fee Token** | Native gas token | Configurable | Any token / sponsored / external | | **Bridging Required** | βœ… | βœ… | ❌ | | **Identity Persistence** | Chain-specific | Chain-specific | Deterministically mapped across chains | | **Verification** | ECDSA only | Custom or EIP-1271 | Cross-chain via UVL (EVM, Solana, etc.) | > In short: UEAs extend smart account logic beyond a single ecosystem, > combining programmable control with cross-chain identity and atomic execution. ## Why the UEA Matters The UEA fundamentally redefines what a blockchain account can be: - A single **execution identity** across chains. - Backed by **universal verification** instead of chain-specific keys. - Capable of **multi-call atomic execution**. - Compatible with **fee abstraction** and **sponsorship models**. Together, these properties make Push Chain the first blockchain where external users can **act natively**. Not through bridges or wrapped assets, but through their own universal accounts. --- # How Chain Executor Account (CEA) Works URL: https://push.org/docs/chain/deep-dives/how-cea-works/ How Chain Executor Account (CEA) Works | Deep Dives | Push Chain Docs The **Chain Executor Account (CEA)** is the destination-chain counterpart to the [UEA](/docs/chain/deep-dives/how-uea-works). Where a UEA is the identity an external-chain wallet uses to act *on Push Chain*, a CEA is the identity a Push Chain account uses to act *on every other chain*. Together they make universal execution **bidirectional**. In this deep dive, we'll explore: - 🧠 What a CEA is - βš™οΈ How transactions flow through it - πŸ”— How its identity is bound to a Push Chain account - 🧩 How it differs from EOAs, Smart Accounts, and UEAs ## What Is a Chain Executor Account (CEA)? A **Chain Executor Account** (CEA) is a *deterministic smart account* that lives on an *external chain* (such as Ethereum, BNB Chain, Base, Solana, etc.) and is **controlled by a Push Chain account**. The Push-side account can be a UEA, a Push-native EOA, or a Push Chain smart contract. It acts as your **on-chain agent on every external chain**: - It holds destination-chain native and tokens. - It executes transactions on destination-chain protocols, with itself as `msg.sender`. - It supports **atomic multicalls** dispatched by the Push-side account. - It can send transactions back to Push Chain through the destination chain's gateway. - Its address is **deterministic**, so you can compute it before any cross-chain activity has happened. > Think of the CEA as your wallet or contract's universal executor on every other chain, controlled entirely from Push Chain. ## The CEA Architecture At a system level, every CEA-mediated transaction flows through **Dispatch β†’ Relay β†’ Execution β†’ (optional) Return**. | Component | Role | Description | |------------|------|-------------| | **UniversalGatewayPC** | Dispatch | Push-Chain-side gateway that accepts outbound requests, locks fees in `$PC`, and emits the outbound event. | | **TSS Network** | Relay & Deployment | Observes the outbound event, derives the CEA on the destination chain, deploys it on first use, and submits the transaction from the CEA. | | **CEA Contract** | Execution | Lives on the destination chain. Runs the dispatched payload with itself as `msg.sender`. | | **CEAFactory** | Identity Binding | Per-chain factory that maps Push-side accounts to deterministic CEA addresses. | | **UniversalGateway (return leg)** | Inbound | Destination-chain gateway that the CEA calls to send results back to Push Chain. | ```mermaid sequenceDiagram participant Push as Push Account(UEA / EOA / Contract) participant UGPC as UniversalGatewayPC(Push Chain) participant TSS as TSS Network participant CEA as CEA(External Chain) participant Target as Target Contract(External Chain) participant ExtGW as UniversalGateway(External Chain) Push->>UGPC: send outbound request UGPC->>UGPC: collect fees, emit event UGPC->>TSS: outbound event TSS->>CEA: deploy if first use, submit tx CEA->>Target: execute payload (msg.sender = CEA) Target-->>CEA: result opt Return Leg CEA->>ExtGW: send return tx ExtGW->>Push: credit Push-side account end ``` ## How It Works When a Push Chain account dispatches an outbound transaction, Push Chain routes and executes it through five stages: **1. Dispatch (UniversalGatewayPC)** The Push-side account calls the gateway with the outbound request. The gateway collects the protocol fee in `$PC`, swaps part of the value into destination-chain gas, and emits the outbound event the TSS network listens for. **2. CEA Resolution (TSS Network)** The TSS network observes the event and derives the CEA on the destination chain. If the CEA has not been deployed yet, the TSS deploys it on first use at the same deterministic address that was already known. **3. CEA Funding (Vault)** The destination-chain Vault forwards the bridged amount (native or ERC20) to the CEA, leaving it ready to pay for the call. **4. Execution (CEA)** The TSS submits the transaction. The CEA runs the dispatched payload (a single call or a multicall) with itself as `msg.sender`. The destination contract has no awareness that the call originated on Push Chain; it just sees a normal address. **5. Optional Return Leg** If the payload calls back into the destination chain's gateway, that gateway emits an inbound event toward Push Chain, and the TSS relays it. Push Chain enforces that the return can only credit the Push-side account that owns the CEA, which is what makes contract-to-contract roundtrips trustless. This design guarantees that every outbound transaction has a stable identity, a verifiable origin, and a predictable settlement path on the destination chain. ### Computing the CEA Address The CEA address is fully deterministic, so applications can compute it any time without waiting for the first transaction. There are two equivalent paths: **Off-chain (SDK)** The SDK exposes [deriveExecutorAccount()](/docs/chain/build/utility-functions/#derive-executor-account), which returns the CEA address for any Push-side account on any supported destination chain, optionally with a deployment-status check. Use this in dApps, scripts, or monitoring tools that don't want to make a destination-chain RPC call. See the [Derive CEA tutorial](/docs/chain/tutorials/power-features/tutorial-derive-chain-executor-account/) for end-to-end examples. **On-chain (CEAFactory)** On every supported external chain, a `CEAFactory` contract exposes the same mapping. Calling `getCEAForPushAccount(pushAccount)` returns the CEA address (deployed or predicted) and a deployment flag. Use this when a destination-chain contract itself needs to whitelist, fund, or attribute calls to a known CEA. Both paths produce the same address. The off-chain path is cheaper and faster for read flows; the on-chain path is the only option from a destination-chain contract. ### Lazy Deployment CEAs are **never** deployed eagerly. The address is predictable and stable, but no on-chain footprint exists until the first transaction needs it. This means: - You can compute and authorize a CEA before any cross-chain activity (whitelist it, fund it, approve it on a protocol). - Deployment gas is paid by the TSS path on first use, calculated and included in the gas limit auto via the SDK. - The deterministic mapping never moves once deployed. ### Fee Mechanics CEA-mediated outbounds consume fees on both sides of the bridge, but the user pays once on Push Chain in `$PC`: | Fee | Paid In | What It Covers | |---|---|---| | Protocol Fee | `$PC` | Push Chain protocol revenue | | Gas Fee | `$PC` (auto-swapped) | Destination-chain gas for the CEA's transaction | | Return-leg Inbound Fee (roundtrips only) | Destination native | The CEA's call into the destination chain's gateway to send the inbound back | The dispatching account quotes the protocol fee plus the gas fee upfront. Surplus `msg.value` is refunded back to the dispatcher on Push Chain. **Roundtrips need funding waiting on Push Chain too.** When the return leg arrives, the Push-side recipient (the originating contract or UEA) must already hold enough `$PC` to execute whatever post-arrival logic it runs. Push Chain delivers the payload; the recipient pays for executing it from its own balance. Plan the recipient's `$PC` balance before initiating the roundtrip, not after. :::warning EOAs cannot execute roundtrip payloads Roundtrip payload execution lands on Push Chain only when the recipient supports the signature scheme, which means a Push Chain contract that implements the inbound handler, or a UEA. A plain EOA on Push Chain is a valid destination for funds but cannot run executable inbound calls. ::: ## How is the Identity Preserved and Linked? Each CEA is deterministically linked to a single Push Chain account. The `CEAFactory` on the destination chain stores both directions of this mapping: - `getCEAForPushAccount(pushAccount)` returns the CEA on this chain (deployed or predicted). - `getPushAccountForCEA(cea)` returns the Push-side account that owns this CEA. This bidirectional mapping is what makes CEAs safe for return legs. When a CEA calls back to the destination chain's gateway with a recipient, the gateway checks that the recipient matches `getPushAccountForCEA(msg.sender)` and rejects any mismatch. A malicious destination-chain contract impersonating a CEA cannot misroute funds, because Push Chain only trusts the on-chain mapping. ### Binding for Contracts For user wallets, the CEA is bound to the user's UEA. The same UEA on Push Chain produces the same CEA on each external chain forever. For Push Chain *contracts*, the CEA is bound to the contract's Push Chain address. A different deployment, even with identical bytecode, has a different CEA on every chain. For proxy patterns, the CEA is bound to the proxy address, so upgrades do not change it. A new contract address means a new CEA, and previous whitelists or balances on the destination chain do not transfer. ## Comparison β€” EOAs vs Smart Accounts vs UEAs vs CEAs | Feature | **EOA** | **Smart Account** | **UEA** | **CEA** | |----------|----------|--------------------|---------|---------| | **Lives On** | Single chain | Single chain | Push Chain | Each external chain | | **Bound To** | Private key | Smart contract logic | An external-chain wallet | A Push Chain account | | **Acts as msg.sender For** | Local-chain tx | Local-chain tx | Push Chain tx | Destination-chain tx | | **Atomic Multicall** | ❌ | βœ… | βœ… | βœ… | | **Deployment** | None | User-initiated | Lazy on first use, by Push validators | Lazy on first use, by TSS network | | **Identity Persistence** | Chain-specific | Chain-specific | Same UEA forever per origin wallet | Same CEA forever per Push account, per chain | | **Initiated By** | User key | Smart contract logic | External-chain signature | Push-Chain-side dispatch | > In short: UEAs and CEAs together make Push Chain the first network where execution flows symmetrically across chain boundaries. ## Why the CEA Matters The CEA completes the universal execution model: - **Symmetric reach.** A Push Chain account can act on any supported destination chain through a stable, deterministic identity. - **Trustless roundtrips.** The CEA-to-Push-account mapping is enforced by the destination-chain gateway, so return-leg credits cannot be misrouted. - **Pre-authorization.** CEA addresses are predictable, so destination-chain protocols can whitelist or fund a CEA before it has been deployed. - **No new mental model.** Same `msg.sender` semantics, same multicall, same lazy-deployment story as UEAs, applied to the destination chain instead of Push Chain. For hands-on usage, see the [Outbound from Push Chain](/docs/chain/build/contract-initiated-examples/outbound-from-push-chain) and [Inbound to Push Chain](/docs/chain/build/contract-initiated-examples/inbound-to-push-chain) examples, the [Derive CEA tutorial](/docs/chain/tutorials/power-features/tutorial-derive-chain-executor-account/), and the [Contract-Initiated Multichain Execution](/docs/chain/build/contract-initiated-multichain-execution) reference. --- # Intro to Push Chain URL: https://push.org/docs/chain/ Introduction | Push Chain Docs Push Chain is the first **True Universal Layer 1** blockchain, built as a **100% EVM-compatible** Proof of Stake (PoS) chain. It runs seamlessly across every chain and wallet. Write your smart contract once, deploy it on Push Chain, and instantly reach users on Ethereum, Solana, and all other supported chains without changing on-chain code. **Ready to build?** - Follow our [Quickstart](/docs/chain/quickstart) to deploy your app in minutes. - Explore core abstractions in [Important Concepts](/docs/chain/important-concepts). ## Hello Push Chain πŸ‘‹ {` // customPropHighlightRegexStart=PushChain\.initialize // customPropHighlightRegexEnd=}\\) // customPropGTagEvent=intro_initialize_client_ethers // Import Push Chain SDK and Ethers // You can use other library like veim, etc import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; async function main() { // 1. Connect to a provider (e.g., Push Chain RPC URL) const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/'); // 2. Create a random wallet (or use your own private key) const wallet = ethers.Wallet.createRandom(provider); // 3. Convert ethers signer to Universal Signer // Most popular libraries can pass just the signer to get universal signer // Or use PushChain.utils.signer.construct to create a custom one const universalSigner = await PushChain.utils.signer.toUniversal(wallet); // Initialize Push Chain SDK for use from Push Chain account const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); console.log(JSON.stringify(pushChainClient, null, 2)); } await main().catch(console.error); `} {` // customPropHighlightRegexStart=PushChain\.initialize // customPropHighlightRegexEnd=}\\) // customPropGTagEvent=intro_initialize_client_ethers_detailed // Import Push Chain SDK and Ethers // You can use other library like veim, etc import { PushChain } from '@pushchain/core' import { ethers } from 'ethers' async function main() { // 1. Connect to a provider (e.g., Push Chain RPC URL) const provider = new ethers.JsonRpcProvider('https://sepolia.gateway.tenderly.co') // 2. Create a random wallet (or use your own private key) const wallet = ethers.Wallet.createRandom(provider) // 3. Convert ethers signer to Universal Signer // Most popular libraries can pass just the signer to get universal signer // Or use PushChain.utils.signer.construct to create a custom one const universalSigner = await PushChain.utils.signer.toUniversal(wallet) // Initialize Push Chain SDK for use from Push Chain account const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }) console.log(JSON.stringify(pushChainClient, null, 2)) } await main().catch(console.error) `} {` // customPropHighlightRegexStart=PushChain\.initialize // customPropHighlightRegexEnd=}\\) // customPropGTagEvent=intro_send_transaction_example // Import Push Chain SDK and Viem // You can use other library like ethers, etc import { PushChain } from '@pushchain/core'; import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts' import { createWalletClient, http } from 'viem' import { sepolia } from 'viem/chains' async function main() { // 1. Construct account const account = privateKeyToAccount(generatePrivateKey()) // 2. Initialize signer const signer = createWalletClient({ transport: http('https://sepolia.gateway.tenderly.co'), // or your preferred RPC URL chain: sepolia, account, // {` // customPropHighlightRegexStart=PushChain\.initialize // customPropHighlightRegexEnd=}\\) // customPropGTagEvent=intro_initialize_client_solana // Import Push Chain SDK and Solana Web3.js // You can use other library like @solana/kit, etc import { PushChain } from '@pushchain/core'; import { Keypair } from '@solana/web3.js'; async function main() { // 1. Generate or import your Solana keypair const solKeypair = Keypair.generate() // 2. Convert the Solana Keypair into a Push Chain universal signer. // We use the helper toUniversalFromKeypair, which internally builds // the necessary adapter (signTransaction, signMessage). const universalSigner = await PushChain.utils.signer.toUniversalFromKeypair(solKeypair, { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, library: PushChain.CONSTANTS.LIBRARY.SOLANA_WEB3JS, }) // 3. Initialize Push Chain SDK const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }) console.log(JSON.stringify(pushChainClient, null, 2)) } await main().catch(console.error); `} ```jsx live // customPropHighlightRegexStart=sendTransaction\( // customPropHighlightRegexEnd=\); // customPropGTagEvent=intro_ui_kit_send_transaction function App() { const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, }; function Component() { const [txnHash, setTxnHash] = useState(null); const [isLoading, setIsLoading] = useState(false); const { connectionStatus } = usePushWalletContext(); const { pushChainClient } = usePushChainClient(); const handleSendTransaction = async () => { if (!pushChainClient) return; setIsLoading(true); try { const res = await pushChainClient.universal.sendTransaction({ to: '0xFaE3594C68EDFc2A61b7527164BDAe80bC302108', value: PushChain.utils.helpers.parseUnits('0.001', 18), // 0.001 PC }); setTxnHash(res.hash); } catch (err) { console.error(err); } finally { setIsLoading(false); } }; return ( {connectionStatus === PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && ( {isLoading ? 'Sending...' : 'Send Transaction'} )} {txnHash && ( <> Txn Hash: {txnHash} View in Explorer )} ); } return ( ); } ``` ## Key Innovations (Why Push Chain) Push Chain provides a unified toolkit to build truly universal dApps. Without custom bridges or multi-chain deployments, you gain: - **Universal Smart Contracts** Deploy your existing Solidity contract on Push Chain and instantly become compatible with every chain (even **different layer 1s**). - **Zero EVM Code Changes** Deploy your existing Solidity contract on Push Chain without modifying ABI, bytecode, or on-chain logic. - **Single Transaction from any Chain** Send transactions from any chain to Push Chain with just a single transaction, massively reducing the complexity of multi-chain users interaction. - **Universal Fee Abstraction** Allow users to pay gas fees in their native tokens (for example ETH or SOL). Push Chain automatically routes fees so users do not have to bridge or hold $PC tokens (native token of Push Chain). - **Wallet Abstraction** Support MetaMask, Phantom, and other wallets as well as social or email login through one unified provider. Users never need to create a new wallet simply to access your dApp. - **True Native Experience** Users from any chain will always feel that they are **interacting natively** with your App. Drawbacks of multi-chain deployments are eliminated and transactions are natively attributed to the correct chain. ## Why Build on Push Chain? Building on Push Chain delivers immediate benefits for developers and users: - **Expand Your Userbase Instantly** Deploy your existing EVM or non-EVM application on Push Chain without any on-chain code changes. Users on Ethereum, Solana, and other supported chains can access your dApp right away. - **Avoid Audit Friction** Since you do not modify your Solidity code, there is no need for a full re-audit. Simply deploy on Push Chain and use our SDKs to enable universal access. - **Deliver a Unified, Seamless UX** One application, any wallet. Users can connect with MetaMask, Phantom, or a social/email login and pay gas fees in their native token (for example ETH or SOL). No bridges or extra steps required. - **Simplify Fee Management** Push Chain automatically routes gas fees under the hood. Users do not need to hold $PC tokens or switch chains to complete transactions. - **Future-Proof Your Application** Your App can orchestrate cross-chain workflows without building separate adapters. Any added chain support on Push natively flows to your app without any codebase changes. - **Consistent Developer Tooling** Use one SDK, one set of JSON-RPC endpoints, and a unified API to build and deploy. Whether you prefer **Viem**, **Ethers**, or our **custom client**, the experience is the same across languages and frameworks. ## Developer SDKs window.open('https://github.com/pushchain/push-chain-sdk', '_blank') } > Javascript window.open('https://github.com/pushchain/push-chain-sdk', '_blank') } > React window.open('https://github.com/pushchain/push-chain-sdk', '_blank') } > React Native ## Experience Push Chain To get started with Push Chain, you can: 1. **Checkout** [Ballsy App](https://ballsy.push.org) to experience Push Chain. *You can log in with your existing wallet, email, or social accounts.* 2. **Goto** [Send Transaction Example](/docs/chain/ui-kit/examples/send-transaction-example/) to learn and play. 3. **Use our live playgrounds** to experiment with code within our documentation. 4. **Deep dive into Push Chain** fundamentals, how it works, and developer resources in our comprehensive [Knowledge Base](https://push.org/knowledge). ## Next Steps - Explore core abstractions in [Important Concepts](/docs/chain/important-concepts) - Jump to Frontend Integration via [UI Kit](/docs/chain/ui-kit/integrate-push-universal-wallet/) - Try a full-app walkthrough in [Tutorials](/docs/chain/tutorials/) - For deep dives visit our [Knowledge Base](https://push.org/knowledge/) --- # Quickstart URL: https://push.org/docs/chain/quickstart/ Quickstart | Push Chain Docs Everything you will need to get up and running in 2 minutes or less! ## Installation ```bash # Core SDK npm install @pushchain/core # plus whichever helpers you need: npm install ethers # for EVM npm install viem # alternative EVM library npm install @solana/web3.js # for Solana ``` ```bash # Core SDK yarn add @pushchain/core # plus whichever helpers you need: yarn add ethers yarn add viem yarn add @solana/web3.js ``` ## Import libraries ```typescript // Import Push Chain SDK and Ethers // You can use other library like veim, etc // THIS EXAMPLE FOLLOWS ETHERS IMPLEMENTATION import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; ``` ```typescript // Import Push Chain SDK and Viem // You can use other library like ethers, etc // THIS EXAMPLE FOLLOWS VIEM IMPLEMENTATION import { PushChain } from '@pushchain/core'; import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts' import { createWalletClient, http } from 'viem' import { sepolia } from 'viem/chains' ``` ```typescript // Import Push Chain SDK and Solana Web3.js // You can use other library like @solana/kit, etc // THIS EXAMPLE FOLLOWS SOLANA WEB3JS IMPLEMENTATION import { PushChain } from '@pushchain/core'; import { Keypair } from '@solana/web3.js'; ``` ## Create a Universal Signer ```typescript // (Inside an async function or top-level-await context) // 1. Connect to a provider (e.g., Push Chain RPC URL) const provider = new ethers.JsonRpcProvider('https://sepolia.gateway.tenderly.co') // 2. Create a random wallet (or use your own private key) const wallet = ethers.Wallet.createRandom(provider) // 3. Convert ethers signer to Universal Signer // Most popular libraries can pass just the signer to get universal signer // Or use PushChain.utils.signer.construct to create a custom one const universalSigner = await PushChain.utils.signer.toUniversal(wallet) ``` ```typescript // (Inside an async function or top-level-await context) // 1. Create a random wallet (or use your own private key) const account = privateKeyToAccount(generatePrivateKey()) // 2. Initialize signer const signer = createWalletClient({ transport: http('https://sepolia.gateway.tenderly.co'), // or your preferred RPC URL chain: sepolia, account, }) // 3. Convert signer to Universal Signer const universalSigner = await PushChain.utils.signer.toUniversal(signer) ``` ```typescript // (Inside an async function or top-level-await context) // 1. Generate or import your Solana keypair const solKeypair = Keypair.generate(); // 2. Convert the Solana Keypair into a Push Chain universal signer. // We use the helper `toUniversalFromKeypair`, which internally builds // the necessary adapter (signTransaction, signMessage). const universalSigner = await PushChain.utils.signer.toUniversalFromKeypair(solKeypair, { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, library: PushChain.CONSTANTS.LIBRARY.SOLANA_WEB3JS, }) ``` ## Initialize Push Chain SDK ```typescript // ONCE UNIVERSAL SIGNER IS CREATED // ALL CHAIN IMPLEMENTATION BECOMES UNIVERSAL // (Inside an async function or top-level-await context) // Initialize Push Chain SDK const pushChainClient = await PushChain.initialize(universalSigner, { network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, }); ``` ## Send Transaction ```typescript // ONCE UNIVERSAL SIGNER IS CREATED // ALL CHAIN IMPLEMENTATION BECOMES UNIVERSAL // (Inside an async function or top-level-await context) // Send a universal transaction (from any chain to Push Chain) const txHash = await pushChainClient.universal.sendTransaction({ to: '0xa54E96d3fB93BD9f6cCEf87c2170aEdB1D47E1cF', // To address on Push Chain value: BigInt(1), // $PC Value to send }); console.log('Transaction sent:', txHash); ``` ## Inspect Accounts ```typescript // ONCE PUSH CHAIN CLIENT IS INITIALIZED // ALL CHAIN IMPLEMENTATION BECOMES UNIVERSAL // Get the account that is connected to Push Chain Client const pushChainAccount = pushChainClient.universal.account; console.log( 'Account connected to Push Chain Client:', pushChainAccount.address ); // Get the account that is connected to Push Chain Client const originAccount = pushChainClient.universal.origin; console.log( 'Origin address that is controlling the account connected to Push Chain Client' ); console.log( "Origin address is only present if other chain's address is connected to Push Chain Client" ); console.log('Else it will be the same as pushChainClient.universal.account'); console.log('Origin address:', originAccount.address); ``` ## Scaffold a Universal Dapp (Alternative) **Don’t want to wire Core SDK manually?** Use our CLI to scaffold a full-stack dApp (Core + UI Kit, with React boilerplate). ```bash npx create-universal-dapp my-app ``` > This is ideal if you’re building a frontend dApp right away. If you only need Core SDK (backend, scripts, integrations), follow the Quickstart steps above. ## Next Steps - Start Building with [Core SDK](/docs/chain/build/) - Explore core abstractions in [Important Concepts](/docs/chain/important-concepts) - Try a full-app walkthrough in [Tutorials](/docs/chain/tutorials/) - For deep dives visit our [Knowledge Base](https://push.org/knowledge/) --- # Important Concepts URL: https://push.org/docs/chain/important-concepts/ Important Concepts | Push Chain Docs Before integrating the SDK, here are the core ideas you need to know to build truly universal dApps on Push Chain. > **Deep dives and conceptual guides** live in our [Knowledge Base](/knowledge). ## 100% EVM Compatibility Push Chain is an EVM-compatible Universal Layer 1 blockchain that runs any Solidity contract as-is. Your existing Ethereum dApp will work without touching a single byte of on-chain code. ## Wallet Integration Across Chains Push Chain introduces groundbreaking support for wallets from different Layer 1 blockchains, enabling them to transact directly on Push Chain. Users can leverage their existing wallets, whether Ethereum-based (MetaMask), Solana-based (Phantom), or wallets from other chains, to execute transactions seamlessly on Push Chain. Under the hood, we: - Detect the source-chain wallet signature. - Map it to a Push Chain Universal Executor Account (UEA). - Route the transaction through our gateway onto Push Chain. **Your users sign exactly as they do today;** Push Chain handles the cross-chain plumbing. ## Fee Abstraction and Cross-Chain Execution Push Chain lets users execute contracts without holding $PC (Push Chain native token). Instead, users can initiate transactions from their source chains, such as Ethereum Sepolia or Solana Devnet, and pay gas fees in their native tokens like ETH or SOL. When a user signs a transaction from a source chain such as Ethereum Sepolia or Solana Devnet, the orchestrator deploys a smart wallet (UEA) on Push Chain for that user, locks the required gas fees in their native tokens, and executes the contract on Push Chain using the signed payload. **Your users interact exactly as they would on their home chain**, with no additional steps. ## Universal Gateway (UG) Contracts Push Chain uses a set of contracts to enable cross-chain transactions. These contracts are deployed on source chains from where the transactions originate and are used to route transactions from source chains to Push Chain. ## Account Types on Push Chain As an EVM-compatible Universal Layer 1 blockchain, Push Chain naturally supports standard Ethereum accounts: - **Externally Owned Accounts (EOAs)** Standard private-key-controlled addresses (e.g. MetaMask wallets). - **Smart Contract Accounts (Smart Accounts)** On-chain contracts that hold logic (e.g. multisigs, social recovery wallets). > Additionally, Push Chain innovates by introducing: - **Universal Executor Accounts (UEAs)** Proxy accounts that represent external chain wallets (users) on Push Chain and act as their execution layer for all on-chain activity. UEAs enable Push Chain to: - Execute transactions on behalf of users without requiring native Push wallets. - Abstract away chain-specific differences in signing and execution. - Provide a consistent execution identity for users across all interactions on Push Chain. In simple terms, a UEA is the user’s execution account on Push Chain. They are always deterministic and are derived from UOAs. - **Universal Origin Accounts (UOAs)** The original source-chain wallet in chain agnostic address format that is behind each UEA. UOAs let you attribute activity back to the user’s home chain and act as the **controller identity** of their corresponding UEA. - **Chain Executor Accounts (CEAs)** Chain-specific executor accounts deployed on external chains (e.g. Ethereum, BNB, Solana) that act on behalf of a user’s Universal Executor Account (UEA). They can also represent Push native accounts (including smart contracts) when interacting with external chains. CEAs enable Push Chain to: - Execute outbound transactions on external chains. - Route responses and callbacks back to Push Chain (inbound flows). - Maintain a consistent execution identity across chains. In simple terms, if a UEA represents a user on Push Chain, a CEA represents that same user on an external chain. > **Mental model** > UOA = user identity, > UEA = execution on Push Chain, > CEA = execution on external chains. ## Understanding Universal Account The `UniversalAccount` is a chain-agnostic way of representing a wallet address, designed to work seamlessly across multiple blockchain ecosystems. {` // customPropHighlightRegexStart=PushChain\.utils\.account\.toUniversal // customPropHighlightRegexEnd=(\\);|\\}\\);) // customPropGTagEvent=concepts_universal_account_example import { PushChain } from '@pushchain/core'; async function main() { // Ethereum Sepolia const ethereumAccount = PushChain.utils.account.toUniversal('0x742d35Cc6370C742Fc60f8b67da6c68F091C42b5', { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, }); console.log(JSON.stringify(ethereumAccount, null, 2)); // Solana Testnet const solanaAccount = PushChain.utils.account.toUniversal('ySYrGNLLJSK9hvGGpoxg8TzWfRe8ftBtDSMECtx2eJR', { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, }); console.log(JSON.stringify(solanaAccount, null, 2)); } await main().catch(console.error); `} - `address` follows each chain’s format (EVM checksummed, Solana base58). - `chain` is the identifier of the origin chain of an address (e.g. `PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA` resolves to `eip155:11155111` for Sepolia) > `UniversalAccount` return a [chain agnostic address](#chain-agnostic-address-examples 'Examples of chain agnostic address') format. It can be used to represent any address from any chain. ## Understanding Universal Signer A `UniversalSigner` extends `UniversalAccount` with signing capabilities. {` // customPropHighlightRegexStart=PushChain\.utils\.signer\.toUniversal // customPropHighlightRegexEnd=(\\);|\\}\\);) // customPropGTagEvent=concepts_universal_signer_example import { PushChain } from '@pushchain/core'; import { ethers } from 'ethers'; import { Keypair } from '@solana/web3.js'; async function main() { // Ethereum Sepolia const ethwallet = ethers.Wallet.createRandom(); const ethprovider = new ethers.JsonRpcProvider('https://sepolia.gateway.tenderly.co'); const signer = ethwallet.connect(ethprovider); const universalSignerFromEth = await PushChain.utils.signer.toUniversal(signer); // Solana Testnet const solKeypair = Keypair.generate(); const universalSignerFromSol = await PushChain.utils.signer.toUniversalFromKeypair(solKeypair, { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, library: PushChain.CONSTANTS.LIBRARY.SOLANA_WEB3JS, }); console.log(JSON.stringify(universalSignerFromEth, null, 2)); console.log(JSON.stringify(universalSignerFromSol, null, 2)); } await main().catch(console.error); `} - `signer` is the signer object from the library you are using (e.g. ethers, viem, solana-web3.js) ## Chain Agnostic Address Examples | Chain | Network | CAIP-10 Identifier | | -------- | ---------------------------- | ------------------------------------------------------------------------------------------------------- | | Ethereum | Mainnet (1) | `eip155:1:0x742d35Cc6370C742Fc60f8b67da6c68F091C42b5` | | Ethereum | Sepolia Testnet (11155111) | `eip155:11155111:0x5FbDB2315678afecB367f032d93F642f64180aa3` | | Solana | Mainnet-Beta (5eykt4...) | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:9xQeWvGFvPEZZY3Yvj5V14xi4tYmEXjfSDrm5sVqTvcAg` | | Solana | Devnet (EtWTRA...) | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1:7fCAbbLejF64HTZ39rjFBUXJEMYT9z7d6NM6ovaoyNaW` | | Cosmos | cosmoshub-4 (cosmos1sk8...) | `cosmos:cosmoshub-4:cosmos1sk8uyz4u6zmxus3aurayrjyvfgtytvpnr685ur` | > The addresses are inspired from [caip-10](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md 'Link to caip-10 proposal') format. ## Universal Chain Namespace Every external chain is represented as a particular constant on Push Chain. Mentioned below are some of the supported testnet and mainnet chain namespaces on Push Chain. Some namespaces are only available on testnet or mainnet. | Chain | Namespace | Assigned Constant | | ------------------------ | ------------------------------------------ | --------------------------- | | Push Testnet (Donut) | `eip155:42101` | `PUSH_TESTNET_DONUT` | | Ethereum Sepolia | `eip155:11155111` | `ETHEREUM_SEPOLIA` | | Solana Devnet | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` | `SOLANA_DEVNET` | | Push Mainnet | `To Be Announced` | `PUSH_MAINNET` | | Ethereum Mainnet | `eip155:1` | `ETHEREUM_MAINNET` | | Solana Mainnet | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | `SOLANA_MAINNET` | > The namespaces are inspired from [caip-10](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md 'Link to caip-10 proposal') format. ## Next Steps - Setup your [Environment and Tooling](/docs/chain/setup/) - See SDK reference in [Build](/docs/chain/build/) - Try your first transaction in [Send Transaction](/docs/chain/build/send-universal-transaction) - Integrate and abstract implementation and UI via [UI Kit](/docs/chain/ui-kit/) - Follow a full walkthrough in [Tutorials](/docs/chain/tutorials/) - Dive deeper in the [Knowledge Base](https://push.org/knowledge/) --- # For AI Agents & LLMs URL: https://push.org/docs/chain/for-ai-agents/ For AI Agents & LLMs | Push Chain Docs Push Chain provides structured, machine-readable resources so AI coding assistants, agents, and automation pipelines can understand and execute cross-chain actions with maximum reliability. ## Context Files AI models have a context window β€” the amount of text they can process at once. Providing structured documentation upfront helps models give precise answers without hallucinating. Push Chain offers two context files: | File | Best for | |---|---| | [`/llms.txt`](pathname:///llms.txt) | Compact summary with links to every resource. Works with most models (100K+ token context). | | [`/llms-full.txt`](pathname:///llms-full.txt) | Full documentation corpus inline. Use when your model has a large context window or you want deep reference without following links. | ## Add to Your AI Code Editor ### Cursor 1. Open **Cursor Settings β†’ Features β†’ Docs** 2. Click **Add new doc** and paste one of the following: ``` https://push.org/llms.txt ``` ``` https://push.org/llms-full.txt ``` 3. Use `@Docs β†’ Push Chain` in the chat to reference Push Chain documentation. ### Windsurf Add to the Cascade window (`CMD+L`) at the start of your conversation: ``` @docs:https://push.org/llms.txt ``` ``` @docs:https://push.org/llms-full.txt ``` ### Claude Code Reference directly in your prompt or CLAUDE.md system context: ``` https://push.org/llms.txt ``` For richer integration, use the structured `/agents/` layer below β€” it provides typed capabilities, execution workflows, and decision trees that go beyond static documentation. ### Zed / Other Editors Paste the URL into your AI assistant's context or system prompt. The `/llms.txt` format is understood by any LLM. ## Agent Layer (`/agents/`) Push Chain goes beyond static documentation with a full machine-readable execution layer at `/agents/`. This is organized as a layered stack: **discovery β†’ capabilities β†’ execution β†’ validation**. | File | What it contains | |---|---| | [/agents/index.json](pathname:///agents/index.json) | Discovery map β€” every file, its purpose, and the recommended traversal order | | [/agents/capabilities.json](pathname:///agents/capabilities.json) | Every SDK capability with inputs, outputs, and method signatures | | [/agents/sdk-capabilities.json](pathname:///agents/sdk-capabilities.json) | Full SDK namespace map including all methods and advanced arguments | | [/agents/supported-chains.json](pathname:///agents/supported-chains.json) | Verified chain list with CAIP-2 IDs, RPC URLs, explorers, and contract addresses | | [/agents/contract-addresses.json](pathname:///agents/contract-addresses.json) | Verified smart contract addresses for Push Chain core contracts, PRC-20 tokens, AMM pools, and all external chain gateways | | [/agents/workflows/index.json](pathname:///agents/workflows/index.json) | Step-by-step execution guides for all common tasks | | [/agents/schemas/index.json](pathname:///agents/schemas/index.json) | JSON schemas for all SDK request/response types | | [/agents/decision-tree.json](pathname:///agents/decision-tree.json) | Branching logic to select the right capability from user intent | | [/agents/task-router.md](pathname:///agents/task-router.md) | Plain-language routing guide mapping goals to capabilities | | [/agents/errors.json](pathname:///agents/errors.json) | Error catalog with recovery actions for every known failure mode | | [/agents/retrieval-map.json](pathname:///agents/retrieval-map.json) | Maps every capability to its authoritative documentation source (for RAG) | ## Canonical Workflows Ready-to-execute step-by-step guides for the most common Push Chain tasks: | Workflow | What it does | |---|---| | [/agents/workflows/initialize-client.md](pathname:///agents/workflows/initialize-client.md) | Create a `PushChainClient` from any signer | | [/agents/workflows/create-universal-signer.md](pathname:///agents/workflows/create-universal-signer.md) | Wrap an EVM or Solana signer into a `UniversalSigner` | | [/agents/workflows/send-universal-transaction.md](pathname:///agents/workflows/send-universal-transaction.md) | Execute a transaction on Push Chain from any origin chain | | [/agents/workflows/send-multichain-transaction.md](pathname:///agents/workflows/send-multichain-transaction.md) | Send to an external chain via CEA or cascade pattern | | [/agents/workflows/track-transaction.md](pathname:///agents/workflows/track-transaction.md) | Monitor universal transaction lifecycle | | [/agents/workflows/sign-universal-message.md](pathname:///agents/workflows/sign-universal-message.md) | Sign a message for off-chain verification | | [/agents/workflows/read-blockchain-state.md](pathname:///agents/workflows/read-blockchain-state.md) | Query on-chain state via EVM clients | | [/agents/workflows/use-contract-helpers.md](pathname:///agents/workflows/use-contract-helpers.md) | Interact with UEA Factory and other native contracts | | [/agents/workflows/constants-reference.md](pathname:///agents/workflows/constants-reference.md) | All chain IDs, token constants, and SDK enums | | [/agents/workflows/configure-dev-environment.md](pathname:///agents/workflows/configure-dev-environment.md) | Install SDK and configure Hardhat / Foundry / Remix | ## Integration Paths ### Human developer 1. Follow the [Quickstart](/docs/chain/quickstart) to run your first transaction 2. Work through [Tutorials](/docs/chain/tutorials/) for end-to-end flows 3. Use the [SDK](/docs/chain/build/) and [UI Kit](/docs/chain/ui-kit/integrate-push-universal-wallet/) for production integration ### AI agent / copilot 1. Fetch [/llms.txt](pathname:///llms.txt) as the entry layer 2. Load [/agents/index.json](pathname:///agents/index.json) to discover all capabilities and workflows 3. Use [/agents/decision-tree.json](pathname:///agents/decision-tree.json) to map user intent to the right capability 4. Execute using the matching workflow from [/agents/workflows/](pathname:///agents/workflows/index.json) 5. Validate with schemas from [/agents/schemas/](pathname:///agents/schemas/index.json) 6. Fetch [/llms-full.txt](pathname:///llms-full.txt) when full inline context is needed ### RAG / retrieval pipeline 1. Index [/llms-full.txt](pathname:///llms-full.txt) as your document corpus 2. Use [/agents/retrieval-map.json](pathname:///agents/retrieval-map.json) to map queries to authoritative sources 3. Ground responses with canonical workflows from [/agents/workflows/](pathname:///agents/workflows/) ## Notes - Treat [/llms.txt](pathname:///llms.txt) as the **entry layer** and [/agents/](pathname:///agents/) as the **execution layer** - Prefer [/agents/workflows/](pathname:///agents/workflows/) over raw docs for execution-oriented tasks β€” workflows are structured for direct SDK use - Use [/agents/schemas/](pathname:///agents/schemas/) for precise input validation before any transaction - [/agents/supported-chains.json](pathname:///agents/supported-chains.json) contains verified RPC URLs, chain IDs, block explorers, and contract addresses β€” use it instead of guessing - [/agents/contract-addresses.json](pathname:///agents/contract-addresses.json) is the authoritative registry for all Push Chain contract addresses β€” prefer it over the human-readable [Smart Contract Address Book](/docs/chain/setup/smart-contract-address-book) for programmatic use - All code examples in [/agents/examples/](pathname:///agents/examples/) are minimal, self-contained, and ready to execute --- # Setup URL: https://push.org/docs/chain/setup/ Setup Section | Push Chain Docs # Setup Section This section covers everything you will require to setup your tooling and environment to start building on Push Chain. --- # Build URL: https://push.org/docs/chain/build/ Build Section | Push Chain Docs # Build Section This section covers everything you will require from Push Chain Core SDK to create your Universal Application. --- # UI Kit URL: https://push.org/docs/chain/ui-kit/ UI Kit Section | Push Chain Docs # UI Kit Section UI Kit from Push Chain allows you to seamlessly make your app compatible with all layer 1s and l2s instantly. It abstracts away the wallet connection logic, no matter from which chain you connect app your from. Integration takes less than 5 minutes and is plug and play for use by any **React based app**, no matter from which chain you connect your app from. --- # Tutorials URL: https://push.org/docs/chain/tutorials/ Tutorials Section | Push Chain Docs # Tutorials Section Tutorials to enable you to build your first Universal Application. --- # Deep Dives URL: https://push.org/docs/chain/deep-dives/ Deep Dives Section | Push Chain Docs # Deep Dives Section Dive into concepts, advanced topics and understand the inner workings of Push Chain. --- # System & Node Tools URL: https://push.org/docs/chain/node-and-system-tools/ Node & System Tools Section | Push Chain Docs # Node & System Tools Section Learn about node and system tools available for Push Chain. This includes walkthroughs around How to run a validator, localnet, JSON-RPC functions, etc. --- # Code Snippet Playground URL: https://push.org/docs/chain/code-snippet/ > Interactive code playground for Push Chain docs Code Snippet Platground | Push Chain Docs --- # Changelog URL: https://push.org/docs/chain/changelog/ Changelog | Push Chain Docs Keep track of the latest changes and updates to the Push Chain Core and UI Kit SDKs. ## [@pushchain/core](https://www.npmjs.com/package/@pushchain/core) ### 5.2.0 (unreleased) #### Other Changes - Route 1 (UOA β†’ Push) now applies Case A/B/C USD-bucket sizing to the - Gas abstraction scope narrowed to R1 (fee-lock USD caps) and R3 (outbound ### 5.1.0 (2026-03-28) #### Other Changes - release: bump to 5.0.0 ### 3.0.4 (2025-11-11) #### Other Changes - refactor: remove debug console logs from Orchestrator class - refactor: fix sendTxWithFunds_new for new UEA ### 3.0.1 (2025-11-10) #### Other Changes - release: bump to 3.0.0 ### 3.0.0 (2025-11-10) #### Features - add getPRC20Mapping utility in utils #### Chores - add new PRC20 token and contract addresses #### Other Changes - refactor: rename ExecuteParams.payWith to payGasWith (breaking) - refactor: implement _buildMulticallPayloadData; allow BSC for multicall; rename MulticallCall type MultiCall - refactor: update RPC URLs and rename currency in Push Testnet Donut config - refactor: internal payload-builders improvements ### 2.1.1 (2025-10-26) #### Other Changes - release: bump to 2.0.21 ### 2.0.0 (2025-09-18) #### Fixes - svm gateway idl - update contracts and price fn ### 1.1.35 (2025-08-26) #### Chores - fix tc ### 1.1.34 (2025-08-18) #### Fixes - update return type for encodeFunctionData method to ensure correct string format ### 1.1.33 (2025-08-13) #### Chores - bump version to 1.1.32 and update GitHub Action test comment - Add fundGas property to sendTransaction - add reinitialize method to PushChain for dynamic signer updates #### Other Changes - refactor: update error messages for fundGas validation in PushChain tests and orchestrator - refactor: add read-only accessors for Orchestrator configuration - refactor: make isReadMode property public in PushChain class - refactor: move isUniversalAccount function to static method in PushChain class - release: bump to 1.1.32 [skip ci] - patch: implement read-only mode for UniversalAccount in PushChain ### 1.1.31 (2025-08-12) #### Chores - update test comment to trigger GitHub Action 10 ### 1.1.30 (2025-08-12) #### Chores - update test comment to trigger GitHub Action 9 ### 1.1.29 (2025-08-11) #### Chores - bump core version to ui-kit ### 0.4.0 (2025-08-11) #### Features - Add UEA proxy (migration support) + testnet module renaming [#190](https://github.com/pushchain/push-chain-sdk/pull/190) ### 0.3.1 (2025-08-08) #### Other Changes - refactor: rename and enhance executor-origin conversion utilities ### 0.3.0 (2025-08-05) #### Features - enhance transaction handling and origin fetching ### 0.2.0 (2025-08-01) #### Features - New response type [#180](https://github.com/pushchain/push-chain-sdk/pull/180) ### 0.1.43 (2025-07-30) #### Other Changes - docs: update README with test change ### 0.1.42 (2025-07-29) #### Features - implement getOriginForUEA function in Utils for Push Testnet ### 0.1.41 (2025-07-21) #### Fixes - refine fee locking logic in Orchestrator to handle UEA deployment and fund sufficiency ### 0.1.40 (2025-07-09) #### Features - 1 Click Signature [#159](https://github.com/pushchain/push-chain-sdk/pull/159) ### 0.1.38 (2025-07-04) #### Fixes - revert erip712Domain - does not work with ethers ### 0.1.37 (2025-07-04) #### Fixes - add eip712Hash ### 0.1.35 (2025-07-03) #### Chores - fix changelog gen [#158](https://github.com/pushchain/push-chain-sdk/pull/158) ### 0.1.34 (2025-07-03) #### Chores - change release script ### 0.1.33 (2025-07-03) #### Chores - revise changelog design, change release commands - revise changelog - changelog scripts changes #### Fixes - revert package version - release fix ## [@pushchain/ui-kit](https://www.npmjs.com/package/@pushchain/ui-kit) ### 5.2.7 (2026-04-24) #### Other Changes - release: bump to 5.2.6 - release: bump to 5.2.5 - release: bump to 5.2.4 ### 5.2.3 (2026-04-20) #### Other Changes - release: bump to 5.1.1 ### 5.2.3 (2026-04-20) #### Other Changes - release: bump to 5.1.1 ### 5.2.3 (2026-04-20) #### Other Changes - release: bump to 5.1.1 ### 5.2.3 (2026-04-20) #### Other Changes - release: bump to 5.1.1 ### 5.2.3 (2026-04-20) #### Other Changes - release: bump to 5.1.1 ### 5.1.0 (2026-03-28) #### Other Changes - release: bump to 5.0.0 ### 5.0.3 (2026-03-28) #### Other Changes - release: bump to 5.0.0 ### 5.0.2 (2026-03-28) #### Other Changes - release: bump to 5.0.0 ### 5.0.1 (2026-03-28) #### Other Changes - release: bump to 5.0.0 ### 4.0.6 (2026-01-13) #### Chores - update yarn lock #### Fixes - apply CSS variables via inline styles ### 4.0.2 (2025-12-25) #### Features - add support for zerion and rabby ### 2.1.5 (2025-11-17) #### Chores - update core sdk to v3.0.8 ### 2.1.3 (2025-11-17) #### Fixes - fix metamask signTypedData to handle BigInt #### Other Changes - release: bump to 2.1.2 - release: bump to 2.1.1 ### 2.1.0 (2025-11-11) #### Chores - update core to v3.0.4, update rpc urls and fix progress toast #### Other Changes - release: bump to 2.0.16 ### 2.0.13 (2025-10-23) #### Features - add bnb and other fixes ### 2.0.11 (2025-10-21) #### Features - Read Only feature implementation - cancel transaction on close drawer - add read only feature for ui-kit #### Chores - update package json #### Fixes - revert environment and polyfil changes - fix the params for reconnect external wallet ### 2.0.10 (2025-10-17) #### Features - add base and arbitrum wallet ### 2.0.9 (2025-10-06) #### Chores - update core version to 2.0.16 ### 2.0.6 (2025-09-24) #### Chores - update core sdk to v2.0.8 ### 2.0.5 (2025-09-24) #### Chores - update yarn lock #### Fixes - update the variables affecting the DOM ### 2.0.2 (2025-09-19) #### Features - update core sdk to 2.0.2 ### 2.0.0 (2025-09-18) #### Features - upgrade core sdk and fix big int check in send txn ### 1.1.34 (2025-09-04) #### Chores - add chain config to the constants ### 1.1.33 (2025-08-13) #### Chores - update GitHub Action test comment to trigger workflow ### 1.1.32 (2025-08-13) #### Fixes - fix the zIndex for the blur background ### 1.1.31 (2025-08-12) #### Chores - add test comment to trigger GitHub Action 1 ### 1.1.30 (2025-08-11) #### Fixes - fix the vertical position of the iframe modal ### 1.1.29 (2025-08-11) #### Chores - upgrade the core sdk to v1.1.29 #### Fixes - add default connected layout #### Other Changes - release: bump to 1.1.28 ### 1.1.28 (2025-07-30) #### Fixes - add default connected layout ### 1.1.27 (2025-07-29) #### Features - new wallet UI #### Chores - update package.json with homepage, keywords, repository, authors, and license #### Other Changes - docs: update README to reflect new branding and provide installation instructions ### 1.1.24 (2025-07-18) #### Other Changes - refactor: add changelog --- # Running Push Validator URL: https://push.org/docs/chain/node-and-system-tools/running-push-validator/ Running Push Validators | Deep Dives | Push Chain Docs **Fast validator setup for Push Chain** ## πŸš€ Quick Start ### Step 1: Install & Start ```bash curl -fsSL https://get.push.network/node/install.sh | bash ``` Automatically installs and starts your validator using snapshot download (no full sync needed). > **Note:** Restart terminal or run `source ~/.bashrc` to use `push-validator` from anywhere. ### Step 2: Verify Sync ```bash push-validator status ``` Wait for: `βœ… Catching Up: false` (snapshot download takes ~5-20 mins depending on connection, then block sync begins) ### Step 3: Register Validator ```bash push-validator register-validator ``` **Wallet Setup:** You'll be prompted to either: 1. **Create new wallet** β€” Generates a new recovery phrase (save it securely!) 2. **Import existing wallet** β€” Use your existing 12/24-word recovery phrase **Configuration:** You'll also set: - **Moniker** β€” Your validator's display name (must be unique on the network) - **Commission Rate** β€” Fee charged to delegators (5-100%, default: 10%) - **Stake Amount** β€” How much to stake (minimum: 1.5 PC) **Requirements:** 2+ PC tokens from [faucet](https://faucet.push.org) **Done! Your validator is running with automatic recovery enabled! πŸŽ‰** ## πŸ“Š Dashboard Monitor your validator in real-time with an interactive dashboard: ```bash push-validator dashboard ``` **Features:** - **Node Status** - Process state, RPC connectivity, resource usage (CPU, memory, disk) - **Chain Sync** - Real-time block height, sync progress with ETA, network latency - **Validator Metrics** - Bonding status, voting power, commission rate, accumulated rewards - **Network Overview** - Connected peers, chain ID, active validators list - **Live Logs** - Stream node activity with search and filtering - **Auto-Refresh** - Updates every 2 seconds for real-time monitoring The dashboard provides everything you need to monitor validator health and performance at a glance. ## πŸ“– Commands ### Core ```bash push-validator start # Start node with snapshot sync push-validator stop # Stop node push-validator status # Check sync & validator status push-validator dashboard # Live interactive monitoring dashboard push-validator register-validator # Register as validator (create or import wallet) push-validator logs # View logs ``` ### Validator Operations ```bash push-validator increase-stake # Increase validator stake and voting power push-validator unjail # Restore jailed validator to active status push-validator withdraw-rewards # Withdraw validator rewards and commission push-validator restake-rewards # Auto-withdraw and restake all rewards to increase validator power ``` ### Monitoring ```bash push-validator sync # Monitor sync progress push-validator peers # Show peer connections (from local RPC) push-validator doctor # Run diagnostic checks on validator setup ``` ### Management ```bash push-validator restart # Restart node push-validator validators # List validators (supports --output json) push-validator balance # Check balance (defaults to validator key) push-validator reset # Reset chain data (keeps address book) push-validator full-reset # ⚠️ Complete reset (deletes ALL keys and data) push-validator backup # Backup config and validator state push-validator update # Update CLI to latest version ``` ## ⚑ Features - **Snapshot Download**: Fast sync (~5-20 mins, no full blockchain download required) - **Interactive Logs**: Real-time log viewer with search and filtering - **Smart Detection**: Monitors for sync stalls and network issues - **Reliable Snapshots**: Uses trusted RPC nodes for recovery - **Multiple Outputs**: JSON, YAML, or text format support ## πŸ”„ Automatic Upgrades (Cosmovisor) Your validator uses [Cosmovisor](https://github.com/cosmos/cosmos-sdk/blob/main/tools/cosmovisor/README.md) for seamless, zero-downtime upgrades. ### How It Works 1. **Governance Proposal** - Network votes on upgrade proposal specifying target block height 2. **Auto-Download** - When approved, Cosmovisor automatically downloads the new binary 3. **Checksum Verification** - Binary verified via SHA256 before use 4. **Seamless Switch** - At upgrade height, node stops, switches binary, and restarts automatically **No manual intervention required** - your validator stays up-to-date automatically. ### Directory Structure ``` ~/.pchain/cosmovisor/ β”œβ”€β”€ genesis/bin/pchaind # Initial binary β”œβ”€β”€ upgrades/ # Upgrade binaries (auto-populated) β”‚ └── {upgrade-name}/bin/pchaind └── current -> genesis/ # Symlink to active version ``` ### Commands ```bash push-validator cosmovisor status # Check versions & pending upgrades ``` ## πŸ”§ Troubleshooting ### Sync Failures / App Mismatch Errors If you encounter sync failures or app hash mismatch errors, reset and restart: ```bash push-validator reset push-validator start ``` This clears the chain data and downloads a fresh snapshot. Snapshot download takes approximately 15-30 minutes depending on your connection, after which block sync will begin automatically. ## πŸ“Š Network - **Chain**: `push_42101-1` (Testnet) - **Min Stake**: 1.5 PC - **Faucet**: https://faucet.push.org - **Explorer**: https://donut.push.network ## πŸ”„ Updates The CLI automatically checks for updates and notifies you: - **Dashboard**: Shows notification in header when update available - **CLI commands**: Shows notification after command completes ### Manual Update ```bash push-validator update # Update to latest version push-validator update --check # Check only, don't install push-validator update --version v1.2.0 # Install specific version ``` Updates download pre-built binaries from GitHub Releases with checksum verification. ## πŸ”§ Advanced Setup (Optional) ### Setup NGINX with SSL ```bash bash scripts/setup-nginx.sh yourdomain.com ``` **Creates:** - `https://yourdomain.com` - Cosmos RPC endpoint - `https://evm.yourdomain.com` - EVM RPC endpoint - Automatic SSL certificates via Let's Encrypt - Rate limiting and security headers **Requirements:** - Domain pointing to your server IP - Ports 80/443 open - Ubuntu/Debian system ### Log Rotation ```bash bash scripts/setup-log-rotation.sh ``` Configures daily rotation with 14-day retention and compression. ### File Locations - **Manager**: `~/.local/bin/push-validator` - **Chain Binary**: `~/.pchain/cosmovisor/current/bin/pchaind` (managed by Cosmovisor) - **Config**: `~/.pchain/config/` - **Data**: `~/.pchain/data/` - **Logs**: `~/.pchain/logs/pchaind.log` - **Backups**: `~/push-node-backups/` --- # Running Localnet URL: https://push.org/docs/chain/node-and-system-tools/localnet/ Localnet | Setup | Push Chain Docs Learn how to deploy localnet of Push Chain to understand or speed up the development of your app. ## Prerequisites Ensure the following tools are installed: - [Go](https://go.dev/doc/install) (v1.23+) - [Foundry](https://book.getfoundry.sh/getting-started/installation) - Git ## Setup & Start Push Chain Localnet ### 1. Clone and install Push Chain ```bash git clone https://github.com/pushchain/push-chain-node cd push-chain make install ``` ### 2. Start the local testnet ```bash make sh-testnet ``` This launches the local Push Chain node. You can now interact with it using the `pchaind` CLI. ## Deploy Protocol Contracts (UEA) To enable universal execution from EVM/Solana chains, you need to deploy protocol contracts to your localnet. ### 1. Clone and build smart account contracts ```bash git clone https://github.com/pushchain/push-chain-core-contracts/ cd push-smart-account-v1 forge build ``` ### 2. Fund Your EVM Wallet on Localnet Before deploying the contracts, your EVM wallet must have enough UPC tokens on the local Push Chain network. To send funds, you need the **Bech32 format** of your EVM wallet address (since Push Chain uses Cosmos SDK-style addresses for transfers). #### Step-by-step 1. **Take your EVM address** (in hex, 0x-prefixed format) β€” for example: ``` 0x778d3206374f8ac265728e18e3fe2ae6b93e4ce4 ``` 2. **Convert it to Bech32 format** using: ```bash pchaind debug addr 778d3206374f8ac265728e18e3fe2ae6b93e4ce4 ``` > πŸ” Replace `778d...ce4` with your **own EVM address** (without the `0x` prefix). 3. You should get an output like this: ``` Address (hex): 778D3206374F8AC265728E18E3FE2AE6B93E4CE4 Bech32 Acc: push1jtdw9kjc2yptl6yjyad69q73v2gcl29xfmmq5a ``` 4. **Use the Bech32 account address** (e.g. `push1jtdw9kjc2yptl6yjyad69q73v2gcl29xfmmq5a`) to receive funds: ```bash pchaind tx bank send push1gjaw568e35hjc8udhat0xnsxxmkm2snrexxz20 push1jtdw9kjc2yptl6yjyad69q73v2gcl29xfmmq5a 100000000000000000000000upc --gas-prices 1000000000upc -y ``` > The `push1gjaw568e35hjc8udhat0xnsxxmkm2snrexxz20` address is pre-funded at genesis with **5 billion UPC**, so it’s safe to use as the sender. ### 3. Deploy UEAFactory & Implementations Once funded, run the deployment script: ```bash forge script script/deployFactory.s.sol --rpc-url http://localhost:8545 --broadcast --private-key ``` This sets up the UEAFactory and registers the EVM + SVM UEA implementations needed for universal execution. ## Add External Chain Configurations Now configure Push Chain to interact with external chains. ### Solana Devnet ```bash pchaind tx ue add-chain-config --chain-config "$(cat config/testnet-donut/solana_devnet_chain_config.json)" --from acc1 --gas-prices 100000000000upc -y ``` ### Ethereum Sepolia ```bash pchaind tx ue add-chain-config --chain-config "$(cat config/testnet-donut/eth_sepolia_chain_config.json)" --from acc1 --gas-prices 100000000000upc -y ``` > If you’ve deployed a custom `FeeGateway` contract, you can include its address in the chain config as well. That’s it! You have successfully set up the Push Chain localnet and are ready to bring your imagination into reality. ## Next steps - Interact and make your app universal via [Core SDK](/docs/chain/build/) - Abstract wallet and gas fee for your Users by implementing [UI Kit](/docs/chain/ui-kit/) --- # JSON-RPC Functions URL: https://push.org/docs/chain/node-and-system-tools/json-rpc-functions/ JSON-RPC Functions | Deep Dives | Push Chain Docs Push Chain is an EVM-compatible blockchain, and thus supports the standard JSON-RPC functions available on all Ethereum-compatible networks. This document provides examples of how to call basic JSON-RPC functions on Push Chain. For comprehensive details, visit the official [Ethereum JSON-RPC Documentation](https://ethereum.org/en/developers/docs/apis/json-rpc/). ## Examples Here are examples showcasing how to make JSON-RPC requests using CURL. ### Get Block by Number Fetch block details for a specific block number: ```bash curl -X POST https://evm.donut.rpc.push.org/ \ -H "Content-Type: application/json" \ -d '{ "jsonrpc":"2.0", "method":"eth_getBlockByNumber", "params":["0x10d4f", true], "id":1 }' ``` ### Get Current Block Number Retrieve the current block number: ```bash curl -X POST https://evm.donut.rpc.push.org/ \ -H "Content-Type: application/json" \ -d '{ "jsonrpc":"2.0", "method":"eth_blockNumber", "params":[], "id":1 }' ``` ### Get Transaction by txHash Fetch transaction details using its hash: ```bash curl -X POST https://evm.donut.rpc.push.org/ \ -H "Content-Type: application/json" \ -d '{ "jsonrpc":"2.0", "method":"eth_getTransactionByHash", "params":["0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238"], "id":1 }' ``` ### Get Account Balance Retrieve the balance of an address: ```bash curl -X POST https://evm.donut.rpc.push.org/ \ -H "Content-Type: application/json" \ -d '{ "jsonrpc":"2.0", "method":"eth_getBalance", "params":["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "latest"], "id":1 }' ``` ## Further Information For more JSON-RPC methods and their usage, refer to the [Ethereum JSON-RPC API Reference](https://ethereum.org/en/developers/docs/apis/json-rpc/). --- # Chain Configuration URL: https://push.org/docs/chain/setup/chain-config/ Chain Configuration | Setup | Push Chain Docs List of all the chain configurations, contract addresses and namespaces deployed. ## Chain Specs ## Chain Contracts | Contract Name | Contract Address | Contract Purpose | | --------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------- | | Universal Exector Factory | `0x00000000000000000000000000000000000000eA` | Factory contract to create Universal Executor Accounts (UEAs) on Push Chain. | | Universal Verification Precompile | `0x00000000000000000000000000000000000000ca` | Precompile module that verifies signature of source-chain wallet (UOAs) | :::info Coming Soon! Push Chain Mainnet is currently in development. Stay tuned for updates! - Follow us on [X](https://x.com/pushchain) for announcements! - Join our [Discord](https://discord.com/invite/pushchain) to be part of the community ::: ## Universal Gateway Contracts | Chain | Contract Address | | ------------------------ | ---------------------------------------------- | | Ethereum Sepolia Testnet | `0x05bD7a3D18324c1F7e216f7fBF2b15985aE5281A` | | Solana Devnet | `CFVSincHYbETh2k7w6u1ENEkjbSLtveRCEBupKidw2VS` | :::info Coming Soon! Push Chain Mainnet is currently in development. Stay tuned for updates! - Follow us on [X](https://x.com/pushchain) for announcements! - Join our [Discord](https://discord.com/invite/pushchain) to be part of the community ::: ## Universal Chain Namespace Every external chain is represented as a particular string on Push Chain. Mentioned below are the supported testnet and mainnet chain namespaces on Push Chain. | Chain | Namespace | Assigned Constant | | ------------------------ | ------------------------------------------ | --------------------------- | | Push Testnet (Donut) | `eip155:42101` | `PUSH_TESTNET_DONUT` | | Ethereum Sepolia | `eip155:11155111` | `ETHEREUM_SEPOLIA` | | Arbitrum Sepolia | `eip155:421614` | `ARBITRUM_SEPOLIA` | | Base Sepolia | `eip155:84532` | `BASE_SEPOLIA` | | BNB Testnet | `eip155:97` | `BNB_TESTNET` | | Solana Devnet | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` | `SOLANA_DEVNET` | | Chain | Namespace | Assigned Constant | | ------------------------ | ------------------------------------------ | --------------------------- | | Push Mainnet | `coming soon` | `PUSH_MAINNET` | | Ethereum Mainnet | `eip155:1` | `ETHEREUM_MAINNET` | | Solana Mainnet | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | `SOLANA_MAINNET` | :::info Coming Soon! Push Chain Mainnet is currently in development. Stay tuned for updates! - Follow us on [X](https://x.com/pushchain) for announcements! - Join our [Discord](https://discord.com/invite/pushchain) to be part of the community ::: ## Next Steps - Create your Universal Signer with [Create Universal Signer](/docs/chain/build/create-universal-signer) - Send your first transaction via [Send Universal Transaction](/docs/chain/build/send-universal-transaction) - Explore on-chain helpers in [Contract Helpers](/docs/chain/build/contract-helpers) - Abstract on the frontend with [UI Kit](/docs/chain/ui-kit/integrate-push-universal-wallet) --- # Wallet Setup URL: https://push.org/docs/chain/setup/tooling/wallet-setup/ Wallet Setup | Tooling | Setup | Push Chain Docs It's recommended to add Push Chain as custom network to your wallet before building your app on Push Chain. This will help you to test your app on Push Chain before deploying it on mainnet. :::note Not Required for Users This is not a requirement for your users as they will connect to Push Chain using their wallet and through [UI Kit](/docs/chain/ui-kit) that will abstract away their wallet connection. ::: ## Add Push Chain Specs to Wallet Add the chain specs to your favorite EVM wallet under custom network. Some of the fields are optional depending on the wallet. ## Next Steps Congrats on setting up Push Chain in your wallet! Next steps: - Get some testnet $PC from [Faucet](/docs/chain/setup/tooling/faucet) --- # Remix IDE URL: https://push.org/docs/chain/setup/smart-contract-environment/configure-remix/ Remix IDE | Smart Contract Environment | Setup | Push Chain Docs Remix is a browser-based Solidity IDE that lets you write, compile, test and deploy smart contracts directly in your browser, no local setup required. Let's use Remix to compile, deploy, and test smart contracts on Push Chain. ## Deploy Smart Contracts with Remix ### 1. Add Push Chain to Your Wallet > If you haven’t yet, follow [Wallet Setup](/docs/chain/setup/tooling/wallet-setup) to add Push Chain as a custom network in your wallet. ### 2. Launch Remix Open [Remix IDE](https://remix.ethereum.org) or use the embedded IDE below. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; contract Counter { uint256 public countPC; event CountIncremented(uint256 indexed countPC, address indexed caller); function increment() public { countPC += 1; emit CountIncremented(countPC, msg.sender); } } ``` ### 3. Configure β€œDeploy & Run Transactions” 1. Click the **Deploy & Run Transactions** plugin in the left sidebar 2. Under **Environment**, choose **Injected Provider - Web3** 3. Approve the connection in your wallet, making sure it’s set to **Push Chain Donut Testnet (42101)** ### 4. Compile Your Contract 1. Open the **Solidity Compiler** plugin 2. Select compiler version **0.8.22** (or match your `pragma`) 3. Click **Compile** next to your contract file ### 5. Deploy Your Contract 1. Return to **Deploy & Run Transactions** 2. Select your contract from the **Contract** dropdown 3. Click **Deploy** 4. Confirm the transaction in your wallet ## Next Steps - Deploy from the console with [Foundry](/docs/chain/setup/smart-contract-environment/configure-foundry) - Script deployments with [Hardhat](/docs/chain/setup/smart-contract-environment/configure-hardhat) - Call your contract from code via the [Push Chain SDK](/docs/chain/build) --- # Configure Foundry URL: https://push.org/docs/chain/setup/smart-contract-environment/configure-foundry/ Configure Foundry | Smart Contract Environment | Tooling | Setup | Push Chain Docs Foundry is a blazing fast, portable, and modular toolkit for Ethereum application development. Get up and running with Foundry on Push Chain testnet. :::info Recommended Practice Instead of reinventing your workflow, keep developing exactly as you would on Ethereumβ€”then just point Foundry at Push Chain RPCs and explorer. ::: ## Deploy Smart Contracts with Foundry ### 1. Install Foundry To install Foundry, run: ```bash curl -L https://foundry.paradigm.xyz | bash ``` Once the foundryup script is downloaded, follow the on-screen instructions to complete installation. After installation, restart your terminal and run: ```bash foundryup ``` This will ensure you have the latest version of Foundry tools (forge, cast, anvil, and chisel) installed. ### 2. Create a New Project Create a new Foundry project: ```bash forge init myToken cd myToken ``` Install the OpenZeppelin contracts: ```bash forge install OpenZeppelin/openzeppelin-contracts ``` This should create a new project with the following structure: ``` myToken/ β”œβ”€β”€ foundry.toml β”œβ”€β”€ lib/ β”œβ”€β”€ out/ β”œβ”€β”€ script/ β”œβ”€β”€ src/ β”œβ”€β”€ test/ ``` ### 3. Configure for Push Chain Now simply modify your `foundry.toml` file to include Push Chain testnet configuration: ```toml [profile.default] solc_version = "0.8.22" src = "src" out = "out" libs = ["lib"] remappings = ["@openzeppelin/=lib/openzeppelin-contracts/"] # Push Chain Testnet configuration [rpc_endpoints] push_testnet = "https://evm.donut.rpc.push.org/" # This is a placeholder - BlockScout doesn't require an API key but Foundry expects a key field [etherscan] push_testnet = { key = "blockscout", url = "https://donut.push.network/api", chain = 42101 } # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options ``` This configuration includes: - Default project structure settings - RPC endpoints for Push Chain Donut testnet - BlockScout explorer configuration for contract verification ### 4. Write a Smart Contract Create a new file at `src/MyToken.sol` with the following ERC20 token implementation: ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.22; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; /** * @title MyToken * @dev A simple ERC20 token for demonstration on PUSH CHAIN */ contract MyToken is ERC20, Ownable { constructor() ERC20("MyToken", "MT") Ownable(msg.sender) { _mint(msg.sender, 1000 * 10**18); } function decimals() public view virtual override returns (uint8) { return 18; } function mint(address to, uint256 amount) public onlyOwner { _mint(to, amount); } } ``` ### 5. Compile the Contract Compile your contract with: ```bash forge build ``` If successful, you should see output similar to: ``` [β ’] Compiling... [β ”] Compiling 18 files with 0.8.22 [β ‘] Solc 0.8.22 finished in 1.51s Compiler run successful! ``` ### 6. Deploy to Push Chain #### 6.1. Set up your deployer account Following best security practices, we'll use Foundry's wallet management system instead of exposing private keys in environment variables: ```bash cast wallet import myKeystore --interactive ``` You'll be prompted to enter your private key and create a password to encrypt it. This securely stores your key in `~/.foundry/keystores`. Read more about [Foundry Wallet Management](https://getfoundry.sh/cast/reference/cast-wallet-import/). :::warning Never use accounts with significant funds for test deployments. Never store private keys in plain text or in your repository. ::: #### 6.2. Get testnet tokens Ensure you have testnet tokens to pay for deployment gas fees. If you don't have any, use the Push Chain [testnet faucet](https://faucet.push.org/). #### 6.3. Deploy your contract ```bash forge create src/MyToken.sol:MyToken \ --rpc-url push_testnet \ --chain 42101 \ --account myKeystore \ --broadcast ``` This command: - Creates your MyToken contract - Uses the Push Chain testnet RPC - Specifies the correct chain ID (42101) - Uses your securely stored account - Broadcasts the transaction to the network **Deployment Results** If successful, you'll see output similar to: ``` Deployer: 0xa89523351BE1e2De64937AA9AF61Ae06eAd199C7 Deployed to: 0xF0f1199A048A39336dFD915F146470de1b5d6dAd Transaction hash: 0x255e64bbe86253979f070b48db0868f6f108a47e7b7f94586bc869fbd2d98800 ``` Save the contract address for verification and interaction later. ### 7. Verify the Contract Verify your contract on the Push Chain BlockScout explorer: ```bash forge verify-contract \ --chain 42101 \ --verifier blockscout \ 0xYourDeployedAddress \ src/MyToken.sol:MyToken ``` > **Note:** Replace `0xYourDeployedAddress` with your actual deployed contract address. If successful, you'll see output similar to: ``` Start verifying contract `0xF0f1199A048A39336dFD915F146470de1b5d6dAd` deployed on 42101 Submitting verification for [src/MyToken.sol:MyToken] 0xF0f1199A048A39336dFD915F146470de1b5d6dAd. Submitted contract for verification: Response: `OK` GUID: `f0f1199a048a39336dfd915f146470de1b5d6dad68494bd5` URL: https://donut.push.network/address/0xf0f1199a048a39336dfd915f146470de1b5d6dad ``` Visit the provided URL to view your verified contract on the Push Chain explorer. That's it! You have successfully deployed and verified your smart contract on Push Chain. ## Next Steps - Deploy with scripts using [Hardhat](/docs/chain/setup/smart-contract-environment/configure-hardhat) - Call your contract from code via the [Push Chain SDK](/docs/chain/build) - Check out all the [Chain Configurations](/docs/chain/setup/chain-config/) --- # Faucet URL: https://push.org/docs/chain/setup/tooling/faucet/ Faucet | Tooling | Setup | Push Chain Docs Faucet is meant to give you some testnet $PC to play with, deploy your smart contracts, and test your app on Push Chain. ## Getting Testnet Tokens from Faucet Push Chain's native token that powers the chain is $PC. You can get $PC by: - Visiting our [Faucet](https://faucet.push.org/). - Enter your wallet address and request testnet $PC. > Incase of any issues, you can always reach out to us on [Discord](https://discord.com/invite/pushchain). ## Next Steps Congrats!! You have successfully added Push Chain to your wallet and got some testnet $PC. Next steps: - Check out [Block Explorer](/docs/chain/setup/tooling/block-explorer) to explore Push Chain transactions, blocks, and accounts. - Setup your smart contract environment to interact with Push Chain. Check out [Smart Contract Environment](/docs/chain/setup/smart-contract-environment). --- # Smart Contract Address Book URL: https://push.org/docs/chain/setup/smart-contract-address-book/ Smart Contract Address Book | Setup | Push Chain Docs Below are the official addresses of the smart contracts deployed on Push Chain Donut Testnet 🍩 or on other chains by Push. > **Scope**: Addresses may change with redeploys. If something looks off, check the changelog or explorer before using in production code. ## Push Chain Donut Testnet ### Push Chain Core Functionalities Core protocol contracts deployed natively on Push Chain Donut Testnet. These contracts power universal execution, enabling apps to send and receive cross-chain transactions through a unified account layer. | Contract | Address | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Universal Core | [0x00000000000000000000000000000000000000C0](https://donut.push.network/address/0x00000000000000000000000000000000000000C0?tab=contract) `Proxy` Mints PRC-20 tokens and manages cross-chain native token pricing Implementation - [0xF1000000000000000000000000000000000000c0](https://donut.push.network/address/0xF1000000000000000000000000000000000000c0?tab=contract)ProxyAdmin - [0xF2000000000000000000000000000000000000c0](https://donut.push.network/address/0xF2000000000000000000000000000000000000c0?tab=contract) | | Universal Gateway PC | [0x00000000000000000000000000000000000000C1](https://donut.push.network/address/0x00000000000000000000000000000000000000C1?tab=contract) `Proxy` Push-side gateway for sending and processing outward cross-chain messages Implementation - [0xF1000000000000000000000000000000000000C2](https://donut.push.network/address/0xF1000000000000000000000000000000000000C2?tab=contract)ProxyAdmin - [0xF2000000000000000000000000000000000000C3](https://donut.push.network/address/0xF2000000000000000000000000000000000000C3?tab=contract) | | Universal Executor Module | [0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7](https://donut.push.network/address/0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7?tab=contract) Module responsible for executing universal transactions on Push Chain | | VaultPC | [0x00000000000000000000000000000000000000B0](https://donut.push.network/address/0x00000000000000000000000000000000000000B0?tab=contract) `Proxy` Reserved vault contract for custody of chain assets Implementation - [0xF1000000000000000000000000000000000000B0](https://donut.push.network/address/0xF1000000000000000000000000000000000000B0?tab=contract)ProxyAdmin - [0xF2000000000000000000000000000000000000B0](https://donut.push.network/address/0xF2000000000000000000000000000000000000B0?tab=contract) | | UEA Factory | [0x00000000000000000000000000000000000000eA](https://donut.push.network/address/0x00000000000000000000000000000000000000eA?tab=contract) `Proxy` Deploys and manages Universal Execution Accounts (UEAs) for each user Implementation - [0xF1000000000000000000000000000000000000eA](https://donut.push.network/address/0xF1000000000000000000000000000000000000eA?tab=contract)ProxyAdmin - [0xF2000000000000000000000000000000000000eA](https://donut.push.network/address/0xF2000000000000000000000000000000000000eA?tab=contract) | | UEA_EVM Implementation | [0x93a31A8DDdCA2686243f1a701AbF82aBA90Fe2eF](https://donut.push.network/address/0x93a31A8DDdCA2686243f1a701AbF82aBA90Fe2eF?tab=contract) Logic contract for EVM-compatible UEAs | | UEA_SVM Implementation | [0x3cab28b2d179258ce3246385977aae4b4A4b40C9](https://donut.push.network/address/0x3cab28b2d179258ce3246385977aae4b4A4b40C9?tab=contract) Logic contract for SVM (Solana)-compatible UEAs | | UProxyAdmin | [0x00000000000000000000000000000000000000aA](https://donut.push.network/address/0x00000000000000000000000000000000000000aA?tab=contract) `Proxy` User-facing proxy admin for universal contracts Implementation - [0xF1000000000000000000000000000000000000aA](https://donut.push.network/address/0xF1000000000000000000000000000000000000aA?tab=contract)ProxyAdmin - [0xF2000000000000000000000000000000000000aA](https://donut.push.network/address/0xF2000000000000000000000000000000000000aA?tab=contract) | ### EVM Default Precompiles Standard precompiles available on all Cosmos EVM chains, including Push Chain. | Precompile | Address | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | p256 | [0x0000000000000000000000000000000000000100](https://donut.push.network/address/0x0000000000000000000000000000000000000100?tab=contract) | | bech32 | [0x0000000000000000000000000000000000000400](https://donut.push.network/address/0x0000000000000000000000000000000000000400?tab=contract) | | staking | [0x0000000000000000000000000000000000000800](https://donut.push.network/address/0x0000000000000000000000000000000000000800?tab=contract) | | distribution | [0x0000000000000000000000000000000000000801](https://donut.push.network/address/0x0000000000000000000000000000000000000801?tab=contract) | | ics20 | [0x0000000000000000000000000000000000000802](https://donut.push.network/address/0x0000000000000000000000000000000000000802?tab=contract) | | vesting | [0x0000000000000000000000000000000000000803](https://donut.push.network/address/0x0000000000000000000000000000000000000803?tab=contract) | | bank | [0x0000000000000000000000000000000000000804](https://donut.push.network/address/0x0000000000000000000000000000000000000804?tab=contract) | | gov | [0x0000000000000000000000000000000000000805](https://donut.push.network/address/0x0000000000000000000000000000000000000805?tab=contract) | | slashing | [0x0000000000000000000000000000000000000806](https://donut.push.network/address/0x0000000000000000000000000000000000000806?tab=contract) | | evidence | [0x0000000000000000000000000000000000000807](https://donut.push.network/address/0x0000000000000000000000000000000000000807?tab=contract) | ### Push Chain Precompiles Custom precompiles deployed on Push Chain for universal transaction verification. | Precompile | Address | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | USigVerifierPrecompile | [0xEC00000000000000000000000000000000000001](https://donut.push.network/address/0xEC00000000000000000000000000000000000001?tab=contract) Verifies Solana signatures for universal execution | ### PRC-20 Supported Tokens (on Push Chain) Push-native representations of tokens bridged from external chains. Each PRC-20 token is minted on Push Chain when its source token is deposited via the respective chain's gateway. | Token Name | Symbol | Source Chain | Token Address on Push Chain | | ---------- | --------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | pETH | pETH | Ethereum_Sepolia | [0x2971824Db68229D087931155C2b8bB820B275809](https://donut.push.network/address/0x2971824Db68229D087931155C2b8bB820B275809?tab=contract) | | WETH.eth | WETH.eth | Ethereum_Sepolia | [0x0d0dF7E8807430A81104EA84d926139816eC7586](https://donut.push.network/address/0x0d0dF7E8807430A81104EA84d926139816eC7586?tab=contract) | | USDT.eth | USDT.eth | Ethereum_Sepolia | [0x0f97A213207703923F5f0C613C9827f7C9A0f96B](https://donut.push.network/address/0x0f97A213207703923F5f0C613C9827f7C9A0f96B?tab=contract) | | stETH.eth | stETH.eth | Ethereum_Sepolia | [0xaf89E805949c628ebde3262e91dc4ab9eA12668E](https://donut.push.network/address/0xaf89E805949c628ebde3262e91dc4ab9eA12668E?tab=contract) | | USDC.eth | USDC.eth | Ethereum_Sepolia | [0x7A58048036206bB898008b5bBDA85697DB1e5d66](https://donut.push.network/address/0x7A58048036206bB898008b5bBDA85697DB1e5d66?tab=contract) | | pSOL | pSOL | Solana_Devnet | [0x5D525Df2bD99a6e7ec58b76aF2fd95F39874EBed](https://donut.push.network/address/0x5D525Df2bD99a6e7ec58b76aF2fd95F39874EBed?tab=contract) | | USDC.sol | USDC.sol | Solana_Devnet | [0x04B8F634ABC7C879763F623e0f0550a4b5c4426F](https://donut.push.network/address/0x04B8F634ABC7C879763F623e0f0550a4b5c4426F?tab=contract) | | USDT.sol | USDT.sol | Solana_Devnet | [0x4f1A3D22d170a2F4Bddb37845a962322e24f4e34](https://donut.push.network/address/0x4f1A3D22d170a2F4Bddb37845a962322e24f4e34?tab=contract) | | DAI.sol | DAI.sol | Solana_Devnet | [0x5861f56A556c990358cc9cccd8B5baa3767982A8](https://donut.push.network/address/0x5861f56A556c990358cc9cccd8B5baa3767982A8?tab=contract) | | pETH.base | pETH.base | Base_Testnet | [0xc7007af2B24D4eb963fc9633B0c66e1d2D90Fc21](https://donut.push.network/address/0xc7007af2B24D4eb963fc9633B0c66e1d2D90Fc21?tab=contract) | | USDT.base | USDT.base | Base_Testnet | [0x148823809B853e1db187BC09A9ac909BC42F971a](https://donut.push.network/address/0x148823809B853e1db187BC09A9ac909BC42F971a?tab=contract) | | USDC.base | USDC.base | Base_Testnet | [0xD7C6cA1e2c0CE260BE0c0AD39C1540de460e3Be1](https://donut.push.network/address/0xD7C6cA1e2c0CE260BE0c0AD39C1540de460e3Be1?tab=contract) | | pETH.arb | pETH.arb | Arbitrum_Sepolia | [0xc0a821a1AfEd1322c5e15f1F4586C0B8cE65400e](https://donut.push.network/address/0xc0a821a1AfEd1322c5e15f1F4586C0B8cE65400e?tab=contract) | | USDC.arb | USDC.arb | Arbitrum_Sepolia | [0x1091cCBA2FF8d2A131AE4B35e34cf3308C48572C](https://donut.push.network/address/0x1091cCBA2FF8d2A131AE4B35e34cf3308C48572C?tab=contract) | | USDT.arb | USDT.arb | Arbitrum_Sepolia | [0xFE6E9DF2BbC9ce05D98b83B1365df6DcA9951891](https://donut.push.network/address/0xFE6E9DF2BbC9ce05D98b83B1365df6DcA9951891?tab=contract) | | pBNB | pBNB | BNB_Testnet | [0x7a9082dA308f3fa005beA7dB0d203b3b86664E36](https://donut.push.network/address/0x7a9082dA308f3fa005beA7dB0d203b3b86664E36?tab=contract) | | USDC.bnb | USDC.bnb | BNB_Testnet | [0x120EBf25Dad7D6a09Ad2316f23f9Be95DBb90639](https://donut.push.network/address/0x120EBf25Dad7D6a09Ad2316f23f9Be95DBb90639?tab=contract) | | USDT.bnb | USDT.bnb | BNB_Testnet | [0x731aF1Da5365259d27528557EE4aFBA4baC90ef2](https://donut.push.network/address/0x731aF1Da5365259d27528557EE4aFBA4baC90ef2?tab=contract) | --- ### Core AMM & Helpers Uniswap V3-compatible AMM contracts deployed on Push Chain for on-chain token swaps and liquidity management. | Contract | Address | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | Factory | [0x81b8Bca02580C7d6b636051FDb7baAC436bFb454](https://donut.push.network/address/0x81b8Bca02580C7d6b636051FDb7baAC436bFb454?tab=contract) | | WPC | [0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9](https://donut.push.network/address/0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9?tab=contract) | | Swap Router | [0x5D548bB9E305AAe0d6dc6e6fdc3ab419f6aC0037](https://donut.push.network/address/0x5D548bB9E305AAe0d6dc6e6fdc3ab419f6aC0037?tab=contract) | | Position Manager | [0xf9b3ac66aed14A2C7D9AA7696841aB6B27a6231e](https://donut.push.network/address/0xf9b3ac66aed14A2C7D9AA7696841aB6B27a6231e?tab=contract) | | QuoterV2 | [0x83316275f7C2F79BC4E26f089333e88E89093037](https://donut.push.network/address/0x83316275f7C2F79BC4E26f089333e88E89093037?tab=contract) | | Tick Lens | [0xb64113Fc16055AfE606f25658812EE245Aa41dDC](https://donut.push.network/address/0xb64113Fc16055AfE606f25658812EE245Aa41dDC?tab=contract) | | Multicall | [0xa8c00017955c8654bfFbb6d5179c99f5aB8B7849](https://donut.push.network/address/0xa8c00017955c8654bfFbb6d5179c99f5aB8B7849?tab=contract) | ### AMM Pools Active official liquidity pools pairing various PRC-20 tokens with WPC (Wrapped Push Chain native token). Fee tiers are in basis points (e.g. 500 = 0.05%). | Pool | Address | Token 0 | Token 1 | Fee | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---- | | pSOL/WPC Pool | [0x0E5914e3A7e2e6d18330Dd33fA387Ce33Da48b54](https://donut.push.network/address/0x0E5914e3A7e2e6d18330Dd33fA387Ce33Da48b54?tab=contract) | [0x5D525Df2bD99a6e7ec58b76aF2fd95F39874EBed](https://donut.push.network/address/0x5D525Df2bD99a6e7ec58b76aF2fd95F39874EBed?tab=contract) | [0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9](https://donut.push.network/address/0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9?tab=contract) | 500 | | pETH/WPC Pool | [0x012d5C099f8AE00009f40824317a18c3A342f622](https://donut.push.network/address/0x012d5C099f8AE00009f40824317a18c3A342f622?tab=contract) | [0x2971824Db68229D087931155C2b8bB820B275809](https://donut.push.network/address/0x2971824Db68229D087931155C2b8bB820B275809?tab=contract) | [0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9](https://donut.push.network/address/0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9?tab=contract) | 500 | | USDT.eth/WPC Pool | [0x1e3f6b38582535A8eB021829853A08Bb1C7b604B](https://donut.push.network/address/0x1e3f6b38582535A8eB021829853A08Bb1C7b604B?tab=contract) | [0x0f97A213207703923F5f0C613C9827f7C9A0f96B](https://donut.push.network/address/0x0f97A213207703923F5f0C613C9827f7C9A0f96B?tab=contract) | [0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9](https://donut.push.network/address/0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9?tab=contract) | 500 | | USDC.eth/WPC Pool | [0x524B9b3e98CEF71a20B30859D6c52e13E17C5BA2](https://donut.push.network/address/0x524B9b3e98CEF71a20B30859D6c52e13E17C5BA2?tab=contract) | [0x7A58048036206bB898008b5bBDA85697DB1e5d66](https://donut.push.network/address/0x7A58048036206bB898008b5bBDA85697DB1e5d66?tab=contract) | [0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9](https://donut.push.network/address/0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9?tab=contract) | 500 | | pETH.base/WPC Pool | [0xF926707689ad2fE9A81e666E5B888b2f3AD33980](https://donut.push.network/address/0xF926707689ad2fE9A81e666E5B888b2f3AD33980?tab=contract) | [0xc7007af2B24D4eb963fc9633B0c66e1d2D90Fc21](https://donut.push.network/address/0xc7007af2B24D4eb963fc9633B0c66e1d2D90Fc21?tab=contract) | [0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9](https://donut.push.network/address/0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9?tab=contract) | 500 | | USDT.base/WPC Pool | [0x0c906B6FE47f1666F4723273Cf8E681b3e35aFF0](https://donut.push.network/address/0x0c906B6FE47f1666F4723273Cf8E681b3e35aFF0?tab=contract) | [0x148823809B853e1db187BC09A9ac909BC42F971a](https://donut.push.network/address/0x148823809B853e1db187BC09A9ac909BC42F971a?tab=contract) | [0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9](https://donut.push.network/address/0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9?tab=contract) | 500 | | USDC.base/WPC Pool | [0xC76D211B1c40775ec340692bA5BC0D728A0dF745](https://donut.push.network/address/0xC76D211B1c40775ec340692bA5BC0D728A0dF745?tab=contract) | [0xD7C6cA1e2c0CE260BE0c0AD39C1540de460e3Be1](https://donut.push.network/address/0xD7C6cA1e2c0CE260BE0c0AD39C1540de460e3Be1?tab=contract) | [0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9](https://donut.push.network/address/0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9?tab=contract) | 500 | | pETH.arb/WPC Pool | [0x1354c9A72F447f60F4811FC34b8C2e084FE338A3](https://donut.push.network/address/0x1354c9A72F447f60F4811FC34b8C2e084FE338A3?tab=contract) | [0xc0a821a1AfEd1322c5e15f1F4586C0B8cE65400e](https://donut.push.network/address/0xc0a821a1AfEd1322c5e15f1F4586C0B8cE65400e?tab=contract) | [0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9](https://donut.push.network/address/0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9?tab=contract) | 3000 | | USDT.arb/WPC Pool | [0x5EBEa067F75C0661EC37577547209E38C8b93c18](https://donut.push.network/address/0x5EBEa067F75C0661EC37577547209E38C8b93c18?tab=contract) | [0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9](https://donut.push.network/address/0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9?tab=contract) | [0xFE6E9DF2BbC9ce05D98b83B1365df6DcA9951891](https://donut.push.network/address/0xFE6E9DF2BbC9ce05D98b83B1365df6DcA9951891?tab=contract) | 500 | | USDC.arb/WPC Pool | [0xB3ccbD470A19D4aB2fAa43c6eE4d43dEF8F4Ee63](https://donut.push.network/address/0xB3ccbD470A19D4aB2fAa43c6eE4d43dEF8F4Ee63?tab=contract) | [0x1091cCBA2FF8d2A131AE4B35e34cf3308C48572C](https://donut.push.network/address/0x1091cCBA2FF8d2A131AE4B35e34cf3308C48572C?tab=contract) | [0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9](https://donut.push.network/address/0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9?tab=contract) | 500 | | pBNB/WPC Pool | [0x826edC20c926653f4ddC01b8d4C7Df31a403e7d6](https://donut.push.network/address/0x826edC20c926653f4ddC01b8d4C7Df31a403e7d6?tab=contract) | [0x7a9082dA308f3fa005beA7dB0d203b3b86664E36](https://donut.push.network/address/0x7a9082dA308f3fa005beA7dB0d203b3b86664E36?tab=contract) | [0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9](https://donut.push.network/address/0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9?tab=contract) | 500 | | USDT.bnb/WPC Pool | [0x735010da121541515CB509339Ea0A0fD4f48d4a9](https://donut.push.network/address/0x735010da121541515CB509339Ea0A0fD4f48d4a9?tab=contract) | [0x731aF1Da5365259d27528557EE4aFBA4baC90ef2](https://donut.push.network/address/0x731aF1Da5365259d27528557EE4aFBA4baC90ef2?tab=contract) | [0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9](https://donut.push.network/address/0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9?tab=contract) | 500 | | USDC.bnb/WPC Pool | [0xf09aD7D5e8800d0863F5ea845509bC1B1aCAe37a](https://donut.push.network/address/0xf09aD7D5e8800d0863F5ea845509bC1B1aCAe37a?tab=contract) | [0x120EBf25Dad7D6a09Ad2316f23f9Be95DBb90639](https://donut.push.network/address/0x120EBf25Dad7D6a09Ad2316f23f9Be95DBb90639?tab=contract) | [0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9](https://donut.push.network/address/0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9?tab=contract) | 500 | --- ## External Chain Gateway Contracts UniversalGateway contracts deployed on external testnets. These contracts accept deposits and initiate cross-chain transactions routed through Push Chain. ## Ethereum Sepolia Contracts deployed on Ethereum Sepolia testnet (Chain ID: 11155111). ### Ethereum Sepolia - Gateway Addresses | Contract | Address | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Universal Gateway | [0x05bD7a3D18324c1F7e216f7fBF2b15985aE5281A](https://sepolia.etherscan.io/address/0x05bD7a3D18324c1F7e216f7fBF2b15985aE5281A#code) `Proxy` Accepts deposits and initiates cross-chain transactions routed through Push Chain Implementation - [0xa594c32593eD1E0Fce83fa1b3A56870b4a1b4ec1](https://sepolia.etherscan.io/address/0xa594c32593eD1E0Fce83fa1b3A56870b4a1b4ec1#code)ProxyAdmin - [0x756C0bEa91F5692384AEe147C10409BB062Bf39b](https://sepolia.etherscan.io/address/0x756C0bEa91F5692384AEe147C10409BB062Bf39b#code) | ### Ethereum Sepolia - CEA Contracts Chain Execution Account (CEA) contracts enable contract-initiated cross-chain transactions on behalf of users. | Contract | Address | | ------------ | ------- | | CEAFactory | [0x5E191fbBe22F8866C5e4250557664fCE760e8870](https://sepolia.etherscan.io/address/0x5E191fbBe22F8866C5e4250557664fCE760e8870#code) `Proxy` Implementation - [0xe5B51807f2252A5Ea9B591fE02285954446c8cAD](https://sepolia.etherscan.io/address/0xe5B51807f2252A5Ea9B591fE02285954446c8cAD#code)ProxyAdmin - [0xF920e3D1420885A117Cb59830d0474aD5690dd82](https://sepolia.etherscan.io/address/0xF920e3D1420885A117Cb59830d0474aD5690dd82#code) | | CEA (logic) | [0x1939376ce03998F638b8760c7a13C9A379A053C0](https://sepolia.etherscan.io/address/0x1939376ce03998F638b8760c7a13C9A379A053C0#code) | | CEAMigration | [0x97BCEba9c6f13B0E12Fde0E4D2697F74A79899de](https://sepolia.etherscan.io/address/0x97BCEba9c6f13B0E12Fde0E4D2697F74A79899de#code) | ### Ethereum Sepolia - Vault Contracts Vault contract custodies user funds deposited via the gateway and coordinates with CEAFactory for finalization. | Contract | Address | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Vault | [0xD019Eb12D0d6eF8D299661f22B4B7d262eD4b965](https://sepolia.etherscan.io/address/0xD019Eb12D0d6eF8D299661f22B4B7d262eD4b965#code) `Proxy` Custodies deposited funds and coordinates cross-chain finalization Implementation - [0x493F3a9Be4841445Db6Cb87FcBe45377f4E82e8C](https://sepolia.etherscan.io/address/0x493F3a9Be4841445Db6Cb87FcBe45377f4E82e8C#code)ProxyAdmin - [0x0c9b4741b9D8744D777d915a20c2C952f1f5aBc3](https://sepolia.etherscan.io/address/0x0c9b4741b9D8744D777d915a20c2C952f1f5aBc3#code) | ### Ethereum Sepolia - Supported Tokens Tokens accepted by the Ethereum Sepolia gateway and their corresponding PRC-20 representations on Push Chain. | Token Name | Source Address | PRC20 Address (on Push Chain) | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | Native ETH | 0x0000000000000000000000000000000000000000 | [0x2971824Db68229D087931155C2b8bB820B275809](https://donut.push.network/address/0x2971824Db68229D087931155C2b8bB820B275809?tab=contract) | | USDC | [0x97F477B7f970D47a87B42869ceeace218106152a](https://sepolia.etherscan.io/address/0x97F477B7f970D47a87B42869ceeace218106152a#code) | [0x7A58048036206bB898008b5bBDA85697DB1e5d66](https://donut.push.network/address/0x7A58048036206bB898008b5bBDA85697DB1e5d66?tab=contract) | | USDT | [0x7169D38820dfd117C3FA1f22a697dBA58d90BA06](https://sepolia.etherscan.io/address/0x7169D38820dfd117C3FA1f22a697dBA58d90BA06#code) | [0x0f97A213207703923F5f0C613C9827f7C9A0f96B](https://donut.push.network/address/0x0f97A213207703923F5f0C613C9827f7C9A0f96B?tab=contract) | | WETH | [0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14](https://sepolia.etherscan.io/address/0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14#code) | [0x0d0dF7E8807430A81104EA84d926139816eC7586](https://donut.push.network/address/0x0d0dF7E8807430A81104EA84d926139816eC7586?tab=contract) | | stETH | [0x3e3FE7dBc6B4C189E7128855dD526361c49b40Af](https://sepolia.etherscan.io/address/0x3e3FE7dBc6B4C189E7128855dD526361c49b40Af#code) | [0xaf89E805949c628ebde3262e91dc4ab9eA12668E](https://donut.push.network/address/0xaf89E805949c628ebde3262e91dc4ab9eA12668E?tab=contract) | --- ## Arbitrum Sepolia Contracts deployed on Arbitrum Sepolia testnet (Chain ID: 421614). ### Arbitrum Sepolia - Gateway Addresses | Contract | Address | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Universal Gateway | [0x2cd870e0166Ba458dEC615168Fd659AacD795f34](https://sepolia.arbiscan.io/address/0x2cd870e0166Ba458dEC615168Fd659AacD795f34#code) `Proxy` Accepts deposits and initiates cross-chain transactions routed through Push Chain Implementation - [0xa81a398289D04503Aab64C4276CdB99Ff1594801](https://sepolia.arbiscan.io/address/0xa81a398289D04503Aab64C4276CdB99Ff1594801#code)ProxyAdmin - [0xF838473Ddc2228267023A319c7305564391313f7](https://sepolia.arbiscan.io/address/0xF838473Ddc2228267023A319c7305564391313f7#code) | ### Arbitrum Sepolia - CEA Contracts Chain Execution Account (CEA) contracts enabling contract-initiated cross-chain transactions on Arbitrum Sepolia. | Contract | Address | | ------------ | ------- | | CEAFactory | [0x65572FFa81c230360a8a53C1682C7f0Ee321E5E7](https://sepolia.arbiscan.io/address/0x65572FFa81c230360a8a53C1682C7f0Ee321E5E7#code) `Proxy` Implementation - [0xd8335e762E42b7f9610293707d6d8A6b97578bFb](https://sepolia.arbiscan.io/address/0xd8335e762E42b7f9610293707d6d8A6b97578bFb#code)ProxyAdmin - [0x6349546d872d483A35bdD165c9ef85757e064D4E](https://sepolia.arbiscan.io/address/0x6349546d872d483A35bdD165c9ef85757e064D4E#code) | | CEA (logic) | [0x2c933Ff6FBcD479055F344691bc628F51DcE871A](https://sepolia.arbiscan.io/address/0x2c933Ff6FBcD479055F344691bc628F51DcE871A#code) | | CEAMigration | [0x81f33160020AaDF47000E85915d332943b69F9f9](https://sepolia.arbiscan.io/address/0x81f33160020AaDF47000E85915d332943b69F9f9#code) | ### Arbitrum Sepolia - Vault Contracts Vault contract on Arbitrum Sepolia that custodies deposited funds and coordinates cross-chain finalization. | Contract | Address | | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Vault | [0x233B1B1B378eb0Aa723097634025A47C4b73A8F7](https://sepolia.arbiscan.io/address/0x233B1B1B378eb0Aa723097634025A47C4b73A8F7#code) `Proxy` Custodies deposited funds and coordinates cross-chain finalization Implementation - [0x60326FA4dD66CEA3637f4Dd6B4D65ad3112B87Ef](https://sepolia.arbiscan.io/address/0x60326FA4dD66CEA3637f4Dd6B4D65ad3112B87Ef#code)ProxyAdmin - [0x3BA9EbE1c6b797BFB04CfF1CF26A8D5500b7c9b2](https://sepolia.arbiscan.io/address/0x3BA9EbE1c6b797BFB04CfF1CF26A8D5500b7c9b2#code) | ### Arbitrum Sepolia - Supported Tokens Tokens accepted by the Arbitrum Sepolia gateway and their corresponding PRC-20 representations on Push Chain. | Token Name | Source Address | PRC20 Address (on Push Chain) | | ---------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | Native ETH | 0x0000000000000000000000000000000000000000 | [0xc0a821a1AfEd1322c5e15f1F4586C0B8cE65400e](https://donut.push.network/address/0xc0a821a1AfEd1322c5e15f1F4586C0B8cE65400e?tab=contract) | | USDC | [0x5dd39b0b3610F666F631a6506b7713EF83e1Ac5C](https://sepolia.arbiscan.io/address/0x5dd39b0b3610F666F631a6506b7713EF83e1Ac5C#code) | [0x1091cCBA2FF8d2A131AE4B35e34cf3308C48572C](https://donut.push.network/address/0x1091cCBA2FF8d2A131AE4B35e34cf3308C48572C?tab=contract) | | USDT | [0x1419d7C74D234fA6B73E06A2ce7822C1d37922f0](https://sepolia.arbiscan.io/address/0x1419d7C74D234fA6B73E06A2ce7822C1d37922f0#code) | [0xFE6E9DF2BbC9ce05D98b83B1365df6DcA9951891](https://donut.push.network/address/0xFE6E9DF2BbC9ce05D98b83B1365df6DcA9951891?tab=contract) | --- ## Base Sepolia Contracts deployed on Base Sepolia testnet (Chain ID: 84532). ### Base Sepolia - Gateway Addresses | Contract | Address | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Universal Gateway | [0xFD4fef1F43aFEc8b5bcdEEc47f35a1431479aC16](https://sepolia.basescan.org/address/0xFD4fef1F43aFEc8b5bcdEEc47f35a1431479aC16#code) `Proxy` Accepts deposits and initiates cross-chain transactions routed through Push Chain Implementation - [0x9f63e2bCFC19994c664a7d7265dCfAb206634612](https://sepolia.basescan.org/address/0x9f63e2bCFC19994c664a7d7265dCfAb206634612#code)ProxyAdmin - [0x0b30F0ECd37B8D44FE1d2b98d5Dc64654d9ac9b3](https://sepolia.basescan.org/address/0x0b30F0ECd37B8D44FE1d2b98d5Dc64654d9ac9b3#code) | ### Base Sepolia - CEA Contracts Chain Execution Account (CEA) contracts enabling contract-initiated cross-chain transactions on Base Sepolia. | Contract | Address | | ------------ | ------- | | CEAFactory | [0x7e8CeeDA043ED1460540616103dD57581a66C856](https://sepolia.basescan.org/address/0x7e8CeeDA043ED1460540616103dD57581a66C856#code) `Proxy` Implementation - [0xd26E793Ef931EB62AeBc6e87DE1FEEF4fDbA01F5](https://sepolia.basescan.org/address/0xd26E793Ef931EB62AeBc6e87DE1FEEF4fDbA01F5#code)ProxyAdmin - [0x413A39fFA85657A25768799f7fd64A917eceDe48](https://sepolia.basescan.org/address/0x413A39fFA85657A25768799f7fd64A917eceDe48#code) | | CEA (logic) | [0x733078bA1dFDDDB68A9E082696A256AEcBFb26b8](https://sepolia.basescan.org/address/0x733078bA1dFDDDB68A9E082696A256AEcBFb26b8#code) | | CEAMigration | [0x95c453fDFf55Afc5754c1fA95Ad6607273D71B20](https://sepolia.basescan.org/address/0x95c453fDFf55Afc5754c1fA95Ad6607273D71B20#code) | ### Base Sepolia - Vault Contracts Vault contract on Base Sepolia that custodies deposited funds and coordinates cross-chain finalization. | Contract | Address | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Vault | [0xb4Ba4D5542D1dD48BD3589543660B265B41f16CB](https://sepolia.basescan.org/address/0xb4Ba4D5542D1dD48BD3589543660B265B41f16CB#code) `Proxy` Custodies deposited funds and coordinates cross-chain finalization Implementation - [0x3F9ba2dFCe97Ef55b7a03C455911fd25f8f12B3b](https://sepolia.basescan.org/address/0x3F9ba2dFCe97Ef55b7a03C455911fd25f8f12B3b#code)ProxyAdmin - [0xdD1aF0f056D290c2BcE8d785340D4c7ab2FAC75d](https://sepolia.basescan.org/address/0xdD1aF0f056D290c2BcE8d785340D4c7ab2FAC75d#code) | ### Base Sepolia - Supported Tokens Tokens accepted by the Base Sepolia gateway and their corresponding PRC-20 representations on Push Chain. | Token Name | Source Address | PRC20 Address (on Push Chain) | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | Native ETH | 0x0000000000000000000000000000000000000000 | [0xc7007af2B24D4eb963fc9633B0c66e1d2D90Fc21](https://donut.push.network/address/0xc7007af2B24D4eb963fc9633B0c66e1d2D90Fc21?tab=contract) | | USDC | [0x5c3504F0E3bA28FDc1F74234fE936518276AaBB8](https://sepolia.basescan.org/address/0x5c3504F0E3bA28FDc1F74234fE936518276AaBB8#code) | [0xD7C6cA1e2c0CE260BE0c0AD39C1540de460e3Be1](https://donut.push.network/address/0xD7C6cA1e2c0CE260BE0c0AD39C1540de460e3Be1?tab=contract) | | USDT | [0x9FF5a186f53F6E6964B00320Da1D2024DE11E0cB](https://sepolia.basescan.org/address/0x9FF5a186f53F6E6964B00320Da1D2024DE11E0cB#code) | [0x148823809B853e1db187BC09A9ac909BC42F971a](https://donut.push.network/address/0x148823809B853e1db187BC09A9ac909BC42F971a?tab=contract) | --- ## BNB Testnet Contracts deployed on BNB Testnet (Chain ID: 97). ### BNB Testnet - Gateway Addresses | Contract | Address | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Universal Gateway | [0x44aFFC61983F4348DdddB886349eb992C061EaC0](https://testnet.bscscan.com//address/0x44aFFC61983F4348DdddB886349eb992C061EaC0#code) `Proxy` Accepts deposits and initiates cross-chain transactions routed through Push Chain Implementation - [0x1f5afA0eEDC2F7E2442D8a51E8A892C98517De1E](https://testnet.bscscan.com//address/0x1f5afA0eEDC2F7E2442D8a51E8A892C98517De1E#code)ProxyAdmin - [0x5Cef317D8392dF9F8C8E8a696c6893FD4112542C](https://testnet.bscscan.com//address/0x5Cef317D8392dF9F8C8E8a696c6893FD4112542C#code) | ### BNB Testnet - CEA Contracts Chain Execution Account (CEA) contracts enabling contract-initiated cross-chain transactions on BNB Testnet. | Contract | Address | | ------------ | ------- | | CEAFactory | [0x3f1B16e0B072d472951C4563d29d3da6a3EE3Ce8](https://testnet.bscscan.com/address/0x3f1B16e0B072d472951C4563d29d3da6a3EE3Ce8#code) `Proxy` Implementation - [0xC0D35725Dd054B09931740DC231cDea89B0FEd3b](https://testnet.bscscan.com/address/0xC0D35725Dd054B09931740DC231cDea89B0FEd3b#code)ProxyAdmin - [0xf33CBb6a1c1D511dF40764063a11978D640C41A7](https://testnet.bscscan.com/address/0xf33CBb6a1c1D511dF40764063a11978D640C41A7#code) | | CEA (logic) | [0xdC3A3a18a17EB4FDa9cF34a8CEee8540e6F2b5Fd](https://testnet.bscscan.com/address/0xdC3A3a18a17EB4FDa9cF34a8CEee8540e6F2b5Fd#code) | | CEAMigration | [0x2a06BF2A9C19dacbb38852f846B42e278e82e855](https://testnet.bscscan.com/address/0x2a06BF2A9C19dacbb38852f846B42e278e82e855#code) | ### BNB Testnet - Vault Contracts Vault contract on BNB Testnet that custodies deposited funds and coordinates cross-chain finalization. | Contract | Address | | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Vault | [0xE52AC4f8DD3e0263bDF748F3390cdFA1f02be881](https://testnet.bscscan.com/address/0xE52AC4f8DD3e0263bDF748F3390cdFA1f02be881#code) `Proxy` Custodies deposited funds and coordinates cross-chain finalization Implementation - [0xc1CD9c126e1F38Ffe016d448FaF563e825eb60CA](https://testnet.bscscan.com/address/0xc1CD9c126e1F38Ffe016d448FaF563e825eb60CA#code)ProxyAdmin - [0xc34eF3cA76d1C18c35AbF5C3664d183B57382AbC](https://testnet.bscscan.com/address/0xc34eF3cA76d1C18c35AbF5C3664d183B57382AbC#code) | ### BNB Testnet - Supported Tokens Tokens accepted by the BNB Testnet gateway and their corresponding PRC-20 representations on Push Chain. | Token Name | Source Address | PRC20 Address (on Push Chain) | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | Native BNB | 0x0000000000000000000000000000000000000000 | [0x7a9082dA308f3fa005beA7dB0d203b3b86664E36](https://donut.push.network/address/0x7a9082dA308f3fa005beA7dB0d203b3b86664E36?tab=contract) | | USDC | [0x64544969ed7EBf5f083679233325356EbE738930](https://testnet.bscscan.com//address/0x64544969ed7EBf5f083679233325356EbE738930#code) | β€” | | USDT | [0xBC14F348BC9667be46b35Edc9B68653d86013DC5](https://testnet.bscscan.com//address/0xBC14F348BC9667be46b35Edc9B68653d86013DC5#code) | [0x731aF1Da5365259d27528557EE4aFBA4baC90ef2](https://donut.push.network/address/0x731aF1Da5365259d27528557EE4aFBA4baC90ef2?tab=contract) | --- ## Solana Devnet Contracts deployed on Solana Devnet. The Solana gateway is a native Solana program (not EVM) and uses a different address format. ### Solana Devnet - Gateway Addresses | Program | Address | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | Universal Gateway | [CFVSincHYbETh2k7w6u1ENEkjbSLtveRCEBupKidw2VS](https://explorer.solana.com/address/CFVSincHYbETh2k7w6u1ENEkjbSLtveRCEBupKidw2VS?cluster=devnet) | ### Solana Devnet - Supported Tokens Tokens accepted by the Solana Devnet gateway and their corresponding PRC-20 representations on Push Chain. | Token Name | Source Address | PRC20 Address (on Push Chain) | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | Native SOL | β€” | [0x5D525Df2bD99a6e7ec58b76aF2fd95F39874EBed](https://donut.push.network/address/0x5D525Df2bD99a6e7ec58b76aF2fd95F39874EBed?tab=contract) | | USDC | [4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU](https://explorer.solana.com/address/4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU?cluster=devnet) | [0x04B8F634ABC7C879763F623e0f0550a4b5c4426F](https://donut.push.network/address/0x04B8F634ABC7C879763F623e0f0550a4b5c4426F?tab=contract) | | USDT (Unofficial) | [EiXDnrAg9ea2Q6vEPV7E5TpTU1vh41jcuZqKjU5Dc4ZF](https://explorer.solana.com/address/EiXDnrAg9ea2Q6vEPV7E5TpTU1vh41jcuZqKjU5Dc4ZF?cluster=devnet) | [0x4f1A3D22d170a2F4Bddb37845a962322e24f4e34](https://donut.push.network/address/0x4f1A3D22d170a2F4Bddb37845a962322e24f4e34?tab=contract) | | DAI (Unofficial) | [G2ZLaRhpohW23KTEX3fBjZXtNTFFwemqCaWWnWVTj4TB](https://explorer.solana.com/address/G2ZLaRhpohW23KTEX3fBjZXtNTFFwemqCaWWnWVTj4TB?cluster=devnet) | [0x5861f56A556c990358cc9cccd8B5baa3767982A8](https://donut.push.network/address/0x5861f56A556c990358cc9cccd8B5baa3767982A8?tab=contract) | --- # Block Explorer URL: https://push.org/docs/chain/setup/tooling/block-explorer/ Block Explorer | Tooling | Setup | Push Chain Docs Explore Push Chain transactions, blocks, and accounts in real time. ## Block Explorer - Mainnet explorer _`Coming Soon`_ - Testnet Explorer https://donut.push.network Enter a transaction hash, block number, or account address to see detailed information. ## Next Steps Congrats on setting up Push Chain in your wallet! Next steps: - Setup your Smart Contract Environment to interact with Push Chain. Check out [Smart Contract Environment](/docs/chain/setup/smart-contract-environment). - Start building your app on Push Chain. Check out [Quickstart](/docs/chain/build). --- # Configure Hardhat URL: https://push.org/docs/chain/setup/smart-contract-environment/configure-hardhat/ Configure Hardhat | Smart Contract Environment | Tooling | Setup | Push Chain Docs Hardhat is another popular development environment for Ethereum software, designed for professionals that need a flexible and extensible tool. Code with vibes and dawn your builder hat with Hardhat. ## Deploy Smart Contracts with Hardhat ### 1. Install Hardhat First, create a new directory for your project and initialize it: ```bash mkdir myToken cd myToken npm init -y ``` Install Hardhat and required dependencies: ```bash npm install --save-dev \ hardhat \ @nomicfoundation/hardhat-toolbox \ @nomicfoundation/hardhat-verify \ dotenv \ @openzeppelin/contracts ``` This installs: - Hardhat core framework - Hardhat toolbox with common plugins - Hardhat verify for contract verification - dotenv for environment variable management - OpenZeppelin contracts library ### 2. Create a New Project Initialize a new Hardhat project: ```bash npx hardhat init ``` Select "Create a JavaScript project" when prompted. This will create a basic project structure: ``` myToken/ β”œβ”€β”€ contracts/ β”œβ”€β”€ scripts/ β”œβ”€β”€ test/ β”œβ”€β”€ hardhat.config.js β”œβ”€β”€ package.json └── node_modules/ ``` ### 3. Configure for Push Chain Update your `hardhat.config.js` file to include Push Chain testnet configuration: ```javascript require('@nomicfoundation/hardhat-toolbox'); require('@nomicfoundation/hardhat-verify'); require('dotenv').config(); /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: { version: '0.8.22', settings: { optimizer: { enabled: true, runs: 200, }, }, }, networks: { push_testnet: { url: 'https://evm.donut.rpc.push.org/', chainId: 42101, accounts: [process.env.PRIVATE_KEY], }, }, etherscan: { apiKey: { // Blockscout doesn't require an actual API key, any non-empty string will work push_testnet: 'blockscout', }, customChains: [ { network: 'push_testnet', chainId: 42101, urls: { apiURL: 'https://donut.push.network/api/v2/verifyContract', browserURL: 'https://donut.push.network/', }, }, ], }, sourcify: { // Disable sourcify for manual verification enabled: false, }, paths: { sources: './contracts', tests: './test', cache: './cache', artifacts: './artifacts', }, mocha: { timeout: 40000, }, }; ``` This configuration includes: - Solidity compiler version and optimization settings - Push Chain testnet RPC endpoints - Blockscout integration for contract verification - Project structure paths - Test configuration ### 4. Write a Smart Contract Create a file at `contracts/MyToken.sol` with this ERC20 token implementation: ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.22; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; /** * @title MyToken * @dev A simple ERC20 token for demonstration on PUSH CHAIN */ contract MyToken is ERC20, Ownable { constructor() ERC20("MyToken", "MT") Ownable(msg.sender) { _mint(msg.sender, 1000 * 10**18); } /** * @dev Returns the number of decimals used to get its user representation. */ function decimals() public view virtual override returns (uint8) { return 18; } /** * @dev Allows the owner to mint new tokens * @param to The address that will receive the minted tokens. * @param amount The amount of tokens to mint. */ function mint(address to, uint256 amount) public onlyOwner { _mint(to, amount); } } ``` ### 5. Compile the Contract Compile your contract with: ```bash npx hardhat compile ``` If successful, you should see output indicating compilation was successful: ``` Compiling 12 files with 0.8.22 Solidity compilation finished successfully ``` ### 6. Deploy to Push Chain #### 6.1. Set up your deployer account Create a `.env` file in your project root to securely store your private key: Then create a deployment script at `scripts/deploy.js`: ```javascript const hre = require('hardhat'); async function main() { console.log('Deploying MyToken to PUSH Chain...'); const myToken = await hre.ethers.deployContract('MyToken'); await myToken.waitForDeployment(); const address = await myToken.getAddress(); console.log(`MyToken deployed to: ${address}`); } // We recommend this pattern to be able to use async/await everywhere // and properly handle errors. main().catch((error) => { console.error(error); process.exitCode = 1; }); ``` :::warning Never commit your .env file to version control. Add .env to your .gitignore file to prevent accidental exposure of private keys. Never use accounts with significant funds for test deployments. ::: #### 6.2. Get testnet tokens Ensure you have testnet tokens to pay for deployment gas fees. If you don't have any, visit the [Push Chain Faucet](https://faucet.push.org/) to request test tokens. #### 6.3. Deploy your contract Run the deployment script with: ```bash npx hardhat run scripts/deploy.js --network push_testnet ``` This command: - Runs your deployment script - Connects to Push Chain testnet - Uses your private key from the .env file - Deploys your contract and waits for confirmation **Deployment Results** If successful, you'll see output similar to: ``` Deploying MyToken to PUSH Chain... MyToken deployed to: 0x0B86e252B035027028C0d4D3B136d80Da4C98Ec1 ``` Save the contract address for verification and interaction. ### 7. Verify the Contract Verify your contract on the Push Chain BlockScout explorer: ```bash npx hardhat verify --network push_testnet 0x0B86e252B035027028C0d4D3B136d80Da4C98Ec1 ``` > **Note**: Replace `0x0B86e252B035027028C0d4D3B136d80Da4C98Ec1` with your actual deployed contract address. > **Note**: If you encounter issues with verification, you can refer to [Blockscout's Hardhat verification plugin documentation](https://docs.blockscout.com/devs/verification/hardhat-verification-plugin) for troubleshooting. When successful, you'll receive a confirmation message with a link to view your verified contract on the Push Chain explorer. That's it! You have successfully deployed and verified your smart contract on Push Chain using Hardhat. ## Next Steps - Jump into building and interacting with your smart contract using the [Push Chain SDK](/docs/chain/build) - Check out [chain configuration](/docs/chain/setup/chain-config/) or [available helper contracts](/docs/chain/build/contract-helpers/) - Abstract everything on frontend with [UI Kit](/docs/chain/ui-kit/integrate-push-universal-wallet/) --- # Tooling URL: https://push.org/docs/chain/setup/tooling/ Tooling Section | Setup | Push Chain Docs # Tooling Section This section covers everything you will require to setup your tooling to start building on Push Chain. --- # Smart Contract Environment URL: https://push.org/docs/chain/setup/smart-contract-environment/ Smart Contract Environment Section | Setup | Push Chain Docs # Smart Contract Environment Section Setting up your smart contract environment to interact with Push Chain, whether you are using Remix, Hardhat, Foundry, or any other tooling, this section will guide you through the process. --- # Build a Counter App URL: https://push.org/docs/chain/tutorials/basics/tutorial-simple-counter/ Build a Counter App | Tutorials | Push Chain Docs In this tutorial, you’ll write, deploy, and interact with a Counter contract on Push Chain. We will start with the most popular smart contract, i.e., `Counter.sol`, that all Solidity devs are familiar with. You would have done the following by the end: - βœ… Build and deploy Counter.sol - βœ… Interact with it from any chain - βœ… Understand the benefits of building Universal Apps - βœ… Use **Live Playground** to test and interact with Counter - πŸ”œ Extend to a Universal Counter that tracks chain specific users ## Write the Contract The process of building a simple smart contract like a counter is exactly similar to any other EVM Chain. You can use the same tools, such as, remix, foundry, hardhat, etc. To get started, you can use the following contract: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; contract Counter { uint256 public countPC; event CountIncremented(uint256 indexed countPC, address indexed caller); function increment() public { countPC += 1; emit CountIncremented(countPC, msg.sender); } function reset() public { countPC = 0; } } ``` ## Understanding the Contract This contract is a minimal counter: - The variable `count` stores the number of times the counter has been incremented. - The `increment()` function adds `+1` each time it is called. - The `getCount()` function lets anyone read the current counter value. > **Key takeaway** > On Push Chain, this contract works **universally**. A user on Ethereum, Solana, Push or any other chain itself can all call `increment()` β€” with no code changes. ## Interact with Counter The easiest way to interact with the contract is through the Live Playground. The Counter is already deployed on Push Chain Testnet. > **Counter Contract Address:** [0x5FbDB2315678afecb367f032d93F642f64180aa3](https://donut.push.network/address/0x5FbDB2315678afecb367f032d93F642f64180aa3?tab=contract) **Steps to interact:** - Connect your wallet to the Live Playground. - You can connect a wallet from any supported chain (Push Chain, Ethereum, or Solana). - Click **Increment Counter** to increase the counter. - Click **Refresh Counter Values** to update the display. - After each transaction, use the transaction hash link to view details in Push Chain Explorer. ## Live Playground ```jsx live // customPropMinimized='true' function CounterExample() { // Define Wallet Config const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, }; // Define Counter ABI, taking minimal ABI for the demo const UCABI = [ { inputs: [], name: 'increment', outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [], name: 'countPC', outputs: [ { internalType: 'uint256', name: '', type: 'uint256', }, ], stateMutability: 'view', type: 'function', }, ]; // Contract address for Counter const CONTRACT_ADDRESS = '0x5FbDB2315678afecb367f032d93F642f64180aa3'; function Component() { const { connectionStatus } = usePushWalletContext(); const { pushChainClient } = usePushChainClient(); // State to store counter values const [countPC, setCountPC] = useState(-1); const [isLoadingIncrement, setIsLoadingIncrement] = useState(false); const [isLoadingReset, setIsLoadingReset] = useState(false); const [txHash, setTxHash] = useState(''); // Function to encode increment transaction data const getIncrementTxData = () => { return PushChain.utils.helpers.encodeTxData({ abi: UCABI, functionName: 'increment', }); }; // Function to fetch counter values const fetchCounters = async () => { try { const provider = new ethers.JsonRpcProvider( 'https://evm.donut.rpc.push.org/' ); const contract = new ethers.Contract(CONTRACT_ADDRESS, UCABI, provider); const pcCount = await contract.countPC(); setCountPC(Number(pcCount)); } catch (err) { console.error('Error fetching counter values:', err); } }; // Fetch counter values on component mount useEffect(() => { fetchCounters(); }, []); // Handle transaction to increment counter const handleSendTransaction = async () => { if (pushChainClient) { try { setIsLoadingIncrement(true); const data = getIncrementTxData(); const tx = await pushChainClient.universal.sendTransaction({ to: CONTRACT_ADDRESS, value: BigInt(0), data: data, }); setTxHash(tx.hash); await tx.wait(); await fetchCounters(); setIsLoadingIncrement(false); } catch (err) { console.error('Transaction error:', err); setIsLoadingIncrement(false); } } }; return ( Counter Example {connectionStatus !== PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && ( Please connect your wallet to interact with the counter. )} Counter: {countPC == -1 ? '...' : countPC} {connectionStatus === PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && ( {isLoadingIncrement ? 'Processing...' : 'Increment Counter'} {txHash && pushChainClient && ( Transaction Hash:{' '} {txHash} )} )} ); } return ( ); } ``` ## Source Code ## What we Achieved This was just a simple tutorial. What we did in this tutorial: - Deployed a counter contract on Push Chain. - Interacted seamlessly with the contract from any chain. (Ethereum, Solana or Push Chain) ## What's Next? The next tutorial introduces the true power of **Universal Apps**. ```mermaid flowchart TD EU[Ethereum User] --> UC[Universal Counter Contract] SU[Solana User] --> UC PU[Push Chain User] --> UC UC --> EC[Ethereum Counter: 5] UC --> SC[Solana Counter: 3] UC --> PC[Push Chain Counter: 8] style UC fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style EC fill:#627eea,stroke:#fff,stroke-width:2px,color:#fff style SC fill:#16c492,stroke:#fff,stroke-width:2px,color:#fff style PC fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style EU fill:#627eea,color:#fff style SU fill:#16c492,color:#fff style PU fill:#dd44b9,color:#fff ``` In the next part, we modify this contract to implement the following: 1. `increment()` called by users of **any chain** will now be attributed to them. 2. The contract will natively detect which chain the `msg.sender` belongs to. 3. The contract will maintain a `count` for each chain based on the caller’s origin. > All of these features will be natively supported in the contract with no requirement of > third-party oracles, interop providers or packages. > **This is only possible on Push Chain.** --- # Batch Transactions (Multicall) URL: https://push.org/docs/chain/tutorials/power-features/tutorial-batch-transactions/ Batch Transactions | Tutorials | Push Chain Docs In this tutorial, you’ll learn how to **execute multiple smart contract calls in a single transaction** on Push Chain, also known as **Multicall** or **Batch Transactions**. This is one of Push Chain’s most powerful features, letting you do multiple actions such as approvals, transfers, or any contract interactions in a single transaction. By the end, you’ll be able to: - βœ… Bundle multiple contract calls into one universal transaction. - βœ… Execute them atomically on Push Chain (all succeed or none do). - βœ… Reuse the same approach for your own app logic. ## Understanding Multicall In traditional dApps, every interaction requires its own transaction β€” users approve, then transfer, then call another contract. With Push Chain’s **Universal Execution Account (UEA)** model, you can include an array of calls in a single `sendTransaction()`. The SDK automatically encodes and executes them in sequence, ensuring atomicity. **Requirements**: Batch transactions run from **external origin chains** and execute atomically on Push Chain. ## Contracts Used We’ll reuse two contracts from earlier tutorials and interact with **both** from an external origin chain in a single universal transaction: - `Counter.sol` from [Simple Counter Tutorial](/docs/chain/tutorials/basics/tutorial-simple-counter/) β†’ to increment the counter. - `ERC20.sol` from [Mint Universal ERC-20 Tutorial](/docs/chain/tutorials/basics/tutorial-mint-erc-20-tokens/) β†’ to mint $UNICORN token. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; contract Counter { uint256 public countPC; event CountIncremented(uint256 indexed countPC, address indexed caller); function increment() public { countPC += 1; emit CountIncremented(countPC, msg.sender); } function reset() public { countPC = 0; } } ``` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract Token is ERC20 { constructor(string memory name, string memory symbol) ERC20(name, symbol) { _mint(msg.sender, 1_000_000 * 10 ** decimals()); } function mint(address to, uint256 amount) external { _mint(to, amount); } } ``` Follow: - [Build a Counter App](/docs/chain/tutorials/basics/tutorial-simple-counter/) - [Mint Universal ERC-20 Token](/docs/chain/tutorials/basics/tutorial-mint-erc-20-tokens/) Then replace the example addresses in this tutorial with your deployments. ## Build the Multicall Payload :::warning Multicall requirements - **Origin-only:** Batch transactions are supported **only from external origin chains** (not native Push). - **Atomicity:** If any call fails, the **entire batch reverts**. - For multicall, the `to` should always be zero address (`0x0000000000000000000000000000000000000000`). The SDK will `console.warn` if you pass any other address. This will become mandatory in a future release. ::: ```ts // rest of the code... // Counter ABI on Push Chain (used in tests) with an increment function const CounterABI = [ { inputs: [], name: 'increment', outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [], name: 'countPC', outputs: [ { internalType: 'uint256', name: '', type: 'uint256', }, ], stateMutability: 'view', type: 'function', }, ]; // Counter deployed on Push Chain Testnet const counterAddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3'; // Create function call for Counter.increment() const incrementData = PushChain.utils.helpers.encodeTxData({ abi: CounterABI, functionName: 'increment', }); // ERC20 ABI on Push Chain (used in tests) with a mint function const ERC20ABI = [ { inputs: [ { name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }, ], name: 'mint', outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [ { name: 'account', type: 'address' }, ], name: 'balanceOf', outputs: [ { name: '', type: 'uint256' }, ], stateMutability: 'view', type: 'function', }, ]; // ERC20 deployed on Push Chain Testnet const erc20Address = '0x0165878A594ca255338adfa4d48449f69242Eb8F'; // Create function call for ERC20.mint() const mintData = PushChain.utils.helpers.encodeTxData({ abi: ERC20ABI, functionName: 'mint', args: [ pushChainClient.universal.account, // recipient is the connected UEA PushChain.utils.helpers.parseUnits('11', 18), // 11 PC in uPC (ie: 18 decimal places), ], }); // rest of the code... // Send batch transaction (multicall) // highlight-start const batchTx = await pushChainClient.universal.sendTransaction({ to: '0x0000000000000000000000000000000000000000', data: [ {to: counterAddress, value: 0n, data: incrementData}, {to: erc20Address, value: 0n, data: mintData}, ] }); // highlight-end // rest of the code... ``` ## Understanding Multicall Payload In this example, we are interacting with two different contracts on Push Chain Testnet from an external origin chain in a single transaction. Let’s break down how this transaction executes step-by-step. - We first construct specific function calls for each contract using the `encodeTxData` helper function. - We pass an array of function calls to the `data` parameter which contains `to`, `value` and `data` instead of a single function call. - We pass the Universal Account address to the `to` parameter instead of a contract address. ```mermaid flowchart LR A[User on Ethereum / Solana / Other Chains] --> B["sendTransaction() with multiple calls"] B --> C[UEA on Push Chain executes all calls atomically] C --> D[Counter incremented + Tokens minted] ``` β€’ **Origin-only error**: Multicall works only when initiated from an external chain. β€’ **Wrong `to`**: The batch `to` **must** be `pushChainClient.universal.account`. β€’ **Bad ABI/data**: Ensure `encodeTxData({ abi, functionName, args })` matches the contract exactly. ## Interact with Multicall Both the counter and universal ERC-20 contracts are deployed on Push Chain Testnet. This app lets any user β€” whether on Ethereum, Solana, or any other external chain to increment the counter and mint $UNICORN tokens in one single transaction. > **Counter Contract Address:** [0x5FbDB2315678afecb367f032d93F642f64180aa3](https://donut.push.network/address/0x5FbDB2315678afecb367f032d93F642f64180aa3?tab=contract) > **Demo Token ERC-20 Contract Address:** [0x0165878A594ca255338adfa4d48449f69242Eb8F](https://donut.push.network/address/0x0165878A594ca255338adfa4d48449f69242Eb8F?tab=contract) **Steps to interact:** 1. Connect your wallet (Ethereum, Solana, or other chains). 2. Click the **Batch Transaction** button β€” this will atomically increment the counter and mint $UNICORN tokens. 3. Wait for the transaction to confirm. 4. Your `Counter` will be incremented and `$UNICORN` balance will update in the UI automatically. 5. (Optional) Click **View in Explorer** to inspect the transaction on Push Chain Explorer. **Note**: You can also batch approvals or swaps the same way. --- > πŸ’‘ **Tip: Why this matters** > Multicall drastically simplifies UX. Instead of asking users to sign multiple actions, you combine them into one universal transactionβ€”fewer popups, fewer confirmations, less friction. ## Live Playground ```jsx live // customPropMinimized='true' function BatchTransactionExample() { // Wallet config const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, login: { email: false, google: false, wallet: { enabled: true, chains: [ PushUI.CONSTANTS.CHAIN.ETHEREUM, PushUI.CONSTANTS.CHAIN.SOLANA ], } } }; // Provider to read balances const provider = new ethers.JsonRpcProvider( "https://evm.donut.rpc.push.org/" ); // Counter ABI on Push Chain (used in tests) with an increment function const CounterABI = [ { inputs: [], name: 'increment', outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [], name: 'countPC', outputs: [ { internalType: 'uint256', name: '', type: 'uint256', }, ], stateMutability: 'view', type: 'function', }, ]; // Counter deployed on Push Chain Testnet const counterAddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3'; // ERC20 ABI on Push Chain (used in tests) with a mint function const ERC20ABI = [ { inputs: [ { name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }, ], name: 'mint', outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [ { name: 'account', type: 'address' }, ], name: 'balanceOf', outputs: [ { name: '', type: 'uint256' }, ], stateMutability: 'view', type: 'function', }, ]; // ERC20 deployed on Push Chain Testnet const erc20Address = '0x0165878A594ca255338adfa4d48449f69242Eb8F'; // main component function Component() { const { connectionStatus } = usePushWalletContext(); const { pushChainClient } = usePushChainClient(); const { PushChain } = usePushChain(); const [counter, setCounter] = useState(-1); const [balance, setBalance] = useState(-1); const [isLoading, setIsLoading] = useState(false); const [txHash, setTxHash] = useState(""); // Fetch balance of connected account const readStatus = async () => { if (pushChainClient && connectionStatus === "connected") { // get counter value and initial balance const counterContract = new ethers.Contract(counterAddress, CounterABI, provider); const count = await counterContract.countPC(); setCounter(Number(count)); const contract = new ethers.Contract(erc20Address, ERC20ABI, provider); const bal = await contract.balanceOf(pushChainClient.universal.account); setBalance(Number(ethers.formatUnits(bal, 18))); } }; useEffect(() => { if (pushChainClient && connectionStatus === "notConnected") { setCounter(-1); setBalance(-1); } readStatus(); }, [connectionStatus, pushChainClient]); // batch call function const doBatchCall = async () => { if (connectionStatus === "connected" && pushChainClient) { try { setIsLoading(true); // Create function call for Counter.increment() const incrementData = PushChain.utils.helpers.encodeTxData({ abi: CounterABI, functionName: 'increment', }); // Create function call for ERC20.mint() const mintData = PushChain.utils.helpers.encodeTxData({ abi: ERC20ABI, functionName: 'mint', args: [ pushChainClient.universal.account, PushChain.utils.helpers.parseUnits('11', 18), // 11 PC in uPC (ie: wei), ], }); // Create batch transaction // highlight-start const batchTx = await pushChainClient.universal.sendTransaction({ to: '0x0000000000000000000000000000000000000000', data: [ {to: counterAddress, value: 0n, data: incrementData}, {to: erc20Address, value: 0n, data: mintData}, ] }); // highlight-end setTxHash(batchTx.hash); await batchTx.wait(); console.log('βœ… Multicall complete! Counter incremented and tokens minted.'); await readStatus(); } catch (err) { console.error("Transaction error:", err); } finally { setIsLoading(false); } } }; return ( Batch Transaction / Multicall {connectionStatus !== PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && ( Please connect your wallet to do batch transactions (multicall). )} Counter: {counter === -1 ? "..." : counter} Balance: {balance === -1 ? "..." : balance} $UNICORN {balance !== -1 && Optional: Add 0x0165878A594ca255338adfa4d48449f69242Eb8F ($UNICORN Token Address) to your wallet to see your balance in the wallet. } {connectionStatus === PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && ( <> {isLoading ? "Executing..." : "Do Batch Transaction"} {txHash && ( Transaction:{" "} {txHash} )} )} ); } return ( ); } ``` ## Source Code ## What we Achieved In this tutorial, we built multiple transactions into a single transaction. - We wrote and deployed a counter contract and ERC-20 contract. - We incremented counter and minted tokens, then confirmed balances via the frontend. All with a single universal transaction from other source chains. This forms the foundation for multi-action DeFi, gaming, and on-chain automation flows. ## What’s Next? Batch transactions are executed by a user’s **Universal Executor Account (UEA)** on Push Chain. Every multicall you just executed was routed through a UEA derived from the user’s origin wallet. Understanding UEAs unlocks how Push Chain enables universal identity, permissions, and execution. ```mermaid flowchart TB A[Ethereum Wallet] --> D[UEA on Push Chain] B[Solana Wallet] --> E[UEA on Push Chain] C[Base Wallet] --> F[UEA on Push Chain] D --> G[Multicall Execution] E --> G F --> G G --> H[Multiple ContractsExecuted Atomically] style D fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style E fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style F fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style A fill:#627eea,color:#fff style B fill:#16c492,color:#fff style C fill:#0052ff,color:#fff ``` Check out the next tutorial to learn how to [derive Universal Executor Accounts](/docs/chain/tutorials/power-features/tutorial-derive-universal-executor-account/). --- # Build a Universal Airdrop URL: https://push.org/docs/chain/tutorials/token-systems/tutorial-universal-airdrop/ Build a Universal Airdrop | Tutorials | Push Chain Docs In this tutorial, you'll build a **Universal Claimable Airdrop** system on Push Chain. You deploy an airdrop contract **once** on Push Chain, and users from **any supported chain** can claim using their existing wallets. By the end of this tutorial you'll be able to: - βœ… Convert addresses from any chain to deterministic Push Chain addresses ([UEAs](/docs/chain/tutorials/power-features/tutorial-derive-universal-executor-account/)) - βœ… Generate Merkle trees for efficient airdrop verification - βœ… Deploy a universal airdrop contract with OpenZeppelin's Merkle proof system - βœ… Build a claim UI that works for users on any chain - βœ… Understand how Universal Executor Accounts enable cross-chain claiming ## What Makes This Universal? Traditional airdrops often require: - Deploying contracts on multiple chains - Managing separate token supplies per chain - Forcing users to claim on a specific chain or do extra wallet ops **Universal Airdrops on Push Chain:** - **Deploy once** on Push Chain - Users from **any chain** claim with their **existing wallet** - **No bridging** or chain-switching required - One contract, one token supply, reaching every chain - No per chain deployments or per chain token supplies ### Example Flow - Alice (Ethereum wallet `0xABC...`) β†’ Claims directly from Ethereum - Bob (Solana wallet `7xKX...`) β†’ Claims directly from Solana - Charlie (Base wallet `0xDEF...`) β†’ Claims directly from Base All three interact with the **same contract** on Push Chain through their [Universal Executor Accounts (UEAs)](/docs/chain/tutorials/power-features/tutorial-derive-universal-executor-account/). > **πŸš€ Why this matters** > > This is the future of token distribution. Deploy once, reach everyone. No multi-chain complexity, no fragmented liquidity, just pure universal access. ## Understanding the Architecture The Universal Airdrop system consists of four key components: ### 1. Address Conversion For each recipient, we convert their origin address to a deterministic **[Universal Executor Account (UEA)](/docs/chain/tutorials/power-features/tutorial-derive-universal-executor-account/)** address on Push Chain: ```mermaid flowchart LR A[Ethereum: 0xABC...] --> D[UEA: 0x123...] B[Solana: 7xKX...] --> E[UEA: 0x456...] C[Base: 0xDEF...] --> F[UEA: 0x789...] D --> G[Universal Airdrop Contract] E --> G F --> G style G fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style A fill:#627eea,color:#fff style B fill:#16c492,color:#fff style C fill:#0052ff,color:#fff ``` ### 2. Merkle Tree Generation We create a Merkle tree where each leaf contains: - UEA address (converted from origin address) - Token amount Note: the demo stores origin address and chain only in the UI for display. They are not part of the Merkle leaf. ### 3. Smart Contract Deployment The airdrop contract: - Stores the Merkle root - Verifies proofs on-chain - Prevents double claiming - Distributes tokens to claimants ### 4. Claim Interface Users connect with their origin wallet and claim tokens through their UEA. ## Write the Contracts We'll need two contracts for this tutorial: 1. **ERC-20 Token Contract** - The token being airdropped ($UNICORN) - see [Mint Universal ERC-20 Tokens](/docs/chain/tutorials/basics/tutorial-mint-erc-20-tokens/) for basics 2. **Universal Airdrop Contract** - Handles Merkle proof verification and token distribution > **Production note:** most real airdrops distribute an existing token. This tutorial deploys a fresh $UNICORN token to keep the demo self-contained. In production, deploy the airdrop against your existing token address and fund it with the distribution supply. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract Token is ERC20 { constructor(string memory name, string memory symbol) ERC20(name, symbol) { _mint(msg.sender, 1_000_000 * 10 ** decimals()); } function mint(address to, uint256 amount) external { _mint(to, amount); } } ``` **Key Features:** - Mints 1,000,000 tokens to the deployer - Has an open `mint()` function for easy testing - Uses OpenZeppelin's battle-tested ERC-20 implementation :::warning Demo Only Token.sol is a demo contract. Do not ship an open `mint()` in production. Gate minting (Ownable/AccessControl) or distribute from a fixed supply. ::: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import "./UniversalAirdrop.sol"; import "./Token.sol"; contract UniversalAirdropFactory { event AirdropCreated( address indexed airdrop, address indexed owner, uint256 totalAmount, bytes32 merkleRoot ); function createAirdrop( uint256 _totalAmount, bytes32 _merkleRoot ) external returns (address) { // Deploy new Token contract Token token = new Token("Unicorn Token", "UNICORN"); // Mint tokens to this factory token.mint(address(this), _totalAmount); // Deploy new UniversalAirdrop contract UniversalAirdrop airdrop = new UniversalAirdrop( address(token), _merkleRoot, msg.sender ); // Transfer tokens to airdrop contract require( token.transfer(address(airdrop), _totalAmount), "Token transfer failed" ); emit AirdropCreated( address(airdrop), msg.sender, _totalAmount, _merkleRoot ); return address(airdrop); } } ``` **Key Features:** - Deploys both the token and airdrop contracts in one transaction - Mints the required tokens automatically - Transfers tokens to the airdrop contract - Emits an event with the deployed addresses ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; contract UniversalAirdrop is Ownable, ReentrancyGuard { IERC20 public immutable token; bytes32 public merkleRoot; mapping(address => bool) public hasClaimed; event Claimed(address indexed claimer, uint256 amount); event MerkleRootUpdated(bytes32 newRoot); constructor( address _token, bytes32 _merkleRoot, address _owner ) Ownable(_owner) { token = IERC20(_token); merkleRoot = _merkleRoot; } function claim( uint256 amount, bytes32[] calldata proof ) external nonReentrant { require(!hasClaimed[msg.sender], "Already claimed"); bytes32 leaf = keccak256( bytes.concat(keccak256(abi.encode(msg.sender, amount))) ); require( MerkleProof.verify(proof, merkleRoot, leaf), "Invalid proof" ); hasClaimed[msg.sender] = true; require( token.transfer(msg.sender, amount), "Token transfer failed" ); emit Claimed(msg.sender, amount); } function updateMerkleRoot(bytes32 newRoot) external onlyOwner { merkleRoot = newRoot; emit MerkleRootUpdated(newRoot); } function withdrawTokens(address to, uint256 amount) external onlyOwner { require(token.transfer(to, amount), "Transfer failed"); } } ``` **Key Features:** - Uses OpenZeppelin's `MerkleProof.verify()` for efficient proof verification - Tracks claims with a mapping to prevent double-claiming - Includes reentrancy protection - Allows owner to update Merkle root for future rounds - Provides emergency withdrawal function ## Understanding the Contracts ### Key Concepts **1. Merkle Proof Verification** The contract uses OpenZeppelin's standard Merkle proof implementation: ```solidity bytes32 leaf = keccak256( bytes.concat(keccak256(abi.encode(msg.sender, amount))) ); require( MerkleProof.verify(proof, merkleRoot, leaf), "Invalid proof" ); ``` **Important:** the leaf encoding used in the frontend and the leaf hash computed in the contract must match exactly. This tutorial uses OpenZeppelin `StandardMerkleTree` for proofs and OpenZeppelin `MerkleProof.verify()` in the contract so the hashing/ordering stays consistent. **2. Universal Executor Accounts (UEAs)** When a user from Ethereum, Solana, or any other chain interacts with this contract: - Their wallet signs a transaction on their origin chain - Push Chain creates/uses their deterministic UEA address - The UEA executes the `claim()` function - `msg.sender` in the contract is the UEA address This means the Merkle tree must contain **UEA addresses**, not origin addresses. **3. Address Conversion Process** ```typescript // Convert origin address to UEA const account = PushChain.utils.account.toUniversal(originAddress, { chain: originChain, }); const executorAddress = await PushChain.utils.account.deriveExecutorAccount(account); ``` This deterministic conversion ensures: - Same origin address always maps to same UEA - Works across all supported chains - No registration or setup required ## Live Playground Now let's build a complete frontend that handles the entire airdrop flow. This example demonstrates all four steps: adding recipients, generating Merkle tree, deploying contracts, and claiming tokens. The deployed contracts are available on Push Chain Testnet: > **Factory Contract:** [0xf5059a5D33d5853360D16C683c16e67980206f36](https://donut.push.network/address/0xf5059a5D33d5853360D16C683c16e67980206f36?tab=contract) **Steps to interact:** 1. **Step 1**: Add wallet addresses from different chains to your airdrop list 2. **Step 2**: Generate Merkle tree and get the root hash 3. **Step 3**: Deploy the airdrop contract with the Merkle root 4. **Step 4**: Connect with a claimer wallet and claim tokens ```jsx live // customPropMinimized='true' function UniversalAirdropTutorial() { const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, }; const UniversalAirdropABI = [ { inputs: [ { internalType: "uint256", name: "amount", type: "uint256" }, { internalType: "bytes32[]", name: "proof", type: "bytes32[]" } ], name: "claim", outputs: [], stateMutability: "nonpayable", type: "function" }, { inputs: [{ internalType: "address", name: "", type: "address" }], name: "hasClaimed", outputs: [{ internalType: "bool", name: "", type: "bool" }], stateMutability: "view", type: "function" } ]; const factoryEventABI = [ "event AirdropCreated(address indexed airdrop, address indexed owner, uint256 totalAmount, bytes32 merkleRoot)" ]; function Component() { const { PushChain } = usePushChain(); const { pushChainClient } = usePushChainClient(); const { connectionStatus } = usePushWalletContext(); const [currentStep, setCurrentStep] = useState(1); const [walletList, setWalletList] = useState([]); const [newWalletAddress, setNewWalletAddress] = useState(""); const [selectedChain, setSelectedChain] = useState(PushChain.CONSTANTS.CHAIN.PUSH_TESTNET); const [airdropAmount, setAirdropAmount] = useState("100"); const [convertedAddresses, setConvertedAddresses] = useState([]); const [merkleRoot, setMerkleRoot] = useState(""); const [merkleTree, setMerkleTree] = useState(null); const [deployedAirdropAddress, setDeployedAirdropAddress] = useState(""); const [isDeploying, setIsDeploying] = useState(false); const [isClaiming, setIsClaiming] = useState(false); const [claimerEligibility, setClaimerEligibility] = useState(null); const [error, setError] = useState(""); const FACTORY_ADDRESS = "0xf5059a5D33d5853360D16C683c16e67980206f36"; const chains = [ { value: PushChain.CONSTANTS.CHAIN.PUSH_TESTNET, label: "Push Chain" }, { value: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, label: "Ethereum Sepolia" }, { value: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, label: "Solana Devnet" }, { value: PushChain.CONSTANTS.CHAIN.BASE_SEPOLIA, label: "Base Sepolia" }, { value: PushChain.CONSTANTS.CHAIN.ARBITRUM_SEPOLIA, label: "Arbitrum Sepolia" }, { value: PushChain.CONSTANTS.CHAIN.BNB_TESTNET, label: "BNB Testnet" }, ]; const factoryInterface = new ethers.Interface(factoryEventABI); useEffect(() => { if (connectionStatus === "connected" && pushChainClient?.universal.account && currentStep === 1) { setWalletList((prevList) => { const isAlreadyAdded = prevList.some( (entry) => entry.address.toLowerCase() === pushChainClient.universal.origin.address.toLowerCase() ); if (!isAlreadyAdded) { return [{ address: pushChainClient.universal.origin.address, chain: pushChainClient.universal.origin.chain, amount: airdropAmount.toString(), }, ...prevList]; } return prevList; }); } }, [connectionStatus, pushChainClient, currentStep, airdropAmount]); const addWalletToList = () => { if (!newWalletAddress.trim()) { setError("Please enter a wallet address"); return; } setWalletList([...walletList, { address: newWalletAddress.trim(), chain: selectedChain, amount: airdropAmount, }]); setNewWalletAddress(""); setError(""); }; const removeWallet = (index) => { setWalletList(walletList.filter((_, i) => i !== index)); }; const convertToPushChainAddresses = async () => { if (walletList.length === 0) { setError("Please add at least one wallet to the list"); return; } try { const addressPromises = walletList.map(async (entry) => { const account = PushChain.utils.account.toUniversal(entry.address, { chain: entry.chain }); const executorAddress = await PushChain.utils.account.deriveExecutorAccount(account); return [ executorAddress.address, ethers.parseUnits(entry.amount, 18).toString(), entry.address, entry.chain ]; }); const addresses = await Promise.all(addressPromises); setConvertedAddresses(addresses); setError(""); setCurrentStep(2); } catch (err) { console.error("Error generating deterministic addresses:", err); setError("Failed to generate deterministic addresses on Push Chain"); } }; const generateMerkleTree = () => { if (convertedAddresses.length === 0) { setError("No converted addresses available"); return; } if (!StandardMerkleTree) { setError("Merkle tree library is still loading. Please wait a moment and try again."); return; } try { const values = convertedAddresses.map(([address, amount]) => [address, amount]); const tree = StandardMerkleTree.of(values, ["address", "uint256"]); const root = tree.root; setMerkleRoot(root); setMerkleTree({ tree, root }); setError(""); setCurrentStep(3); } catch (err) { console.error("Error generating merkle tree:", err); setError("Failed to generate merkle tree"); } }; const deployAirdrop = async () => { if (!pushChainClient || !merkleRoot) { setError("Wallet not connected or Merkle root not generated"); return; } setIsDeploying(true); setError(""); try { const totalAmount = convertedAddresses.reduce( (sum, [, amount]) => sum + BigInt(amount), BigInt(0) ); const factoryABI = [{ inputs: [ { internalType: "uint256", name: "_totalAmount", type: "uint256" }, { internalType: "bytes32", name: "_merkleRoot", type: "bytes32" } ], name: "createAirdrop", outputs: [{ internalType: "address", name: "", type: "address" }], stateMutability: "nonpayable", type: "function" }]; const txData = PushChain.utils.helpers.encodeTxData({ abi: factoryABI, functionName: "createAirdrop", args: [totalAmount, merkleRoot] }); const tx = await pushChainClient.universal.sendTransaction({ to: FACTORY_ADDRESS, data: txData, value: BigInt(0), }); const receipt = await tx.wait(); if (!receipt.logs || receipt.logs.length === 0) { setError("No logs found in transaction receipt"); setIsDeploying(false); return; } const factoryLog = receipt.logs.find( (log) => log.address.toLowerCase() === FACTORY_ADDRESS.toLowerCase() ); if (!factoryLog) { setError("Airdrop creation event not found"); setIsDeploying(false); return; } let parsed; try { parsed = factoryInterface.parseLog(factoryLog); } catch (e) { setError("Failed to decode AirdropCreated event"); setIsDeploying(false); return; } const airdropAddress = parsed.args.airdrop; setDeployedAirdropAddress(airdropAddress); setCurrentStep(4); setIsDeploying(false); } catch (err) { console.error("Error deploying airdrop:", err); setError("Failed to deploy airdrop contract"); setIsDeploying(false); } }; useEffect(() => { const checkClaimerEligibility = async () => { if (connectionStatus === "connected" && pushChainClient?.universal.account && deployedAirdropAddress) { const claimerAddress = pushChainClient.universal.account; const entry = convertedAddresses.find( ([addr]) => addr.toLowerCase() === claimerAddress.toLowerCase() ); if (entry) { const [executorAddr, amount] = entry; let hasClaimed = false; try { const provider = new ethers.JsonRpcProvider("https://evm.donut.rpc.push.org/"); const contract = new ethers.Contract(deployedAirdropAddress, UniversalAirdropABI, provider); hasClaimed = await contract.hasClaimed(claimerAddress); } catch (err) { console.error("Error checking claim status:", err); } setClaimerEligibility({ isEligible: true, hasClaimed: hasClaimed, amount: amount, executorAddress: executorAddr, }); } else { setClaimerEligibility({ isEligible: false, hasClaimed: false, amount: "0", executorAddress: "", }); } } else { setClaimerEligibility(null); } }; checkClaimerEligibility(); }, [connectionStatus, pushChainClient, deployedAirdropAddress, convertedAddresses]); const claimAirdrop = async () => { if (!pushChainClient || !deployedAirdropAddress) { setError("Wallet not connected or no deployed airdrop contract"); return; } setIsClaiming(true); setError(""); try { const claimAddr = pushChainClient.universal.account; const entry = convertedAddresses.find( ([addr]) => addr.toLowerCase() === claimAddr.toLowerCase() ); if (!entry) { setError("Address not found in airdrop list"); setIsClaiming(false); return; } const [, amount] = entry; const provider = new ethers.JsonRpcProvider("https://evm.donut.rpc.push.org/"); const contract = new ethers.Contract(deployedAirdropAddress, UniversalAirdropABI, provider); const hasClaimed = await contract.hasClaimed(claimAddr); if (hasClaimed) { setError("This address has already claimed the airdrop"); setIsClaiming(false); setClaimerEligibility({ ...claimerEligibility, hasClaimed: true }); return; } if (!merkleTree) { setError("Merkle tree not generated"); setIsClaiming(false); return; } let proof = []; for (const [i, v] of merkleTree.tree.entries()) { if (v[0].toLowerCase() === claimAddr.toLowerCase()) { proof = merkleTree.tree.getProof(i); break; } } const txData = PushChain.utils.helpers.encodeTxData({ abi: UniversalAirdropABI, functionName: "claim", args: [amount, proof] }); const tx = await pushChainClient.universal.sendTransaction({ to: deployedAirdropAddress, data: txData, value: BigInt(0), }); await tx.wait(); alert(`Successfully claimed ${ethers.formatEther(amount)} $UNICORN tokens!`); setIsClaiming(false); setClaimerEligibility({ ...claimerEligibility, hasClaimed: true }); } catch (err) { console.error("Error claiming airdrop:", err); setError("Failed to claim airdrop"); setIsClaiming(false); } }; return ( Universal Claimable Airdrop Deploy once on Push Chain. Users from any chain can claim with their existing wallet. {connectionStatus !== "connected" && ( Please connect your wallet to start the airdrop setup. )} {[1, 2, 3, 4].map((step) => { const locked = (step === 2 && convertedAddresses.length === 0) || (step === 3 && !merkleRoot) || (step === 4 && !deployedAirdropAddress); return ( !locked && setCurrentStep(step)} disabled={locked} style={{ padding: "8px 16px", backgroundColor: currentStep === step ? "#d946ef" : "#e0e0e0", color: currentStep === step ? "white" : "#666", cursor: locked ? "not-allowed" : "pointer", opacity: locked ? 0.5 : 1, border: "none", borderRadius: "6px", fontWeight: "bold", }} > Step {step} ); })} {connectionStatus === "connected" && currentStep === 1 && ( Step 1: Add Claimable Wallets Add wallet addresses from different chains to create your airdrop list. Chain setSelectedChain(e.target.value)} style={{ width: "100%", padding: "10px", borderRadius: "6px", border: "1px solid #ddd" }} > {chains.map((chain) => ( {chain.label} ))} Wallet Address setNewWalletAddress(e.target.value)} placeholder="0x..." style={{ width: "100%", padding: "10px", borderRadius: "6px", border: "1px solid #ddd" }} /> Amount ($UNICORN) setAirdropAmount(e.target.value)} placeholder="100" style={{ width: "100%", padding: "10px", borderRadius: "6px", border: "1px solid #ddd" }} /> Add to List {walletList.length > 0 && ( Wallet List ({walletList.length}) {walletList.map((wallet, index) => ( {PushChain.utils.chains.getChainName(wallet.chain)} {wallet.address} Amount: {wallet.amount} tokens removeWallet(index)} style={{ padding: "8px 12px", backgroundColor: "#dc3545", color: "white", border: "none", borderRadius: "6px", cursor: "pointer", }} > Remove ))} Convert to Push Chain Addresses )} {error && ( {error} )} )} {connectionStatus === "connected" && currentStep === 2 && ( Step 2: Generate Merkle Tree Review the converted addresses and generate the Merkle Tree for the airdrop. Converted Addresses ({convertedAddresses.length}) {convertedAddresses.map(([executorAddr, amount, originalAddr, chain], index) => { const chainLabel = chains.find(c => c.value === chain)?.label || chain; return `// Original: ${originalAddr} (${chainLabel})\n[\n "${executorAddr}",\n "${amount}"\n]${index Generate Merkle Tree and Proofs {merkleRoot && ( Merkle Root Generated! {merkleRoot} )} )} {connectionStatus === "connected" && currentStep === 3 && ( Step 3: Deploy Airdrop Contract Deploy the airdrop contract with the Merkle root. The factory will mint $UNICORN tokens automatically. πŸ”‘ Key Concept Users from any chain will interact through their Universal Executor Account (UEA) - the deterministic addresses we generated in Step 1. Deployment Summary Token: $UNICORN (ERC-20) Total Amount:{" "} {ethers.formatEther(convertedAddresses.reduce((sum, [, amount]) => sum + BigInt(amount), BigInt(0)))} $UNICORN Recipients: {convertedAddresses.length} addresses Merkle Root: {merkleRoot} {isDeploying ? "Deploying..." : "Deploy Airdrop Contract"} )} {connectionStatus === "connected" && currentStep === 4 && ( Step 4: Claim Airdrop Your airdrop contract has been deployed! Users can now claim their $UNICORN tokens. πŸŽ‰ Deployment Successful! Contract Address: Test Claim Use your connected wallet to claim tokens from the airdrop. {connectionStatus === "connected" && claimerEligibility && ( {claimerEligibility.isEligible ? "βœ… Eligible for Airdrop!" : "❌ Not Eligible"} {claimerEligibility.isEligible && ( <> Amount: {ethers.formatEther(claimerEligibility.amount)} $UNICORN {claimerEligibility.hasClaimed ? ( Status: Already Claimed βœ“ ) : ( {isClaiming ? "Claiming..." : "Claim Airdrop"} )} )} )} )} ); } return ( ); } ``` ## Understanding the Code ### Step 1: Address Conversion ```typescript const account = PushChain.utils.account.toUniversal(entry.address, { chain: entry.chain, }); const executorAddress = await PushChain.utils.account.deriveExecutorAccount(account); ``` This converts each origin address to its deterministic UEA address on Push Chain. The same origin address will always produce the same UEA address. ### Step 2: Merkle Tree Generation ```typescript const values = convertedAddresses.map(([address, amount]) => [address, amount]); const tree = StandardMerkleTree.of(values, ['address', 'uint256']); const root = tree.root; ``` We use OpenZeppelin's `StandardMerkleTree` which implements the same double-hashing pattern as the contract's `MerkleProof.verify()`. ### Step 3: Contract Deployment ```typescript const txData = PushChain.utils.helpers.encodeTxData({ abi: factoryABI, functionName: 'createAirdrop', args: [totalAmount, merkleRoot], }); const tx = await pushChainClient.universal.sendTransaction({ to: FACTORY_ADDRESS, data: txData, value: BigInt(0), }); ``` The factory deploys both the token and airdrop contracts, mints tokens, and transfers them to the airdrop contract. ### Step 4: Claiming Tokens ```typescript // Generate proof for the claimer let proof = []; for (const [i, v] of merkleTree.tree.entries()) { if (v[0].toLowerCase() === claimAddr.toLowerCase()) { proof = merkleTree.tree.getProof(i); break; } } // Encode and send claim transaction const txData = PushChain.utils.helpers.encodeTxData({ abi: UniversalAirdropABI, functionName: 'claim', args: [amount, proof], }); const tx = await pushChainClient.universal.sendTransaction({ to: deployedAirdropAddress, data: txData, value: BigInt(0), }); ``` The claimer's UEA executes the `claim()` function with their proof, and the contract verifies and distributes tokens. ## Source Code ## What We Achieved In this tutorial, we built a complete universal airdrop system: - **Converted addresses** from multiple chains to deterministic UEA addresses - **Generated Merkle trees** for efficient on-chain verification - **Deployed contracts** that mint and distribute tokens automatically - **Built a claim UI** that works seamlessly across all chains ## Key Takeaways **1. One Deployment, Infinite Reach** - Deploy your airdrop contract once on Push Chain - Users from any chain can claim with their existing wallet - No multi-chain deployment complexity **2. Universal Executor Accounts** - Every address on every chain has a deterministic UEA on Push Chain - UEAs enable cross-chain interactions without bridges - Same origin address always maps to the same UEA **3. Standard Merkle Proofs** - Uses OpenZeppelin's battle-tested implementation - No custom cryptography or modifications needed - Efficient on-chain verification **4. Seamless User Experience** - Users never leave their preferred chain - No token bridging or chain-switching required - One click to claim from any wallet ## What's Next? Now that you've built a universal airdrop system, you can extend this pattern to create more advanced token distribution mechanisms. **Understanding the Foundation:** - Every claim was executed through a [Universal Executor Account (UEA)](/docs/chain/tutorials/power-features/tutorial-derive-universal-executor-account/) - The same UEA derivation enables all universal interactions on Push Chain - Merkle proofs provide efficient verification for large-scale distributions **Advanced Patterns to Explore:** - **Multi-round airdrops** with updatable Merkle roots - **Vesting schedules** with time-locked claims - **Conditional claims** based on on-chain activity - **Cross-chain governance** token distribution - **Staking rewards** distributed to any chain The possibilities are endless when you build universal! --- # Build a Universal Counter App URL: https://push.org/docs/chain/tutorials/basics/tutorial-universal-counter/ Build a Universal Counter App | Tutorials | Push Chain Docs :::info Extends Counter App This tutorial builds on the [Counter](/docs/chain/tutorials/basics/tutorial-simple-counter). If you haven’t completed it yet, go there first as this tutorial builds directly on top of it. ::: In the last tutorial, you built a counter that worked across chains with no code changes. Now, let’s take it further: instead of one shared counter, we’ll track **counts per chain**. This is your first truly **Universal App**. Let’s dive in 🀿. By the end of this tutorial you’ll be able to: - βœ… Build a counter app that tracks transactions from **different chains**. - βœ… Use the **UEAFactory interface** to detect a user’s origin. - βœ… Work with the **UniversalAccountId struct** to fetch chain details. ## What’s Unique About This App? In the **Counter**, every increment was added to a single shared value. That worked fine, but it didn’t tell us *who* was incrementing or *from where*. With the **Universal Counter**, we take the next step: - Each chain gets its **own counter** (`countEth`, `countPC`, `countSol`, …). - The contract can **natively detect the origin** of the caller (`msg.sender`). - The `increment()` function updates **only the counter for the caller’s chain**. ### Example - Alice (Ethereum) β†’ calls `increment()` β†’ only `countEth` goes up. - Bob (Push Chain) β†’ calls `increment()` β†’ only `countPC` goes up. > **πŸš€ Why this matters** > > You’re not just tracking clicks anymore. You’re building logic that’s aware of where your users come from. This is the foundation of truly universal apps, and it’s all **natively supported on Push Chain**. ## Write the Contract Below is the Solidity code for the Universal Counter. In the Beginner version, chains are hardcoded for simplicity. :::tip Beginner vs Pro - **Beginner:** Easier to follow. Great if you’re new to Solidity or Push Chain. - **Pro (Dynamic):** Slightly more advanced. Switch to the **Pro (Dynamic)** version once you’re comfortable β€” it scales to any chain without edits. ::: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; // Universal Account ID Struct and IUEAFactory Interface struct UniversalAccountId { string chainNamespace; string chainId; bytes owner; } interface IUEAFactory { function getOriginForUEA(address addr) external view returns (UniversalAccountId memory account, bool isUEA); } contract UniversalCounter { uint256 public countEth; uint256 public countSol; uint256 public countPC; event CountIncremented( uint256 newCount, address indexed caller, string chainNamespace, string chainId ); constructor() {} function increment() public { address caller = msg.sender; (UniversalAccountId memory originAccount, bool isUEA) = IUEAFactory(0x00000000000000000000000000000000000000eA).getOriginForUEA(caller); if (!isUEA) { // If it's a native Push Chain EOA (isUEA = false) countPC += 1; } else { bytes32 chainHash = keccak256(abi.encodePacked(originAccount.chainNamespace, originAccount.chainId)); if (chainHash == keccak256(abi.encodePacked("solana","EtWTRABZaYq6iMfeYKouRu166VU2xqa1"))) { countSol += 1; } else if (chainHash == keccak256(abi.encodePacked("eip155","11155111"))) { countEth += 1; } else { revert("Invalid chain"); } } emit CountIncremented(getCount(), caller, originAccount.chainNamespace, originAccount.chainId); } function getCount() public view returns (uint256) { return countEth + countSol + countPC; } } ``` ```solidity // Note: Unlike the Beginner version, this contract also tracks unique users per chain. // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; // Universal Account ID Struct and IUEAFactory Interface struct UniversalAccountId { string chainNamespace; string chainId; bytes owner; } interface IUEAFactory { function getOriginForUEA(address addr) external view returns (UniversalAccountId memory account, bool isUEA); } contract UniversalCounter { // Counter mapping to maintain individual chain counts mapping(bytes => uint256) public chainCount; mapping(bytes => uint256) public chainCountUnique; // Array of chain IDs to track unique chains bytes[] public chainIds; // Array of chain users to track unique counts mapping(address => bool) public chainUsers; event CountIncremented( uint256 newCount, uint256 newCountUnique, address indexed caller, string chainNamespace, string chainId ); constructor() {} function increment() public { address caller = msg.sender; (UniversalAccountId memory originAccount, bool isUEA) = IUEAFactory(0x00000000000000000000000000000000000000eA).getOriginForUEA(caller); // Calculate chain hash bytes memory chainHash = abi.encodePacked(originAccount.chainNamespace, ":", originAccount.chainId); if (chainCount[chainHash] == 0) { // Add new chain to chainIds if it doesn't exist chainIds.push(chainHash); } if (chainUsers[caller] == false) { // add to chain unique count if user is not already counted chainCountUnique[chainHash] += 1; chainUsers[caller] = true; } // Add to chain count chainCount[chainHash] += 1; (uint256 totalCount, uint256 totalCountUnique) = getCount(); emit CountIncremented(totalCount, totalCountUnique, caller, originAccount.chainNamespace, originAccount.chainId); } function getCount() public view returns (uint256 count, uint256 countUnique) { uint256 totalCount = 0; uint256 totalCountUnique = 0; for (uint256 i = 0; i ## Understanding the Contract This contract can now instantly determine key details about any user **(msg.sender)** instantly and natively. In simpler terms, for any given **msg.sender** address, the contract is able to quickly identify: 1. _the actual source chain of the caller_ 2. _the chain id of the source chain of the caller._ 3. _the address of the caller on the source chain._ These details are natively available for any smart contract built on Push Chain. This is enabled via **[UEAFactory Interface](https://github.com/pushchain/push-chain-core-contracts/blob/main/src/Interfaces/IUEAFactory.sol)**. ### Understanding UEAFactory Interface We use UEAFactory interface to decide transaction origin of the user. It stands for **Universal Execution Account** (UEA). Think of a UEA like a passport contract, it proves which chain a user comes from. _This can either be imported or directly included in your contract._ This interfaces provides you with the function - `getOriginForUEA()`. ```solidity /** * @dev Returns the owner key (UOA) for a given UEA address * @param addr Any given address ( msg.sender ) on push chain * @return account The Universal Account information associated with this UEA * @return isUEA True if the address addr is a UEA contract. Else it is a native EOA of PUSH chain (i.e., isUEA = false) */ function getOriginForUEA(address addr) external view returns (UniversalAccountId memory account, bool isUEA); ``` The function mainly returns 2 crucial values: - The **UniversalAccountId** of the user, and - A boolean that indicates whether or not this caller is a UEA. ### Designing the Increment Function ```solidity title="UniversalCounter.sol" ... function increment() public { address caller = msg.sender; // highlight-start (UniversalAccountId memory originAccount, bool isUEA) = IUEAFactory(0x00000000000000000000000000000000000000eA).getOriginForUEA(caller); // highlight-end if (!isUEA) { // If it's a native Push Chain EOA (isUEA = false) countPC += 1; } else { bytes32 chainHash = keccak256(abi.encodePacked(originAccount.chainNamespace, originAccount.chainId)); if (chainHash == keccak256(abi.encodePacked("solana","EtWTRABZaYq6iMfeYKouRu166VU2xqa1"))) { countSol += 1; } else if (chainHash == keccak256(abi.encodePacked("eip155","11155111"))) { countEth += 1; } // ... } // ... } // ... ``` The `increment` function is the main logic of this contract that updates the count variables based on user’s origin type. In order to achieve this, the `increment` function does the following: - calls the `getOriginForUEA()` with **msg.sender** as argument - this provides us with **isUEA and UniversalAccountId** for the caller. - then we check if **isUEA is false,** this means the caller is a native Push User. - for such users, the function increments `countPC` variable by 1 Why isUEA = false means native Push User and true means other chains? 1. Every external chain user (ETH, Solana, etc) in Push Chain has a UEA account deployed for them. 2. These UEA accounts represent the external chain users on Push Chain and are directly controlled by their signatures. 3. UEAs allow external users to interact and use Push Chain apps without natively being on Push Chain. 4. Therefore, for a given msg.sender: isUEA = false β†’ the caller is a native Push Chain account and not an external chain user. isUEA = true β†’ the caller is an external chain user interacting via a UEA. For such a user, the UniversalAccountId provides all information like \{ chainName, chainId, ownerAddress \}. ## Interact with Universal Counter The easiest way to interact with the contract is through the Live Playground. The Universal Counter is already deployed on Push Chain Testnet. > **UniversalCounter (Beginner) :** [0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512](https://donut.push.network/address/0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512?tab=contract) > **UniversalCounter (Dynamic / Pro) :** [0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9](https://donut.push.network/address/0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9?tab=contract) **Steps to interact:** - Connect your wallet to the Live Playground. - You can connect a wallet from any supported chain (Push Chain, Ethereum, or Solana). - Click **Increment Counter** to increase the counter for your chain. - Click **Refresh Counter Values** to see updated counts across chains. - Click **View in Explorer** to open the transaction in Push Chain Explorer. ## Live Playground ```jsx live // customPropMinimized='true' function UniversalCounterExample() { // Define Wallet Config const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, }; // Define Universal Counter ABI, taking minimal ABI for the demo const UCABI = [ { inputs: [], name: 'increment', outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [], name: 'countEth', outputs: [ { internalType: 'uint256', name: '', type: 'uint256', }, ], stateMutability: 'view', type: 'function', }, { inputs: [], name: 'countPC', outputs: [ { internalType: 'uint256', name: '', type: 'uint256', }, ], stateMutability: 'view', type: 'function', }, { inputs: [], name: 'countSol', outputs: [ { internalType: 'uint256', name: '', type: 'uint256', }, ], stateMutability: 'view', type: 'function', }, ]; // Contract address for Universal Counter const CONTRACT_ADDRESS = '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512'; function Component() { const { connectionStatus } = usePushWalletContext(); const { pushChainClient } = usePushChainClient(); // State to store counter values const [countEth, setCountEth] = useState(-1); const [countSol, setCountSol] = useState(-1); const [countPC, setCountPC] = useState(-1); const [isLoading, setIsLoading] = useState(false); const [txHash, setTxHash] = useState(''); // Function to encode transaction data const getTxData = () => { return PushChain.utils.helpers.encodeTxData({ abi: UCABI, functionName: 'increment', }); }; // Function to fetch counter values const fetchCounters = async () => { try { // Create a contract instance for read operations const provider = new ethers.JsonRpcProvider( 'https://evm.donut.rpc.push.org/' ); const contract = new ethers.Contract(CONTRACT_ADDRESS, UCABI, provider); // Fetch counter values const ethCount = await contract.countEth(); const solCount = await contract.countSol(); const pcCount = await contract.countPC(); // Update state setCountEth(Number(ethCount)); setCountSol(Number(solCount)); setCountPC(Number(pcCount)); } catch (err) { console.error('Error fetching counter values:', err); } }; // Fetch counter values on component mount useEffect(() => { fetchCounters(); }, []); // Handle transaction to increment counter const handleSendTransaction = async () => { if (pushChainClient) { try { setIsLoading(true); const data = getTxData(); const tx = await pushChainClient.universal.sendTransaction({ to: CONTRACT_ADDRESS, value: BigInt(0), data: data, }); setTxHash(tx.hash); // Wait for transaction to be mined await tx.wait(); // Refresh counter values await fetchCounters(); setIsLoading(false); } catch (err) { console.error('Transaction error:', err); setIsLoading(false); } } }; // Function to determine which chain is winning const getWinningChain = () => { if (countEth === -1 || countSol === -1 || countPC === -1) return null; if (countEth > countSol && countEth > countPC) { return `Ethereum is winning with ${countEth} counts`; } else if (countSol > countEth && countSol > countPC) { return `Solana is winning with ${countSol} counts`; } else if (countPC > countEth && countPC > countSol) { return `Push Chain is winning with ${countPC} counts`; } else { // Handle ties if (countEth === countSol && countEth === countPC && countEth > 0) { return `It's a three-way tie with ${countEth} counts each`; } else if (countEth === countSol && countEth > countPC) { return `Ethereum and Solana are tied with ${countEth} counts each`; } else if (countEth === countPC && countEth > countSol) { return `Ethereum and Push Chain are tied with ${countEth} counts each`; } else if (countSol === countPC && countSol > countEth) { return `Solana and Push Chain are tied with ${countSol} counts each`; } else { return null; // No winner yet or all zeros } } }; const winningMessage = getWinningChain(); return ( Universal Counter Example {connectionStatus !== PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && ( Please connect your wallet to interact with the counter. )} Total Universal Count:{' '} {countEth == -1 ? '...' : countEth + countSol + countPC} ETH Counter: {countEth == -1 ? '...' : countEth} Sol Counter: {countSol == -1 ? '...' : countSol} PC Counter: {countPC == -1 ? '...' : countPC} {connectionStatus === PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && ( {isLoading ? 'Processing...' : 'Increment Counter'} Refresh Counter Values {winningMessage && ( {winningMessage} )} {txHash && pushChainClient && ( Transaction Hash:{' '} {txHash} )} )} ); } return ( ); } ``` ```jsx live // customPropMinimized='true' function UniversalCounterExample() { // Define Wallet Config const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, }; // Define Universal Counter ABI, taking minimal ABI for the demo const UCDynamicABI = [ { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "newCount", "type": "uint256" }, { "indexed": false, "internalType": "uint256", "name": "newCountUnique", "type": "uint256" }, { "indexed": true, "internalType": "address", "name": "caller", "type": "address" }, { "indexed": false, "internalType": "string", "name": "chainNamespace", "type": "string" }, { "indexed": false, "internalType": "string", "name": "chainId", "type": "string" } ], "name": "CountIncremented", "type": "event" }, { "inputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], "name": "chainCount", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], "name": "chainCountUnique", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "name": "chainIds", "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "", "type": "address" }], "name": "chainUsers", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "getCount", "outputs": [ { "internalType": "uint256", "name": "count", "type": "uint256" }, { "internalType": "uint256", "name": "countUnique", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "increment", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, ]; // Contract address for Universal Counter const COUNTER_CONTRACT_ADDRESS = '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9'; function Component() { const { connectionStatus } = usePushWalletContext(); const { pushChainClient } = usePushChainClient(); const { PushChain } = usePushChain(); // State to store counter values const [counter, setCounter] = useState(0); const [chainData, setChainData] = useState>([]); const [isLoading, setIsLoading] = useState(false); const [txHash, setTxHash] = useState(""); // Function to encode transaction data const getTxData = () => { return PushChain.utils.helpers.encodeTxData({ abi: UCDynamicABI, functionName: 'increment', }); }; // Function to fetch counter values const fetchCounter = async () => { try { const provider = new ethers.JsonRpcProvider( 'https://evm.donut.rpc.push.org/' ); const contract = new ethers.Contract( COUNTER_CONTRACT_ADDRESS, UCDynamicABI, provider ); const [totalCount] = await contract.getCount(); setCounter(Number(totalCount)); // Get all chain data const newChainData: Array = []; let chainIndex = 0; try { while (true) { const chainHash = await contract.chainIds(chainIndex); const count = await contract.chainCount(chainHash); const uniqueCount = await contract.chainCountUnique(chainHash); newChainData.push({ chainHash: ethers.hexlify(chainHash), count: Number(count), uniqueCount: Number(uniqueCount) }); chainIndex++; } } catch (error) { // Expected error when we reach the end of the array } setChainData(newChainData); } catch (err) { console.error("Error reading counter:", err); } }; // Handle transaction to increment counter const handleSendTransaction = async () => { if (pushChainClient) { try { setIsLoading(true); // Send transaction to increment counter const tx = await pushChainClient.universal.sendTransaction({ to: COUNTER_CONTRACT_ADDRESS, data: PushChain.utils.helpers.encodeTxData({ abi: UCDynamicABI, functionName: "increment", }), value: BigInt(0), }); setTxHash(tx.hash); // Wait for transaction to be mined await tx.wait(); // Refresh counter values await fetchCounter(); setIsLoading(false); } catch (err) { console.error("Transaction error:", err); setIsLoading(false); } } else { console.log("Please connect your wallet first"); } }; // Read counter value on component mount useEffect(() => { fetchCounter(); }, []); return ( Universal Counter Example {connectionStatus !== PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && ( Please connect your wallet to interact with the counter. )} Total Universal Count:{' '} {counter == -1 ? '...' : counter} {chainData.length > 0 && ( Chain Data Chain Name Count Unique Count {chainData.map((chain, index) => ( {PushChain.utils.chains.getChainName(ethers.toUtf8String(chain.chainHash))} {chain.count} {chain.uniqueCount} ))} )} {connectionStatus === PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && ( {isLoading ? 'Processing...' : 'Increment Counter'} Refresh Counter Values {txHash && pushChainClient && ( Transaction Hash:{' '} {txHash} )} )} ); } return ( ); } ``` ## Source Code ## What we Achieved With Universal Counter, you can now: - Identify callers natively from *any* chain. - Build logic that adapts to the user’s origin chain. - Simplify the developer experience for multi-chain apps. - Eliminate reliance on third-party tooling or oracles. This makes your Counter smart contract **truly universal, all in just a few lines of Solidity**. ## What's Next? The next tutorial introduces **Universal ERC-20** tokens. Your tokens that can be minted by users of any chain. ```mermaid flowchart TD EU[Ethereum User] --> UTOKEN[Universal ERC-20 Contract] SU[Solana User] --> UTOKEN PU[Push Chain User] --> UTOKEN UTOKEN --> EC[1000 $UNICORN] UTOKEN --> SC[1000 $UNICORN] UTOKEN --> PC[1000 $UNICORN] style EC fill:#627eea,stroke:#fff,stroke-width:2px,color:#fff style SC fill:#16c492,stroke:#fff,stroke-width:2px,color:#fff style PC fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style EU fill:#627eea,color:#fff style SU fill:#16c492,color:#fff style PU fill:#dd44b9,color:#fff ``` In the next tutorial, you’ll learn how to: 1. Deploy `ERC-20` contract. 2. Introduce `mint()` functionality accessible to any user. 3. Mint from any chain. > All of these features will be natively supported in the contract with no requirement of > third-party oracles, interop providers or packages. > **This is only possible on Push Chain.** --- # Derive Universal Executor Accounts (UEAs) URL: https://push.org/docs/chain/tutorials/power-features/tutorial-derive-universal-executor-account/ Derive Universal Executor Accounts | Tutorials | Push Chain Docs In this tutorial, you'll learn how to **derive Universal Executor Accounts (UEAs)** from any wallet address on any blockchain. This is the foundational concept that enables Push Chain's universal execution model. By the end of this tutorial, you'll be able to: - βœ… Understand how UEAs map origin wallets to Push Chain addresses - βœ… Derive UEA addresses from any wallet (Ethereum, Solana, etc.) - βœ… Query UEAs programmatically using the SDK - βœ… Use the UEAFactory contract to derive UEAs on-chain ## Understanding Universal Executor Accounts (UEAs) A **Universal Executor Account (UEA)** is a deterministic smart account on Push Chain, derived from an origin wallet (chain namespace + chain id + owner), that serves as the execution account for that origin wallet on Push Chain. :::note Common Misconceptions - A UEA is not a new wallet on the origin chain - No private keys are created or stored on Push Chain - UEA addresses are deterministic, but the smart account is deployed lazily on first use ::: ### Key Concepts **Origin Wallet β†’ UEA Mapping** - Every wallet on every chain has a unique, deterministic UEA on Push Chain - Same origin address always produces the same UEA - The UEA is the execution surface for all transactions on Push Chain **Example:** ``` Ethereum Wallet: 0xABC...123 ↓ (deterministic derivation) Push Chain UEA: 0x456...789 Solana Wallet: 7xKX...ABC ↓ (deterministic derivation) Push Chain UEA: 0x789...DEF ``` ### Why UEAs Matter **Traditional Multi-Chain Problem:** - Users need different wallets for different chains - Each chain requires separate gas tokens - No unified identity across chains **Push Chain Solution with UEAs:** - One origin wallet β†’ One UEA on Push Chain - UEA is controlled by the origin wallet of the user - The UEA executes transactions on behalf of the origin wallet - Users interact from their preferred chain seamlessly ```mermaid flowchart TB A[Ethereum Wallet] --> D[UEA on Push Chain] B[Solana Wallet] --> E[UEA on Push Chain] C[Base Wallet] --> F[UEA on Push Chain] D --> G[Execute Transactions] E --> G F --> G G --> H[Smart Contracts on Push Chain] style D fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style E fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style F fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style A fill:#627eea,color:#fff style B fill:#16c492,color:#fff style C fill:#0052ff,color:#fff ``` ## Deriving UEAs with the SDK The Push Chain SDK provides utilities to derive UEAs from any wallet address. #### Basic UEA Derivation ```typescript // Convert origin address to Universal Account const account = PushChain.utils.account.toUniversal( '0xYourEthereumAddress', { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA } ); // Derive the UEA address const executorAddress = await PushChain.utils.account.deriveExecutorAccount(account); console.log('UEA Address:', executorAddress.address); // Output: 0x... (deterministic Push Chain address) ``` #### Understanding the Process **Step 1: Create Universal Account** ```typescript const account = PushChain.utils.account.toUniversal(originAddress, { chain }); ``` This creates a `UniversalAccount` object containing: - `chainNamespace`: e.g., "eip155" for EVM chains, "solana" for Solana - `chainId`: e.g., "11155111" for Ethereum Sepolia - `owner`: The origin wallet address **Step 2: Derive the Executor Account** ```typescript const executorAddress = await PushChain.utils.account.deriveExecutorAccount(account); ``` This performs the deterministic derivation to get the UEA address on Push Chain. The returned object also includes a `deployed` boolean indicating whether the UEA contract has been lazily deployed yet (pass `{ skipNetworkCheck: true }` to skip the deployment check and get the address only). #### Supported Chains The SDK supports UEA derivation for supported chains: | Chain | Namespace | Example Chain ID | |-------|-----------|------------------| | Ethereum | eip155 | 11155111 (Sepolia) | | Solana | solana | EtWTRABZaYq6iMfeYKouRu166VU2xqa1 (Devnet) | | Base | eip155 | 84532 (Sepolia) | | Arbitrum | eip155 | 421614 (Sepolia) | | BNB Chain | eip155 | 97 (Testnet) | | Push Chain | eip155 | 42101 (Testnet) | To get a list of all supported chains, see the [Get Supported Chains](/docs/chain/build/utility-functions/#get-supported-chains) utility function. ## Deriving UEAs in Smart Contracts You can also derive UEAs directly in your Solidity contracts using the `UEAFactory` precompile. #### UEAFactory Contract The `UEAFactory` is deployed at a fixed address on Push Chain: ``` 0x00000000000000000000000000000000000000eA ``` #### Interface ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; struct UniversalAccountId { string chainNamespace; string chainId; bytes owner; } interface IUEAFactory { function getUEAForOrigin( UniversalAccountId memory account ) external view returns (address uea, bool isDeployed); function getOriginForUEA( address uea ) external view returns (UniversalAccountId memory, bool); } ``` #### Example Contract ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import "push-chain-core-contracts/src/Interfaces/IUEAFactory.sol"; contract UEALookup { IUEAFactory constant FACTORY = IUEAFactory(0x00000000000000000000000000000000000000eA); // Get UEA address for any origin wallet function getUEAForUser( string memory chainNamespace, string memory chainId, bytes memory owner ) public view returns (address uea, bool isDeployed) { UniversalAccountId memory account = UniversalAccountId({ chainNamespace: chainNamespace, chainId: chainId, owner: owner }); return FACTORY.getUEAForOrigin(account); } // Get origin wallet info from UEA address function getOriginForUEA(address ueaAddress) public view returns ( string memory chainNamespace, string memory chainId, bytes memory owner, bool exists ) { (UniversalAccountId memory account, bool found) = FACTORY.getOriginForUEA(ueaAddress); return ( account.chainNamespace, account.chainId, account.owner, found ); } // Check if a UEA is deployed function isUEADeployed( string memory chainNamespace, string memory chainId, bytes memory owner ) public view returns (bool) { UniversalAccountId memory account = UniversalAccountId({ chainNamespace: chainNamespace, chainId: chainId, owner: owner }); (, bool deployed) = FACTORY.getUEAForOrigin(account); return deployed; } } ``` **Key Methods:** - **`getUEAForOrigin()`** - Get the UEA address for any origin wallet - Returns the UEA address and whether it's been deployed - Works for any chain (Ethereum, Solana, etc.) - **`getOriginForUEA()`** - Reverse lookup: get origin wallet from UEA - Returns the chain namespace, chain ID, and owner address - Useful for verifying the origin of a transaction #### Usage Example ```solidity // Get UEA for an Ethereum Sepolia wallet (address uea, bool deployed) = getUEAForUser( "eip155", "11155111", abi.encodePacked(0xYourEthereumAddress) ); // Get UEA for a Solana wallet (address solanaUEA, bool deployed) = getUEAForUser( "solana", "EtWTRABZaYq6iMfeYKouRu166VU2xqa1", abi.encodePacked("Base58SolanaAddress") ); ``` :::warning Derivation Note For Solana wallets, owner must be the raw public key bytes (decoded from base58), not the base58 string. ::: ## Understanding UEA Derivation #### The Derivation Process ```mermaid flowchart LR A[Origin Address] --> B[Chain Namespace + Chain ID] B --> C[Universal Account] C --> D[Deterministic Hash] D --> E[UEA Address on Push Chain] style E fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff ``` **Step-by-step:** 1. **Input**: Origin wallet address + chain information 2. **Create Universal Account**: Combine namespace, chain ID, and owner 3. **Deterministic Derivation**: Apply cryptographic hash function 4. **Output**: UEA address on Push Chain #### Key Properties **Deterministic** - Same input always produces same output - Can be computed off-chain before any transaction - No registration or setup required **Unique** - Each origin wallet has exactly one UEA - Different chains produce different UEAs for the same address - Example: `0xABC` on Ethereum β‰  `0xABC` on Base **Bidirectional** - Can derive UEA from origin wallet - Can query origin wallet from UEA (using `getOriginForUEA`) ## What UEAs Unlock UEAs enable app patterns that are extremely complex, awkward, or outright impossible to implement on single-chain stacks. #### Frictionless Multi-Chain Onboarding Skip the "create a new wallet for chain X" step entirely. Users keep their existing Ethereum, Solana, or any other wallet. Your contract sees their UEA regardless of which chain they signed from. New-user activation collapses from minutes to seconds with no chain-switching, no bridging, and no extra gas token setup. #### One Contract, Every Chain Deploy a single contract on Push Chain and accept interactions from users on any chain. No per-chain deployments, no relayer infrastructure, no state fragmentation across instances. One address, one state, every chain. #### Unified Protocol Liquidity Pool liquidity, balances, and protocol state under a single contract instead of sharding it across chains. Users from Ethereum, Solana, and Base all contribute to and draw from the same pool. This eliminates bridging friction and gives your protocol a single, accurate view of total liquidity. #### Universal Identity and Permissions Allowlists, reputation scores, KYC attestations, and access tiers all key against UEAs. The same person has the same UEA whether they sign from Ethereum or Solana, so trust accrues to them and not to a per-chain alias they have to maintain. ## What You Can Build UEAs are the primitive. Here are the product verticals they unlock: ### Universal RWAs Tokenized real-world assets accessible to holders on any chain. Ownership records, compliance checks, and transfer logic all live in one contract, keyed by UEA. ### Universal DeFi Lending, borrowing, swaps, and yield strategies that accept liquidity from every chain without bridges. Users interact from their home chain; your protocol sees a single, unified pool. ### Universal Airdrops Distribute tokens to recipients across Ethereum, Solana, Base, and BNB from a single contract. Recipients claim with their existing wallet on their existing chain, with no bridging and no per-chain deployments. Build it end-to-end: [Build a Universal Airdrop](/docs/chain/tutorials/token-systems/tutorial-universal-airdrop/) ### Universal Agents AI agents or autonomous bots that hold a UEA and act on behalf of users across chains with no per-chain wallets or bridging logic required. ### Universal Gaming A single game contract where players from every chain compete on the same leaderboard. UEA-keyed state means a Solana player and an Ethereum player appear side-by-side without per-chain accounts. See [Ballsy](https://ballsy.push.org) for a live example of chain-vs-chain gameplay built on UEA primitives. ## Live Playground Try deriving UEAs from any wallet address in real-time: ```jsx live // customPropMinimized='true' function DeriveUEAExample() { const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, }; function Component() { const { connectionStatus } = usePushWalletContext(); const { pushChainClient } = usePushChainClient(); const { PushChain } = usePushChain(); const [manualLookupAddress, setManualLookupAddress] = useState(""); const [manualLookupChain, setManualLookupChain] = useState( PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA ); const [manualLookupResult, setManualLookupResult] = useState(""); const [isCheckingUEA, setIsCheckingUEA] = useState(false); const [error, setError] = useState(""); const chains = [ { value: PushChain.CONSTANTS.CHAIN.PUSH_TESTNET, label: "Push Chain" }, { value: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA, label: "Ethereum Sepolia" }, { value: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, label: "Solana Devnet" }, { value: PushChain.CONSTANTS.CHAIN.BASE_SEPOLIA, label: "Base Sepolia" }, { value: PushChain.CONSTANTS.CHAIN.ARBITRUM_SEPOLIA, label: "Arbitrum Sepolia" }, { value: PushChain.CONSTANTS.CHAIN.BNB_TESTNET, label: "BNB Testnet" }, ]; const handleDeriveUEA = async () => { if (!manualLookupAddress.trim()) { setError("Please enter an address"); return; } setIsCheckingUEA(true); setError(""); setManualLookupResult(""); try { const account = PushChain.utils.account.toUniversal( manualLookupAddress, { chain: manualLookupChain } ); const executorAddress = await PushChain.utils.account.deriveExecutorAccount(account); setManualLookupResult(executorAddress.address); } catch (err) { console.error("Error deriving UEA:", err); setError("Failed to derive Universal Executor Account"); } finally { setIsCheckingUEA(false); } }; return ( Derive Universal Executor Account Enter any wallet address to derive its deterministic UEA on Push Chain {connectionStatus === PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && pushChainClient && ( πŸ”‘ Your Connected Wallet Origin Wallet: {pushChainClient.universal.origin.address} Chain: {PushChain.utils.chains.getChainName(pushChainClient.universal.origin.chain)} Universal Executor Account (UEA): {pushChainClient.universal.account} )} Derive UEA from Any Wallet Enter any wallet address and chain to derive its Universal Executor Account: Chain: setManualLookupChain(e.target.value)} style={{ width: "100%", padding: "10px", marginBottom: "16px", borderRadius: "6px", border: "1px solid #ddd", fontSize: "14px" }} > {chains.map((chain) => ( {chain.label} ))} Wallet Address: setManualLookupAddress(e.target.value)} placeholder="Enter address (e.g., 0x...)" style={{ width: "100%", padding: "10px", marginBottom: "16px", borderRadius: "6px", border: "1px solid #ddd", fontSize: "14px", fontFamily: "monospace" }} /> {isCheckingUEA ? "Deriving..." : "Derive UEA"} {error && ( {error} )} {manualLookupResult && ( βœ… Universal Executor Account (UEA): {manualLookupResult} )} πŸ’‘ How It Works The same origin address always produces the same UEA UEAs are deterministic and can be computed off-chain No manual deployment needed - UEAs are lazily and gaslessly deployed Works for any blockchain (Ethereum, Solana, etc.) ); } return ( ); } ``` ## Source Code ## What We Achieved In this tutorial, we explored Universal Executor Accounts: - **Understood UEAs** - How origin wallets map to Push Chain addresses - **Derived UEAs** - Used the SDK to compute UEAs from any wallet - **On-Chain Queries** - Used UEAFactory to derive UEAs in smart contracts - **Practical Applications** - Saw how UEAs enable universal systems ## Key Takeaways **1. One Wallet, One UEA** - Every wallet on every chain has a unique UEA on Push Chain - The mapping is deterministic and permanent - No setup or registration required **2. Universal Identity** - UEAs provide a unified identity across all chains - Users interact from their preferred chain - Developers build once, reach everyone **3. Powerful Primitives** - UEAs enable universal airdrops, allowlists, and more - On-chain derivation with UEAFactory - Off-chain computation with the SDK ## What's Next? UEAs are how external wallets execute **on** Push Chain. The mirror image, how a Push Chain account executes **on every external chain**, is the **Chain Executor Account (CEA)**. Every Push-side account, whether a user, a contract, or an AI agent, gets a deterministic CEA on every supported destination, ready to be authorised on day zero. ```mermaid flowchart LR A[UEA on Push Chain] --> B[CEA on Ethereum] A --> C[CEA on BNB] A --> D[CEA on Solana] style A fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style B fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff style C fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff style D fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff ``` In the next tutorial, you'll learn how to [derive Chain Executor Accounts (CEAs)](/docs/chain/tutorials/power-features/tutorial-derive-chain-executor-account/), off-chain via the SDK and on-chain via `ICEAFactory`, and use them to pre-authorise destination-chain protocols before any cross-chain activity has happened. --- # Mint Universal ERC-20 Tokens URL: https://push.org/docs/chain/tutorials/basics/tutorial-mint-erc-20-tokens/ Mint Universal ERC-20 Tokens | Tutorials | Push Chain Docs In this tutorial, you'll create, deploy, and interact with a simple ERC-20 token on Push Chain. The main difference is that this ERC-20 token is a universal token that can be minted by anyone on any chain. By the end of this tutorial you’ll be able to: - βœ… Build a universal ERC-20 token, we will call it **$UNICORN**. - βœ… Have users from any chain connect and interact with it. ## Write the Contract We will start with taking the ERC-20 contract from OpenZeppelin. The only change we will make is removing the restriction of `onlyOwner` modifier in the functionality of minting new tokens. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract Token is ERC20 { constructor(string memory name, string memory symbol) ERC20(name, symbol) { _mint(msg.sender, 1_000_000 * 10 ** decimals()); } function mint(address to, uint256 amount) external { _mint(to, amount); } } ``` ## Understanding the Contract This contract is a simple ERC-20 token: - We are minting 1,000,000 tokens with 18 decimals and giving it to the deployer of the contract. - We have removed `onlyOwner` from `mint()` function in order to enable anyone to mint freely from this contract. ## Interact with Universal ERC-20 Token The Universal ERC-20 contract is deployed on Push Chain Testnet. This app lets any user β€” whether on Ethereum, Solana, or Push Chain β€” mint `$UNICORN` tokens directly from the frontend. > **Demo Token ERC-20 Contract Address:** [0x0165878A594ca255338adfa4d48449f69242Eb8F](https://donut.push.network/address/0x0165878A594ca255338adfa4d48449f69242Eb8F?tab=contract) **Steps to interact:** 1. Connect your wallet (Ethereum, Solana, or Push Chain). 2. Click the **Mint Token** button β€” this calls the ERC-20’s `mint()` function through Push Universal Transactions. 3. Wait for the transaction to confirm. 4. Your `$UNICORN` balance will update in the UI automatically. 5. (Optional) Click **View in Explorer** to inspect the transaction on Push Chain Explorer. --- > πŸ’‘ **Tip: Why this matters** > This is the simplest form of a **Universal Token**. > Without bridges or wrapped assets, users on *any chain* can mint and hold the same ERC-20 natively on Push Chain. ## Live Playground ```jsx live // customPropMinimized='true' function UniversalMintExample() { // Wallet config const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, }; // Define ERC-20 ABI, taking minimal ABI for the demo const ERC20ABI = [ { "inputs": [ { "name": "to", "type": "address" }, { "name": "amount", "type": "uint256" } ], "name": "mint", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "name": "account", "type": "address" } ], "name": "balanceOf", "outputs": [ { "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" } ]; // ERC-20 deployed on Push Chain Testnet const CONTRACT_ADDRESS = "0x0165878A594ca255338adfa4d48449f69242Eb8F"; // Provider to read balances const provider = new ethers.JsonRpcProvider( "https://evm.donut.rpc.push.org/" ); function Component() { const { connectionStatus } = usePushWalletContext(); const { pushChainClient } = usePushChainClient(); const [balance, setBalance] = useState(-1); const [isLoading, setIsLoading] = useState(false); const [txHash, setTxHash] = useState(""); // Fetch balance of connected account const readBalance = async () => { if (connectionStatus === "connected" && pushChainClient) { const contract = new ethers.Contract(CONTRACT_ADDRESS, ERC20ABI, provider); const bal = await contract.balanceOf(pushChainClient.universal.account); setBalance(Number(ethers.formatUnits(bal, 18))); } }; useEffect(() => { if (connectionStatus === "connected" && pushChainClient) { readBalance(); } else { setBalance(-1); } }, [connectionStatus, pushChainClient]); // Mint function const mintToken = async () => { if (connectionStatus === "connected" && pushChainClient) { try { setIsLoading(true); const tx = await pushChainClient.universal.sendTransaction({ to: CONTRACT_ADDRESS, data: PushChain.utils.helpers.encodeTxData({ abi: ERC20ABI, functionName: "mint", args: [pushChainClient.universal.account, PushChain.utils.helpers.parseUnits("100", 18)], // Mint 100 tokens }), value: BigInt(0), }); setTxHash(tx.hash); await tx.wait(); await readBalance(); } catch (err) { console.error("Transaction error:", err); } finally { setIsLoading(false); } } }; return ( Universal ERC-20 Mint {connectionStatus !== PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && ( Please connect your wallet to mint tokens. )} Balance: {balance === -1 ? "..." : balance} $UNICORN {balance !== -1 && Optional: Add 0x0165878A594ca255338adfa4d48449f69242Eb8F ($UNICORN Token Address) to your wallet to see your balance in the wallet. } {connectionStatus === PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && ( <> {isLoading ? "Minting..." : "Mint 100 Tokens"} {txHash && ( Transaction:{" "} {txHash} )} )} ); } return ( ); } ``` ## Source Code ## What we Achieved In this tutorial, we built the simplest possible ERC-20-style token interaction. - We wrote and deployed a minimal ERC-20 contract with an open mint() function. - We minted tokens and confirmed balances via the frontend. With just these two functions, you now have the foundation of a token system. ## What's Next? Up until now, we’ve been sending one transaction at a time. In real-world apps, users often need to perform several actions together (for example: approve β†’ transfer, or multiple contract calls in a single flow). ```mermaid flowchart TD EU[Ethereum User] --> UB[Universal Batcher] SU[Solana User] --> UB OU[Other Chain User] --> UB UB --> TX1[Execute Transaction 1] TX1 --> TX2[Execute Transaction 2] TX2 --> TX3[Execute Transaction 3] TX3 --> EOL[Complete] style EOL fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style UB fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style EU fill:#627eea,color:#fff style SU fill:#16c492,color:#fff style OU fill:#853dff,color:#fff ``` In the next tutorial, we’ll explore batching multiple transactions as one universal transaction: 1. How to combine several actions into a single transaction. 2. Why batching improves user experience (fewer wallet popups, less friction). 3. Practical code examples of executing multiple calls in sequence or atomically. This will help you take the step from simple token interactions to more powerful app logic on Push Chain. πŸš€ --- # Derive Chain Executor Accounts (CEAs) URL: https://push.org/docs/chain/tutorials/power-features/tutorial-derive-chain-executor-account/ Derive Chain Executor Accounts | Tutorials | Push Chain Docs In this tutorial, you'll learn how to **derive Chain Executor Accounts (CEAs)**. A CEA is the destination-chain identity a Push Chain account uses when it executes on an external chain. This tutorial pairs with the [Derive UEA tutorial](/docs/chain/tutorials/power-features/tutorial-derive-universal-executor-account). UEAs are how external wallets execute on Push Chain; CEAs work in the opposite direction. By the end of this tutorial, you'll be able to: - βœ… Understand how CEAs map Push Chain accounts to addresses on every external chain - βœ… Derive CEA addresses from any Push Chain account using the SDK - βœ… Derive CEAs on-chain from an external EVM contract via `ICEAFactory` - βœ… Use CEA derivation to fund and pre-authorize cross-chain flows ## Understanding Chain Executor Accounts (CEAs) A **Chain Executor Account (CEA)** is a deterministic smart account on an **external chain** (Ethereum, Solana, BSC, and others), derived from a **Push Chain account** (a UEA, a Push-native EOA, or a Push Chain contract). It is the execution surface a Push-side account uses when it dispatches an outbound. Every outbound goes through `UniversalGatewayPC`, which is Push's cross-chain gateway contract. ### CEAs vs UEAs | | UEA | CEA | |---|---|---| | Lives on | Push Chain | External chain (one per chain) | | Derived from | An external-chain wallet | A Push Chain account (UEA, EOA, or contract) | | Acts as `msg.sender` for | Push Chain transactions | External-chain transactions | | Bound to | A user wallet | A user **or** a contract | | Deployed by | UEAFactory on Push Chain (lazy) | CEAFactory on the destination chain (lazy, by TSS) | ### CEAs are deterministic - Every Push Chain account has a unique, deterministic CEA on every supported external chain. - Same Push-side account always produces the same CEA on the same destination chain. - A different destination chain produces a different CEA (CEA addresses are scoped per chain). ``` Push Chain Account: 0xABC...123 (UEA, EOA, or contract) ↓ (deterministic derivation, per destination chain) CEA on Ethereum Sepolia: 0x111...222 CEA on BNB Testnet: 0x333...444 CEA on Base Sepolia: 0x555...666 ``` ### Why CEAs matter - **Self-Custody** CEAs are self-custodial. They can only be controlled by the Push-side account, so your funds and actions always remain under your control. - **Identity portability** Your account identity follows you across all chains without any bridging or wrapping. - **Contract-initiated execution** When a Push Chain contract dispatches outbound, its CEA acts as _`msg.sender`_ on the destination chain. The CEA ensures your identity and actions are preserved across chains. ```mermaid flowchart TB A[UEA on Push Chain] --> D[CEA on Ethereum] A --> E[CEA on BNB Chain] B[Push Contract] --> F[CEA on Ethereum] B --> G[CEA on Base] D --> H["Execute on Ethereum(msg.sender = CEA)"] F --> I["Dispatch from Push contract(msg.sender = contract's CEA)"] style A fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style B fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style D fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff style E fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff style F fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff style G fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff ``` ## Deriving CEAs with the SDK The Push Chain SDK derives CEAs from any Push Chain account using `deriveExecutorAccount` with a `chain` option that selects the destination chain. #### Basic CEA Derivation ```typescript // Step 1: wrap the Push-side account in a UniversalAccount. const pushAccount = PushChain.utils.account.toUniversal( '0x98cA97d2FB78B3C0597E2F78cd11868cACF423C5', { chain: PushChain.CONSTANTS.CHAIN.PUSH_TESTNET } ); // Step 2: derive the CEA on the destination chain. const ceaResult = await PushChain.utils.account.deriveExecutorAccount( pushAccount, { chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET } ); console.log('CEA on BNB Testnet:', ceaResult.address); console.log('Deployed:', ceaResult.deployed); ``` #### Deriving from non-Push origins `deriveExecutorAccount` accepts any `UniversalAccount`. When the input is a UOA (an origin wallet on an external chain), the SDK first resolves its UEA and then derives the CEA on the target chain. You can answer "where will this user act on chain X?" with a single call. ```typescript const solanaAccount = PushChain.utils.account.toUniversal( 'EUYcfSUScdFgKMbB3rRdgRZwXmcxY7QCRQa2JwrchP1Q', { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET } ); const ceaSolanaUserOnBnb = await PushChain.utils.account.deriveExecutorAccount( solanaAccount, { chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET } ); ``` `skipNetworkCheck: true` returns the deterministic address without making an RPC call. Use this when you need the address but do not need to know whether the CEA has been deployed yet. ```typescript const result = await PushChain.utils.account.deriveExecutorAccount( pushAccount, { chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET, skipNetworkCheck: true } ); // result.address is set; result.deployed is omitted. ``` For the full SDK reference (arguments, return shape, and a live playground), see [deriveExecutorAccount in Utility Functions](/docs/chain/build/utility-functions/#derive-executor-account). #### Supported destination chains Use `PushChain.CONSTANTS.CHAIN` values for the `chain` option. The supported set matches the chains documented in the [Smart Contract Address Book](/docs/chain/setup/smart-contract-address-book) (Ethereum Sepolia, Base Sepolia, Arbitrum Sepolia, BNB Testnet, Solana Devnet, ...). ## Deriving CEAs in Smart Contracts `ICEAFactory` is deployed on **each external chain** (not on Push Chain) and exposes the on-chain mapping between a Push-side account and its CEA on that chain. From an external EVM chain, you can look up or pre-compute any CEA without making a call back to Push Chain. #### ICEAFactory Interface ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.26; interface ICEAFactory { /// @notice Returns the CEA address and deployment status for a given Push Chain account. /// @dev If the CEA has not been deployed, returns (predictedAddress, false). /// @param _pushAccount Address of the Push Chain account (UEA, EOA, or contract). /// @return cea CEA address (deployed or predicted via CREATE2). /// @return isDeployed True if the CEA has code at that address. function getCEAForPushAccount(address _pushAccount) external view returns (address cea, bool isDeployed); /// @notice Returns true if `_cea` was deployed by this factory. function isCEA(address _cea) external view returns (bool); /// @notice Reverse lookup: returns the Push Chain account mapped to this CEA. function getPushAccountForCEA(address _cea) external view returns (address pushAccount); } ``` :::info CEAFactory address The CEAFactory is deployed on each external chain. Find the per-chain address in the [Smart Contract Address Book](/docs/chain/setup/smart-contract-address-book). Unlike `IUEAFactory`, there is no precompile address for the CEAFactory; it is a regular contract. ::: #### Example: pre-compute and authorize a Push contract's CEA A common pattern is to whitelist a Push Chain contract's CEA on a destination-chain protocol before the Push contract has dispatched anything. Compute the CEA on-chain via `getCEAForPushAccount`, then authorize it. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; interface ICEAFactory { function getCEAForPushAccount(address _pushAccount) external view returns (address cea, bool isDeployed); function getPushAccountForCEA(address _cea) external view returns (address pushAccount); } contract CEALookup { ICEAFactory public immutable FACTORY; address public immutable OWNER; mapping(address => bool) public allowedCEAs; error NotOwner(); modifier onlyOwner() { if (msg.sender != OWNER) revert NotOwner(); _; } constructor(address ceaFactory) { FACTORY = ICEAFactory(ceaFactory); OWNER = msg.sender; } /// @notice Look up the CEA on this chain for a given Push-side account. function ceaFor(address pushAccount) external view returns (address cea, bool isDeployed) { return FACTORY.getCEAForPushAccount(pushAccount); } /// @notice Pre-authorize a Push-side account's CEA before it has been deployed. /// Useful for whitelists that need to allow contract-initiated cross-chain /// calls on day zero. /// @dev Owner-gated. Without this guard, any caller could whitelist an /// arbitrary Push account's CEA and grant themselves permission. function preAuthorize(address pushAccount) external onlyOwner { (address cea, ) = FACTORY.getCEAForPushAccount(pushAccount); allowedCEAs[cea] = true; } /// @notice Reverse: given a CEA, find which Push-side account owns it. function pushAccountFor(address cea) external view returns (address) { return FACTORY.getPushAccountForCEA(cea); } } ``` #### Example: gate destination-chain calls to a known agent's CEA A second common pattern: a destination-chain vault that only accepts calls coming from an authorized Push-side agent's CEA. Use `getPushAccountForCEA(msg.sender)` to resolve `msg.sender` back to the Push-side account, then check it against an allowlist. Anyone calling from an unauthorized address (or from a non-CEA address) reverts. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; interface ICEAFactory { function getCEAForPushAccount(address _pushAccount) external view returns (address cea, bool isDeployed); function getPushAccountForCEA(address _cea) external view returns (address pushAccount); } contract AgentGatedVault { ICEAFactory public immutable FACTORY; address public owner; /// @dev Push-side agent address => authorized. mapping(address => bool) public authorizedAgents; event AgentAuthorized(address indexed agentPushAccount, address indexed cea); error UnknownAgent(); error NotOwner(); modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); _; } constructor(address ceaFactory) { FACTORY = ICEAFactory(ceaFactory); owner = msg.sender; } /// @notice Authorize a known AI agent (or any Push-side contract / account) /// to call this vault. The CEA address is logged for auditability. function authorizeAgent(address agentPushAccount) external onlyOwner { (address cea, ) = FACTORY.getCEAForPushAccount(agentPushAccount); authorizedAgents[agentPushAccount] = true; emit AgentAuthorized(agentPushAccount, cea); } /// @notice Resolve msg.sender's CEA back to its Push-side owner and check /// that the owner is authorized. Calls from unknown CEAs or from /// non-CEA addresses revert. function execute(bytes calldata data) external { address agentPushAccount = FACTORY.getPushAccountForCEA(msg.sender); if (!authorizedAgents[agentPushAccount]) revert UnknownAgent(); _execute(data); } function _execute(bytes calldata /* data */) internal { // Vault-specific logic goes here. } } ``` This is the pattern most multi-agent or multi-tenant cross-chain protocols want: every authorized Push-side actor maps to exactly one CEA on this chain, and the protocol enforces that mapping at the destination layer without trusting any off-chain signal. :::warning Push Chain has no on-chain CEA derivation There is no precompile on Push Chain for deriving CEAs. `ICEAFactory` only exists on external chains. From a Push Chain contract, derive the CEA off-chain via the SDK and pass it in as a constructor argument or an authorized configuration update. ::: ## What CEAs Unlock CEAs make a Push Chain account a first-class citizen on every external chain. One key, one identity, real on-chain authority everywhere. #### True Self-Custody on Every Chain Assets in a CEA belong to exactly one person: the Push-side account that owns it. No bridge operator, no relayer, no protocol admin can move them. Acting on a CEA requires the original wallet's signature, whether that signer lives on Ethereum, Solana, or any other supported chain. The custody chain runs unbroken from origin wallet to destination-chain action. #### Deterministic Cross-Chain Identity Every Push-side account has a pre-computable, stable address on every external chain. You can whitelist it, fund it, or pre-authorize it on a destination protocol on day zero, before a single cross-chain transaction has happened. When the first one does, the TSS network deploys the CEA contract at exactly the address you computed months earlier. #### Programmable Reach Without Bots Push Chain *contracts*, not just users, get their own CEAs. A single contract on Push can act on Ethereum, BNB, Base, and Arbitrum through a stable on-chain identity per chain. No off-chain bots, no per-chain hot keys, no relayer infrastructure to maintain. Cross-chain orchestration becomes ordinary on-chain logic. #### Native `msg.sender` on Every Destination A CEA looks like an ordinary address on its destination chain. Destination protocols need zero Push-specific awareness; they whitelist or fund the CEA, and Push Chain enforces who can speak for it. Universal execution is invisible at the destination layer. ## What You Can Build CEAs are the primitive. Here are the product verticals they unlock: ### Universal Self-Custody One key, every chain's assets. ETH on Ethereum, NFTs on Base, LP positions on Arbitrum, staked tokens on BSC, all sitting in CEAs controlled by a single Solana or Ethereum signer on Push Chain. No wrapped tokens, no bridges, no custodian. ### Cross-Chain DeFi Lending with isolated risk per CEA, yield strategies that route to the deepest liquidity on whichever chain has it, perp trading that settles on the destination chain natively. Each user's CEA is a self-contained position; collateral, debt, and health factor can never bleed across users. ### Cross-Chain AI Agents AI agents and autonomous bots deployed as Push Chain contracts that interact with Aave on Ethereum, perp DEXes on Arbitrum, and vaults on Base, all under one verifiable identity. [EIP-8004](https://eips.ethereum.org/EIPS/eip-8004) cross-chain native from day one. ### Cross-Chain Keepers and Liquidators Liquidation engines, oracle pokes, vault rebalancers, scheduled jobs. The Push contract watches conditions and dispatches the destination call through `UniversalGatewayPC`; destination protocols see a stable, attributable Push-side actor. No race-the-other-bot infrastructure required. ### Multi-Chain Treasury and Payroll A DAO whose treasury is conceptually on Push but physically lives on whichever chain has the deepest liquidity, the cheapest gas, or the contributor's preferred wallet. Pay contributors in the chain they want, deploy idle funds where they earn most, repatriate when needed. ## Live Playground Connect a Push wallet and watch its deterministic CEA appear on every supported external chain at once. CEAs derive from your **UEA on Push Chain**, so it doesn't matter whether you log in via MetaMask, Phantom, email, or social; the same Push account always produces the same CEAs. ```jsx live // customPropMinimized='true' function DeriveCEAExample() { const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, }; function Component() { const { connectionStatus } = usePushWalletContext(); const { pushChainClient } = usePushChainClient(); const { PushChain } = usePushChain(); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const ueaAddress = useMemo( () => (pushChainClient ? pushChainClient.universal.account : null), [pushChainClient] ); useEffect(() => { if (!ueaAddress || !PushChain) { setRows([]); return; } let cancelled = false; (async () => { setLoading(true); setError(""); try { // Step 1: wrap the UEA into a UniversalAccount on Push Chain. const pushAccount = PushChain.utils.account.toUniversal(ueaAddress, { chain: PushChain.CONSTANTS.CHAIN.PUSH_TESTNET, }); // Step 2: list every chain supported on the current network. const { chains } = PushChain.utils.chains.getSupportedChains( PushChain.CONSTANTS.PUSH_NETWORK.TESTNET ); // Step 3: derive the CEA on each external chain via the // documented PushChain.utils.account.deriveExecutorAccount(account, { chain }) call. // The `chain` option flips the call from "UEA on Push" to "CEA on chain". const out = []; for (const chain of chains) { if (chain === PushChain.CONSTANTS.CHAIN.PUSH_TESTNET) continue; const chainName = PushChain.utils.chains.getChainName(chain) ?? chain; try { const cea = await PushChain.utils.account.deriveExecutorAccount( pushAccount, { chain } ); out.push({ chainNamespace: chain, chainName, ceaAddress: cea.address, isDeployed: cea.deployed === true, }); } catch (err) { out.push({ chainNamespace: chain, chainName, ceaAddress: "", isDeployed: false, error: err instanceof Error ? err.message : String(err), }); } } if (!cancelled) setRows(out); } catch (err) { if (!cancelled) setError(err instanceof Error ? err.message : String(err)); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [ueaAddress, PushChain]); return ( Derive Chain Executor Accounts Connect a Push wallet and see its deterministic CEA on every supported external chain {connectionStatus === PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && pushChainClient && ( πŸ”‘ Your Push Chain Account Origin Wallet: {pushChainClient.universal.origin.address} Chain: {PushChain.utils.chains.getChainName(pushChainClient.universal.origin.chain) ?? pushChainClient.universal.origin.chain} UEA on Push Chain: {pushChainClient.universal.account} )} {connectionStatus === PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && pushChainClient && ( πŸ›°οΈ CEAs Across External Chains {loading && ( Deriving CEAs across supported chains… )} {error && ( Failed to load CEAs: {error} )} {!loading && rows.map((row) => ( {row.chainName} {row.chainNamespace} {row.error ? ( β€” {row.error} ) : ( {row.ceaAddress} )} {row.isDeployed ? "Deployed" : "Not deployed"} ))} πŸ’‘ How CEAs Activate CEAs start undeployed. They activate the first time you target their chain via a contract-initiated outbound or a user-initiated cross-chain transaction. Once deployed, the CEA can also originate transactions back to Push Chain. )} ); } return ( ); } ``` ## Source code ## What we achieved In this tutorial, we explored Chain Executor Accounts: - **Understood CEAs**: how Push Chain accounts map to addresses on every external chain - **Derived CEAs with the SDK**: `deriveExecutorAccount` with a `chain` option - **Derived CEAs on-chain**: `ICEAFactory.getCEAForPushAccount` from an external EVM chain - **Practical applications**: lending, self-custody, omnichain yield, perpetuals, and AI agent identity ## Key takeaways **1. One Push account, one CEA per chain** - Same Push-side account always derives the same CEA on the same destination chain - Different destination chains have different CEAs - Mappings are deterministic and persistent **2. CEAs are bound to the Push-side address** - For users: bound to their UEA - For contracts: bound to the contract's Push Chain address - A new contract deployment at a different address has a different CEA **3. Powerful primitives** - Pre-compute CEAs off-chain via SDK or on the destination chain via `ICEAFactory` - Whitelist them before any cross-chain activity has happened - Reverse-lookup to credit the right Push-side user for inbound flows ## What's Next? You can derive a Push contract's CEA on every destination. Now wire one Push contract to drive state on three chains at once. The next tutorial fans out a single click on Push Chain into simultaneous `increment()` calls on Ethereum Sepolia, BNB Testnet, and Arbitrum Sepolia, all signed by the orchestrator's deterministic CEA on each. ```mermaid flowchart LR A[Push contract] --> B[CEA on Ethereum] A --> C[CEA on BNB] A --> D[CEA on Arbitrum] B --> E[ExternalCounter] C --> F[ExternalCounter] D --> G[ExternalCounter] style A fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style B fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff style C fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff style D fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff ``` Check out the next tutorial to learn how to [build universal cross-chain counters](/docs/chain/tutorials/power-features/tutorial-universal-cross-chain-counters/): one Push contract, many destination chains, one transaction. --- # Build Universal Cross-Chain Counters URL: https://push.org/docs/chain/tutorials/power-features/tutorial-universal-cross-chain-counters/ Build Universal Cross-Chain Counters | Tutorials | Push Chain Docs In the [Counter](/docs/chain/tutorials/basics/tutorial-simple-counter/) and [Universal Counter](/docs/chain/tutorials/basics/tutorial-universal-counter/) tutorials every increment landed on **one** Push Chain contract. Here we flip the model. A single click on a Push Chain contract increments separate counters on **Ethereum Sepolia, BNB Testnet, and Arbitrum Sepolia** at once. One contract orchestrates many. By the end of this tutorial you'll be able to: - βœ… Deploy a Push Chain contract that fans out to multiple destination chains in one call - βœ… Use **IUniversalGatewayPC** (UGPC) to dispatch outbound transactions - βœ… Compute a contract's destination-chain CEA before any cross-chain activity has happened - βœ… See and verify `msg.sender` on the destination chain resolve to the deterministic CEA - βœ… (Optional) Harden the destination by gating `increment()` to the CEA so only your Push contract can drive it :::info Builds on Derive CEA This tutorial puts the [Derive Chain Executor Account (CEA)](/docs/chain/tutorials/power-features/tutorial-derive-chain-executor-account/) primitive to work. If you haven't read that one yet, skim it first. We'll be deriving CEAs and authorising them on destination chains. ::: ## Understanding the Pattern In order to create ticks across multiple chains, we need to design two contracts. - **MultiChainCounter**: This contract runs on Push Chain and is responsible for orchestrating the cross-chain increments. - **ExternalCounter**: This contract runs on each destination chain and is responsible for incrementing the counter. The transaction from users from any chain lands on Push Chain, where the `MultiChainCounter` contract is deployed. It then uses the **IUniversalGatewayPC** (UGPC) to dispatch outbound transactions to each of the **ExternalCounter** contracts on the destination chains. ```mermaid flowchart LR Click([User clicks tickAll on Push]) Push[MultiChainCounterPush Chain] UGPC[UniversalGatewayPC0x...C1] CEAEth[CEA on Ethereum] CEABnb[CEA on BNB] CEAArb[CEA on Arbitrum] CntEth[ExternalCounterEthereum Sepolia] CntBnb[ExternalCounterBNB Testnet] CntArb[ExternalCounterArbitrum Sepolia] Click --> Push --> UGPC UGPC -->|TSS relay| CEAEth --> CntEth UGPC -->|TSS relay| CEABnb --> CntBnb UGPC -->|TSS relay| CEAArb --> CntArb style Push fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style UGPC fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style CEAEth fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff style CEABnb fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff style CEAArb fill:#b45309,stroke:#fbbf24,stroke-width:2px,color:#fff ``` > **πŸš€ Why this matters** > > No off-chain bots. No relayer keys. No per-chain hot wallets. The orchestrator lives entirely on Push Chain and reaches every external chain through a deterministic identity (its CEA) that destination protocols can pre-authorise from day zero. ## Write the Contracts ### Per Destination Chain β†’ ExternalCounter.sol This contract lives on **each destination chain**. It stores a `count` and records `lastCaller` on every increment so you can observe the orchestrator's CEA showing up after each tick. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; contract ExternalCounter { uint256 public count; /// The address that most recently incremented. After a tickAll this will /// be the orchestrator's deterministic CEA on this chain. address public lastCaller; event CountIncremented(uint256 indexed newCount, address indexed caller); function increment() external { unchecked { count += 1; } lastCaller = msg.sender; emit CountIncremented(count, msg.sender); } } ``` When the orchestrator dispatches via UGPC, the TSS network signs the destination tx **as the orchestrator's CEA on this chain**, so `lastCaller` ends up being that deterministic CEA address. From the destination contract's perspective the CEA is just a regular address, but Push Chain guarantees only one Push-side contract can produce calls from it. Visible proof of identity, with no mandatory destination-side auth. :::info Why is `ExternalCounter.increment()` open? We deliberately keep `increment()` callable by anyone in this tutorial so you can run the live playground below without a per-chain redeploy. To **enforce** that only the orchestrator's CEA can drive it, see the [production hardening snippet at the bottom of this tutorial](#production-hardening-gate-increment-to-the-cea). ::: ### On Push Chain β†’ MultiChainCounter.sol This contract lives on **Push Chain**. It holds the list of destinations and fans out a single payload (the `increment()` calldata) to each one through `UGPC`. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; /// @notice UGPC outbound request shape. Mirrors the production type so this /// tutorial doesn't need a hard dependency on push-chain-gateway-contracts. struct UniversalOutboundTxRequest { bytes recipient; // bytes-packed ExternalCounter address on the destination chain address token; // address(0) β€” we are not bridging funds, just executing a payload uint256 amount; // 0 because we are not bridging funds uint256 gasLimit; // gas the destination CEA gets to run `increment()` uint256 gasPrice; // 0 β†’ per-chain default from UniversalCore (new in SDK v6) uint256 maxPCForGas; // 0 β†’ no cap on PC the AMM may spend on the gas swap (new in SDK v6) bytes payload; // ABI-encoded calldata for the destination contract address revertRecipient; // refunded if the outbound cannot finalise } interface IUniversalGatewayPC { function sendUniversalTxOutbound(UniversalOutboundTxRequest calldata req) external payable; } interface IExternalCounter { function increment() external; } contract MultiChainCounter { /// @notice Predeploy address of UniversalGatewayPC on every Push Chain network. IUniversalGatewayPC public constant UGPC = IUniversalGatewayPC(0x00000000000000000000000000000000000000C1); struct Destination { bytes target; // bytes-packed ExternalCounter address uint256 gasLimit; // destination-chain gas budget for the CEA's call } Destination[] public destinations; address public immutable OWNER; event DestinationAdded(uint256 indexed index, bytes target); event DestinationGasLimitUpdated(uint256 indexed index, uint256 oldGasLimit, uint256 newGasLimit); event Ticked(uint256 nDestinations, uint256 totalValue); error NotOwner(); error LengthMismatch(); error InsufficientValue(); error InvalidIndex(); error ZeroGasLimit(); modifier onlyOwner() { if (msg.sender != OWNER) revert NotOwner(); _; } constructor() { OWNER = msg.sender; } /// @notice Register an `ExternalCounter` on a destination chain. function addDestination(bytes calldata target, uint256 gasLimit) external onlyOwner { if (gasLimit == 0) revert ZeroGasLimit(); destinations.push(Destination({ target: target, gasLimit: gasLimit })); emit DestinationAdded(destinations.length - 1, target); } /// @notice Update the gas budget granted to a registered destination's CEA /// without redeploying. function setDestinationGasLimit(uint256 index, uint256 newGasLimit) external onlyOwner { if (index >= destinations.length) revert InvalidIndex(); if (newGasLimit == 0) revert ZeroGasLimit(); uint256 oldGasLimit = destinations[index].gasLimit; destinations[index].gasLimit = newGasLimit; emit DestinationGasLimitUpdated(index, oldGasLimit, newGasLimit); } /// @notice Tick every registered destination's counter. /// @param perCallFee protocolFee + gasFee for each destination, quoted via /// `UniversalCore.getOutboundTxGasAndFees(...)`. /// @param revertRecipient Push-side address credited if any outbound reverts. function tickAll(uint256[] calldata perCallFee, address revertRecipient) external payable { uint256 n = destinations.length; if (perCallFee.length != n) revert LengthMismatch(); // Checks-Effects-Interactions: sum and validate msg.value BEFORE // dispatching any UGPC outbound, so an under-funded call reverts // immediately without burning gas on outbounds the contract would // have to back-fund from its own balance. uint256 total; for (uint256 i = 0; i A few things to note about the outbound shape: - **`token` is `address(0)`.** We're executing a payload, not bridging funds. UGPC infers `TX_TYPE = GAS_AND_PAYLOAD` from `token == address(0) && payload.length > 0`. - **`recipient` is `bytes`, not `address`.** UGPC supports non-EVM destinations too, so addresses are bytes-packed (`abi.encodePacked(externalCounterAddress)` for EVM destinations). - **`payload` is the raw single-call calldata.** No multicall marker prefix β€” that's only required for multi-step destination payloads. - **Per-call fees, not flat.** Each destination has its own protocolFee + gasFee depending on its current gas price and configured `gasLimit`. Quote each one separately off-chain. ## Wire It Up Three things to wire up, then we tick. ```mermaid flowchart LR A[1. Deploy] --> B[2. Register destinations] B --> C[3. tickAll] style A fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style C fill:#22c55e,stroke:#fff,stroke-width:2px,color:#fff ``` ### Deploy the contracts Deploy `MultiChainCounter` on Push Chain and `ExternalCounter` on each destination β€” Foundry, Hardhat, or Remix all work. See [smart contract deployment](/docs/chain/setup/smart-contract-environment/) for setup. ```bash # Push Chain Donut Testnet (orchestrator) forge create src/MultiChainCounter.sol:MultiChainCounter \ --rpc-url $PUSH_TESTNET_RPC --private-key $DEPLOYER_KEY # Each destination β€” same command, different RPC + key forge create src/ExternalCounter.sol:ExternalCounter \ --rpc-url $DEST_RPC --private-key $DEST_KEY ``` ### Register each destination ```typescript const DESTINATIONS = [ { counter: '0xCounterEth...', gasLimit: 1_000_000n }, { counter: '0xCounterBnb...', gasLimit: 1_000_000n }, { counter: '0xCounterArb...', gasLimit: 1_000_000n }, ]; for (const d of DESTINATIONS) { await pushChainClient.universal.sendTransaction({ to: MULTICHAIN_COUNTER_ADDRESS, data: PushChain.utils.helpers.encodeTxData({ abi: MULTICHAIN_COUNTER_ABI, functionName: 'addDestination', args: [encodePacked(['address'], [d.counter]), d.gasLimit], }), }); } ``` ### Tick every counter ```typescript const PER_CALL_FEE = 5n * 10n ** 18n; // 5 PC per destination β€” comfortable testnet headroom const fees = DESTINATIONS.map(() => PER_CALL_FEE); const total = fees.reduce((a, b) => a + b, 0n); await pushChainClient.universal.sendTransaction({ to: MULTICHAIN_COUNTER_ADDRESS, value: total, data: PushChain.utils.helpers.encodeTxData({ abi: MULTICHAIN_COUNTER_ABI, functionName: 'tickAll', args: [fees, REVERT_RECIPIENT_ADDRESS], }), }); ``` A few seconds later, once the TSS network has relayed each outbound. You will see every `ExternalCounter` will have ticked exactly once. Verify with `count()` and `lastCaller()` reads on each destination chain. :::warning Use a roomy `gasLimit` (>= 1_000_000) The destination's CEA has to execute the Vault wrapper, decode the payload, and call your target, which requires more gas than the bare `increment()` itself.Tight budgets revert with selector `0xff633a38`. **Default to 1_000_000 per destination**, bump higher for complex payloads, and use `setDestinationGasLimit(index, newGasLimit)` to fix a too-low budget without redeploying. ::: ## Understanding msg.sender on the Destination When `ExternalCounter.increment()` runs on Ethereum from a `tickAll`, what does `msg.sender` resolve to? ``` msg.sender == lastCaller == CEAFactory.getCEAForPushAccount(MULTICHAIN_COUNTER_ADDRESS) == ceaOnEth.address ``` This is the unique guarantee CEAs provide: - The Ethereum contract sees a normal `msg.sender` address. No exotic format, no special handling. - That address can only be **controlled** by exactly one Push Chain contract (`MultiChainCounter`). - The relationship is enforced by the destination chain's `CEAFactory`, not by anything off-chain. In this base example anyone *can* still call `increment()`, but only the orchestrator's CEA will appear as `lastCaller` when the call comes through UGPC. To enforce that only the CEA can drive the counter, see the next section. ## Production hardening β†’ gate _increment()_ to the CEA {#production-hardening-gate-increment-to-the-cea} When you're ready to make the counter authoritative, where destination state can only advance through the orchestrator, swap `ExternalCounter` for the auth-gated variant. The diff is small: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; contract ExternalCounter { uint256 public count; address public immutable AUTHORIZED_CEA; event CountIncremented(uint256 indexed newCount, address indexed caller); error NotAuthorizedCEA(); constructor(address authorizedCEA) { AUTHORIZED_CEA = authorizedCEA; } function increment() external { if (msg.sender != AUTHORIZED_CEA) revert NotAuthorizedCEA(); unchecked { count += 1; } emit CountIncremented(count, msg.sender); } } ``` Pass the orchestrator's per-chain CEA (from step 2 above) as the constructor argument. Now `increment()` reverts for anyone except the orchestrator's CEA. The destination chain's `CEAFactory` enforces that the CEA can only be controlled by your `MultiChainCounter` on Push, so the gating is end-to-end without any off-chain trust. You can also pre-authorise the CEA on third-party destination protocols this way (whitelists, vault gating, etc.) before any cross-chain activity has happened. That's the [pre-authorise pattern from the Derive CEA tutorial](/docs/chain/tutorials/power-features/tutorial-derive-chain-executor-account#example-pre-compute-and-authorize-a-push-contracts-cea). ## Live Playground Connect a wallet and click **Tick all destinations**. One Push transaction fans out three outbounds; when each `lastCaller` matches the derived CEA, you've watched the identity round-trip end to end. A **Lifecycle events** panel below the buttons surfaces every step from the SDK's `tx.progressHook(callback)`. Wired to a reference deployment: | Contract | Chain | Address | |---|---|---| | `MultiChainCounter` | Push Donut Testnet | [`0x7dd8...fBDd`](https://donut.push.network/address/0x7dd80f17C593F73292b3c4B785C4dD0100C4fBDd) | | `ExternalCounter` | Ethereum Sepolia | [`0xCf5D...Ee92`](https://sepolia.etherscan.io/address/0xCf5DB8F40F7dAA8Aa8Cb36C880F7207a65e2Ee92) | | `ExternalCounter` | BNB Testnet | [`0xfEe7...57D5`](https://testnet.bscscan.com/address/0xfEe777Fbd341AC02d105037022fc03D3CcD757D5) | | `ExternalCounter` | Arbitrum Sepolia | [`0xb3fB...6E79`](https://sepolia.arbiscan.io/address/0xb3fB98A3C6EEA643532198CF22cc50BC48026E79) | To drive your own deployments, edit `ORCHESTRATOR` and `DEMO_DESTINATIONS` at the top of the playground source. ```jsx live // customPropMinimized='true' function CrossChainCounterExample() { // Pre-deployed demo addresses. Replace with your own deploys to drive your own counters. const ORCHESTRATOR = "0x7dd80f17C593F73292b3c4B785C4dD0100C4fBDd"; const PER_CALL_FEE_PC_WEI = 5n * 10n ** 18n; // 5 PC per destination, comfortable testnet headroom const DEMO_DESTINATIONS = [ { label: "Ethereum Sepolia", chainKey: "ETHEREUM_SEPOLIA", counterAddress: "0xCf5DB8F40F7dAA8Aa8Cb36C880F7207a65e2Ee92", rpc: "https://ethereum-sepolia-rpc.publicnode.com", }, { label: "BNB Testnet", chainKey: "BNB_TESTNET", counterAddress: "0xfEe777Fbd341AC02d105037022fc03D3CcD757D5", rpc: "https://bsc-testnet-rpc.publicnode.com", }, { label: "Arbitrum Sepolia", chainKey: "ARBITRUM_SEPOLIA", counterAddress: "0xb3fB98A3C6EEA643532198CF22cc50BC48026E79", rpc: "https://sepolia-rollup.arbitrum.io/rpc", }, ]; const MULTICHAIN_ABI = [ { inputs: [ { name: 'perCallFee', type: 'uint256[]' }, { name: 'revertRecipient', type: 'address' }, ], name: 'tickAll', outputs: [], stateMutability: 'payable', type: 'function', }, ]; const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET }; function Component() { const { connectionStatus } = usePushWalletContext(); const { pushChainClient } = usePushChainClient(); const { PushChain } = usePushChain(); const [ceaAddrs, setCeaAddrs] = useState({}); const [counts, setCounts] = useState({}); const [lastCallers, setLastCallers] = useState({}); const [loading, setLoading] = useState(false); const [tickLoading, setTickLoading] = useState(false); const [txHash, setTxHash] = useState(""); const [error, setError] = useState(""); const [progressEvents, setProgressEvents] = useState([]); useEffect(() => { if (!PushChain) return; (async () => { const orchestratorOnPush = PushChain.utils.account.toUniversal(ORCHESTRATOR, { chain: PushChain.CONSTANTS.CHAIN.PUSH_TESTNET, }); const next = {}; for (const d of DEMO_DESTINATIONS) { try { const cea = await PushChain.utils.account.deriveExecutorAccount(orchestratorOnPush, { chain: PushChain.CONSTANTS.CHAIN[d.chainKey], skipNetworkCheck: true, }); next[d.label] = cea.address; } catch (e) { // Skip destinations the SDK can't derive on this network. } } setCeaAddrs(next); })(); }, [PushChain]); const refresh = async () => { setLoading(true); const c = {}; const lc = {}; await Promise.all( DEMO_DESTINATIONS.map(async (d) => { try { const res = await fetch(d.rpc, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", method: "eth_call", params: [{ to: d.counterAddress, data: "0x06661abd" }, "latest"], id: 1, }), }).then((r) => r.json()); c[d.label] = BigInt(res.result || "0x0"); const res2 = await fetch(d.rpc, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", method: "eth_call", params: [{ to: d.counterAddress, data: "0x6c428e98" }, "latest"], id: 2, }), }).then((r) => r.json()); lc[d.label] = "0x" + (res2.result || "").slice(-40); } catch (e) { // Skip RPC errors silently. } }) ); setCounts(c); setLastCallers(lc); setLoading(false); }; useEffect(() => { refresh(); }, []); const tickAll = async () => { if (!pushChainClient || !PushChain) return; setTickLoading(true); setError(""); setTxHash(""); setProgressEvents([]); try { const fees = DEMO_DESTINATIONS.map(() => PER_CALL_FEE_PC_WEI); const total = fees.reduce((a, b) => a + b, 0n); const data = PushChain.utils.helpers.encodeTxData({ abi: MULTICHAIN_ABI, functionName: "tickAll", args: [fees, ORCHESTRATOR], }); const tx = await pushChainClient.universal.sendTransaction({ to: ORCHESTRATOR, value: total, data, }); setTxHash(tx.hash); // Subscribe to per-step lifecycle events the SDK fires for this tx. // Each event has { id, title, message, level, response, timestamp }. tx.progressHook((event) => { setProgressEvents((prev) => [...prev, event]); }); await tx.wait(); for (let i = 0; i setTimeout(r, 15000)); await refresh(); } } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setTickLoading(false); } }; return ( Universal Cross-Chain Counters One Push tx, three destination chains tick at once Chain CEA count lastCaller {DEMO_DESTINATIONS.map((d) => { const cea = ceaAddrs[d.label]; const lc = lastCallers[d.label]; const match = cea && lc && cea.toLowerCase() === lc.toLowerCase(); const short = (a) => (a && a.length >= 12 ? `${a.slice(0, 6)}...${a.slice(-4)}` : "-"); return ( {d.label} {short(cea)} {counts[d.label] !== undefined ? counts[d.label].toString() : "-"} {short(lc)} {match && βœ“ CEA} ); })} {loading ? "Reading..." : "Refresh"} {connectionStatus === PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && ( {tickLoading ? "Ticking..." : "Tick all destinations"} )} {error && ( {error} )} {progressEvents.length > 0 && ( Lifecycle events from tx.progressHook(...) {progressEvents.map((p, i) => { const dot = p.level === "SUCCESS" ? "#16a34a" : p.level === "ERROR" ? "#b91c1c" : "#6b7280"; return ( {p.id} {p.title} {p.message && ( {p.message} )} ); })} )} {txHash && pushChainClient && ( View Push tx on explorer β†’ )} ); } return ( ); } ``` ## Source code ## What we achieved In this tutorial, we built a true cross-chain orchestrator: - **One Push contract, many destinations.** `MultiChainCounter` fans out to Ethereum, BNB, and Arbitrum in a single Push transaction. - **Deterministic identity.** Each destination's CEA is computable from the orchestrator's address alone, before any deployment. - **Visible identity proof.** `ExternalCounter.lastCaller()` records exactly the deterministic CEA after each tick. - **Optional production gating.** Drop in the auth-gated `ExternalCounter` and only the orchestrator's CEA can drive destination state. - **One-shot UX.** Users (or other contracts) call `tickAll()` once on Push, and three chains' state updates as a result. ## Key takeaways **1. CEAs are computed before they exist** - `getCEAForPushAccount` (or the SDK's `deriveExecutorAccount`) returns a deterministic address. - That address can be authorised on destination protocols on day zero. - The TSS network deploys the actual contract on first use. **2. The Push-side address is the identity anchor** - A different `MultiChainCounter` deployment has a different CEA on every chain. - Re-deploying the orchestrator means re-authorising on every destination. - For upgradeable systems, use a proxy. The CEA stays bound to the proxy address. **3. Destination protocols don't need any Push Chain awareness** - `ExternalCounter` is plain Solidity. No Push-specific imports, no signatures to verify, no gateway calls. - The CEA looks like a normal EOA from the destination's perspective. - Universal execution is invisible at the destination layer. ## What's Next? You've fanned **one Push contract out to many chains**. Now flip the direction. Let users on every chain claim from a single Push Chain contract. The Universal Airdrop tutorial uses Merkle proofs and UEAs to distribute tokens to recipients on Ethereum, Solana, BNB, and beyond, all from one contract on Push. ```mermaid flowchart LR A[Ethereum claimer] --> D[Universal Airdrop on Push] B[Solana claimer] --> D C[BNB claimer] --> D D --> E[Tokens claimed] style A fill:#627eea,color:#fff style B fill:#16c492,color:#fff style C fill:#f0b90b,color:#000 style D fill:#dd44b9,stroke:#fff,stroke-width:2px,color:#fff style E fill:#22c55e,stroke:#fff,stroke-width:2px,color:#fff ``` Check out the next tutorial to learn how to [build a Universal Airdrop](/docs/chain/tutorials/token-systems/tutorial-universal-airdrop/): one contract, every chain, no bridging. --- # Setup Scaffold-ETH for Push URL: https://push.org/docs/chain/tutorials/integration-and-tooling/tutorial-scaffoldeth2/ {`Configure Scaffold‑ETHΒ 2 for Push Chain: Deploy and Interact with a Contract | Tutorials | Push Chain Docs`} Welcome! In this tutorial, you’ll set up a fresh **Scaffold‑ETHΒ 2** project, **deploy a new smart contract to the Push Chain Donut Testnet**, and **interact with it from the Scaffold‑ETHΒ 2 app**. We’ll cover: 1. Add Push Chain Donut Testnet to Scaffold‑ETHΒ 2 2. Configure **Hardhat** for Push Chain 3. Create a new example contract (`Governance.sol`) 4. Write and run a **deploy script** to deploy on Push Chain 5. Interact with the deployed contract from the Scaffold‑ETHΒ 2 app If you already use Scaffold‑ETHΒ 2, this will feel familiarβ€”you’ll point the template at a new network, add a contract, and deploy it. Let’s go 🀿. ## Part 1: Configure Scaffold‑ETHΒ 2 for Push Chain Donut Testnet ### 1.1. Create a new Scaffold‑ETHΒ 2 workspace ```bash npx create-eth@latest ``` When prompted by the `create-eth` wizard, **select the Hardhat option** for your smart contract environment. This ensures your project is set up to deploy contracts to Push Chain using Hardhat. ### 1.2. Add Push Chain Donut Testnet to `scaffold.config.ts` Open **`packages/nextjs/scaffold.config.ts`** and add a custom chain entry for **Push Chain Donut Testnet**, then include it in `targetNetworks` so the app knows about it. ```ts title="packages/nextjs/scaffold.config.ts" targetNetworks: readonly chains.Chain[]; pollingInterval: number; alchemyApiKey: string; rpcOverrides?: Record; walletConnectProjectId: string; onlyLocalBurnerWallet: boolean; }; process.env.NEXT_PUBLIC_ALCHEMY_KEY ?? 'REPLACE_ME'; // highlight-start // Push Chain Donut Testnet id: 42101, name: 'Push Chain Donut Testnet', nativeCurrency: { name: 'Push', symbol: 'PC', decimals: 18 }, rpcUrls: { default: { http: [ 'https://evm.donut.rpc.push.org/', ], }, public: { http: [ 'https://evm.donut.rpc.push.org/', ], }, }, blockExplorers: { default: { name: 'Push Donut Explorer', url: 'https://evm-explorer-testnet.push.org', }, }, }; // highlight-end const scaffoldConfig = { // highlight-start targetNetworks: [chains.hardhat, pushDonutChain], // highlight-end pollingInterval: 30000, alchemyApiKey: DEFAULT_ALCHEMY_API_KEY, rpcOverrides: {}, walletConnectProjectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID ?? 'YOUR_WALLETCONNECT_ID', onlyLocalBurnerWallet: true, } as const satisfies ScaffoldConfig; ``` > βœ… **Why this matters**: `targetNetworks` drives the chain list in the app and wagmi connectors. Adding `pushDonutChain` makes the UI aware of Push Chain. ### 1.3. Configure Hardhat for Push Chain Edit **`packages/hardhat/hardhat.config.ts`** to add a Push Chain Donut Testnet network. ```ts title="packages/hardhat/hardhat.config.ts" dotenv.config(); import '@nomicfoundation/hardhat-ethers'; import '@nomicfoundation/hardhat-chai-matchers'; import '@typechain/hardhat'; import 'hardhat-gas-reporter'; import 'solidity-coverage'; import '@nomicfoundation/hardhat-verify'; import 'hardhat-deploy'; import 'hardhat-deploy-ethers'; const providerApiKey = process.env.ALCHEMY_API_KEY; const deployerPrivateKey = process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY const etherscanApiKey = process.env.ETHERSCAN_V2_API_KEY; const config: HardhatUserConfig = { solidity: { compilers: [ { version: '0.8.20', settings: { optimizer: { enabled: true, runs: 200, }, }, }, ], }, defaultNetwork: 'localhost', namedAccounts: { deployer: { default: 0, }, }, networks: { hardhat: { forking: { url: `https://eth-mainnet.alchemyapi.io/v2/${providerApiKey}`, enabled: process.env.MAINNET_FORKING_ENABLED === 'true', }, }, // highlight-start // Push Chain Donut Testnet pushDonut: { url: 'https://evm.donut.rpc.push.org/', chainId: 42101, accounts: [deployerPrivateKey], }, // highlight-end }, etherscan: { apiKey: etherscanApiKey, }, verify: { etherscan: { apiKey: etherscanApiKey, }, }, sourcify: { enabled: false, }, }; task('deploy').setAction(async (args, hre, runSuper) => { await runSuper(args); await generateTsAbis(hre); }); ``` ### 1.4. Generate a deployer account and fund it You’ll need a funded testnet account to deploy contracts to Push Chain Donut. Generate a fresh account using the built‑in script: ```bash # from the repo root yarn generate # This prints a new Address ``` Fund the generated address with Push Chain Donut testnet $PC using the Faucet. If you need funds, request them here: - Faucet docs: [Faucet](https://pushchain.github.io/push-chain-website/pr-preview/pr-1067/docs/chain/setup/tooling/faucet/) - Direct faucet: `https://faucet.push.org/` ## Part 2: Add and deploy the governance contract ### 2.1. Add `Governance.sol` Place your contract at **`packages/hardhat/contracts/Governance.sol`**: #### What this sample contract does This is a minimal governance example to demonstrate deployment and app wiring: - **Create proposals**: Anyone can call `createProposal(description, duration)` which assigns an incremental id, stores the description, and sets a deadline as `block.timestamp + duration`. Emits `ProposalCreated`. - **Vote once per address**: Call `vote(id, support)` before the deadline to cast a yes/no vote. Each address can vote only once per proposal. Emits `Voted`. - **Read state**: Use `getProposal(id)` to fetch description, deadline, yes/no counts, and `hasVoted(id, voter)` to check if an address has voted. - **Purposely simple**: No token‑weighting, quorum, execution, or proposal states beyond open/closed. It’s for tutorial/demo purposes. ```solidity title="packages/hardhat/contracts/Governance.sol" // SPDX-License-Identifier: MIT pragma solidity >=0.8.0 bool) voted; bool exists; } uint256 public proposalCount; mapping(uint256 => Proposal) internal _proposals; function createProposal(string calldata description, uint256 duration) external returns (uint256 id) { require(duration > 0, "duration must be > 0"); id = ++proposalCount; Proposal storage p = _proposals[id]; p.description = description; p.deadline = block.timestamp + duration; p.exists = true; emit ProposalCreated(id, msg.sender, description, p.deadline); } function vote(uint256 id, bool support) external { Proposal storage p = _getProposal(id); require(block.timestamp 0 && id ( 'SimpleGovernance', deployer ); console.log('πŸ‘‹ Initial proposal count:', await yourContract.proposalCount()); }; deployYourContract.tags = ['SimpleGovernance']; ``` ### 2.3. Deploy to Push Chain Donut From the repo root, run: ```bash yarn deploy --network pushDonut ``` You should see the contract address and the β€œInitial proposal count” log. ## Part 3: Interact from the Debug UI After deployment, open your app and go to the `/debug` page. The **Debug Contracts** UI will automatically pick up your deployed contracts and expose handy actions. You can: - Create a proposal using `createProposal(description, duration)` - Vote on proposals with `vote(id, true|false)` - Read current state via `getProposal(id)` and `hasVoted(id, address)` This gives you a ready‑made interface to test your contract on Push Chain without building a custom UI first. ## Conclusion You’ve configured **Scaffold‑ETHΒ 2** to recognize **Push Chain Donut Testnet**, added a new contract (**SimpleGovernance**), wired a **Hardhat** network, and **deployed**. From here, you can keep iterating on contracts and UI as usualβ€”just keep the Push Chain network in your configs. ### Next Steps - **Explore universal transactions and cross‑chain UX** - Learn about [Universal Transactions](/docs/chain/build/send-universal-transaction) and [Universal Message Signing](/docs/chain/build/sign-universal-message) for seamless cross-chain interactions - **Integrate Push Universal Wallet** - Add wallet abstraction to your app with our [UI Kit integration guide](/docs/chain/ui-kit/integrate-push-universal-wallet) For more about the framework used here, see the official Scaffold‑ETHΒ 2 docs: `https://docs.scaffoldeth.io/`. --- # Basics URL: https://push.org/docs/chain/tutorials/basics/ Basics Section | Tutorials | Push Chain Docs # Basics Section Learn the basics of building on Push Chain. Start with the most popular smart contracts, i.e., `Counter.sol`, that all Solidity devs are familiar with and scale it to make it work on any chain. --- # Power Features URL: https://push.org/docs/chain/tutorials/power-features/ Power Features Section | Tutorials | Push Chain Docs # Power Features Section Learn the power features of Push Chain and how to leverage them to build apps of the future. --- # Token Systems URL: https://push.org/docs/chain/tutorials/token-systems/ Token Systems Section | Tutorials | Push Chain Docs # Token Systems Section Learn how to create token systems that are universal, how to do claimable airdrops that are available for users of all chains and everything in between. --- # Payments and DeFi URL: https://push.org/docs/chain/tutorials/integration-and-tooling/ Integration and Tooling Section | Tutorials | Push Chain Docs # Integration and Tooling Section Learn how to enable popular integrations or tooling frameworks for Push Chain. --- # Integrate Push Universal Wallet URL: https://push.org/docs/chain/ui-kit/integrate-push-universal-wallet/ {`Integrate Push Universal Wallet | Customizations | UI Kit | Push Chain Docs`} ## Overview Push Universal Wallet gives your app: - **Seamless cross-chain** wallet integration (EVM, Solana, etc.) - **Provider/consumer** hook API for global state - Built-in **UI components** for connect buttons and modals - **Eliminates the need** to manage complex blockchain connections, authentication states, and cross-chain interactions manually ## How It Works The Push Universal Wallet operates on a simple provider-consumer pattern: 1. **Provider Setup**: Wrap your application with `PushUniversalWalletProvider` to initialize wallet functionality 2. **Button Integration**: Add `PushUniversalAccountButton` components where users should connect 3. **State Management**: Use the `usePushWalletContext` hook to access wallet state and methods throughout your app The Push Universal Wallet provides a unified interface for connecting to multiple blockchains, automatically handling authentication and cross-chain interactions without complex blockchain-specific implementations. ## Installation ```bash npm install @pushchain/ui-kit ``` ```bash yarn add @pushchain/ui-kit ``` :::info Note If you are using **Vite with Plug'n'Play (PnP)** or encounter a **buffer error**, simply install `buffer` as a dependency. The UI-kit already includes the necessary polyfills, so no extra configuration is required. ::: ## Quickstart ```jsx live // customPropHighlightRegexStart= // customPropGTagEvent=ui_kit_basic_integration // customPropMinimized='false' // Import necessary components from @pushchain/ui-kit function App() { // Define Wallet Config const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, }; return ( ); } ``` ```jsx live // customPropHighlightRegexStart= // customPropGTagEvent=ui_kit_hooks_integration // customPropMinimized='false' // Import necessary components from @pushchain/ui-kit function App() { // Define Wallet Config const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, }; function WalletUI() { const { connectionStatus } = usePushWalletContext(); const { pushChainClient } = usePushChainClient(); return ( {connectionStatus == PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && ( LFG! Push Chain Executor Account (UEA): $ {pushChainClient?.universal.account} )} ); } return ( ); } ``` ```jsx live // customPropHighlightRegexStart= // customPropGTagEvent=ui_kit_customizations_integration // customPropMinimized='false' // Import necessary components from @pushchain/ui-kit function App() { // Define Wallet Config const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, login: { email: true, google: true, wallet: { enabled: true, }, appPreview: true, }, modal: { loginLayout: PushUI.CONSTANTS.LOGIN.LAYOUT.SPLIT, connectedLayout: PushUI.CONSTANTS.CONNECTED.LAYOUT.HOVER, appPreview: true, }, }; // Define Your App Preview const appMetadata = { logoUrl: 'https://plus.unsplash.com/premium_photo-1746731481770-08b2f71661d0?q=80&w=2671&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', title: 'Test App Title', description: 'Test App Description', }; function WalletUI() { const { connectionStatus } = usePushWalletContext(); const { pushChainClient } = usePushChainClient(); return ( {connectionStatus == PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && ( LFG! Push Chain Executor Account (UEA): $ {pushChainClient?.universal.account} )} ); } return ( ); } ``` ```jsx live // customPropHighlightRegexStart= // customPropGTagEvent=ui_kit_send_transaction // customPropMinimized='false' // Import necessary components from @pushchain/ui-kit function App() { // Define Wallet Config const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, login: { email: true, google: true, wallet: { enabled: true, }, appPreview: true, }, modal: { loginLayout: PushUI.CONSTANTS.LOGIN.LAYOUT.SPLIT, connectedLayout: PushUI.CONSTANTS.CONNECTED.LAYOUT.HOVER, appPreview: true, }, }; // Define Your App Preview const appMetadata = { logoUrl: 'https://plus.unsplash.com/premium_photo-1746731481770-08b2f71661d0?q=80&w=2671&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', title: 'Test App Title', description: 'Test App Description', }; function WalletUI() { const [txnHash, setTxnHash] = useState(null); const [isLoading, setIsLoading] = useState(false); const { connectionStatus } = usePushWalletContext(); const { pushChainClient } = usePushChainClient(); const handleSendTransaction = async () => { if (pushChainClient) { setIsLoading(true); try { const res = await pushChainClient.universal.sendTransaction({ to: '0xFaE3594C68EDFc2A61b7527164BDAe80bC302108', value: PushChain.utils.helpers.parseUnits('0.01', 18), // 0.01 PC }); setTxnHash(res.hash); } catch (err) { console.log(err); } finally { setIsLoading(false); } } }; return ( {connectionStatus == PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && ( Send Transaction )} {txnHash && ( <> Txn Hash: {txnHash} View in Explorer )} ); } return ( ); } ``` :::info You can quickly scaffold a new Push Chain dapp with **UI-Kit** already configured using our CLI. ::: ```bash npx create-universal-dapp my-app ``` ## Customization Parameters Customize the parameters as per your need and the wallet functionality being used. ### PushUniversalWalletProvider Below is a list of **minimum parameters** required to customize the Push Universal Wallet Provider. Check out [PushUniversalWalletProvider](/docs/chain/ui-kit/customizations/push-universal-wallet-provider/) for more information on how to use and customize the Push Universal Wallet Provider. | Arguments | Type | Default | Description | | ------------------ | -------------------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | _`config`_ | `PushUniversalWalletProviderConfig` | - | Core configuration object for wallet connections. | | _`config.network`_ | `PushUI.CONSTANTS.PUSH_NETWORK.TESTNET` | - | Push Chain network to conenct to. For example: `PushUI.CONSTANTS.PUSH_NETWORK.TESTNET` `PushUI.CONSTANTS.PUSH_NETWORK.TESTNET` `PushUI.CONSTANTS.PUSH_NETWORK.TESTNET_DONUT` `PushUI.CONSTANTS.PUSH_NETWORK.LOCALNET` | | `config.app` | `{ title, description, logoUrl }` | - | (Optional) app metadata for login/preview in the modal. | | `config.login` | `{ email, google, wallet, appPreview }` | - | Toggle login methods. | | `config.modal` | `{ loginLayout, connectedLayout, connectedInteraction, appPreview }` | - | Customize the modal layout and interaction. | | `themeMode` | `'light'` \| `'dark'` | `light` | Force a particular theme. | | `themeOverrides` | `Record & { light, dark }` | - | Override CSS vars globally or per theme. | ### PushUniversalAccountButton Check out [PushUniversalAccountButton](/docs/chain/ui-kit/customizations/push-universal-account-button/) for more information on how to use and customize the Push Universal Account Button. ## Next Steps - Customize the provider in [PushUniversalWalletProvider](/docs/chain/ui-kit/customizations/push-universal-wallet-provider/) - Style your connect button in [PushUniversalAccountButton](/docs/chain/ui-kit/customizations/push-universal-account-button/) - Master hooks with [usePushWalletContext](/docs/chain/ui-kit/customizations/use-push-wallet-context/) --- # Push Universal Wallet Provider URL: https://push.org/docs/chain/ui-kit/customizations/push-universal-wallet-provider/ {`Push Universal Wallet Provider | Customizations | UI Kit | Push Chain Docs`} `PushUniversalWalletProvider` is the top-level context provider component that initializes wallet functionality across your app, handling: - **Login Configuration**: What logins and wallets you want to enable in your app (email, OAuth, wallets). - **Application Metadata**: Allows you to display your application metadata such as logo, name, etc. - **Theme Overrides**: Customize or override default styles. ## Installation ```bash # UI Kit SDK npm install @pushchain/ui-kit ``` ```bash # UI Kit SDK yarn add @pushchain/ui-kit ``` ## Usage Wrap your application with `PushUniversalWalletProvider` to make wallet functionality available to all child components. ```typescript live // customPropHighlightRegexStart= // customPropGTagEvent=ui_kit_basic_wallet_provider function App() { return ( ); } ``` ## Props | Property | Type | Default | Description | | ---------------- | ------------------------ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | _`config`_ | `Object` | _\*\*_ | Used to configure the wallet connection, logins, and modals. _\*\*_ See `config` prop for more info. | | `app` | `Object` | _\*\*_ | Used to display your application metadata such as logo, name, etc. _\*\*_ See `app` prop for more info. | | `themeMode` | `PushUI.CONSTANTS.THEME` | `PushUI.CONSTANTS.THEME.LIGHT` | Theme mode to apply, you can use `LIGHT` or `DARK` option. `PushUI.CONSTANTS.THEME.LIGHT` `PushUI.CONSTANTS.THEME.DARK` | | `themeOverrides` | `Object` | _\*\*_ | Used to override default styles. _\*\*_ See `themeOverrides` prop for more info. | ### _`config`_ prop (required) Customize the behavior of the wallet connection, logins, and modals by using the `config` prop. | Property | Type | Default | Description | | ------------- | ------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | _`network`_ | `PushUI.CONSTANTS.PUSH_NETWORK` | - | Push Chain network to connect to. For example: `PushUI.CONSTANTS.PUSH_NETWORK.TESTNET` `PushUI.CONSTANTS.PUSH_NETWORK.TESTNET` `PushUI.CONSTANTS.PUSH_NETWORK.TESTNET_DONUT` `PushUI.CONSTANTS.PUSH_NETWORK.LOCALNET` | | `login` | `Object` | _\*\*_ | Login method configuration. _\*\*_ See `config.login` Options. | | `modal` | `Object` | _\*\*_ | Global defaults for login and connected modal instances. _\*\*_ See `config.modal` Options. | | `uid` | `string` | 'default' | Unique identifier for this provider. instance | | `rpcUrl` | `string` | Public endpoints | Custom JSON-RPC endpoint for supported chains. | | `chainConfig` | `Object` | _\*\*_ | Custom settings to configure the SDK instance. _\*\*_ See `config.chainConfig` Options. | | Property | Type | Default | Description | | ---------------------- | ---------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `email` | `boolean` | `true` | Enables email sign in when `true` | | `google` | `boolean` | `true` | Enables google sign in when `true` | | `phone` | `boolean` | `true` | Enables phone sign in when `true` | | `socials` | `Object` | _\*\*_ | Enables additional social login providers. _\*\*_ See `config.login.socials` Options. | | `wallet` | `Object` | _\*\*_ | External wallet configuration. _\*\*_ See `config.login.wallet` Options. | | `appPreview` | `boolean` | `false` | Show app preview in modal | | Property | Type | Default | Description | | ---------------------- | ---------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `discord` | `boolean` | `true` | Enables discord sign in when `true` | | `github` | `boolean` | `true` | Enables github sign in when `true` | | `x` | `boolean` | `true` | Enables twitter sign in when `true` | | `bluesky` | `boolean` | `true` | Enables bluesky sign in when `true` | | Property | Type | Default | Description | | --------- | -------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `enabled` | `boolean` | `true` | Allow external wallet connections | | `chains` | `PushUI.CONSTANTS.CHAIN[]` | All supported chains | You can choose to enable specific chains by passing them in an array. `PushUI.CONSTANTS.CHAIN` | | `excludedChains` | `PushUI.CONSTANTS.CHAIN[]` | `[]` | You can choose to disable specific chains by passing them in an array. `PushUI.CONSTANTS.CHAIN` | | Property | Type | Default | Description | | ---------------------- | ---------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `loginLayout` | `PushUI.CONSTANTS.LOGIN.LAYOUT` | `PushUI.CONSTANTS.LOGIN.LAYOUT.SIMPLE` | Login modal layout type. `PushUI.CONSTANTS.LOGIN.LAYOUT.SIMPLE` `PushUI.CONSTANTS.LOGIN.LAYOUT.SPLIT` | | `appPreview` | `boolean` | `false` | Show app preview in modal | | `bgImage` | `string` | `null` | Background image for the login modal | | `connectedLayout` | `PushUI.CONSTANTS.CONNECTED.LAYOUT` | `PushUI.CONSTANTS.CONNECTED.LAYOUT.HOVER` | Connected modal layout type. `PushUI.CONSTANTS.CONNECTED.LAYOUT.FULL` `PushUI.CONSTANTS.CONNECTED.LAYOUT.HOVER` | | `connectedInteraction` | `PushUI.CONSTANTS.CONNECTED.INTERACTION` | `PushUI.CONSTANTS.CONNECTED.INTERACTION.INTERACTABLE` | Connected modal outside interaction type. `PushUI.CONSTANTS.CONNECTED.INTERACTION.INTERACTABLE` `PushUI.CONSTANTS.INTERACTION.BLUR` | | Property | Type | Default | Description | | ---------------- | --------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------- | | `rpcUrls` | `Partial>` | `{}` | Custom RPC URLs mapped by chain IDs. | | `blockExplorers` | `Partial` | `{[CHAIN.PUSH_TESTNET_DONUT]: ['https://donut.push.network']}` | Custom block explorer URLs mapped by chain IDs. | | `printTraces` | `boolean` | `false` | When true, console logs the internal trace logs for debugging requests to nodes | ```jsx live // customPropHighlightRegexStart=walletConfig\s= // customPropHighlightRegexEnd= ); } ``` ```jsx live // customPropHighlightRegexStart=walletConfig\s= // customPropHighlightRegexEnd=', // custom rpc url to connect to chainConfig: {}, // custom chain config to pass to push chain client if needed }; return ( ); } ``` ### `app` prop Display your app metadata in login screens and preview panes by using the `app` prop. **Note**: You will also need to enable `appPreview` in the `login` and `modal` section of `config` props to show them in different sections of the UI. | Property | Type | Description | | ------------- | -------- | ------------------------------------ | | `logoUrl` | `string` | URL to application logo or icon | | `title` | `string` | Application name or title | | `description` | `string` | Brief description of the application | ```jsx live // customPropHighlightRegexStart=appMetadata\s= // customPropHighlightRegexEnd= ); } ``` ### `themeOverrides` prop Override different theme settings by using the `themeOverrides` prop. Check out all the supported theme variables in [Theme Variables](/docs/chain/ui-kit/customizations/theme-variables/). | Type | Default | Description | | ---------------- | ------- | --------------------------- | | `ThemeOverrides` | `{}` | Override the theme settings | ```jsx live // customPropHighlightRegexStart=themeOverrides={{ // customPropHighlightRegexEnd=}} // customPropGTagEvent=ui_kit_custom_theme_overrides // customPropMinimized='false' // Import necessary components from @pushchain/ui-kit function App() { // Define Wallet Config const walletConfig = { uid: 'custom-theme', network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, }; return ( ); } ``` ## Next Steps - Customize the connect button with [Push Universal Account Button](/docs/chain/ui-kit/customizations/push-universal-account-button/) - Use wallet context hooks via [usePushWalletContext](/docs/chain/ui-kit/customizations/use-push-wallet-context/) - Access the Push Chain Client with [usePushChainClient](/docs/chain/ui-kit/customizations/use-push-chain-client/) --- # Single Wallet Example URL: https://push.org/docs/chain/ui-kit/examples/single-wallet-example/ Single Wallets Example | Examples | UI Kit | Push Chain Docs This example demonstrates: - Basic wallet integration with the Push Universal Wallet. - Basic app metadata integration with the Push Universal Wallet. ## Live playground ```jsx live // customPropMinimized='true' function App() { const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, login: { email: true, google: true, wallet: { enabled: true, }, appPreview: true, }, modal: { loginLayout: PushUI.CONSTANTS.LOGIN.LAYOUT.SPLIT, connectedLayout: PushUI.CONSTANTS.CONNECTED.LAYOUT.HOVER, appPreview: true, }, }; const appMetadata = { logoUrl: 'https://plus.unsplash.com/premium_photo-1746731481770-08b2f71661d0?q=80&w=2671&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', title: 'Test App Title', description: 'Test App Description', }; return ( ); } ``` ## Next Steps - Explore connecting multiple wallets with [Multiple Wallets Example](/docs/chain/ui-kit/examples/multiple-wallet-example) - Themize UI Kit with [Theme Overrides Example](/docs/chain/ui-kit/examples/theme-overrides-example) or Wallet Connect Button with [Button Theme Overrides Example](/docs/chain/ui-kit/examples/button-theme-overrides-example) - Check out step by step implementation of App in end to end [Tutorials](/docs/chain/tutorials) --- # Push Universal Account Button URL: https://push.org/docs/chain/ui-kit/customizations/push-universal-account-button/ Push Universal Account Button | Customizations | UI Kit | Push Chain Docs `PushUniversalAccountButton` is a versatile, state-aware button component for wallet connections in the Push Chain ecosystem. It handles the complete user journey from connection initiation through authentication to displaying the connected account state, with extensive customization options for each state. ## Installation ```bash # UI Kit SDK npm install @pushchain/ui-kit ``` ```bash # UI Kit SDK yarn add @pushchain/ui-kit ``` ## Usage Place the button in your UI where users should be able to connect. The button must be used within a `PushUniversalWalletProvider`. ```jsx live // customPropHighlightRegexStart= // customPropGTagEvent=ui_kit_account_button_basic function App() { return ( ); } ``` ```jsx live // customPropHighlightRegexStart= // customPropGTagEvent=ui_kit_account_button_custom function App() { // Custom loading component const CustomLoader = () => ( {` @keyframes spin { to { transform: rotate(360deg); } } `} Loading wallet... ); return ( } /> ); } ``` ## Props | Property | Type | Default | Description | | ------------------- | ----------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `connectButtonText` | `string` | `Connect Account` | Text for the connect button. | | `loadingComponent` | `React.ReactNode` | Uses default loader | Custom loading indicator. | | `uid` | `string` | Uses default provider | Optional ID for targeting a specific wallet instance, must match `config.uid` of specific `PushUniversalWalletProvider` instance. See [multiple wallet example](/docs/chain/ui-kit/examples/multiple-wallet-example/) for usage. | | `themeOverrides` | `ThemeOverrides` | `{}` | Button theme overrides. Check out all the supported theme variables in [Theme Variables](/docs/chain/ui-kit/customizations/theme-variables/). | | `loginAppOverride` | `Object` | _\*\*_ | Used to override app preview in the login screen. _\*\*_ See `loginAppOverride` prop for more info. | | `modalAppOverride` | `Object` | _\*\*_ | Used to override app preview in the modal presented. _\*\*_ See `modalAppOverride` prop for more info. | | `customConnectComponent` | `React.ReactNode` | - | Custom component to replace the default Connect Wallet button when the user is not connected. | | `customConnectedComponent` | `React.ReactNode` | - | Custom component to replace the default connected wallet button after the user connects. | | `connectButtonClassName` | `string` | - | CSS class applied to the default Connect Wallet button. | | `connectedButtonClassName` | `string` | - | CSS class applied to the default connected wallet button. | ### `loginAppOverride` props Use this to override app preview that is provided on login screen for a particular button instance. This will override the app preview that is provided in the `app` prop of `PushUniversalWalletProvider`. | Property | Type | Default | Description | | ------------- | -------- | ---------------------------------- | ----------------------------------------- | | `logoUrl` | `string` | From `Provider's` -> `app.logoUrl` | Override app logo in login screen. | | `title` | `string` | From `app.title` | Override app title in login screen. | | `description` | `string` | From `app.description` | Override app description in login screen. | ### `modalAppOverride` props Use this to override app preview that is provided on wallet modal for a particular button instance. This will override the app preview that is provided in the `app` prop of `PushUniversalWalletProvider`. | Property | Type | Default | Description | | ------------- | -------- | ---------------------- | ---------------------------------- | | `logoUrl` | `string` | From `app.logoUrl` | Override app logo in modal. | | `title` | `string` | From `app.title` | Override app title in modal. | | `description` | `string` | From `app.description` | Override app description in modal. | ## Handling Connection Lifecycle You can track and customize the wallet connection lifecycle with [usePushWalletContext](/docs/chain/ui-kit/customizations/use-push-wallet-context/). ## Next Steps - Track the wallet connection lifecycle with [usePushWalletContext](/docs/chain/ui-kit/customizations/use-push-wallet-context/) - Get initialized wallet instance with [usePushChainClient](/docs/chain/ui-kit/customizations/use-push-chain-client/) - Customize user experience with [Theme Variables](/docs/chain/ui-kit/customizations/theme-variables/) --- # Multiple Wallet Example URL: https://push.org/docs/chain/ui-kit/examples/multiple-wallet-example/ Multiple Wallets Example | Examples | UI Kit | Push Chain Docs This example demonstrates: - How to connect multiple wallets to your app. - Using multiple chains wallets simultaneously for same app / user. ## Live playground ```jsx live // customPropMinimized='true' function App() { const walletConfig = { uid: 'wallet1', network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, login: { email: true, google: true, wallet: { enabled: true, }, appPreview: false, }, modal: { loginLayout: PushUI.CONSTANTS.LOGIN.LAYOUT.SPLIT, connectedLayout: PushUI.CONSTANTS.CONNECTED.LAYOUT.HOVER, appPreview: true, }, }; const secondWalletConfig = { uid: 'wallet2', network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, login: { email: false, google: false, wallet: { enabled: true, }, appPreview: true, }, }; const appMetadata = { logoUrl: 'https://plus.unsplash.com/premium_photo-1746731481770-08b2f71661d0?q=80&w=2671&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', title: 'Test App Title', description: 'Test App Description', }; const secondAppMetadata = { logoUrl: 'https://plus.unsplash.com/premium_photo-1746731481770-08b2f71661d0?q=80&w=2671&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', title: 'Test App Title', description: 'Test App Description', }; function WalletUI() { return ( ); } return ( ); } ``` ## Next Steps - Up your app vibe with themizing UI Kit from [Theme Overrides Example](/docs/chain/ui-kit/examples/theme-overrides-example) - Ensure consistent styling of wallet connect button with [Button Theme Overrides Example](/docs/chain/ui-kit/examples/button-theme-overrides-example) - Check out step by step implementation of App in end to end [Tutorials](/docs/chain/tutorials) --- # usePushWalletContext URL: https://push.org/docs/chain/ui-kit/customizations/use-push-wallet-context/ usePushWalletContext | Customizations | UI Kit | Push Chain Docs The `usePushWalletContext` hook provides access to the current wallet state and methods to interact with the Push Wallet. This hook must be used within a component that's wrapped by a `PushUniversalWalletProvider`. ## Usage ```jsx live // customPropHighlightRegexStart=const { // customPropHighlightRegexEnd=usePushWalletContext\(\) // customPropGTagEvent=ui_kit_use_wallet_context_hook import { PushUniversalWalletProvider, PushUniversalAccountButton, usePushWalletContext, PushUI } from '@pushchain/ui-kit'; function App() { // Create a component that uses the hook inside the provider context const WalletContextComponent = () => { const { connectionStatus, handleConnectToPushWallet, handleUserLogOutEvent } = usePushWalletContext(); // optional: uid parameter for targeting a specific wallet instance return ( Wallet Status: {connectionStatus} {connectionStatus === PushUI.CONSTANTS.CONNECTION.STATUS.NOT_CONNECTED && ( handleConnectToPushWallet()} > Connect via Hook Call )} {connectionStatus === PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && ( handleUserLogOutEvent()} > Disconnect via Hook Call )} ); }; return ( ); } ``` ### Parameters | Arguments | Type | Description | | --------- | -------- | ------------------------------------------- | | `uid` | `string` | Optional ID for targeting a specific wallet instance, must match `config.uid` of specific `PushUniversalWalletProvider` instance. See [multiple wallet example](/docs/chain/ui-kit/examples/multiple-wallet-example/) for usage. | ### Returns | Property | Type | Default | Description | | --------------------------- | ------------ | --------------- | ------------------------------------------------------------------------------------------------- | | `connectionStatus` | `PushUI.CONSTANTS.CONNECTION.STATUS` | `PushUI.CONSTANTS.CONNECTION.STATUS.NOT_CONNECTED`| It will be from one of the following: `PushUI.CONSTANTS.CONNECTION.STATUS.NOT_CONNECTED` `PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTING` `PushUI.CONSTANTS.CONNECTION.STATUS.AUTHENTICATING` `PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED` `PushUI.CONSTANTS.CONNECTION.STATUS.RETRY` | | `handleConnectToPushWallet` | `() => void` | _function_ | Call to open the wallet-connect modal (or trigger your chosen flow). | | `handleUserLogOutEvent` | `() => void` | _function_ | Call to disconnect the wallet and clear the session. | ## Next Steps - Learn how to use [usePushChainClient](/docs/chain/ui-kit/customizations/use-push-chain-client/) in your app - Dive into theme customizations with [Theme Variables](/docs/chain/ui-kit/customizations/theme-variables/) - Check out how to implement usePushWalletContext in [Examples](/docs/chain/ui-kit/examples/) --- # Theme Overrides Example URL: https://push.org/docs/chain/ui-kit/examples/theme-overrides-example/ Theme Overrides Example | Examples | UI Kit | Push Chain Docs List of examples that demostrates: - How to customize the overall theme of Push UI Kit. - How to customize the theme for light and dark modes. ## Live playground ```jsx live // customPropMinimized='true' // Import necessary components from @pushchain/ui-kit function App() { const walletConfig = { uid: 'basic', network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, login: { email: true, google: true, wallet: { enabled: true, }, appPreview: true, }, modal: { loginLayout: PushUI.CONSTANTS.LOGIN.LAYOUT.SPLIT, connectedLayout: PushUI.CONSTANTS.CONNECTED.LAYOUT.HOVER, appPreview: true, }, }; const appMetadata = { logoUrl: 'https://plus.unsplash.com/premium_photo-1746731481770-08b2f71661d0?q=80&w=2671&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', title: 'Test App Title', description: 'Test App Description', }; function WalletUI() { const { universalAccount } = usePushWalletContext('basic'); return ( ); } return ( ); } ``` ```jsx live // customPropMinimized='true' function App() { const [theme, setTheme] = React.useState('dark'); const walletConfig = { uid: 'light_dark', network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, login: { email: true, google: true, wallet: { enabled: true, }, appPreview: true, }, modal: { loginLayout: PushUI.CONSTANTS.LOGIN.LAYOUT.SPLIT, connectedLayout: PushUI.CONSTANTS.CONNECTED.LAYOUT.HOVER, appPreview: true, }, }; const appMetadata = { logoUrl: 'https://plus.unsplash.com/premium_photo-1746731481770-08b2f71661d0?q=80&w=2671&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', title: 'Test App Title', description: 'Test App Description', }; function WalletUI() { const { universalAccount } = usePushWalletContext('light_dark'); return ( setTheme('light')} /> Light setTheme('dark')} /> Dark ); } return ( ); } ``` ## Next Steps - Explore how to themize Push Universal Account Button from [Button Theme Overrides Example](/docs/chain/ui-kit/examples/button-theme-overrides-example) - Check out the send transaction feature of pushChainClient in [Send Transaction Example](/docs/chain/ui-kit/examples/send-transaction-example) - Check out step by step implementation of App in end to end [Tutorials](/docs/chain/tutorials) --- # usePushChainClient URL: https://push.org/docs/chain/ui-kit/customizations/use-push-chain-client/ usePushChainClient | Customizations | UI Kit | Push Chain Docs The `usePushChainClient` hook initializes and manages a Push Chain client instance for blockchain interactions. It integrates with your wallet connection and handles network configuration automatically. Like `usePushWalletContext`, This hook must also be used within a component that's wrapped by a `PushUniversalWalletProvider`. ## Usage ```jsx live // customPropHighlightRegexStart=usePushChainClient\( // customPropHighlightRegexEnd=\); // customPropGTagEvent=ui_kit_use_chain_client_hook function App() { // Create a component that uses the hook inside the provider context const ClientComponent = () => { const { pushChainClient, isInitialized, error } = usePushChainClient(); // optional: pass uid parameter for targeting a specific wallet instance return ( Chain Client Status:{' '} {isInitialized ? 'Initialized πŸŽ‰' : 'Not Initialized'} {pushChainClient && ( <> Executor: {pushChainClient.universal.account} Origin: {pushChainClient.universal.origin.address} | Chain:{' '} {pushChainClient.universal.origin.chain} )} {error && Error: {error.message}} ); }; return ( ); } ``` ### Parameters | Arguments | Type | Description | | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `uid` | `string` | Optional ID for targeting a specific wallet instance, must match `config.uid` of specific `PushUniversalWalletProvider` instance. See [multiple wallet example](/docs/chain/ui-kit/examples/multiple-wallet-example/) for usage. | ### Returns | Property | Type | Default | Description | | ----------------- | --------------------------- | ------- | --------------------------------------- | | `pushChainClient` | `PushChainClient` \| `null` | - | Your initialized client (once ready). | | `isInitialized` | `boolean` | `false` | `false` while the client is booting up. | | `error` | `Error` \| `null` | - | Failure information, if any. | ## Next Steps - Learn how to use [usePushChain](/docs/chain/ui-kit/customizations/use-push-chain/) in your app - Explore utilizing usePushChainClient by sending transactions in [Examples](/docs/chain/ui-kit/examples/) - Check out end to end [Tutorials](/docs/chain/tutorials) to see step by step implementation of Apps --- # Button Theme Overrides Example URL: https://push.org/docs/chain/ui-kit/examples/button-theme-overrides-example/ Button Theme Overrides Example | Examples | UI Kit | Push Chain Docs List of examples that demostrates: - How to customize the theme of Push Universal Account Button. - How to customize the theme for light and dark modes. - How to extend customization of Push Universal Account Button for all css styles. ## Live playground ```jsx live // customPropMinimized='true' function App() { const walletConfig = { uid: 'basic', network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, login: { email: true, google: true, wallet: { enabled: true, }, appPreview: true, }, modal: { loginLayout: PushUI.CONSTANTS.LOGIN.LAYOUT.SPLIT, connectedLayout: PushUI.CONSTANTS.CONNECTED.LAYOUT.HOVER, appPreview: true, }, }; const appMetadata = { logoUrl: 'https://plus.unsplash.com/premium_photo-1746731481770-08b2f71661d0?q=80&w=2671&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', title: 'Test App Title', description: 'Test App Description', }; function WalletUI() { const { universalAccount } = usePushWalletContext('basic'); return ( ); } return ( ); } ``` ```jsx live // customPropMinimized='true' function App() { const [theme, setTheme] = React.useState('dark'); const walletConfig = { uid: 'light-dark', network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, login: { email: true, google: true, wallet: { enabled: true, }, appPreview: true, }, modal: { loginLayout: PushUI.CONSTANTS.LOGIN.LAYOUT.SPLIT, connectedLayout: PushUI.CONSTANTS.CONNECTED.LAYOUT.HOVER, appPreview: true, }, }; const appMetadata = { logoUrl: 'https://plus.unsplash.com/premium_photo-1746731481770-08b2f71661d0?q=80&w=2671&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', title: 'Test App Title', description: 'Test App Description', }; function WalletUI() { const { universalAccount } = usePushWalletContext('light-dark'); return ( setTheme('light')} /> Light setTheme('dark')} /> Dark ); } return ( ); } ``` ```jsx live // customPropMinimized='true' function App() { const walletConfig = { uid: 'extend', network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, login: { email: true, google: true, wallet: { enabled: true, }, appPreview: true, }, modal: { loginLayout: PushUI.CONSTANTS.LOGIN.LAYOUT.SPLIT, connectedLayout: PushUI.CONSTANTS.CONNECTED.LAYOUT.HOVER, appPreview: true, }, }; const appMetadata = { logoUrl: 'https://plus.unsplash.com/premium_photo-1746731481770-08b2f71661d0?q=80&w=2671&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', title: 'Test App Title', description: 'Test App Description', }; function WalletUI() { const { universalAccount } = usePushWalletContext('extend'); return ( ); } return ( ); } ``` ## Next Steps - Check out the send transaction feature of pushChainClient in [Send Transaction Example](/docs/chain/ui-kit/examples/send-transaction-example) - Check out advance concepts of Push Chain in [Deep Dives](/docs/chain/deep-dives) - Check out step by step implementation of App in end to end [Tutorials](/docs/chain/tutorials) --- # usePushChain URL: https://push.org/docs/chain/ui-kit/customizations/use-push-chain/ usePushChain | Customizations | UI Kit | Push Chain Docs The `usePushChain` hook provides direct access to the `PushChain` core SDK from the `@pushchain/core` package. This hook makes it easier to use Push Chain utilities, constants and initialization methods. It is particularly useful when you want to interact with core functionalities like account utilities, helper fucntions, signer construction, and the PushChain.initialize() method to create your own client. ## Usage ```jsx live // customPropHighlightRegexStart=usePushChainClient\( // customPropHighlightRegexEnd=\); // customPropGTagEvent=ui_kit_use_push_chain_hook function App() { // Create a component that uses the hook inside the provider context const Component = () => { const { PushChain } = usePushChain(); const { pushChainClient, isInitialized } = usePushChainClient(); return ( <> {isInitialized && pushChainClient && ( Chain Agnostic: { PushChain.utils.account.toChainAgnostic( pushChainClient.universal.origin.address, { chain: pushChainClient.universal.origin.chain } )} )} ); } return ( ); } ``` ### Returns | Property | Type | Description | | ----------------- | --------------------------- | --------------------------------------- | | `PushChain` | `PushChain` | Your core SDK. | ## Next Steps - Customize UI Kit look and feel with [Theme Variables](/docs/chain/ui-kit/customizations/theme-variables/) - Explore more about PushChain core SDK in [Build](/docs/chain/build/) - Check out end to end [Tutorials](/docs/chain/tutorials) to see step by step implementation of Apps --- # Send Transaction Example URL: https://push.org/docs/chain/ui-kit/examples/send-transaction-example/ Send Transaction Example | Examples | UI Kit | Push Chain Docs This example demonstrates: - Basic send transaction functionality using `pushChainClient` of `usePushChainClient`. ## Live playground ```jsx live // Import necessary components from @pushchain/ui-kit function App() { // Define Wallet Config const walletConfig = { network: PushUI.CONSTANTS.PUSH_NETWORK.TESTNET, }; function Component() { const [txnHash, setTxnHash] = useState(null); const [isLoading, setIsLoading] = useState(false); const { connectionStatus } = usePushWalletContext(); const { pushChainClient } = usePushChainClient(); const handleSendTransaction = async () => { if (pushChainClient) { setIsLoading(true); try { const res = await pushChainClient.universal.sendTransaction({ to: '0xFaE3594C68EDFc2A61b7527164BDAe80bC302108', value: PushChain.utils.helpers.parseUnits('1', 18), // 1 PC in uPC }); setTxnHash(res.hash); } catch (err) { console.log(err); } finally { setIsLoading(false); } } }; return ( {connectionStatus === PushUI.CONSTANTS.CONNECTION.STATUS.CONNECTED && Send Transaction } {txnHash && ( <> Txn Hash: {txnHash} View in Explorer )} ); } return ( ); } ``` ## Next Steps - Explore [Chain Tools](/docs/chain/node-and-system-tools/) to learn more about running validators, localnet or everything in between. - Dive deeper into concepts of Push Chain in [Deep Dives](/docs/chain/deep-dives) - Check out step by step implementation of App in end to end [Tutorials](/docs/chain/tutorials) - Follow or give a shoutout on X to our Intern at [@PushChain](https://x.com/PushChain)! --- # Theme Variables URL: https://push.org/docs/chain/ui-kit/customizations/theme-variables/ Theme Variables | Customizations | UI Kit | Push Chain Docs The UI Kit SDK lets you customize its look by overriding CSS variables (aka theme tokens). You can apply **global overrides** (affecting both light & dark) or **theme-specific** overrides via `light` and `dark` sub-objects. ## Usage - Pass the `themeOverrides` prop to the **PushUniversalWalletProvider** component to override app wide theme variables. - You can further extend this by passing the `themeOverrides` prop to the **PushUniversalAccountButton** for supported variables. These always begins with `--pwauth-`. ```jsx live // customPropHighlightRegexStart=themeOverrides= // customPropHighlightRegexEnd=}} // customPropGTagEvent=ui_kit_theme_app_level function App() { return ( ); } ``` ```jsx live // customPropHighlightRegexStart=themeOverrides= // customPropHighlightRegexEnd=}} // customPropGTagEvent=ui_kit_theme_light_dark_mode function App() { const [theme, setTheme] = useState('dark'); return ( setTheme('light')} /> Light setTheme('dark')} /> Dark ); } ``` ```jsx live // customPropHighlightRegexStart=themeOverrides= // customPropHighlightRegexEnd=}} // customPropGTagEvent=ui_kit_theme_button_specific function App() { const [theme, setTheme] = useState('dark'); return ( setTheme('light')} /> Light setTheme('dark')} /> Dark ); } ``` :::note Override Order Top‑level properties apply to both themes; then `light` and `dark` objects override those values when the corresponding theme is active. ::: ## List of Supported Theme Variables ### Global Overrides Use these tokens for settings that should apply regardless of theme: | Category | Variable | Default | | ------------------- | ---------------------------------- | ---------------- | | Typography & Layout | --pw-core-font-family | `FK Grotesk Neu` | | | --pw-core-text-size | `26px` | | Spacing & Border | --pw-core-list-spacing | `12px` | | | --pw-core-modal-border | `2px` | | | --pw-core-modal-border-radius | `24px` | | | --pw-core-modal-width | `376px` | | | --pw-core-modal-padding | `24px` | | | --pw-core-btn-border-radius | `12px` | | | --pwauth-btn-connect-border-radius | `12px` | ### Colors These tokens have different defaults in light vs. dark themes: | Variable | Default (Light) | Default (Dark) | | --------------------------------- | ------------------------------- | ------------------------------- | | --pw-core-brand-primary-color | | | | --pw-core-text-primary-color | | | | --pw-core-text-secondary-color | | | | --pw-core-text-tertiary-color | | | | --pw-core-text-link-color | | | | --pw-core-text-disabled-color | | | | --pw-core-bg-primary-color | | | | --pw-core-bg-secondary-color | | | | --pw-core-bg-tertiary-color | | | | --pw-core-bg-disabled-color | | | | --pw-core-success-primary-color | | | | --pw-core-error-primary-color | | | | --pw-core-modal-border-color | | | | --pw-core-btn-primary-bg-color | | | | --pw-core-btn-primary-text-color | | | | --pwauth-btn-connect-text-color | | | | --pwauth-btn-connect-bg-color | | | | --pwauth-btn-connected-text-color | | | | --pwauth-btn-connected-bg-color | | | ## Next Steps - Try out [Theme Overrides Example](/docs/chain/ui-kit/examples/theme-overrides-example/) or [Button Theme Overrides Example](/docs/chain/ui-kit/examples/button-theme-overrides-example/) - Check out various other examples in [Examples section](/docs/chain/ui-kit/examples/single-wallet-example/) - Dive into [end to end tutorials](/docs/chain/tutorials) to see step by step implementation of App --- # Customizations URL: https://push.org/docs/chain/ui-kit/customizations/ Customizations Section | UI Kit | Push Chain Docs # Customizations Section This section covers all the components of the Push Chain UI Kit. --- # Examples URL: https://push.org/docs/chain/ui-kit/examples/ Examples Section | UI Kit | Push Chain Docs # Examples Section This section covers the examples of each component of the Push Chain Ui Kit.