# 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) --- # Upgrade Universal Account URL: https://push.org/docs/chain/build/advanced/upgrade-universal-account/ Upgrade Universal Account | Advanced | Build | Push Chain Docs {/* Content Start */} ## 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) --- # Initialize Push Chain Client URL: https://push.org/docs/chain/build/initialize-push-chain-client/ Initialize Push Chain Client | Build | Push Chain Docs {/* Content Start */} ## 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) --- # 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 {/* Content Start */} ## 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 {/* Content Start */} ## 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 {/* Content Start */} ## 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}; } `} {` // customPropHighlightRegexStart=universal\.sendTransaction // customPropHighlightRegexEnd=\\); // customPropGTagEvent=send_transaction_pay_gas_erc20 // Pay gas with USDC instead of native ETH while bridging USDT and calling a contract 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 }); 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, }); const usdt = PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_SEPOLIA.USDT; const usdc = PushChain.CONSTANTS.PAYABLE.TOKEN.ETHEREUM_SEPOLIA.USDC; await rl.question( ':::prompt:::Send the following to ' + wallet.address + ' on Sepolia, then press Enter:\\\\n • at least 0.005 ETH (Sepolia signing gas)\\\\n • at least 0.2 USDT (ERC-20, bridged)\\\\n • at least 5 USDC (Push Chain gas, paid in USDC — each tx costs ~3 USDC in gas)\\\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia\\\\nMint USDT: https://sepolia.etherscan.io/address/' + usdt.address + '#writeContract#F6\\\\nMint USDC: https://sepolia.etherscan.io/address/' + usdc.address + '#writeContract#F6' ); const data = PushChain.utils.helpers.encodeTxData({ abi: COUNTER_ABI, functionName: 'increment', }); try { const tx = await client.universal.sendTransaction({ to: COUNTER_ADDRESS, data, funds: { amount: PushChain.utils.helpers.parseUnits('0.1', { decimals: usdt.decimals }), // 0.1 USDT token: usdt, }, payGasWith: { token: usdc, // pay gas with USDC instead of native ETH }, }); console.log('✅ Push Chain TX Hash:', tx.hash); const receipt = await tx.wait(); console.log('📦 Status:', receipt.status === 1 ? 'Success' : 'Failed'); } catch (err) { console.error('❌ Failed:', err && err.message ? err.message : err); console.log('Note: Ensure your wallet holds both USDT (to bridge) and USDC (for gas) on Sepolia.'); } } await main().catch(console.error); `} --> ## 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 {/* Content Start */} ## 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[]): 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 const result = await pushChainClient.universal.executeTransactions([step1, step2]); ``` | **Arguments** | **Type** | **Description** | | ------------- | -------- | --------------- | | `txs` | `PreparedUniversalTx[]` | Ordered array of prepared transactions from `prepareTransaction`. | > 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_counter // UOA on Sepolia - 3 hops in one signature: // Hop 0 (Route 1): UOA → increment counter on Push Chain // Hop 1 (Route 2): UOA → increment counter on BNB Testnet via CEA // Hop 2 (Route 2): UOA → increment counter on Solana Devnet 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 }); const COUNTER_ABI = [ { inputs: [], name: 'increment', outputs: [], stateMutability: 'nonpayable', 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 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 + per-hop gas-token swap for each Route 2 outbound)\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia'); const calldata = PushChain.utils.helpers.encodeTxData({ abi: COUNTER_ABI, functionName: 'increment' }); // Hop 0 (Route 1): UOA → increment counter directly on Push Chain const hop0 = await client.universal.prepareTransaction({ to: COUNTER_PUSH, value: BigInt(0), data: calldata, }); console.log('✅ hop0 prepared - route:', hop0.route); // Hop 1 (Route 2): UOA → increment counter on BNB Testnet via CEA const hop1 = await client.universal.prepareTransaction({ to: { address: COUNTER_BNB, chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET }, value: BigInt(0), data: calldata, }); console.log('✅ hop1 prepared - route:', hop1.route); // Hop 2 (Route 2): UOA → 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 hop2 = await client.universal.prepareTransaction({ to: { address: SOL_TEST_PROGRAM, chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, }, value: BigInt(0), data: solCalldata, }); console.log('✅ hop2 prepared - route:', hop2.route); // Execute all 3 hops as one user-signed Push Chain transaction const cascade = await client.universal.executeTransactions([hop0, hop1, hop2]); console.log('🚀 Cascade submitted - initialTxHash:', cascade.initialTxHash); console.log('📦 hopCount:', cascade.hopCount); const result = await cascade.wait({ progressHook: (e) => console.log(' [Hop ' + e.hopIndex + '] ' + e.status + ' on ' + e.chain), }); console.log('🏁 All hops complete. Success:', result.success); } await main().catch(console.error); `} ## More Examples ### Cross-Chain AMM Swap: ETH → pSOL via Push Chain AMM Pull ETH from an Ethereum Sepolia CEA into Push Chain, swap pETH → pSOL on the Push Chain AMM (Uniswap V3 fork), then bridge the pSOL out to the user's Solana Devnet CEA. All three hops execute under a single user signature. {` // customPropHighlightRegexStart=executeTransactions // customPropHighlightRegexEnd=\\); // customPropGTagEvent=execute_transactions // 3-hop multichain cascade: // Hop 0 (CEA_TO_PUSH) - Pull ETH from Ethereum Sepolia CEA into Push Chain // Hop 1 (UOA_TO_PUSH) - Swap pETH → pSOL on Push Chain AMM // Hop 2 (UOA_TO_CEA) - Send pSOL out to Solana Devnet 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 }); // Push Chain AMM (Uniswap V3 fork) - swap router on testnet const SWAP_ROUTER_ADDRESS = '0x81b8Bca02580C7d6b636051FDb7baAC436bFb454'; const SWAP_ROUTER_ABI = [ { inputs: [ { components: [ { internalType: 'address', name: 'tokenIn', type: 'address' }, { internalType: 'address', name: 'tokenOut', type: 'address' }, { internalType: 'uint24', name: 'fee', type: 'uint24' }, { internalType: 'address', name: 'recipient', type: 'address' }, { internalType: 'uint256', name: 'amountIn', type: 'uint256' }, { internalType: 'uint256', name: 'amountOutMinimum', type: 'uint256' }, { internalType: 'uint160', name: 'sqrtPriceLimitX96', type: 'uint160' }, ], internalType: 'struct ISwapRouter.ExactInputSingleParams', name: 'params', type: 'tuple', }, ], name: 'exactInputSingle', outputs: [{ internalType: 'uint256', name: 'amountOut', type: 'uint256' }], stateMutability: 'payable', type: 'function', }, ]; // Synthetic token addresses on Push Chain testnet const pETH_ADDRESS = '0x2971824Db68229D087931155C2b8bB820B275809'; const pSOL_ADDRESS = '0x5D525Df2bD99a6e7ec58b76aF2fd95F39874EBed'; // Amount to move: 0.001 ETH const AMOUNT_IN = PushChain.utils.helpers.parseUnits('0.001', 18); async function main() { const wallet = ethers.Wallet.createRandom(); const provider = new ethers.JsonRpcProvider(RPC_SEPOLIA); const signer = wallet.connect(provider); console.log('🔑 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 Sepolia CEA (source of Hop 0 funds) so the user knows where to send ETH. const ceaFactorySepolia = new ethers.Contract( '0x8ED594A83301FEc545fC6c19fc12cF7111777029', ['function getCEAForPushAccount(address) view returns (address, bool)'], provider, ); const [sepoliaCEA] = await ceaFactorySepolia.getCEAForPushAccount(client.universal.account); console.log('📍 Sepolia CEA (Hop 0 source):', sepoliaCEA); 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 + 0.002 pETH\\n • CEA ' + sepoliaCEA + ' on Sepolia — at least 0.005 ETH (Hop 0 pulls 0.001 ETH from here)\\nSepolia faucet: https://cloud.google.com/application/web3/faucet/ethereum/sepolia'); // ── Hop 0: CEA_TO_PUSH ───────────────────────────────────────────────── // Pull 0.001 ETH from the Ethereum Sepolia CEA into Push Chain. // The 'from' field signals this originates from the Sepolia CEA. const hop0 = await client.universal.prepareTransaction({ from: { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA }, to: wallet.address, // receive into this UOA on Push Chain value: BigInt(0), data: '0x', funds: { amount: AMOUNT_IN, token: PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_SEPOLIA.ETH, }, }); console.log('✅ hop0 prepared - route:', hop0.route); // ── Hop 1: UOA_TO_PUSH ───────────────────────────────────────────────── // Swap pETH → pSOL on the Push Chain AMM using exactInputSingle. const hop1 = await client.universal.prepareTransaction({ to: SWAP_ROUTER_ADDRESS, value: BigInt(0), data: PushChain.utils.helpers.encodeTxData({ abi: SWAP_ROUTER_ABI, functionName: 'exactInputSingle', args: [{ tokenIn: pETH_ADDRESS, tokenOut: pSOL_ADDRESS, fee: 3000, // 0.3% pool fee recipient: wallet.address, amountIn: AMOUNT_IN, amountOutMinimum: BigInt(0), sqrtPriceLimitX96: BigInt(0), }], }), }); console.log('✅ hop1 prepared - route:', hop1.route); // ── Hop 2: UOA_TO_CEA ────────────────────────────────────────────────── // Derive the CEA (Chain Executor Account) on Solana Devnet for this wallet. 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 (destination):', solanaCEA.address); const hop2 = await client.universal.prepareTransaction({ to: { address: solanaCEA.address, chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET }, value: BigInt(0), data: '0x', funds: { amount: AMOUNT_IN, token: PushChain.CONSTANTS.MOVEABLE.TOKEN.PUSH_TESTNET_DONUT.pSOL, }, }); console.log('✅ hop2 prepared - route:', hop2.route); // ── Execute all 3 hops as one user-signed transaction ────────────────── const cascade = await client.universal.executeTransactions([hop0, hop1, hop2]); console.log('🚀 Cascade submitted - initialTxHash:', cascade.initialTxHash); console.log('Hop Count:', cascade.hopCount); const result = await cascade.wait({ progressHook: (e) => console.log(' [Hop ' + e.hopIndex + '] ' + e.status + ' on ' + e.chain), }); console.log('🏁 All hops complete. Success:', result.success); } await main().catch(console.error); `} ### Fund BNB CEA then Increment Counter on BNB Testnet Increment a counter on Push Chain, then use the CEA on BNB Testnet to increment a counter there as well. Both hops are composed into a single signature, and the playground reads the counter values before and after to confirm the changes. {` // customPropHighlightRegexStart=executeTransactions // customPropHighlightRegexEnd=\\); // customPropGTagEvent=execute_transactions_fund_and_call // 2-hop cascade from a Sepolia UOA: // Hop 0 (Route 1): increment counter on Push Chain // Hop 1 (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 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 + per-hop gas-token swap for each Route 2 outbound)\\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()); const calldata = PushChain.utils.helpers.encodeTxData({ abi: COUNTER_ABI, functionName: 'increment' }); // Hop 0 (Route 1): increment counter on Push Chain const hop0 = await client.universal.prepareTransaction({ to: COUNTER_PUSH, value: BigInt(0), data: calldata, }); console.log('✅ hop0 prepared - route:', hop0.route); // Hop 1 (Route 2): increment counter on BNB Testnet via CEA const hop1 = await client.universal.prepareTransaction({ to: { address: COUNTER_BNB, chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET }, value: BigInt(0), data: calldata, }); console.log('✅ hop1 prepared - route:', hop1.route); const cascade = await client.universal.executeTransactions([hop0, hop1]); console.log('🚀 Cascade submitted - initialTxHash:', cascade.initialTxHash); console.log('📦 hopCount:', cascade.hopCount); const result = await cascade.wait({ progressHook: (e) => console.log(' [Hop ' + e.hopIndex + '] ' + e.status + ' on ' + e.chain), }); 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); `} ### 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 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 + per-hop gas-token swap for each Route 2 outbound)\\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()); const calldata = PushChain.utils.helpers.encodeTxData({ abi: COUNTER_ABI, functionName: 'increment' }); // Hop 0 (Route 1): increment counter on Push Chain const hop0 = await client.universal.prepareTransaction({ to: COUNTER_PUSH, value: BigInt(0), data: calldata, }); console.log('✅ hop0 prepared - route:', hop0.route); // Hop 1 (Route 2): increment counter on BNB Testnet via CEA const hop1 = await client.universal.prepareTransaction({ to: { address: COUNTER_BNB, chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET }, value: BigInt(0), data: calldata, }); console.log('✅ hop1 prepared - route:', hop1.route); // Hop 2 (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 hop2 = await client.universal.prepareTransaction({ to: { address: SOL_TEST_PROGRAM, chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, }, value: BigInt(0), data: solCalldata, }); console.log('✅ hop2 prepared - route:', hop2.route); const cascade = await client.universal.executeTransactions([hop0, hop1, hop2]); console.log('🚀 Cascade submitted - initialTxHash:', cascade.initialTxHash); console.log('📦 hopCount:', cascade.hopCount); const result = await cascade.wait({ progressHook: (e) => console.log(' [Hop ' + e.hopIndex + '] ' + e.status + ' on ' + e.chain), }); 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); `} ## 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**: Use `cascade.wait({ progressHook })` to receive live status updates per hop as they propagate across chains. ## 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 {/* Content Start */} ## Overview Contract-Initiated Multichain Execution is a distinct capability that lets a **Push Chain smart contract trigger execution on an external chain** through its CEA, 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. ## 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. Examples include liquidation triggers, scheduled jobs, governance outcomes, and user actions that fan out across chains. ## Key Concepts ### Contract CEA Every Push Chain smart contract has a deterministically derived **Chain Executor Account (CEA)** on each supported external chain. This is the same concept used in user-initiated transactions, but it is 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 ```mermaid flowchart LR PC["Push ContractPush Chain"] UGPC["UGPCUniversalGatewayPCPush Chain"] 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 ``` ### 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 and, when applicable, burns or locks PRC20 tokens. It then emits the event that the TSS network listens for. ### Universal Executor Module The `UNIVERSAL_EXECUTOR_MODULE` is the privileged address on Push Chain authorized to deliver inbound cross-chain payloads. When a CEA executes on an external chain and a response needs to come back, the module calls `executeUniversalTx()` on your Push contract. **Only this address should be trusted to deliver inbound payloads**. :::warning Always validate inbound in your contract Always validate `msg.sender` in your inbound handler. Not doing so will result in unauthorized execution. ::: ## Interfaces and Constants ### IUniversalGatewayPC **_`sendUniversalTxOutbound(UniversalOutboundTxRequest): void`_** is external payable **Deployed Address**: **_`0x00000000000000000000000000000000000000C1`_** The on-chain gateway your contract calls to dispatch an outbound cross-chain transaction. UGPC burns or locks the PRC20 tokens, collects the protocol fee from `msg.value`, and emits the event that the TSS network listens for. **Commonly used for**: - Triggering a contract call on an external chain from a Push Chain contract - Bridging PRC20 tokens alongside a cross-chain payload - Dispatching a fire-and-forget outbound with no expected inbound response In order to use `IUniversalGatewayPC` in your contract, you can either: #### 1. Import it directly from the Push Chain Core Repository ```solidity import "push-chain-core-contracts/src/Interfaces/IUniversalGatewayPC.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 UniversalOutboundTxRequest { bytes recipient; // CEA or target address on the external chain (bytes-encoded) address token; // PRC20 token address on Push Chain to bridge (address(0) for none) uint256 amount; // Amount of PRC20 to bridge uint256 gasLimit; // Gas limit for external-chain execution (0 = default) 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; } ``` ```solidity /** * @notice Dispatches an outbound cross-chain transaction through the Push Chain gateway. * @dev msg.value must cover the protocol fee. Approve UGPC for `req.amount` before calling if bridging tokens. * @param req The outbound transaction request struct. */ 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. Use `0` to let UGPC estimate it automatically. Not recommended for advanced scenarios. | | _`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. | ```solidity function dispatchOutbound( address token, uint256 amount, bytes calldata recipient, bytes calldata payload, address revertRecipient ) external payable { if (amount > 0) { IPRC20(token).approve(ugpc, amount); } IUniversalGatewayPC(ugpc).sendUniversalTxOutbound{value: msg.value}( UniversalOutboundTxRequest({ recipient: recipient, token: token, amount: amount, gasLimit: 0, payload: payload, revertRecipient: revertRecipient }) ); } ``` ### UNIVERSAL_EXECUTOR_MODULE The trusted executor module on Push Chain that delivers inbound cross-chain payloads to your contract. **Deployed Address**: **_`0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7`_** ### executeUniversalTx (Inbound Handler) **_`executeUniversalTx(string, bytes, bytes, uint256, address, bytes32): void`_** is external payable The function your Push Chain contract exposes to receive inbound cross-chain payloads. The `UNIVERSAL_EXECUTOR_MODULE` calls this after the CEA has executed on the external chain and a response needs to be delivered back to Push Chain. **Commonly used for**: - Receiving staking confirmations, swap results, or any response from external chain execution - Updating on-chain state based on what the contract's CEA did on another chain - Triggering further logic on Push Chain after an external event completes ```solidity /** * @notice Delivers an inbound cross-chain payload to this contract. * @dev Only callable by UNIVERSAL_EXECUTOR_MODULE. Must validate msg.sender and guard against replay via txId. * @param sourceChainNamespace CAIP-2 namespace of the originating chain, e.g. "eip155:97". * @param ceaAddress CEA address on the source chain, bytes-encoded. * @param payload ABI-encoded action data from the external chain. * @param amount Amount of PRC20 tokens bridged with this inbound tx. * @param prc20 PRC20 token address on Push Chain. * @param txId Unique cross-chain transaction identifier for replay protection. */ function executeUniversalTx( string calldata sourceChainNamespace, bytes calldata ceaAddress, bytes calldata payload, uint256 amount, address prc20, bytes32 txId ) external payable; ``` :::warning Always validate the caller Only **UNIVERSAL_EXECUTOR_MODULE** is authorized to call this function. Always guard it with an `onlyUniversalExecutor` modifier and track executed **TxIds** to prevent replay attacks. ::: | 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 inbound transaction. | | _`prc20`_ | `address` | PRC20 token address on Push Chain corresponding to the bridged asset. | | _`txId`_ | `bytes32` | Unique cross-chain transaction identifier. Use this to prevent replay attacks. | ```solidity mapping(bytes32 => bool) public executedTxIds; address public universalExecutorModule = 0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7; modifier onlyUniversalExecutor() { if (msg.sender != universalExecutorModule) 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 handle the inbound action (uint8 action, address user,) = abi.decode(payload, (uint8, address, bytes)); if (action == 0) { stakedBalance[user][prc20] += amount; emit Staked(user, prc20, amount, txId); } emit InboundReceived(txId, sourceChainNamespace, ceaAddress, prc20, amount); } ``` ## Minimal Integration Pattern The following contract shows the minimum integration surface for contract-initiated multichain execution. It dispatches an outbound call through UGPC, accepts an inbound callback through `executeUniversalTx()`, validates the trusted executor module, and prevents replay using txId. - **Outbound**: calls `dispatchOutbound()` → optionally approves UGPC → calls `sendUniversalTxOutbound()` - **Inbound**: **UNIVERSAL_EXECUTOR_MODULE** calls `executeUniversalTx()` → contract validates caller → replay protects with txId → decodes payload and applies app logic ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; /// @notice Minimal outbound request type for UGPC. struct UniversalOutboundTxRequest { bytes recipient; address token; uint256 amount; uint256 gasLimit; bytes payload; address revertRecipient; } /// @notice Minimal UGPC interface. interface IUniversalGatewayPC { function sendUniversalTxOutbound(UniversalOutboundTxRequest calldata req) external payable; } /// @notice Minimal PRC20 interface for approvals. interface IPRC20 { function approve(address spender, uint256 amount) external returns (bool); } contract MinimalContractInitiatedExecutor { // ------------------------------------------------------------------------- // Constants / Config // ------------------------------------------------------------------------- address public immutable ugpc; address public immutable universalExecutorModule; // ------------------------------------------------------------------------- // State // ------------------------------------------------------------------------- mapping(bytes32 => bool) public executedTxIds; mapping(address => uint256) public creditedAmount; // ------------------------------------------------------------------------- // Events // ------------------------------------------------------------------------- event OutboundDispatched( bytes indexed recipient, address indexed token, uint256 amount, bytes payload, address revertRecipient ); event InboundExecuted( bytes32 indexed txId, string sourceChainNamespace, bytes ceaAddress, address prc20, uint256 amount ); // ------------------------------------------------------------------------- // Errors // ------------------------------------------------------------------------- error NotUniversalExecutor(); error TxAlreadyExecuted(); error ZeroAddress(); error UnsupportedAction(); // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- constructor(address _ugpc, address _universalExecutorModule) { if (_ugpc == address(0) || _universalExecutorModule == address(0)) { revert ZeroAddress(); } ugpc = _ugpc; universalExecutorModule = _universalExecutorModule; } // ------------------------------------------------------------------------- // Modifiers // ------------------------------------------------------------------------- modifier onlyUniversalExecutor() { if (msg.sender != universalExecutorModule) revert NotUniversalExecutor(); _; } // ------------------------------------------------------------------------- // Outbound: Push Chain -> External Chain // ------------------------------------------------------------------------- /// @notice Dispatch an outbound cross-chain execution from this contract. /// @dev If bridging PRC20 tokens, approve UGPC before calling. /// @param token PRC20 token on Push Chain. Use address(0) if not bridging tokens. /// @param amount Amount of PRC20 to bridge. Use 0 if not bridging. /// @param recipient Bytes-encoded CEA or target address on the external chain. /// @param gasLimit Gas limit for the external-chain execution. Use 0 for default. /// @param payload ABI-encoded calldata or app payload for the external-chain action. /// @param revertRecipient Address to receive bridged funds if the external tx reverts. 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, payload: payload, revertRecipient: revertRecipient }) ); emit OutboundDispatched(recipient, token, amount, payload, revertRecipient); } // ------------------------------------------------------------------------- // Inbound: External Chain -> Push Chain // ------------------------------------------------------------------------- /// @notice Receive an inbound cross-chain payload. /// @dev Only UNIVERSAL_EXECUTOR_MODULE should be allowed to call this. /// Replay protect using txId. /// This example assumes payload is encoded as: /// abi.encode(uint8 action, address beneficiary) /// /// Example actions: /// 0 = CREDIT beneficiary with bridged amount 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; (uint8 action, address beneficiary) = abi.decode(payload, (uint8, address)); if (action == 0) { creditedAmount[beneficiary] += amount; } else { revert UnsupportedAction(); } emit InboundExecuted(txId, sourceChainNamespace, ceaAddress, prc20, amount); } receive() external payable {} } ``` ## Outbound Flow: Push Chain → External Chain ### Approve and call UGPC If bridging tokens, approve UGPC to pull the PRC20 amount before calling: ```solidity if (amount > 0) { IPRC20(token).approve(ugpc, amount); } UniversalOutboundTxRequest memory req = UniversalOutboundTxRequest({ recipient: recipient, // bytes-encoded CEA or target on external chain token: token, // PRC20 on Push Chain amount: amount, gasLimit: gasLimit, // 0 = network default payload: payload, // ABI-encoded calldata for the CEA to execute revertRecipient: revertRecipient // fallback address if external tx reverts }); IUniversalGatewayPC(ugpc).sendUniversalTxOutbound{value: msg.value}(req); ``` `msg.value` must cover protocol fees. The UGPC burns the PRC20 tokens and emits an event the TSS network listens for. ### TSS network picks up the event The TSS validators observe the UGPC event, derive the contract's CEA on the target chain, and submit the transaction. If the CEA has not been deployed yet, the TSS network deploys it on first use. ### CEA executes on the external chain The CEA runs the encoded `payload` on the target chain. From the external contract's perspective, `msg.sender` is the contract's CEA address. It has no awareness that the call originated from Push Chain. ## Inbound Flow: External Chain → Push Chain When the CEA on the external chain needs to send a response back to Push Chain, it triggers an inbound call. The `UNIVERSAL_EXECUTOR_MODULE` delivers this by calling `executeUniversalTx()` on your contract. ### Security: validate the caller ```solidity modifier onlyUniversalExecutor() { if (msg.sender != universalExecutorModule) { revert NotExecutorModule(); } _; } ``` Only the `UNIVERSAL_EXECUTOR_MODULE` address is authorized to call `executeUniversalTx()`. Anyone else calling it with fabricated data must be rejected. ### Replay protection Each inbound call carries a unique `txId`. Track executed IDs to prevent replay: ```solidity mapping(bytes32 => bool) public executedTxIds; function executeUniversalTx(..., bytes32 txId) external payable onlyUniversalExecutor { if (executedTxIds[txId]) revert TxAlreadyExecuted(); executedTxIds[txId] = true; _handleInboundPayload(payload, prc20, amount, txId); emit InboundReceived(txId, sourceChainNamespace, ceaAddress, prc20, amount); } ``` ### Decoding the payload The `payload` passed to `executeUniversalTx` contains `UniversalPayload.data` — an `abi.encode`d blob. Your contract defines the encoding. A typical pattern: ```solidity // abi.encode(uint8 action, address user, bytes executionPayload) function _handleInboundPayload( bytes calldata data, address prc20, uint256 amount, bytes32 txId ) internal { (uint8 action, address user,) = abi.decode(data, (uint8, address, bytes)); if (action == 0) { // e.g. STAKE: credit the user stakedBalance[user][prc20] += amount; emit Staked(user, prc20, amount, txId); } else if (action == 1) { // e.g. UNSTAKE: debit the user if (stakedBalance[user][prc20] Only **UNIVERSAL_EXECUTOR_MODULE** can legitimately deliver inbound payloads. Always guard **executeUniversalTx()** with the **onlyUniversalExecutor** modifier. Anyone else calling it with fabricated data must be rejected. - **Replay protection** Each inbound call carries a unique **txId**. Maintain a **mapping(bytes32 => bool) executedTxIds** and revert on duplicates. Without this, the same result could be applied more than once. - **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. - **Inbound execution requires $PC** The CEA inbound to Push Chain needs $PC for execution fees. Funding your Push Chain contract with $PC is your responsibility. - **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. Design accordingly. 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. On slower chains this can take some time. 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.** If multiple outbound calls can be in flight simultaneously, track them by ID to route inbound results unambiguously. - **Keep inbound handlers lean.** The inbound handler runs as a Push Chain transaction submitted by the module. Decode payload, update state, emit events. Avoid cascading outbound calls inside it. - **Protect inbound handlers with nonReentrant.** The handler is called by an external module account, so apply re-entrancy guards if it calls other contracts. - **Fund the Push-side contract before dispatching.** Verify the push-side contract has sufficient $PC to cover inbound execution fees. ## Limitations | Area | Constraint | |------|------------| | **No synchronous result** | Outbound and inbound are always separate transactions. There is no in-call return value. | | **No cross-chain atomicity** | A failed external call does not revert Push-side state. Handle partial failure explicitly. | | **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. Supported chains are enumerated in `PushChain.CONSTANTS.CHAIN`. | ## 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 - Understand the routing model and CEA concepts this builds on with [Understanding Universal Transactions](/docs/chain/build/understanding-universal-transactions) - Compare with user-initiated flows in [Send Universal Transaction](/docs/chain/build/send-universal-transaction) - Sequence multi-step cross-chain flows with [Send Multichain Transactions](/docs/chain/build/send-multichain-transactions) - Monitor dispatched transaction status with [Track Universal Transaction](/docs/chain/build/track-universal-transaction) - Explore Push Chain Solidity utilities and system interfaces in [Contract Helpers](/docs/chain/build/contract-helpers) --- # Track Universal Transaction URL: https://push.org/docs/chain/build/track-universal-transaction/ Track Universal Transaction | Build | Push Chain Docs {/* Content Start */} ## 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 {/* Content Start */} ## Overview This section covers the most commonly used helpers in the Push Chain Core SDK to simplify common workflows. ## Helper Utilities {/* API Section Start*/} ### 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(); `} {/* API Section Ends*/} {/* API Section Start*/} ### 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(); `} {/* API Section Ends*/} {/* API Section Start*/} ### 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(); `} {/* API Section Ends*/} ## Chain Utilities {/* API Section Start*/} ### 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(); `} {/* API Section Ends*/} {/* API Section Start*/} ### 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(); `} {/* API Section Ends*/} {/* API Section Start*/} ### 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(); `} {/* API Section Ends*/} {/* API Section Start*/} ### 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(); `} {/* API Section Ends*/} ## Account Utilities {/* API Section Begins*/} ### 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() `} {/* API Section Ends*/} {/* API Section Begins*/} ### 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() `} {/* API Section Ends*/} {/* API Section Begins*/} ### 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(); `} {/* API Section Ends*/} {/* API Section Begins*/} ### 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( uoaAccount, { skipNetworkCheck: true } ); console.log('Deterministic derivation:'); console.log(JSON.stringify(deterministicResult, null, 2)); } await main().catch(console.error) `} {/* API Section Ends*/} {/* API Section Begins*/} ### 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) `} {/* API Section Ends*/} ## Signer Utilities {/* API Section Begins*/} ### 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); `} {/* API Section Ends*/} ## Token Utilities {/* API Section Start*/} ### 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(); `} {/* API Section Ends*/} {/* API Section Start*/} ### 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(); `} {/* API Section Ends*/} {/* API Section Start*/} ### 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(); `} {/* API Section Ends*/} ## Conversion Utilities {/* API Section Start*/} ### 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(); `} {/* API Section Ends*/} {/* API Section Begins*/} ### 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); `} {/* API Section Ends*/} ## Explorer Utilities {/* API Section Start*/} ### 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); `} {/* API Section Ends*/} {/* API Section Start*/} ### 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); `} {/* API Section Ends*/} {/* API Section Start*/} ### 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); `} {/* API Section Ends*/} ## Deprecated Methods {/* API Section Begins*/} ### Convert Origin to Executor Account > **⚠️ Deprecated**: This function is deprecated and will be removed in v6.0.0. Use [deriveExecutorAccount](#derive-executor-account) instead. " className="alert alert--live-play"> Gives the deterministic executor account address for a given origin account (UOA).   Also helps in checking if a given origin account (UOA) has a universal executor account (UEA) deployed on Push Chain or a chain executor account (CEA) deployed on any external chain.   ```typescript const info = await PushChain.utils.account.convertOriginToExecutor(universalAccount); ``` | Deprecated Function | Replacement | Key Changes | |---------------------|-------------|-------------| | `convertOriginToExecutor` | `deriveExecutorAccount` | • Parameter renamed: `onlyCompute` → `skipNetworkCheck`• Generic `account` parameter accepts string instead of UniversalAccount object• Clearer naming aligned with identity ↔ execution model | | **Arguments** | **Type** | **Default** | **Description** | | ---------------- | ------------------ | ----------- | ---------------------------------------------------------------------------------------------------------- | | _`account`_ | `UniversalAccount` | - | A UniversalAccount object containing chain and address information. | | `options.onlyCompute` | `boolean` | `false` | Whether to check if the computed executor account is deployed. Set to `true` to include deployment status. | " className="alert alert--fn-args" > ```typescript // ExecutorAccountInfo object { address: '0x98cA97d2FB78B3C0597E2F78cd11868cACF423C5', deployed: true } ``` {` // customPropHighlightRegexStart=PushChain\.utils\.account\.convertOriginToExecutor // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_convert_origin_to_executor async function main() { // Create a universal account for Solana Devnet const account = PushChain.utils.account.toUniversal( 'EUYcfSUScdFgKMbB3rRdgRZwXmcxY7QCRQa2JwrchP1Q', { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET, }) // Convert to executor address with deployment status const executorInfo = await PushChain.utils.account.convertOriginToExecutor(account, { onlyCompute: true, }) console.log(JSON.stringify(executorInfo, null, 2)); // Convert without deployment status const executorSimple = await PushChain.utils.account.convertOriginToExecutor(account, { onlyCompute: false, }) console.log(JSON.stringify(executorSimple, null, 2)); } await main().catch(console.error) `} {/* API Section Ends*/} {/* API Section Begins*/} **⚠️ Deprecated**: This function is deprecated and will be removed in v6.0.0. Use [resolveControllerAccount](#resolve-controller-account) instead. " className="alert alert--live-play"> Gives the origin account (UOA) for a given universal executor account (UEA on Push Chain) or a chain executor account (CEA on any external chain).   ```typescript const { account, exists } = await PushChain.utils.account.convertExecutorToOrigin(ueaOrCeaAddress); ``` | **Arguments** | **Type** | **Default** | **Description** | | ---------------- | ------------------ | ----------- | ---------------------------------------------------------------------------------------------------------- | | _`address`_ | `string` | - | Address of the account on any blockchain. | | `options.chain` | `CHAIN` | `undefined` | Optional. If provided, treats the account as a CEA (Chain Executor Account) instead of UEA (Universal Executor Account) or Push Account. Use `PushChain.CONSTANTS.CHAIN` values. | " className="alert alert--fn-args" > ```typescript // OriginAccountInfo object { account: { chain: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', address: 'FNDJWigdNWsmxXYGrFV2gCvioLYwXnsVxZ4stL33wFHf' }, exists: true } ``` {` // customPropHighlightRegexStart=PushChain\.utils\.account\.convertExecutorToOrigin // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_convert_executor_to_origin async function main() { // Create executor account object const ueaAccount = '0xbCfaD05E5f19Ae46feAab2F72Ad9977BC239b395'; // Convert UEA to origin account const result = await PushChain.utils.account.convertExecutorToOrigin( ueaAccount ); console.log('Origin account from UEA:'); console.log(JSON.stringify(result, null, 2)); // Example with options.chain (treats as CEA) const ceaAccount = '0x5d71c70571789F0cd3bE84513523a9993740BDf6'; const result2 = await PushChain.utils.account.convertExecutorToOrigin( ceaAccount, { chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET } ); console.log('Origin account from CEA:'); console.log(JSON.stringify(result2, null, 2)); } await main().catch(console.error) `} --> {/* API Section Ends*/} {/* API Section Begins*/} ### Convert Executor Address to Origin Account > **⚠️ Deprecated**: This function is deprecated and will be removed in v6.0.0. Use [resolveControllerAccount](#resolve-controller-account) instead. " className="alert alert--live-play"> Gives the origin account (UOA) for a given Push Chain address.   ```typescript const { account, exists } = await PushChain.utils.account.convertExecutorToOriginAccount(pushChainAddress); ``` | Deprecated Function | Replacement | Key Changes | |---------------------|-------------|-------------| | `convertExecutorToOriginAccount` | `resolveControllerAccount` | • Same functionality with clearer naming• Part of unified identity ↔ execution model | | **Arguments** | **Type** | **Default** | **Description** | | ---------------- | ------------------ | ----------- | ---------------------------------------------------------------------------------------------------------- | | _`pushChainAddress`_ | `string` | - | Address of the account on Push Chain. | " className="alert alert--fn-args" > ```typescript // OriginAccountInfo object { account: { chain: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', address: 'FNDJWigdNWsmxXYGrFV2gCvioLYwXnsVxZ4stL33wFHf' }, exists: true } ``` {` // customPropHighlightRegexStart=PushChain\.utils\.account\.convertExecutorToOriginAccount // customPropHighlightRegexEnd=\\); // customPropGTagEvent=utility_convert_executor_to_origin_account async function main() { const ueaAddress = '0xbCfaD05E5f19Ae46feAab2F72Ad9977BC239b395'; const result = await PushChain.utils.account.convertExecutorToOriginAccount( ueaAddress ); console.log('Has Executor Account'); console.log(JSON.stringify(result, null, 2)); const notUeaAddress = '0x35B84d6848D16415177c64D64504663b998A6ab4'; const result2 = await PushChain.utils.account.convertExecutorToOriginAccount( notUeaAddress ); console.log('Does not have Executor Account'); console.log(JSON.stringify(result2, null, 2)); } await main().catch(console.error) `} {/* API Section Ends*/} ## 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 {/* Content Start */} ## 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, }); // Convert executor to origin with chain override const originInfo = await PushChain.utils.account.convertExecutorToOrigin( 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_MAINNET.ETH` | Ethereum Mainnet | Native ETH token | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_MAINNET.USDT` | Ethereum Mainnet | Tether USD stablecoin | | `PushChain.CONSTANTS.MOVEABLE.TOKEN.ETHEREUM_MAINNET.WETH` | Ethereum Mainnet | Wrapped ETH | --> | 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.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 ETH 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_MAINNET.ETH` | Ethereum Mainnet | Native ETH token | | `PushChain.CONSTANTS.PAYABLE.TOKEN.ETHEREUM_MAINNET.USDT` | Ethereum Mainnet | Tether USD stablecoin | | `PushChain.CONSTANTS.PAYABLE.TOKEN.ETHEREUM_MAINNET.WETH` | Ethereum Mainnet | Wrapped ETH | --> | 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.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. --- # 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. --- # 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 | [0xCA0C5E6F002A389E1580F0DB7cd06e4549B5F9d3](https://donut.push.network/address/0xCA0C5E6F002A389E1580F0DB7cd06e4549B5F9d3?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 | [0x2C455189D2af6643B924A981a9080CcC63d5a567](https://donut.push.network/address/0x2C455189D2af6643B924A981a9080CcC63d5a567?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 | [0x76Ad08339dF606BeEDe06f90e3FaF82c5b2fb2E9](https://donut.push.network/address/0x76Ad08339dF606BeEDe06f90e3FaF82c5b2fb2E9?tab=contract) | | USDT.bnb | USDT.bnb | BNB_Testnet | [0x2f98B4235FD2BA0173a2B056D722879360B12E7b](https://donut.push.network/address/0x2f98B4235FD2BA0173a2B056D722879360B12E7b?tab=contract) | | pBNB | pBNB | BNB_Testnet | [0x7a9082dA308f3fa005beA7dB0d203b3b86664E36](https://donut.push.network/address/0x7a9082dA308f3fa005beA7dB0d203b3b86664E36?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/WPC Pool | [0x2d46b2b92266f34345934F17039768cd631aB026](https://donut.push.network/address/0x2d46b2b92266f34345934F17039768cd631aB026?tab=contract) | [0xCA0C5E6F002A389E1580F0DB7cd06e4549B5F9d3](https://donut.push.network/address/0xCA0C5E6F002A389E1580F0DB7cd06e4549B5F9d3?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 | [0x1cE819E742b44f922D2F05fdFFd17b4997f4CD15](https://donut.push.network/address/0x1cE819E742b44f922D2F05fdFFd17b4997f4CD15?tab=contract) | [0x2C455189D2af6643B924A981a9080CcC63d5a567](https://donut.push.network/address/0x2C455189D2af6643B924A981a9080CcC63d5a567?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 | [0xF95B20Cf3f2dE495747EB3d33611D0FFEA29F448](https://donut.push.network/address/0xF95B20Cf3f2dE495747EB3d33611D0FFEA29F448?tab=contract) | [0x76Ad08339dF606BeEDe06f90e3FaF82c5b2fb2E9](https://donut.push.network/address/0x76Ad08339dF606BeEDe06f90e3FaF82c5b2fb2E9?tab=contract) | [0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9](https://donut.push.network/address/0xE17DD2E0509f99E9ee9469Cf6634048Ec5a3ADe9?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 | [0x435875db8a76cCAA9cbf73690C6Dc1913BBC9168](https://donut.push.network/address/0x435875db8a76cCAA9cbf73690C6Dc1913BBC9168?tab=contract) | [0x2f98B4235FD2BA0173a2B056D722879360B12E7b](https://donut.push.network/address/0x2f98B4235FD2BA0173a2B056D722879360B12E7b?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 | [0x8ED594A83301FEc545fC6c19fc12cF7111777029](https://sepolia.etherscan.io/address/0x8ED594A83301FEc545fC6c19fc12cF7111777029#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) | [0xCA0C5E6F002A389E1580F0DB7cd06e4549B5F9d3](https://donut.push.network/address/0xCA0C5E6F002A389E1580F0DB7cd06e4549B5F9d3?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 | [0x88DC189275078Cf509E4Cc773F089c8ad07b7EA2](https://sepolia.arbiscan.io/address/0x88DC189275078Cf509E4Cc773F089c8ad07b7EA2#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) | [0x76Ad08339dF606BeEDe06f90e3FaF82c5b2fb2E9](https://donut.push.network/address/0x76Ad08339dF606BeEDe06f90e3FaF82c5b2fb2E9?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 | [0x0A75ca7736b488Eb41675ADc3b3156BACF659F55](https://sepolia.basescan.org/address/0x0A75ca7736b488Eb41675ADc3b3156BACF659F55#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) | [0x2C455189D2af6643B924A981a9080CcC63d5a567](https://donut.push.network/address/0x2C455189D2af6643B924A981a9080CcC63d5a567?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 | [0xe2182dae2dc11cBF6AA6c8B1a7f9c8315A6B0719](https://testnet.bscscan.com/address/0xe2182dae2dc11cBF6AA6c8B1a7f9c8315A6B0719#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) | [0x2f98B4235FD2BA0173a2B056D722879360B12E7b](https://donut.push.network/address/0x2f98B4235FD2BA0173a2B056D722879360B12E7b?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.convertOriginToExecutor(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.convertOriginToExecutor(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.convertOriginToExecutor(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.convertOriginToExecutor(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: Convert to Executor Address** ```typescript const executorAddress = await PushChain.utils.account.convertOriginToExecutor(account); ``` This performs the deterministic derivation to get the UEA address on Push Chain. ### 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`) ## Use Cases ### 1. Airdrop Systems Derive UEAs for all recipients to create a universal airdrop: ```typescript const recipients = [ { address: '0xEth...', chain: ETHEREUM_SEPOLIA }, { address: '7xSol...', chain: SOLANA_DEVNET }, ]; const ueas = await Promise.all( recipients.map(async (r) => { const account = PushChain.utils.account.toUniversal(r.address, { chain: r.chain }); return await PushChain.utils.account.convertOriginToExecutor(account); }) ); ``` ### 2. Cross-Chain Identity Verify a user's identity across chains: ```solidity function verifyUser(address uea) public view returns (bool) { (UniversalAccountId memory origin, bool exists) = FACTORY.getOriginForUEA(uea); return exists && isAllowedChain(origin.chainNamespace); } ``` ### 3. Universal Allowlists Create allowlists that work across all chains: ```solidity mapping(address => bool) public allowlist; function addToAllowlist( string memory chainNamespace, string memory chainId, bytes memory owner ) external onlyOwner { (address uea, ) = FACTORY.getUEAForOrigin( UniversalAccountId(chainNamespace, chainId, owner) ); allowlist[uea] = true; } ``` ## 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.convertOriginToExecutor(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? Now that you understand how Universal Executor Accounts (UEAs) are derived, you can start building real universal systems on Push Chain. - [Build a Universal Airdrop](/docs/chain/tutorials/token-systems/tutorial-universal-airdrop/) Deploy once on Push Chain and let users from any chain claim tokens using their existing wallet. - [Contract Helpers](/docs/chain/build/contract-helpers/) Go deeper with advanced UEA patterns, including on-chain derivation, reverse lookups, and contract-level integrations. ```mermaid flowchart LR A[Origin Wallet] --> B[Derive UEA] B --> C[Universal App on Push Chain] C --> D[Universal Airdrop] C --> E[Other Universal Systems] ``` --- # 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. 🚀 --- # 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`} {/* Content Start */} 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= {/* Your app logic */} {/* Push Universal Wallet Button to show the functionality */} ); } ``` ```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 ( {/* Your app logic */} {/* Push Universal Wallet Button to show the functionality */} ); } ``` ### `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= {/* Your app logic */} {/* Push Universal Wallet Button to show the functionality */} ); } ``` ### `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 ( {/* Your App Logic */} ); } ``` ```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 ( {/* Button-level overrides */} setTheme('light')} /> Light setTheme('dark')} /> Dark {/* Your App Logic */} ); } ``` :::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.