XChainJS Ethereum SDK: Build Cross-Chain dApps in TypeScript







Developer Guide · Web3 SDK Series

XChainJS Ethereum SDK: The Developer’s Guide to Cross-Chain Web3 in TypeScript

⏱ 14 min read
🗓 Updated July 2025
🔧 TypeScript · EVM · DeFi
xchainjs
ethereum sdk
typescript
cross-chain
web3
evm
defi
erc20
gas estimation

Building on Ethereum has never been easier — or, depending on your tooling, more
needlessly complicated. You have ethers.js,
Web3.js, viem, wagmi, and a growing graveyard of SDK projects that promised simplicity
and delivered another three-hundred-page API surface. So when
XChainJS
entered the scene with a clear proposition — one unified TypeScript SDK for every major
chain — it was worth paying attention.

The @xchainjs/xchain-ethereum
package is the EVM gateway of that ecosystem. It handles
Ethereum address generation, ETH and ERC20 transfers,
gas fee estimation, Ethereum transaction signing, and
full RPC client connectivity — all through the same interface you’d use
to interact with Bitcoin, Cosmos, or BNB Chain in the same codebase.

This guide is written for TypeScript developers who want to understand not just how
to use the XChainJS Ethereum client, but why its architecture makes sense for
cross-chain wallet development, DeFi application development,
and any project that needs to talk to more than one blockchain without losing its mind
in the process.


What Is XChainJS and Why Should Ethereum Developers Care?

XChainJS
is an open-source, modular cross-chain SDK written entirely in
TypeScript. It was designed from the ground up to give developers a single,
consistent API across a heterogeneous set of blockchains — EVM-compatible chains,
UTXO-based chains (Bitcoin, Litecoin), and Cosmos SDK chains — without forcing
you to understand the internal wire protocol of each one. Think of it as a polyglot
interpreter that speaks Ethereum, Bitcoin, and Cosmos fluently, while letting you
write all your application logic in one language.

The SDK is built around a core abstraction called XChainClient, which
every chain-specific package implements. Whether you’re instantiating an
Ethereum client
or a Bitcoin client, you call the same methods: getAddress(),
getBalance(), transfer(), getTransactions().
The chain-specific complexity — EVM nonce management, UTXO selection, Cosmos sequence
numbers — lives inside the package, invisible to your application layer. This is not
magic; it’s just a well-designed interface.

For Ethereum developers specifically, this matters for two reasons.
First, the XChainJS Ethereum SDK wraps ethers.js
internally, which means you get battle-tested EVM primitives without manually wiring
providers, signers, and contract factories yourself. Second, if your product ever
expands beyond Ethereum — to Arbitrum, Avalanche, BNB Smart Chain, or a completely
different chain architecture — you add a new XChainJS package rather than integrating
an entirely different SDK. The architectural debt stays near zero.

🔗
Chain-Agnostic API

Same interface for ETH, BTC, ATOM, BNB, and 15+ chains. Write once, deploy everywhere.

🔐
HD Wallet Native

BIP39 mnemonic → BIP44 derivation path → addresses across all supported chains from one seed.

ethers.js Under the Hood

Battle-tested EVM primitives. XChainJS adds the abstraction layer; ethers.js handles the heavy lifting.

🧩
Modular Architecture

Install only the chains you need. No bloated monolith — just the packages your project requires.


Installation, Configuration, and Your First Ethereum Client

Getting started with the XChainJS Ethereum SDK takes about four
minutes. You install the package, import the client, hand it a mnemonic phrase and
an RPC URL, and you’re live. There’s no CLI scaffolding to run, no config file
format to learn, and no mandatory cloud service to sign up for. If you’ve used any
Node.js library before, this will feel immediately familiar.

bash · npm install

npm install @xchainjs/xchain-ethereum @xchainjs/xchain-crypto ethers

Three packages: the Ethereum client itself, the XChainJS crypto
utilities (used for HD wallet derivation and mnemonic handling), and ethers.js as
a peer dependency. That’s your entire dependency surface for a fully functional
TypeScript Ethereum SDK integration.

Instantiating the Client

The Client constructor accepts a configuration object. At minimum you
need a phrase (BIP39 mnemonic) and a network
(Network.Mainnet, Network.Testnet, or
Network.Stagenet). For mainnet use, you’ll also pass a provider URL —
Infura, Alchemy, or any standard Ethereum RPC endpoint.

TypeScript · client instantiation

import { Client, defaultEthParams } from '@xchainjs/xchain-ethereum'
import { Network }                  from '@xchainjs/xchain-client'

const phrase = 'your twelve word bip39 mnemonic phrase goes right here trust me'

const client = new Client({
  ...defaultEthParams,
  network: Network.Mainnet,
  phrase,
  // Replace with your Infura / Alchemy / custom RPC URL
  providers: {
    [Network.Mainnet]: new ethers.providers.JsonRpcProvider(
      'https://mainnet.infura.io/v3/YOUR_PROJECT_ID'
    ),
  },
})

defaultEthParams ships sane defaults: mainnet chain ID, standard
derivation path (m/44'/60'/0'/0/0), and pre-configured provider
settings. You override only what you need. For Ethereum testnet
development (Sepolia, Goerli), swap the network flag and point the provider at
the appropriate testnet RPC. The rest of your code doesn’t change — which is
exactly the point.

🔒 Security Note

Never hardcode a mnemonic phrase in source code. Use environment variables
(process.env.WALLET_PHRASE) and a secrets manager for any production
or CI environment. The XChainJS SDK handles the cryptographic derivation correctly;
the threat model you need to manage is secrets exposure, not key derivation.

Ethereum Address Generation

Once the client is instantiated, Ethereum address generation
is a single method call. XChainJS derives addresses deterministically from the
mnemonic using the BIP44 path, so the same phrase always produces the same address.
You can also generate addresses at arbitrary wallet indices for multi-account
architectures — useful when building Web3 wallet products that
manage multiple user accounts under one seed.

TypeScript · address generation

// Default wallet index (0)
const address = client.getAddress()
console.log('ETH Address:', address)
// → 0x71C7656EC7ab88b098defB751B7401B5f6d8976F

// Explicit wallet index for HD wallet derivation
const address2 = client.getAddress(1)
const address3 = client.getAddress(2)

Under the hood, this is standard secp256k1 key derivation — nothing proprietary.
You could independently verify the output against MetaMask using the same mnemonic.
The XChainJS layer handles path construction, key derivation, and checksum address
formatting (EIP-55), so you get production-ready Ethereum addresses
without touching a single line of elliptic curve math.


Ethereum Balance Lookup and RPC Client Connectivity

Reading on-chain state is arguably the most common operation any
Ethereum client performs. Whether you’re building a wallet dashboard,
a DeFi portfolio tracker, or a cross-chain liquidity interface, you need reliable
balance data with minimal latency. The XChainJS Ethereum RPC client
makes this straightforward by proxying all read operations through the ethers.js
provider you configured at initialization.

TypeScript · ETH balance lookup

import { assetAmount, assetToBase, baseToAsset } from '@xchainjs/xchain-util'
import { AssetETH }                               from '@xchainjs/xchain-ethereum'

// Fetch native ETH balance
const balances = await client.getBalance(client.getAddress())
const ethBalance = balances.find(b => b.asset.symbol === 'ETH')

console.log(
  'ETH Balance:',
  baseToAsset(ethBalance!.amount).amount().toFixed(6),
  'ETH'
)

getBalance() returns an array of Balance objects — one
for native ETH and one for each ERC20 token the address holds, if you pass token
configurations. Amounts are returned in base units (wei) and wrapped in
XChainJS’s BaseAmount type, which gives you safe arithmetic helpers
to prevent the classic floating-point rounding bugs that haunt direct wei-to-ether
conversions.

Working With the Underlying EVM Client

The EVM client abstraction doesn’t lock you out of the underlying
provider. When you need functionality that sits below the XChainJS interface —
custom contract calls, event listeners, ENS resolution — you can access the
ethers.js provider directly via client.getProvider(). This escape
hatch is important: it means you’re not fighting the SDK when you need low-level
control, only guided by it when you don’t.

TypeScript · direct provider access

// Access the underlying ethers.js provider for custom operations
const provider = client.getProvider()

// Custom contract call example
const abi    = ['function balanceOf(address) view returns (uint256)']
const contract = new ethers.Contract(TOKEN_ADDRESS, abi, provider)
const rawBalance = await contract.balanceOf(userAddress)
console.log('Token balance (raw):', rawBalance.toString())

This design pattern — opinionated interface on top, raw access underneath — is what
distinguishes a mature Web3 SDK from a restrictive framework. You
get 90% of your work handled by well-tested abstractions, and a clean exit for the
other 10%. For teams building anything beyond a toy project, that balance matters
enormously in practice.

💡 RPC Provider Tip

Use Alchemy or Infura for production — they support eth_getTransactionCount,
eth_estimateGas, and eth_feeHistory endpoints required by
XChainJS fee estimation. Public RPCs like Cloudflare’s cloudflare-eth.com
often rate-limit or drop these calls under load.


Gas Fee Estimation: No More Manual Math

Gas fee estimation is one of those problems that looks trivial until
you’re dealing with it at production scale during a network congestion spike. Hardcoding
gas prices gets transactions stuck. Fetching from a third-party oracle adds a dependency
and latency. The XChainJS fee module solves this by pulling real-time data from your
configured RPC provider via eth_feeHistory and eth_maxPriorityFeePerGas,
then calculating three tiers — average, fast, and
fastest — that you can hand directly to the transaction builder.

TypeScript · gas fee estimation

import { FeeOption } from '@xchainjs/xchain-client'

// Fetch current gas fees from the network
const fees = await client.getFees()

console.log('Average fee:', fees[FeeOption.Average].amount().toString(), 'wei')
console.log('Fast fee:   ', fees[FeeOption.Fast].amount().toString(),    'wei')
console.log('Fastest fee:', fees[FeeOption.Fastest].amount().toString(), 'wei')

The returned Fees object contains BaseAmount values for
each tier. You pass your preferred FeeOption directly into the
transfer() call, and XChainJS applies it to both gas price
(legacy transactions) or maxFeePerGas / maxPriorityFeePerGas
(EIP-1559 transactions) automatically based on what the network supports. You don’t
need to write branching logic for pre- and post-London hardfork transaction formats.

For more granular control — say, you’re building a DeFi development
interface that lets users set a custom gas limit for complex smart contract
interactions — you can pass an explicit gasLimit override into the
transfer params. This is especially relevant for ERC20 transfers, which consume
roughly 65,000 gas units versus ~21,000 for native ETH sends, and for DeFi contract
calls that may reach 200,000+ gas units depending on logic complexity.


ETH Transfers, ERC20 Tokens, and Transaction Signing

Sending ETH is the “Hello, World” of any Ethereum developer tools
test suite. With XChainJS, it’s exactly as simple as it should be: one method, one
set of params, one returned transaction hash. The SDK handles nonce retrieval, fee
application, transaction construction, Ethereum transaction signing
with the derived private key, and broadcast to the network — in a single awaitable call.

TypeScript · native ETH transfer

import { assetToBase, assetAmount } from '@xchainjs/xchain-util'
import { AssetETH }                  from '@xchainjs/xchain-ethereum'
import { FeeOption }                 from '@xchainjs/xchain-client'

const txHash = await client.transfer({
  asset:     AssetETH,
  recipient: '0xRecipientAddressHere',
  amount:    assetToBase(assetAmount(0.01)),   // 0.01 ETH → wei
  memo:      '',                               // optional memo / data field
  feeOption: FeeOption.Fast,
})

console.log('Transaction broadcast:', txHash)
// → 0xabc123...def456

assetToBase(assetAmount(0.01)) converts human-readable ETH to
BaseAmount in wei. This utility-first approach prevents the classic
developer mistake of accidentally passing 0.01 as a raw number to
an ethers.js method — which would attempt to send 0.01 wei, approximately nothing,
rather than 0.01 ETH. Small guardrails, large consequences avoided.

ERC20 Token Transfers

ERC20 transfers through XChainJS follow an identical pattern,
with one additional configuration step: defining the token asset. You pass an
asset descriptor that includes the contract address, and the SDK handles the
ABI encoding, approve() management (where applicable), and
transfer() call construction against the token contract.

TypeScript · ERC20 token transfer

import { assetFromString, assetToBase, assetAmount } from '@xchainjs/xchain-util'

// Define the ERC20 asset (USDC on mainnet)
const USDC = assetFromString('ETH.USDC-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48')

const txHash = await client.transfer({
  asset:     USDC!,
  recipient: '0xRecipientAddressHere',
  amount:    assetToBase(assetAmount(100, 6)),  // 100 USDC (6 decimals)
  feeOption: FeeOption.Average,
})

Note the decimal precision: USDC uses 6 decimal places, LINK uses 18,
and each ERC20 token defines its own. XChainJS exposes the assetAmount(value, decimals)
helper precisely for this reason. Get the decimals wrong and you’ll either send a
thousand times too much or a million times too little — both outcomes are unfortunate
in production. The type system catches most of these errors at compile time when
you work within the SDK’s amount utilities, which is another reason to use them
rather than raw BigNumber manipulation.

Understanding Transaction Signing Internals

Ethereum transaction signing inside XChainJS works through a
Wallet instance derived from your mnemonic at the requested index.
The private key never leaves the SDK — it’s used to sign the transaction object
in-memory, and the signed serialized transaction is what gets broadcast to the
network via the RPC provider. This is standard non-custodial wallet architecture:
the private key material is derived on-demand, used for signing, and not persisted
anywhere by the library itself.

For developers building server-side transaction pipelines — automated DeFi bots,
rebalancing engines, cross-chain bridge relayers — this model works well. For
browser-based dApps where you want to sign with MetaMask or WalletConnect rather
than a raw mnemonic, you’d extend the client with an external signer. XChainJS
supports this through provider injection, keeping the architecture flexible for
both custodial and non-custodial use cases across Web3 wallet development
contexts.


Cross-Chain Wallet Architecture: Where XChainJS Really Shines

The Ethereum client is excellent on its own — but the real leverage of
XChainJS
reveals itself when you add a second chain. The moment you instantiate a Bitcoin client
alongside your Ethereum client using the same mnemonic, you’ve created the foundational
layer of a cross-chain wallet with a unified key management model.
Both addresses are derived from the same seed. Both balances are retrieved through the
same getBalance() interface. Both transfers are executed through the same
transfer() signature.

TypeScript · multi-chain wallet pattern

import { Client as EthClient }  from '@xchainjs/xchain-ethereum'
import { Client as BtcClient }  from '@xchainjs/xchain-bitcoin'
import { Client as BnbClient }  from '@xchainjs/xchain-binance'
import { Network }              from '@xchainjs/xchain-client'

const PHRASE = process.env.WALLET_PHRASE!

// All three clients share the same mnemonic; addresses are derived independently
const ethClient = new EthClient({ ...defaultEthParams, phrase: PHRASE, network: Network.Mainnet })
const btcClient = new BtcClient({ phrase: PHRASE, network: Network.Mainnet })
const bnbClient = new BnbClient({ phrase: PHRASE, network: Network.Mainnet })

// Unified balance aggregation across chains
const [ethBalances, btcBalances, bnbBalances] = await Promise.all([
  ethClient.getBalance(ethClient.getAddress()),
  btcClient.getBalance(btcClient.getAddress()),
  bnbClient.getBalance(bnbClient.getAddress()),
])

This is the architecture powering wallets like THORWallet — a real production
application built on XChainJS that manages assets across a dozen chains from a
single interface. The SDK handles the chain-specific complexity (UTXO selection for
BTC, sequence number management for BNB, EIP-1559 fees for ETH), leaving the
application developer free to focus on UX and business logic.

For DeFi development specifically, this architecture opens up
interesting possibilities. Cross-chain swaps, multi-chain portfolio rebalancing,
and bridge interfaces all require the same fundamental operations — balance reads,
transaction construction, fee estimation — across multiple chains simultaneously.
With XChainJS, those operations are idiomatic. With a fragmented set of
chain-specific libraries, they’re an integration nightmare that grows linearly
with every new chain you add.

XChainJS vs. ethers.js vs. Web3.js: An Honest Comparison

It’s worth being honest about the trade-offs here, because “XChainJS replaces
ethers.js” is not an accurate characterization and setting that expectation would
be doing you a disservice. The three tools occupy different layers of the stack.

Feature XChainJS ethers.js v6 Web3.js v4
Multi-chain support ✓ 15+ chains ✗ Ethereum only ✗ Ethereum only
TypeScript-first ✓ Native ✓ Native ~ Partial
Unified wallet API ✓ Core feature ✗ Manual ✗ Manual
Raw EVM control ~ Via provider access ✓ Full ✓ Full
ERC20 transfer helper ✓ Built-in ~ Manual ABI ~ Manual ABI
Gas fee estimation ✓ Tiered, automatic ~ Manual calculation ~ Manual calculation
HD wallet derivation ✓ Built-in ✓ Via HDNode ~ Plugin required
Bundle size impact ~ Moderate (modular) ✓ Small ✗ Large
Production usage ✓ THORWallet, ShapeShift ✓ Ubiquitous ~ Declining

The decision framework is simple: if you’re building an Ethereum-only application
with heavy smart contract interaction requirements, ethers.js v6 is still the right
tool. If you’re building a crypto wallet SDK, a multi-chain DeFi
interface, or any product where the user expects to manage assets across more than
one blockchain, XChainJS will save you a significant amount of integration time
and architectural complexity. The two aren’t mutually exclusive — you can use
XChainJS for your cross-chain orchestration layer and drop to raw ethers.js for
specific Ethereum operations where you need fine-grained control.


Real-World Patterns for Web3 and DeFi Development

Knowing the API is one thing; knowing how to structure a real application around
it is another. In production Web3 development contexts, the most
common patterns you’ll encounter are: multi-account wallet management, fee-aware
transaction queuing, and chain-fallback strategies for RPC reliability. XChainJS
handles the core of each without requiring you to build the scaffolding from scratch.

Building a Fee-Aware Transaction Queue

A common pattern in DeFi development is batching transactions
during low-fee windows. The approach with XChainJS is to poll getFees()
on an interval, compare the Average tier against your threshold, and
execute pending transfers when conditions are met. Because getFees()
is a lightweight read call with no signing involved, you can call it frequently
without side effects or gas costs.

TypeScript · fee-aware transaction dispatch

import { FeeOption } from '@xchainjs/xchain-client'
import { baseToAsset } from '@xchainjs/xchain-util'

// Target: only send when average gas fee is below 25 Gwei
const GAS_THRESHOLD_GWEI = 25n * 10n ** 9n  // 25 Gwei in wei

async function dispatchWhenCheap(transferParams: TransferParams) {
  const fees    = await client.getFees()
  const avgFee  = BigInt(fees[FeeOption.Average].amount().toFixed(0))

  if (avgFee <= GAS_THRESHOLD_GWEI) {
    const hash = await client.transfer({ ...transferParams, feeOption: FeeOption.Average })
    console.log('Dispatched at low fee:', hash)
    return hash
  }

  console.log('Fee too high, holding transaction...')
  return null
}

This pattern is trivially extensible: add a priority queue, a maximum wait timeout,
a WebSocket-based fee listener, or a Telegram alert for fee drops. The XChainJS layer
provides the data retrieval and transaction execution primitives; your application
logic handles the orchestration. Clean separation of concerns that scales well.

Testing and Local Development

For local Ethereum developer tools workflows, XChainJS works
seamlessly with Hardhat
and Anvil (Foundry)
local chains. Point the provider at http://127.0.0.1:8545, use one of
the well-known test mnemonics, and your full transfer/estimation/signing flow works
against a local EVM fork without touching mainnet or spending real ETH. This makes
integration testing straightforward — you test the same code path that runs in
production, just against a sandboxed network.

One area where the SDK's design earns particular appreciation is error handling.
Errors from the RPC provider are propagated through typed exceptions rather than
swallowed or converted to opaque strings. When a transaction fails due to
insufficient gas, nonce mismatch, or reverted contract execution, you get a
structured error with enough context to debug the issue without grepping through
raw JSON-RPC responses. For teams maintaining long-running blockchain
TypeScript SDK
integrations, this alone saves significant debugging time.


Frequently Asked Questions

How does XChainJS compare to ethers.js for Ethereum development?

ethers.js is a low-level, Ethereum-specific library with
precise control over EVM internals — ideal for Ethereum-only applications and
deep smart contract interaction. XChainJS wraps ethers.js
internally and adds a chain-agnostic abstraction layer on top. This means you
write wallet management, transfer, and fee estimation logic once, and it runs
identically across Ethereum, Bitcoin, Cosmos, and 15+ other chains. If you're
touching only Ethereum, ethers.js is leaner and more direct. If you're building
multi-chain infrastructure or a cross-chain wallet, XChainJS
eliminates weeks of abstraction work and keeps your codebase maintainable
as you add new chains over time.

Does XChainJS support ERC20 transfers and gas fee estimation?

Yes, both are fully supported. ERC20 transfers use the same
transfer() method as native ETH — you define the token asset with
its contract address and decimal precision, and the SDK handles ABI encoding
and contract interaction internally. Gas fee estimation is
available via getFees(), which returns three tiers (Average, Fast,
Fastest) calculated from live RPC data using eth_feeHistory. You
pass your preferred tier directly into the transfer call; EIP-1559 vs. legacy
transaction format selection is handled automatically based on network support.
Manual gasLimit overrides are supported for complex contract interactions.

Can I use XChainJS to build a cross-chain Web3 wallet?

That's precisely what XChainJS was designed for. By instantiating multiple
chain clients (Ethereum, Bitcoin, BNB Chain, Cosmos, Litecoin, etc.) under
a single BIP39-derived HD wallet, you get a unified balance, transfer, and
signing interface across all supported chains. Every chain shares the same
mnemonic seed; addresses are derived independently using chain-specific BIP44
paths. The pattern is proven in production — THORWallet and
ShapeShift have both built on this architecture.
For Web3 wallet development, XChainJS provides the SDK layer;
your application handles the UI, state management, and user authentication on top.


📐 Semantic Core & Keyword Clusters

Reference cluster map used for this article's SEO targeting.

Core
xchainjs
xchain ethereum
xchainjs ethereum
xchainjs sdk
@xchainjs/xchain-ethereum

Developer Tools
ethereum sdk
typescript ethereum sdk
web3 sdk
ethereum client
evm client
evm sdk
ethereum rpc client
ethers js
blockchain typescript sdk
ethereum developer tools

Cross-Chain
cross chain sdk
cross chain wallet
crypto wallet sdk
web3 wallet development
web3 development
defi development

Operations
erc20 transfer
ethereum transaction signing
gas fee estimation
ethereum address generation
ethereum balance lookup

LSI / Semantic Enrichment
HD wallet derivation
BIP39 mnemonic
BIP44 path
EIP-1559
provider abstraction
chain-agnostic API
non-custodial wallet
Infura RPC
Alchemy node
nonce management
transaction broadcast
EVM-compatible chains
multi-chain dApp
BaseAmount
THORWallet