Bank Precompile
Learn how Paxeer's Bank Precompile at 0x1001 lets EVM contracts send tokens, query balances, and fetch metadata for HPX and IBC assets.
Address: 0x0000000000000000000000000000000000001001
The Paxeer bank precompile allows EVM applications to interact directly with Paxeer's native banking system through standard smart contract calls. This enables querying balances, transferring tokens, and accessing token metadata for both native HPX tokens and Cosmos SDK-based assets, providing seamless integration between EVM and Cosmos ecosystems.
How Does the Bank Precompile Work?
The bank precompile at address 0x0000000000000000000000000000000000001001 exposes functions like send(), sendNative(), balance(), all_balances(), and token metadata queries.
- Direct Integration: EVM contracts and dApps can call banking functions like any other smart contract method.
- Native Execution: Operations are executed at the Cosmos SDK level for maximum efficiency and security.
- Cross-Chain Assets: Manage both native HPX tokens and IBC assets seamlessly from EVM contracts.
When to use send vs sendNative: send moves an arbitrary denom (any IBC or factory token) between two EVM addresses (0x...) by reading the balance directly from the bank module - no msg.value is attached. It is gated to the registered ERC20 native pointer for that denom, so it is typically invoked from the auto-deployed pointer contract rather than from arbitrary user code. sendNative is for sending native HPX (the attached msg.value) from the EVM caller to a Cosmos bech32 (pax1...) destination, which is useful for crossing the EVM→Cosmos boundary when the recipient has no associated EVM address (for example, paying a Cosmos-only contract or account).
Use Cases
- DeFi Applications: Build decentralized finance protocols that can handle native HPX and Cosmos assets.
- Portfolio Management: Build tools to track and manage multi-asset portfolios across Cosmos and EVM.
- Token Information Services: Query comprehensive token metadata for UI display and analytics.
What You'll Learn in This Guide
By the end of this guide, you'll be able to:
- Execute Token Transfers - Send both native HPX and custom tokens between addresses
- Query Account Balances - Check single and multi-asset balances for any address
- Access Token Metadata - Retrieve names, symbols, decimals, and supply information
Functions
The bank precompile exposes the following functions:
Transaction Functions
/// Sends non-native tokens from one address to another. Callable only by the
/// registered ERC20 native pointer contract for the given denom.
/// @param fromAddress The EVM address (0x...) to send funds from.
/// @param toAddress The EVM address (0x...) to send funds to.
/// @param denom The denomination of funds to send.
/// @param amount The amount of the above denom to send.
/// @return success Whether the send was successfully executed.
function send(
address fromAddress,
address toAddress,
string memory denom,
uint256 amount
) external returns (bool success);
/// Sends native HPX (the attached msg.value) from the EVM caller to a Cosmos
/// bech32 recipient. Requires a non-zero msg.value.
/// @param toNativeAddress The bech32 (pax1...) address of the recipient.
/// @return success Whether the tokens were successfully sent.
function sendNative(
string memory toNativeAddress
) payable external returns (bool success);
Query Functions
/// Queries the balance of the given account for the specified denom.
/// @param acc The EVM address (0x...) of the account to query.
/// @param denom The denomination to query for.
/// @return amount The amount of denom held by acc.
function balance(
address acc,
string memory denom
) external view returns (uint256 amount);
/// Queries the balance of the given account for all balances.
/// @param acc The EVM address (0x...) of the account to query.
/// @return response Balances for all coins/denoms.
function all_balances(
address acc
) external view returns (Coin[] memory response);
/// Queries the name of the specified denom.
/// @param denom The denomination to query about.
/// @return response The name of the specified denom.
function name(
string memory denom
) external view returns (string memory response);
/// Queries the symbol of the specified denom.
/// @param denom The denomination to query about.
/// @return response The symbol of the specified denom.
function symbol(
string memory denom
) external view returns (string memory response);
/// Queries the number of decimal places for the specified denom.
/// @param denom The denomination to query about.
/// @return response The number of decimals for the specified denom.
function decimals(
string memory denom
) external view returns (uint8 response);
/// Queries the total supply of the specified denom.
/// @param denom The denomination to query about.
/// @return response The total supply of the specified denom.
function supply(
string memory denom
) external view returns (uint256 response);
Using the Precompile
Setup
Prerequisites
Before getting started, ensure you have:
- Node.js (v18 or higher)
- npm or yarn package manager
- EVM-compatible wallet
- HPX tokens for gas and testing transfers
- Hardhat for development and testing
Install Dependencies
Install the required packages for interacting with Paxeer precompiles:
# Instantiate a new Hardhat 3 project (choose "Hardhat 3" and the Mocha + Ethers.js TypeScript setup)
npx hardhat --init
# Install ethers.js for smart contract interactions
npm install ethers
# Install Paxeer EVM bindings for precompile addresses and ABIs
npm install @sei-js/precompiles@2.1.2
Setup Hardhat Environment
Create a hardhat.config.ts file with the following content:
import { defineConfig, configVariable } from 'hardhat/config';
import hardhatToolboxMochaEthers from '@nomicfoundation/hardhat-toolbox-mocha-ethers';
export default defineConfig({
solidity: '0.8.28',
networks: {
hpx: {
type: 'http',
chainId: 1329,
url: 'https://evm-rpc.sei-apis.com',
accounts: [configVariable('PRIVATE_KEY')]
}
},
plugins: [hardhatToolboxMochaEthers]
});
Store your private key in Hardhat's encrypted keystore (no plaintext .env file needed):
npx hardhat keystore set PRIVATE_KEY
Import Precompile Components
// Import Bank precompile address and ABI
// View the entire ABI here: https://github.com/paxeer-network/paxeer-chain/tree/main/precompiles/bank
import { BANK_PRECOMPILE_ABI, BANK_PRECOMPILE_ADDRESS } from '@sei-js/precompiles';
import { ethers } from 'ethers';0x0000000000000000000000000000000000001001 Contract Initialization
Set up your provider, signer, and contract instance:
// Using EVM-compatible wallet as the signer and provider
const provider = new ethers.BrowserProvider(window.ethereum);
await provider.send('eth_requestAccounts', []);
const signer = await provider.getSigner();
// Create a contract instance for the bank precompile
const bank = new ethers.Contract(BANK_PRECOMPILE_ADDRESS, BANK_PRECOMPILE_ABI, signer);Native HPX vs Custom Tokens
Native HPX Transfers:
- Use
sendNative()with payable value - Denomination is always
uhpx(micro-HPX) - Parse to 18 digits when calling
sendNative()
Custom Token Transfers:
- Use
send()with specific denomination - Requires prior token approval or ownership
- Support various decimal configurations
Step-by-Step Guide: Using the Bank Precompile
Send Native HPX Tokens
// Send native HPX tokens to another address
const recipientAddress = 'pax1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6s8dpw';
const amountInPax = '0.1'; // 0.1 HPX
try {
// msg.value is in wei (18 decimals), like any other EVM transaction
const amountInWei = ethers.parseUnits(amountInPax, 18); // equivalent to ethers.parseEther(amountInPax)
// Send native HPX
const tx = await bank.sendNative(recipientAddress, {
value: amountInWei,
gasLimit: 300000n
});
const receipt = await tx.wait();
console.log('Native HPX transfer successful:', receipt.hash);
console.log(`Sent ${amountInPax} HPX to ${recipientAddress}`);
} catch (error) {
console.error('Native HPX transfer failed:', error);
}Send Custom Tokens
send() can only be called by the registered ERC20 native pointer contract for the given denom - calling it from a regular wallet will revert. The snippet below only illustrates the arguments a pointer contract passes. As a user, transfer native denoms through the denom's ERC20 pointer contract (transfer()), or use sendNative() for HPX.// Illustration: how a registered ERC20 pointer contract calls send()
const fromAddress = '0x1234567890123456789012345678901234567890';
const toAddress = '0x9876543210987654321098765432109876543210';
const tokenDenom = 'uhpx';
const amount = '1';
// Amounts are in the denom's base units - check decimals() for each denom.
// uhpx has 6 decimals (1 HPX = 1,000,000 uhpx)
const tx = await bank.send(
fromAddress,
toAddress,
tokenDenom,
ethers.parseUnits(amount, 6),
{
gasLimit: 300000n
}
);
const receipt = await tx.wait();
console.log('Custom token transfer successful:', receipt.hash);
console.log(`Sent ${amount} ${tokenDenom} from ${fromAddress} to ${toAddress}`);Query Account Balance
// Query specific token balance
const accountAddress = '0x1234567890123456789012345678901234567890';
const tokenDenom = 'uhpx';
try {
const balance = await bank.balance(accountAddress, tokenDenom);
// Convert uhpx to HPX for display
if (tokenDenom === 'uhpx') {
const hpxBalance = ethers.formatUnits(balance, 6);
console.log(`HPX Balance: ${hpxBalance} HPX`);
} else {
console.log(`${tokenDenom} Balance: ${balance.toString()}`);
}
} catch (error) {
console.error('Balance query failed:', error);
}Query All Balances
// Query all token balances for an account
const accountAddress = '0x1234567890123456789012345678901234567890';
try {
const allBalances = await bank.all_balances(accountAddress);
console.log(`Account: ${accountAddress}`);
console.log('All Balances:');
allBalances.forEach((coin, index) => {
if (coin.denom === 'uhpx') {
const paxAmount = ethers.formatUnits(coin.amount, 6);
console.log(` ${index + 1}. ${paxAmount} HPX (${coin.denom})`);
} else {
console.log(` ${index + 1}. ${coin.amount.toString()} ${coin.denom}`);
}
});
} catch (error) {
console.error('All balances query failed:', error);
}Query Token Metadata
// Query comprehensive token metadata
const tokenDenom = 'uhpx';
try {
// Get all metadata in parallel
const [name, symbol, decimals, totalSupply] = await Promise.all([bank.name(tokenDenom), bank.symbol(tokenDenom), bank.decimals(tokenDenom), bank.supply(tokenDenom)]);
console.log('Token Metadata:');
console.log(` Denomination: ${tokenDenom}`);
console.log(` Name: ${name}`);
console.log(` Symbol: ${symbol}`);
console.log(` Decimals: ${decimals}`);
console.log(` Total Supply: ${ethers.formatUnits(totalSupply, decimals)}`);
} catch (error) {
console.error('Metadata query failed:', error);
}
// Function to get formatted token info
async function getTokenInfo(denom) {
try {
const metadata = {
denom: denom,
name: await bank.name(denom),
symbol: await bank.symbol(denom),
decimals: await bank.decimals(denom),
supply: await bank.supply(denom)
};
return {
...metadata,
formattedSupply: ethers.formatUnits(metadata.supply, metadata.decimals)
};
} catch (error) {
console.error(`Failed to get info for ${denom}:`, error);
return null;
}
}Complete Integration Example
import { BANK_PRECOMPILE_ABI, BANK_PRECOMPILE_ADDRESS } from '@sei-js/precompiles';
import { ethers } from 'ethers';
class PaxeerTokenManager {
private bank: ethers.Contract;
private signer: ethers.Signer;
constructor(signer: ethers.Signer) {
this.signer = signer;
this.bank = new ethers.Contract(BANK_PRECOMPILE_ADDRESS, BANK_PRECOMPILE_ABI, signer);
}
// Send native HPX tokens
async sendHpx(recipientAddress: string, amountInPax: string) {
try {
const amountInWei = ethers.parseUnits(amountInPax, 18);
const tx = await this.bank.sendNative(recipientAddress, {
value: amountInWei,
gasLimit: 100000
});
const receipt = await tx.wait();
return {
success: true,
transactionHash: receipt.hash,
amount: amountInPax,
recipient: recipientAddress
};
} catch (error) {
return { success: false, error: error.message };
}
}
// Get user's portfolio
async getPortfolio(address: string) {
try {
const balances = await this.bank.all_balances(address);
const portfolio = [];
for (const coin of balances) {
try {
const metadata = await this.getTokenMetadata(coin.denom);
portfolio.push({
denom: coin.denom,
amount: coin.amount.toString(),
formattedAmount: ethers.formatUnits(coin.amount, metadata.decimals),
...metadata
});
} catch (metadataError) {
// If metadata fails, still include the balance
portfolio.push({
denom: coin.denom,
amount: coin.amount.toString(),
formattedAmount: coin.amount.toString(),
name: 'Unknown',
symbol: 'Unknown',
decimals: 0
});
}
}
return { success: true, portfolio };
} catch (error) {
return { success: false, error: error.message };
}
}
// Get token metadata
async getTokenMetadata(denom: string) {
const [name, symbol, decimals, supply] = await Promise.all([this.bank.name(denom).catch(() => 'Unknown'), this.bank.symbol(denom).catch(() => 'Unknown'), this.bank.decimals(denom).catch(() => 0), this.bank.supply(denom).catch(() => 0n)]);
return { name, symbol, decimals, supply: supply.toString() };
}
// Transfer custom tokens
async transferToken(toAddress: string, denom: string, amount: string, decimals: number = 18) {
try {
const fromAddress = await this.signer.getAddress();
const parsedAmount = ethers.parseUnits(amount, decimals);
const tx = await this.bank.send(fromAddress, toAddress, denom, parsedAmount, {
gasLimit: 150000
});
const receipt = await tx.wait();
return {
success: true,
transactionHash: receipt.hash,
from: fromAddress,
to: toAddress,
denom,
amount
};
} catch (error) {
return { success: false, error: error.message };
}
}
// Batch operations
async batchBalanceCheck(addresses: string[], denom: string) {
try {
const balances = await Promise.allSettled(addresses.map((addr) => this.bank.balance(addr, denom)));
return balances.map((result, index) => ({
address: addresses[index],
balance: result.status === 'fulfilled' ? result.value.toString() : '0',
success: result.status === 'fulfilled'
}));
} catch (error) {
throw new Error(`Batch balance check failed: ${error.message}`);
}
}
}
// Usage example
async function bankExample() {
const provider = new ethers.BrowserProvider(window.ethereum);
await provider.send('eth_requestAccounts', []);
const signer = await provider.getSigner();
const tokenManager = new PaxeerTokenManager(signer);
console.log('=== Paxeer Token Manager Demo ===');
// 1. Get current user's portfolio
const userAddress = await signer.getAddress();
const portfolio = await tokenManager.getPortfolio(userAddress);
if (portfolio.success) {
console.log('User Portfolio:');
portfolio.portfolio.forEach((token, index) => {
console.log(` ${index + 1}. ${token.formattedAmount} ${token.symbol} (${token.name})`);
});
}
// 2. Send native HPX
const sendResult = await tokenManager.sendHpx('pax1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6s8dpw', '0.01');
if (sendResult.success) {
console.log('HPX transfer successful:', sendResult.transactionHash);
}
// 3. Check balances for multiple addresses
const addresses = ['0x1234567890123456789012345678901234567890', '0x9876543210987654321098765432109876543210'];
const batchBalances = await tokenManager.batchBalanceCheck(addresses, 'uhpx');
console.log('Batch balance results:', batchBalances);
}Security Considerations & Risks
Transaction Security
- Amount Validation: Always validate transfer amounts and ensure sufficient balances
- Address Verification: Verify recipient addresses are valid before sending tokens
- Reentrancy Protection: Be aware of potential reentrancy when combining with other contracts
Permission Management
// Example of secure permission patterns
modifier onlyTokenHolder(string memory denom, uint256 minAmount) {
require(BANK.balance(msg.sender, denom) >= minAmount, "Insufficient token balance");
_;
}
modifier validRecipient(address recipient) {
require(recipient != address(0), "Invalid recipient");
require(recipient != address(this), "Cannot send to contract");
_;
}
Troubleshooting
Common Issues and Solutions
Transaction Failures
// Handle common transfer errors
try {
const tx = await bank.sendNative(recipient, { value: amount });
await tx.wait();
} catch (error) {
if (error.message.includes('insufficient funds')) {
console.error('Insufficient balance for transfer');
} else if (error.message.includes('invalid address')) {
console.error('Invalid recipient address format');
} else {
console.error('Transfer failed:', error.message);
}
}
Balance Query Issues
// Safe balance checking with error handling
async function safeGetBalance(address: string, denom: string) {
try {
const balance = await bank.balance(address, denom);
return { success: true, balance: balance.toString() };
} catch (error) {
if (error.message.includes('not found')) {
return { success: true, balance: '0' }; // No balance = 0
}
return { success: false, error: error.message };
}
}
Error Code Reference
| Error | Cause | Solution |
|---|---|---|
insufficient funds | Not enough balance for transfer | Check balance before transfer |
invalid address | Malformed address format | Use proper EVM (0x...) or Paxeer (pax1...) format |
unknown denomination | Token denom doesn't exist | Verify token denomination format |
amount overflow | Amount exceeds uint256 limits | Use appropriate amount ranges |
metadata not found | Token metadata not available | Handle missing metadata gracefully |
Important Notes
Remember: Always verify token denominations and handle errors gracefully in production applications!
- HPX Native: While reading from chain it is formatted to 6 decimal places (1 HPX = 1,000,000 uhpx) and while writing to chain it is parsed to 18 decimal places.
- Custom Tokens: Check decimals using
decimals()function - Display Formatting: Always format amounts with proper decimal places for user display
Gas Optimization
- Batch Operations: Use batch functions for multiple operations to save gas
- Query Efficiency: Cache frequently accessed token metadata
- Error Handling: Implement proper error handling to avoid failed transaction costs
Integration Best Practices
- Balance Checks: Always verify sufficient balance before transfers
- Error Recovery: Implement retry logic for failed transactions
- User Experience: Provide clear feedback on transaction status
- Decimal Handling: Use proper decimal formatting for different token types