This guide covers everything you need to set up your development environment for building dApps on BitBadges Chain with EVM compatibility.
Chain Configuration
Chain ID
The BitBadges Chain EVM uses a custom Chain ID for EVM compatibility:
Local Development: 90123
Testnet: 50025 (claimed in ethereum-lists/chains registry)
Mainnet: 50024 (claimed in ethereum-lists/chains registry)
The Chain ID is configured in app/params/constants.go:
// EVMChainIDMainnet is the EVM chain ID for BitBadges mainnet// Chain ID: 50024 (claimed in ethereum-lists/chains registry)// This should match the chain_id in genesis under app_state.evm.params.chain_config.chain_idEVMChainIDMainnet="50024"// EVMChainIDTestnet is the EVM chain ID for BitBadges testnet// Chain ID: 50025 (claimed in ethereum-lists/chains registry)// This should match the chain_id in genesis under app_state.evm.params.chain_config.chain_idEVMChainIDTestnet="50025"EVMChainID="90123"// Default for local development/testing
Important: The Chain ID in your genesis file (app_state.evm.params.chain_config.chain_id) must match this value.
RPC Endpoints
BitBadges Chain provides two RPC interfaces:
Tendermint RPC (Port 26657)
Used for Cosmos SDK queries and transactions
Endpoint: http://localhost:26657
Supports standard Cosmos SDK operations
EVM JSON-RPC (Port 8545)
Used for Ethereum-compatible operations
Endpoint: http://localhost:8545
Supports standard Ethereum JSON-RPC methods
Required for MetaMask, ethers.js, web3.js, etc.
Starting the Chain with EVM
To start the chain with EVM support, use the --json-rpc.enable flag:
Or use the provided startup script:
The startup script automatically enables EVM JSON-RPC on port 8545.
Note: The EVM module is always enabled in the chain binary. You only need to enable the JSON-RPC server to interact with it via Ethereum-compatible tools (MetaMask, ethers.js, etc.).
MetaMask Configuration
To connect MetaMask to your local BitBadges chain:
Open MetaMask
Go to Settings > Networks > Add Network
Add the following network configuration:
For Local Development:
Network Name: BitBadges Local
RPC URL: http://localhost:8545 (EVM JSON-RPC)
Chain ID: 90123
Currency Symbol: BADGE
Block Explorer: (Optional, leave blank for local)
For Testnet:
Network Name: BitBadges Testnet
RPC URL: https://evm-rpc-testnet.bitbadges.io
Chain ID: 50025
Currency Symbol: BADGE
Block Explorer: (Testnet block explorer URL)
For Mainnet:
Network Name: BitBadges Mainnet
RPC URL: https://evm-rpc.bitbadges.io
Chain ID: 50024
Currency Symbol: BADGE
Block Explorer: https://explorer.bitbadges.io
Note: Use port 8545 (EVM JSON-RPC), not 26657 (Tendermint RPC).
Getting Test Tokens
After starting your local chain, fund your MetaMask account:
Simple dApp Setup
Project Structure
A typical dApp project structure:
Basic Contract Template
Here's a minimal contract that uses the tokenization precompile:
Deployment Script (TypeScript/ethers.js)
Frontend Integration (React/ethers.js)
Contract Snippets
Helper Library
The repository includes a comprehensive helper library at contracts/libraries/TokenizationHelpers.sol that provides utility functions for:
Creating UintRange structs (single values, sequences, full ranges)
# From the bitbadgeschain repository root
bitbadgeschaind start --json-rpc.enable --json-rpc.address 0.0.0.0:8545
# From the bitbadgeschain repository root
./start-chain.sh start
# Get your MetaMask address (0x... format)
# Then fund it using the chain's genesis or by transferring from a validator
# Option 1: Transfer from a validator account
bitbadgeschaind tx bank send \
$(bitbadgeschaind keys show alice -a --keyring-backend test) \
<your-metamask-address> \
1000000000ubadge \
--chain-id bitbadges \
--keyring-backend test \
--yes
# Option 2: Use the chain's genesis accounts if configured
import { ethers } from "ethers";
import * as fs from "fs";
async function deploy() {
// Connect to EVM JSON-RPC
const provider = new ethers.JsonRpcProvider("http://localhost:8545");
// Get deployer wallet
const privateKey = process.env.PRIVATE_KEY || "";
if (!privateKey) {
throw new Error("PRIVATE_KEY environment variable required");
}
const wallet = new ethers.Wallet(privateKey, provider);
console.log("Deployer address:", wallet.address);
// Check balance
const balance = await provider.getBalance(wallet.address);
console.log("Balance:", ethers.formatEther(balance), "BADGE");
// Deploy contract
const contractFactory = new ethers.ContractFactory(
CONTRACT_ABI,
CONTRACT_BYTECODE,
wallet
);
const contract = await contractFactory.deploy(collectionId);
await contract.waitForDeployment();
const address = await contract.getAddress();
console.log("Contract deployed at:", address);
// Save deployment info
fs.writeFileSync(
"deployed.json",
JSON.stringify({ address, abi: CONTRACT_ABI }, null, 2)
);
}
deploy().catch(console.error);
import { ethers } from "ethers";
import { useState, useEffect } from "react";
export function useContract() {
const [contract, setContract] = useState<ethers.Contract | null>(null);
const [provider, setProvider] = useState<ethers.BrowserProvider | null>(null);
useEffect(() => {
if (typeof window.ethereum !== "undefined") {
const provider = new ethers.BrowserProvider(window.ethereum);
setProvider(provider);
// Load deployed contract
const contractAddress = "0x..."; // Your deployed contract address
const contract = new ethers.Contract(
contractAddress,
CONTRACT_ABI,
await provider.getSigner()
);
setContract(contract);
}
}, []);
const transfer = async (to: string, amount: bigint, tokenId: bigint) => {
if (!contract) throw new Error("Contract not loaded");
const tx = await contract.transfer(to, amount, tokenId);
await tx.wait();
};
return { contract, provider, transfer };
}
import "./libraries/TokenizationHelpers.sol";
contract MyContract {
function example() external {
// Create a single token ID range
TokenizationTypes.UintRange memory tokenId =
TokenizationHelpers.createSingleTokenIdRange(123);
// Create a full ownership time range (1 to max uint256)
TokenizationTypes.UintRange memory fullTime =
TokenizationHelpers.createFullOwnershipTimeRange();
// Create a token ID sequence (range from start to end)
TokenizationTypes.UintRange memory sequence =
TokenizationHelpers.createTokenIdSequence(1, 100);
// Create a UintRange array from arrays
uint256[] memory starts = new uint256[](2);
uint256[] memory ends = new uint256[](2);
starts[0] = 1; ends[0] = 10;
starts[1] = 20; ends[1] = 30;
TokenizationTypes.UintRange[] memory ranges =
TokenizationHelpers.createUintRangeArray(starts, ends);
}
}
import "./libraries/TokenizationHelpers.sol";
contract HelperExample {
ITokenizationPrecompile constant precompile =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
function transferWithHelpers(
uint256 collectionId,
address to,
uint256 amount,
uint256 tokenId
) external returns (bool) {
address[] memory recipients = new address[](1);
recipients[0] = to;
// Use helper library for cleaner code
TokenizationTypes.UintRange[] memory tokenIds = new TokenizationTypes.UintRange[](1);
tokenIds[0] = TokenizationHelpers.createSingleTokenIdRange(tokenId);
TokenizationTypes.UintRange[] memory ownershipTimes = new TokenizationTypes.UintRange[](1);
ownershipTimes[0] = TokenizationHelpers.createFullOwnershipTimeRange();
return precompile.transferTokens(
collectionId,
recipients,
amount,
tokenIds,
ownershipTimes
);
}
}
contract ComplianceToken {
ITokenizationPrecompile constant precompile =
ITokenizationPrecompile(0x0000000000000000000000000000000000001001);
uint256 public kycRegistryId;
uint256 public collectionId;
function initialize(uint256 _collectionId) external {
collectionId = _collectionId;
// Create KYC registry (default: false = not KYC'd)
kycRegistryId = precompile.createDynamicStore(
false,
"ipfs://...", // URI
"" // customData
);
}
function setKYC(address user, bool isKYCd) external {
precompile.setDynamicStoreValue(kycRegistryId, user, isKYCd);
}
function isKYCd(address user) public view returns (bool) {
string memory getValueJson = TokenizationJSONHelpers.getDynamicStoreValueJSON(
kycRegistryId,
user
);
DynamicStoreValueResult memory result = precompile.getDynamicStoreValue(getValueJson);
// Use the field that matches your store's value type (e.g. result.verified for a boolean store)
return result.verified;
}
function transfer(address to, uint256 amount, uint256 tokenId) external {
require(isKYCd(msg.sender), "Sender not KYC'd");
require(isKYCd(to), "Recipient not KYC'd");
// Proceed with transfer...
}
}
bitbadgeschaind tx bank send <validator> <your-address> 1000000000ubadge --keyring-backend test