Second-class EVM chain? If this is a second-class EVM chain integration,
disregard the rest of this skill. Load and follow the contract at
.claude/contracts/second-class-evm-chain.md instead - it contains the
complete, authoritative checklist of every integration point required.
Use the contract as your build todo list, checking off items as you go.
Chain Integration Skill
You are helping integrate a new blockchain as a second-class citizen into ShapeShift Web and HDWallet. This means basic support (native asset send/receive, account derivation, swaps to/from the chain) using the "poor man's" approach similar to Monad, Tron, and Sui - public RPC, no microservices, minimal features.
When This Skill Activates
Use this skill when the user wants to:
- "Add support for [ChainName]"
- "Integrate [ChainName] as second-class citizen"
- "Implement basic [ChainName] support"
- "Add [ChainName] with native wallet only"
Critical Understanding
Second-Class Citizen Pattern
Recent examples: Monad (EVM), Tron (UTXO-like), Sui (non-EVM)
What it includes:
- ✅ Native asset sends/receives
- ✅ Account derivation (Native wallet required, Ledger optional)
- ✅ Swap to/from the chain
- ✅ Poor man's balance updates (public RPC polling)
- ✅ Poor man's tx status (RPC polling with eth_getTransactionReceipt or equivalent)
- ✅ Feature flag gating
What it DOESN'T include:
- ❌ Full transaction history (no microservices)
- ❌ First-class Unchained API support
- ❌ Advanced features (staking, DeFi, etc.)
- ❌ All wallet support (usually just Native initially)
Development Flow
ALWAYS follow this order:
- HDWallet Native Support (workspace packages under
packages/hdwallet-*) - Web Basic Support (poor man's chain adapter)
- Web Plugin & Integration (wire everything up)
- Ledger Support (
packages/hdwallet-ledger- if chain is supported by Ledger)
Phase 0: Deep Research & Information Gathering
CRITICAL: This phase determines the entire integration strategy. Take time to research thoroughly.
Step 0.1: Initial Chain Discovery
First, search for basic chain information:
Search for official chain website and docs
- Use WebSearch to find: "[ChainName] blockchain official website"
- Look for: developer docs, whitepaper, GitHub repos
Determine chain architecture
- CRITICAL QUESTION: Is this an EVM-compatible chain?
- Search: "[ChainName] EVM compatible"
- Look for keywords: "Ethereum Virtual Machine", "Solidity", "EVM-compatible", "Ethereum fork"
- Check if they mention Metamask compatibility
Find RPC endpoints
- Search: "[ChainName] public RPC endpoint"
- Check official docs for RPC URLs
- Look for ChainList entry: https://chainlist.org
- Check: https://github.com/arddluma/awesome-list-rpc-nodes-providers
Why this matters:
- EVM chains (like Monad): 90% less code! Just add to EVM chains list. Auto-supported by all EVM wallets.
- Non-EVM chains (like Tron, Sui): Need full custom implementation with crypto adapters.
Step 0.2: Interactive Information Gathering
Use the AskUserQuestion tool with the Claude inquiry UI to gather information.
Question 1 - Chain Architecture (MOST IMPORTANT):
Does the user know if this is an EVM-compatible chain?
Options:
1. "Yes, it's EVM-compatible" → Proceed with EVM integration path (much simpler!)
2. "No, it's a custom blockchain" → Proceed with non-EVM integration path
3. "Not sure - can you research it?" → Perform web research (search for EVM compatibility indicators)
Context: EVM-compatible chains like Monad require minimal code changes (just add to supported chains list). Non-EVM chains like Tron/Sui require full custom crypto adapters.
Question 2 - RPC Endpoint:
Do you have a public RPC endpoint URL?
Options:
1. "Yes, here's the URL: [input]" → Use provided URL
2. "No, can you find one?" → Search ChainList.org, official docs, and GitHub for public RPC
3. "Need both HTTP and WebSocket" → Search for both endpoint types
Context: We need a reliable public RPC for the poor man's chain adapter. WebSocket is optional but nice for real-time updates.
Question 3 - SLIP44 Coin Type:
Do you know the SLIP44 coin type (BIP44 derivation path)?
Options:
1. "Yes, it's [number]" → Use provided coin type
2. "No, can you look it up?" → Search SLIP44 registry: https://github.com/satoshilabs/slips/blob/master/slip-0044.md
3. "Use the same as Ethereum (60)" → Common for EVM chains
Context: This determines the BIP44 derivation path: m/44'/[TYPE]'/0'/0/0
Step 0.3: Structured Information Collection
After determining chain type (EVM or non-EVM), collect remaining details:
Use AskUserQuestion to ask:
For ALL chains:
Chain Basic Info
- Chain name (exact capitalization, e.g., "Monad", "Tron", "Sui")
- SLIP44 coin type (from Step 0.2 above)
- Chain ID (numeric or string, e.g., "1" for Ethereum, "monad-1", etc.)
Documentation Links
- Official website URL
- Developer documentation URL
- Block explorer URL
- GitHub repository (if available)
Asset Information
- Native asset symbol (e.g., MON, TRX, SUI)
- Native asset name (e.g., "Monad", "Tron", "Sui")
- Decimals/precision (usually 18 for EVM, varies for others)
- CoinGecko ID (search: "coingecko [chainname]" or ask user)
For EVM chains only:
- EVM-Specific Info
- Network/Chain ID (numeric, e.g., 41454 for Monad)
- Token standard: ERC20 (always)
- Block explorer API (etherscan-like)?
- Any non-standard behavior vs Ethereum?
For non-EVM chains only:
Chain Architecture Details
- Transaction structure/format (link to docs)
- Signing algorithm (secp256k1, ed25519, etc.)
- Address format (base58, bech32, hex, etc.)
- Official SDK (npm package name if available)
- Token standard name (e.g., "TRC20", "SUI Coin", "SPL")
Ledger Hardware Wallet Support
- Search: "Ledger [ChainName] support"
- Check: https://www.ledger.com/supported-crypto-assets
- Ask user: "Does Ledger support [ChainName]?"
- If yes, note the Ledger app name
Action: Don't proceed until you have:
- ✅ Confirmed EVM vs non-EVM architecture
- ✅ At least one working RPC endpoint
- ✅ SLIP44 coin type
- ✅ Official documentation links
- ✅ Basic asset information (symbol, name, decimals)
Pro Tips:
- For EVM chains: Integration is 10x easier. You mostly just add constants.
- For non-EVM: Budget extra time for crypto adapter implementation.
- Missing RPC? Check ChainList.org, official Discord, or GitHub repos.
- Missing SLIP44? Check if it's in SLIP-0044 registry or propose one.
- Can't find CoinGecko ID? Search their API or website directly.
Integration Path Decision
Based on Phase 0 research, choose your path:
Path A: EVM Chain Integration (SIMPLE)
Examples: Monad, Base, Arbitrum, Optimism
Characteristics:
- ✅ Uses Ethereum Virtual Machine
- ✅ Solidity smart contracts
- ✅ ERC20 token standard
- ✅ Web3/ethers.js compatible
- ✅ Auto-supported by MetaMask, Ledger Ethereum app
What you'll do:
- HDWallet: Just add chain ID to EVM chains list (~10 lines of code)
- Web: Extend EvmBaseAdapter (~100 lines)
- Everything else: Add constants and config
Time estimate: 2-4 hours for basic integration
Path B: Non-EVM Chain Integration (COMPLEX)
Examples: Tron, Sui, Cosmos, Solana
Characteristics:
- ❌ Custom virtual machine (not EVM)
- ❌ Custom smart contract language
- ❌ Custom token standard
- ❌ Custom transaction format
- ❌ Requires chain-specific crypto implementation
What you'll do:
- HDWallet: Implement full chain module with crypto adapters (~500-1000 lines)
- Web: Implement full IChainAdapter interface (~500-1000 lines)
- Everything else: Add constants and config
Time estimate: 1-2 days for basic integration
Phase 1: HDWallet Native Support
Working Directory: Same monorepo — hdwallet packages are at packages/hdwallet-*
Step 1.0: Choose Implementation Strategy
If EVM chain: Continue with Step 1.2-EVM below (MINIMAL hdwallet work - ~30 minutes) If non-EVM chain: Continue with Step 1.1 below (COMPLEX - 1-2 days)
⚡ EVM Chains: Minimal HDWallet Work Required
For EVM-compatible chains (like Monad, HyperEVM, Base), you need MINIMAL changes to hdwallet:
What EVM chains DON'T need:
- ❌ No new core interfaces (TronWallet, SuiWallet, etc.)
- ❌ No crypto adapters (address derivation, signing)
- ❌ No wallet mixins
- ✅ Use existing Ethereum crypto (secp256k1, Keccak256)
What EVM chains DO need:
- ✅ Wallet support flags (
_supportsChainName: boolean) - ✅ Support function (
supportsChainName()) - ✅ Set flags on all wallet implementations (~14 files)
- ✅ Build and verify with
pnpm run hdwallet:build
Why? Each wallet type (Native, Ledger, MetaMask, etc.) needs to explicitly declare support for the chain, even though the crypto is identical. This enables wallet-specific gating in the UI.
Reference PRs:
- Monad hdwallet: https://github.com/shapeshift/hdwallet/pull/753
- HyperEVM hdwallet: https://github.com/shapeshift/hdwallet/pull/756
Time estimate: 30 minutes for hdwallet changes (vs 1-2 days for non-EVM)
Step 1.1: Research HDWallet Patterns (Non-EVM Only)
Examine existing implementations to understand patterns:
For non-EVM chains (like Tron, Sui):
# In the monorepo
cat packages/hdwallet-core/src/tron.ts
cat packages/hdwallet-native/src/tron.ts
cat packages/hdwallet-native/src/crypto/isolation/adapters/tron.ts
Key pattern: Need new core interfaces, native implementation, and crypto adapters for signing.
Step 1.2-EVM: EVM Chain Implementation (SIMPLE PATH)
For EVM chains only (like Monad):
File: packages/hdwallet-core/src/ethereum.ts
Add your chain to supported EVM chains:
// Find the list of supported chain IDs and add yours
export const SUPPORTED_EVM_CHAINS = [
1, // Ethereum
10, // Optimism
// ... other chains
41454, // Add your chain ID here (example: Monad)
]
File: packages/hdwallet-core/src/utils.ts
Register SLIP44 if not using Ethereum's (60):
// If your chain uses a different SLIP44 than Ethereum
{ slip44: YOUR_SLIP44, symbol: 'SYMBOL', name: 'ChainName' }
That's it for hdwallet! EVM chains don't need crypto adapters. Skip to Step 1.6 (Version Bump).
Step 1.2-EVM: EVM Chain HDWallet Support (MINIMAL WORK - ~30 minutes)
For EVM chains only (like Monad, HyperEVM). Follow these PRs as reference:
- Monad hdwallet: https://github.com/shapeshift/hdwallet/pull/753
- HyperEVM hdwallet: https://github.com/shapeshift/hdwallet/pull/756
File: packages/hdwallet-core/src/ethereum.ts
Add your chain's support flag to the ETHWalletInfo interface:
export interface ETHWalletInfo extends HDWalletInfo {
// ... existing flags
readonly _supportsMonad: boolean;
readonly _supportsHyperEvm: boolean; // ADD THIS
// ...
}
File: packages/hdwallet-core/src/wallet.ts
Add support function after supportsMonad:
export function supportsMonad(wallet: HDWallet): wallet is ETHWallet {
return isObject(wallet) && (wallet as any)._supportsMonad;
}
export function supports[ChainName](wallet: HDWallet): wallet is ETHWallet {
return isObject(wallet) && (wallet as any)._supports[ChainName];
}
Set flags on ALL wallet implementations (~12 files):
For second-class EVM chains (HyperEVM, Monad, Plasma):
Set readonly _supports[ChainName] = true on:
- packages/hdwallet-native/src/ethereum.ts
- packages/hdwallet-metamask-multichain/src/shapeshift-multichain.ts (uses standard EVM cryptography)
- packages/hdwallet-ledger/src/ledger.ts (uses Ethereum app, supports all EVM chains)
- packages/hdwallet-trezor/src/trezor.ts (uses Ethereum app, supports all EVM chains)
- packages/hdwallet-walletconnectv2/src/walletconnectv2.ts (chain-agnostic, supports all EVM chains)
Set readonly _supports[ChainName] = false on:
- packages/hdwallet-coinbase/src/coinbase.ts
- packages/hdwallet-gridplus/src/gridplus.ts
- packages/hdwallet-keepkey/src/keepkey.ts
- packages/hdwallet-keplr/src/keplr.ts
- packages/hdwallet-phantom/src/phantom.ts
- packages/hdwallet-vultisig/src/vultisig.ts
For non-EVM chains:
Set readonly _supports[ChainName] = true for Native only:
- packages/hdwallet-native/src/ethereum.ts (or appropriate chain file)
Set readonly _supports[ChainName] = false on all other wallet types listed above.
Then: Skip to Step 1.6 (Build & Verify)
Step 1.2-NonEVM: Non-EVM Core Interfaces (COMPLEX PATH)
File: packages/hdwallet-core/src/[chainname].ts
Create core TypeScript interfaces following the pattern:
import { addressNListToBIP32, slip44ByCoin } from "./utils";
import { BIP32Path, HDWallet, HDWalletInfo, PathDescription } from "./wallet";
export interface [Chain]GetAddress {
addressNList: BIP32Path;
showDisplay?: boolean;
pubKey?: string;
}
export interface [Chain]SignTx {
addressNList: BIP32Path;
// Chain-specific tx data fields
rawDataHex?: string; // or other fields
}
export interface [Chain]SignedTx {
serialized: string;
signature: string;
}
export interface [Chain]TxSignature {
signature: string;
}
export interface [Chain]GetAccountPaths {
accountIdx: number;
}
export interface [Chain]AccountPath {
addressNList: BIP32Path;
}
export interface [Chain]WalletInfo extends HDWalletInfo {
readonly _supports[Chain]Info: boolean;
[chainLower]GetAccountPaths(msg: [Chain]GetAccountPaths): Array<[Chain]AccountPath>;
[chainLower]NextAccountPath(msg: [Chain]AccountPath): [Chain]AccountPath | undefined;
}
export interface [Chain]Wallet extends [Chain]WalletInfo, HDWallet {
readonly _supports[Chain]: boolean;
[chainLower]GetAddress(msg: [Chain]GetAddress): Promise<string | null>;
[chainLower]SignTx(msg: [Chain]SignTx): Promise<[Chain]SignedTx | null>;
}
export function [chainLower]DescribePath(path: BIP32Path): PathDescription {
const pathStr = addressNListToBIP32(path);
const unknown: PathDescription = {
verbose: pathStr,
coin: "[ChainName]",
isKnown: false,
};
if (path.length != 5) return unknown;
if (path[0] != 0x80000000 + 44) return unknown;
if (path[1] != 0x80000000 + slip44ByCoin("[ChainName]")) return unknown;
if ((path[2] & 0x80000000) >>> 0 !== 0x80000000) return unknown;
if (path[3] !== 0) return unknown;
if (path[4] !== 0) return unknown;
const index = path[2] & 0x7fffffff;
return {
verbose: `[ChainName] Account #${index}`,
accountIdx: index,
wholeAccount: true,
coin: "[ChainName]",
isKnown: true,
};
}
// Standard BIP44 derivation: m/44'/SLIP44'/<account>'/0/0
export function [chainLower]GetAccountPaths(msg: [Chain]GetAccountPaths): Array<[Chain]AccountPath> {
const slip44 = slip44ByCoin("[ChainName]");
return [{ addressNList: [0x80000000 + 44, 0x80000000 + slip44, 0x80000000 + msg.accountIdx, 0, 0] }];
}
Export from core:
// In packages/hdwallet-core/src/index.ts
export * from './[chainname]'
Register SLIP44:
// In packages/hdwallet-core/src/utils.ts
// Add to slip44Table
{ slip44: SLIP44, symbol: '[SYMBOL]', name: '[ChainName]' }
Step 1.3: Implement Native Wallet Support
File: packages/hdwallet-native/src/[chainname].ts
import * as core from "@shapeshiftoss/hdwallet-core";
import { Isolation } from "./crypto";
import { [Chain]Adapter } from "./crypto/isolation/adapters/[chainname]";
import { NativeHDWalletBase } from "./native";
export function MixinNative[Chain]WalletInfo<TBase extends core.Constructor<core.HDWalletInfo>>(Base: TBase) {
return class MixinNative[Chain]WalletInfo extends Base implements core.[Chain]WalletInfo {
readonly _supports[Chain]Info = true;
[chainLower]GetAccountPaths(msg: core.[Chain]GetAccountPaths): Array<core.[Chain]AccountPath> {
return core.[chainLower]GetAccountPaths(msg);
}
[chainLower]NextAccountPath(msg: core.[Chain]AccountPath): core.[Chain]AccountPath | undefined {
throw new Error("Method not implemented");
}
};
}
export function MixinNative[Chain]Wallet<TBase extends core.Constructor<NativeHDWalletBase>>(Base: TBase) {
return class MixinNative[Chain]Wallet extends Base {
readonly _supports[Chain] = true;
[chainLower]Adapter: [Chain]Adapter | undefined;
async [chainLower]InitializeWallet(masterKey: Isolation.Core.BIP32.Node): Promise<void> {
const nodeAdapter = await Isolation.Adapters.BIP32.create(masterKey);
this.[chainLower]Adapter = new [Chain]Adapter(nodeAdapter);
}
[chainLower]Wipe() {
this.[chainLower]Adapter = undefined;
}
async [chainLower]GetAddress(msg: core.[Chain]GetAddress): Promise<string | null> {
return this.needsMnemonic(!!this.[chainLower]Adapter, () => {
return this.[chainLower]Adapter!.getAddress(msg.addressNList);
});
}
async [chainLower]SignTx(msg: core.[Chain]SignTx): Promise<core.[Chain]SignedTx | null> {
return this.needsMnemonic(!!this.[chainLower]Adapter, async () => {
const address = await this.[chainLower]GetAddress({
addressNList: msg.addressNList,
showDisplay: false,
});
if (!address) throw new Error("Failed to get [ChainName] address");
const signature = await this.[chainLower]Adapter!.signTransaction(
msg.rawDataHex,
msg.addressNList
);
return {
serialized: msg.rawDataHex + signature,
signature,
};
});
}
};
}
Integrate into NativeHDWallet:
// In packages/hdwallet-native/src/native.ts
// Add mixin to class hierarchy
// Add initialization in initialize() method
// Add wipe in wipe() method
Step 1.4: Create Crypto Adapter (Non-EVM only)
File: packages/hdwallet-native/src/crypto/isolation/adapters/[chainname].ts
Implement chain-specific cryptography:
- Address generation algorithm
- Transaction signing
- Any chain-specific encoding
Reference: See tron.ts adapter for a complete example
Export adapter:
// In packages/hdwallet-native/src/crypto/isolation/adapters/index.ts
export * from './[chainname]'
Step 1.5: Update Core Wallet Interface
File: packages/hdwallet-core/src/wallet.ts
Add support check function:
export function supports[Chain](wallet: HDWallet): wallet is [Chain]Wallet {
return !!(wallet as any)._supports[Chain]
}
Step 1.6: Build & Verify
# Build hdwallet packages to verify
pnpm run hdwallet:build
# Run hdwallet tests
pnpm run hdwallet:test
No version bumps or publishing needed — hdwallet packages are workspace packages (workspace:^).
Phase 2: Web Chain Adapter (Poor Man's Approach)
Step 3.1: Add Chain Constants
File: packages/caip/src/constants.ts
// Add chain ID constant
export const [chainLower]ChainId = '[caip19-format]' as const // e.g., 'eip155:1', 'tron:0x2b6653dc', etc.
// Add asset ID constant
export const [chainLower]AssetId = '[caip19-format]' as AssetId
// Add asset reference constant
export const ASSET_REFERENCE = {
// ...
[ChainName]: 'slip44:COINTYPE',
}
// Add to KnownChainIds enum
export enum KnownChainIds {
// ...
[ChainName]Mainnet = '[caip2-chain-id]',
}
// Add to asset namespace if needed (non-EVM chains)
export const ASSET_NAMESPACE = {
// ...
[tokenStandard]: '[namespace]', // e.g., trc20, suiCoin
}
File: packages/types/src/base.ts
// Add to KnownChainIds enum (duplicate but required)
export enum KnownChainIds {
// ...
[ChainName]Mainnet = '[caip2-chain-id]',
}
File: src/constants/chains.ts
// Add to second-class chains array
export const SECOND_CLASS_CHAINS = [
// ...
KnownChainIds.[ChainName]Mainnet,
]
// Add to feature-flag gated chains
Step 3.2: Create Chain Adapter
Directory: packages/chain-adapters/src/[adaptertype]/[chainname]/
For EVM Chains (SIMPLE!)
Extend SecondClassEvmAdapter - you only need ~50 lines!
File: packages/chain-adapters/src/evm/[chainname]/[ChainName]ChainAdapter.ts
import { ASSET_REFERENCE, [chainLower]AssetId } from '@shapeshiftoss/caip'
import type { AssetId } from '@shapeshiftoss/caip'
import type { RootBip44Params } from '@shapeshiftoss/types'
import { KnownChainIds } from '@shapeshiftoss/types'
import { ChainAdapterDisplayName } from '../../types'
import { SecondClassEvmAdapter } from '../SecondClassEvmAdapter'
import type { TokenInfo } from '../SecondClassEvmAdapter'
const SUPPORTED_CHAIN_IDS = [KnownChainIds.[ChainName]Mainnet]
const DEFAULT_CHAIN_ID = KnownChainIds.[ChainName]Mainnet
export type ChainAdapterArgs = {
rpcUrl: string
knownTokens?: TokenInfo[]
}
export const is[ChainName]ChainAdapter = (adapter: unknown): adapter is ChainAdapter => {
return (adapter as ChainAdapter).getType() === KnownChainIds.[ChainName]Mainnet
}
export class ChainAdapter extends SecondClassEvmAdapter<KnownChainIds.[ChainName]Mainnet> {
public static readonly rootBip44Params: RootBip44Params = {
purpose: 44,
coinType: Number(ASSET_REFERENCE.[ChainName]),
accountNumber: 0,
}
constructor(args: ChainAdapterArgs) {
super({
assetId: [chainLower]AssetId,
chainId: DEFAULT_CHAIN_ID,
rootBip44Params: ChainAdapter.rootBip44Params,
supportedChainIds: SUPPORTED_CHAIN_IDS,
rpcUrl: args.rpcUrl,
knownTokens: args.knownTokens ?? [],
})
}
getDisplayName() {
return ChainAdapterDisplayName.[ChainName]
}
getName() {
return '[ChainName]'
}
getType(): KnownChainIds.[ChainName]Mainnet {
return KnownChainIds.[ChainName]Mainnet
}
getFeeAssetId(): AssetId {
return this.assetId
}
}
export type { TokenInfo }
That's it! SecondClassEvmAdapter automatically provides:
- ✅ Account balance fetching (native + ERC-20 tokens via multicall)
- ✅ Fee estimation
- ✅ Transaction broadcasting
- ✅ Transaction parsing with ERC-20 event decoding (for execution price)
- ✅ Rate limiting via PQueue
- ✅ Multicall batching for token balances
Just follow the pattern from HyperEVM, Monad, or Plasma adapters.
For Non-EVM Chains (COMPLEX)
Implement IChainAdapter interface - requires custom crypto adapters and ~500-1000 lines.
Key Methods to Implement:
getAccount()- Get balances (native + tokens)getAddress()- Derive address from walletgetFeeData()- Estimate transaction feesbroadcastTransaction()- Submit signed tx to networkbuildSendApiTransaction()- Build unsigned txsignTransaction()- Sign with walletparseTx()- Parse transaction (can stub out)getTxHistory()- Get tx history (stub out - return empty)
Poor Man's Patterns:
- No Unchained: Use public RPC directly (@mysten/sui, tronweb, etc.)
- No TX History: Stub out
getTxHistory()to return empty array - Direct RPC Polling: Use chain-specific RPC for tx status
File: packages/chain-adapters/src/[chainname]/[ChainName]ChainAdapter.ts
See SuiChainAdapter.ts or TronChainAdapter.ts for complete examples.
Export:
// In packages/chain-adapters/src/[adaptertype]/[chainname]/index.ts
export * from './[ChainName]ChainAdapter'
export * from './types'
// In packages/chain-adapters/src/[adaptertype]/index.ts
export * as [chainLower] from './[chainname]'
// In packages/chain-adapters/src/index.ts
export * from './[adaptertype]'
Step 3.2a: Implement parseTx (Iterative Approach)
CRITICAL: The parseTx() method parses transaction data after broadcast. This determines:
- Whether the transaction shows in history (if applicable)
- Execution price calculation for swaps
- Transfer display (from/to/value)
Reference Implementations (use these as patterns):
- EVM chains:
SecondClassEvmAdapter.parseTx()- handles ERC-20 Transfer events automatically - Sui:
SuiChainAdapter.parseTx()- parses SUI native and coin transfers - Tron:
TronChainAdapter.parseTx()- parses TRC-20 transfers - NEAR:
NearChainAdapter.parseTx()+parseNep141Transfers()- parses NEP-141 token logs
Iterative Development Flow:
- Start with naive implementation - Parse native asset transfers only:
async parseTx(txHash: string, pubkey: string): Promise<Transaction> {
const result = await this.rpcCall('getTransaction', [txHash])
// Basic structure
const status = result.success ? TxStatus.Confirmed : TxStatus.Failed
const fee = { assetId: this.assetId, value: result.fee.toString() }
const transfers: Transfer[] = []
// Parse native transfers (naive - just native asset)
if (result.value) {
transfers.push({
assetId: this.assetId,
from: [result.from],
to: [result.to],
type: result.from === pubkey ? TransferType.Send : TransferType.Receive,
value: result.value.toString(),
})
}
return { txid: txHash, status, fee, transfers, /* ... */ }
}
User testing reveals issues - User tests sends/swaps and reports:
- "Native send works but tokens don't show"
- "Swap execution price is wrong"
- Provides RPC response from debugger
Refine with actual RPC response - User provides debugger scope:
// Example: User provides RPC response showing token events in logs
// You then add token parsing logic based on actual data structure
private parseTokenTransfers(result: RpcResult, pubkey: string): Transfer[] {
const transfers: Transfer[] = []
// Parse token events from logs/events
for (const event of result.events || []) {
if (event.type === 'token_transfer') {
// Token-specific parsing based on actual RPC structure
}
}
return transfers
}
Key considerations for parseTx:
| Aspect | Native Asset | Tokens | Internal Transfers |
|---|---|---|---|
| Where to find | Transaction value field | Event logs / receipts | Nested calls / traces |
| Asset ID | this.assetId |
chainId/namespace:contractAddress |
Varies |
| pubkey comparison | Usually sender field | Event old_owner/new_owner | May be nested |
Common patterns by chain type:
EVM Chains (SecondClassEvmAdapter handles automatically):
- Native:
tx.value - Tokens: ERC-20 Transfer events in logs
- Uses
ethers.Interface.parseLog()to decode events
Non-EVM Chains (must implement manually):
// NEAR pattern - EVENT_JSON logs
for (const log of receipt.outcome.logs) {
if (!log.startsWith('EVENT_JSON:')) continue
const event = JSON.parse(log.slice('EVENT_JSON:'.length))
if (event.standard === 'nep141' && event.event === 'ft_transfer') {
// Parse transfer from event.data
}
}
// Sui pattern - coin type from object changes
for (const change of result.objectChanges) {
if (change.type === 'mutated' && change.objectType.includes('::coin::Coin<')) {
// Extract coin type and amount
}
}
// Tron pattern - TRC20 logs
for (const log of result.log || []) {
if (log.topics[0] === TRC20_TRANSFER_TOPIC) {
// Decode TRC20 transfer
}
}
pubkey vs account ID gotcha:
- Some chains pass
pubkeyas hex public key - But logs/events use account addresses (e.g.,
alice.near, base58, etc.) - May need to convert:
const accountId = pubKeyToAddress(pubkey)
When to ask user for debugger scope:
- Initial naive implementation doesn't catch tokens
- Swap execution prices are wrong
- Internal transfers missing
Example request to user:
"The parseTx implementation needs refinement for token transfers. Can you:
- Make a token send/swap
- Set a breakpoint in parseTx()
- Share the
resultvariable from the RPC response This will help me see the actual data structure for token events."
Step 3.3: Add Utility Functions
File: packages/utils/src/getAssetNamespaceFromChainId.ts
case [chainLower]ChainId:
return ASSET_NAMESPACE.[tokenStandard]
File: packages/utils/src/getChainShortName.ts
case KnownChainIds.[ChainName]Mainnet:
return '[SHORT]'
File: packages/utils/src/getNativeFeeAssetReference.ts
case KnownChainIds.[ChainName]Mainnet:
return ASSET_REFERENCE.[ChainName]
File: packages/utils/src/chainIdToFeeAssetId.ts
[chainLower]ChainId: [chainLower]AssetId,
File: packages/utils/src/assetData/baseAssets.ts
// Add base asset
export const [chainLower]BaseAsset: Asset = {
assetId: [chainLower]AssetId,
chainId: [chainLower]ChainId,
name: '[ChainName]',
symbol: '[SYMBOL]',
precision: [DECIMALS],
icon: '[iconUrl]',
explorer: '[explorerUrl]',
// ... other fields
}
File: packages/utils/src/assetData/getBaseAsset.ts
case [chainLower]ChainId:
return [chainLower]BaseAsset
Step 3.4: Create Chain Utils (Transaction Status)
File: src/lib/utils/[chainname].ts
import { [chainLower]ChainId } from '@shapeshiftoss/caip'
import type { ChainAdapter } from '@shapeshiftoss/chain-adapters'
import { assertUnreachable } from '@/lib/utils'
import type { TxStatus } from '@/state/slices/txHistorySlice/txHistorySlice'
export const is[Chain]ChainAdapter = (adapter: unknown): adapter is [ChainAdapter] => {
return (adapter as ChainAdapter).getChainId() === [chainLower]ChainId
}
export const assertGet[Chain]ChainAdapter = (
adapter: ChainAdapter,
): asserts adapter is [ChainAdapter] => {
if (!is[Chain]ChainAdapter(adapter)) {
throw new Error('[ChainName] adapter required')
}
}
// Implement getTxStatus using chain-specific RPC calls
export const get[Chain]TransactionStatus = async (
txHash: string,
adapter: [ChainAdapter],
): Promise<TxStatus> => {
// Use chain client to check transaction status
// Return 'confirmed', 'failed', or 'unknown'
// See monad.ts / sui.ts for examples
}
Step 3.5: Wire Up Transaction Status Polling
File: src/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsx
Add case for your chain:
case KnownChainIds.[ChainName]Mainnet:
txStatus = await get[Chain]TransactionStatus(txHash, adapter)
break
Step 3.6: Add Account Derivation
File: src/lib/account/[chainname].ts
import { [chainLower]ChainId, toAccountId } from '@shapeshiftoss/caip'
import type { AccountMetadata, AccountMetadataByAccountId } from '@shapeshiftoss/types'
import { KnownChainIds } from '@shapeshiftoss/types'
import { assertGet[Chain]ChainAdapter, is[Chain]ChainAdapter } from '@/lib/utils/[chainname]'
export const derive[Chain]AccountIdsAndMetadata = async (
args: // standard args
): Promise<AccountMetadataByAccountId> => {
const { accountNumber, chainIds, wallet } = args
const adapter = adapterManager.get([chainLower]ChainId)
if (!adapter) throw new Error('[ChainName] adapter not available')
assertGet[Chain]ChainAdapter(adapter)
const address = await adapter.getAddress({
wallet,
accountNumber,
})
const accountId = toAccountId({ chainId: [chainLower]ChainId, account: address })
const account = await adapter.getAccount(address)
const metadata: AccountMetadata = {
accountType: 'native',
bip44Params: adapter.getBip44Params({ accountNumber }),
}
return {
[accountId]: metadata,
}
}
Wire into account dispatcher:
// In src/lib/account/account.ts
case KnownChainIds.[ChainName]Mainnet:
return derive[Chain]AccountIdsAndMetadata(args)
Step 3.7: Add Wallet Support Detection
File: src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts
// Add to switch statement
case KnownChainIds.[ChainName]Mainnet:
return supports[Chain](wallet) // from hdwallet-core
Step 3.8: Add Asset Support Detection (CRITICAL!)
IMPORTANT: This was missing for recent chains (Tron, SUI, Monad, HyperEVM, Plasma) and caused assets not to show up properly!
File: src/state/slices/portfolioSlice/utils/index.ts
Add your chain to the isAssetSupportedByWallet function around line 367:
// 1. Import your chain ID at the top
import {
// ... existing imports
[chainLower]ChainId,
} from '@shapeshiftoss/caip'
// 2. Import the support function from hdwallet-core
import {
// ... existing imports
supports[ChainName],
} from '@shapeshiftoss/hdwallet-core'
// 3. Add case to the switch statement in isAssetSupportedByWallet
export const isAssetSupportedByWallet = (assetId: AssetId, wallet: HDWallet): boolean => {
if (!assetId) return false
const { chainId } = fromAssetId(assetId)
switch (chainId) {
// ... existing cases
case [chainLower]ChainId:
return supports[ChainName](wallet)
// ... rest of cases
default:
return false
}
}
Why this matters: This function determines if a wallet can use a particular asset. Without it, assets for your chain won't appear in wallet UIs even if everything else is configured correctly!
Example: For HyperEVM, add:
case hyperEvmChainId:
return supportsHyperEvm(wallet)
Phase 4: Web Plugin & Feature Flags
Step 4.1: Create Plugin
File: src/plugins/[chainname]/index.tsx
import { [chainLower]ChainId } from '@shapeshiftoss/caip'
import { [chainLower] } from '@shapeshiftoss/chain-adapters'
import { KnownChainIds } from '@shapeshiftoss/types'
import { getConfig } from '@/config'
import type { Plugins } from '@/plugins/types'
export default function register(): Plugins {
return [
[
'[chainLower]ChainAdapter',
{
name: '[chainLower]ChainAdapter',
featureFlag: ['[ChainName]'],
providers: {
chainAdapters: [
[
KnownChainIds.[ChainName]Mainnet,
() => {
return new [chainLower].ChainAdapter({
rpcUrl: getConfig().VITE_[CHAIN]_NODE_URL,
// Add other config as needed
})
},
],
],
},
},
],
]
}
Register plugin:
// In src/plugins/activePlugins.ts
import [chainLower] from './[chainname]'
export const activePlugins = [
// ...
[chainLower],
]
Gate in provider:
// In src/context/PluginProvider/PluginProvider.tsx
// Add feature flag check for your chain
Step 4.2: Environment Variables
File: .env
VITE_[CHAIN]_NODE_URL=[default-public-rpc]
VITE_FEATURE_[CHAIN]=false
File: .env.development
VITE_[CHAIN]_NODE_URL=[dev-rpc]
VITE_FEATURE_[CHAIN]=true
File: .env.production
VITE_[CHAIN]_NODE_URL=[prod-rpc]
VITE_FEATURE_[CHAIN]=false
Step 4.3: Config Validation
File: src/config.ts
const validators = {
// ...
VITE_[CHAIN]_NODE_URL: url(),
VITE_FEATURE_[CHAIN]: bool({ default: false }),
}
Step 4.4: Feature Flag State
File: src/state/slices/preferencesSlice/preferencesSlice.ts
export type FeatureFlags = {
// ...
[ChainName]: boolean
}
const initialState: PreferencesState = {
featureFlags: {
// ...
[ChainName]: getConfig().VITE_FEATURE_[CHAIN],
},
}
Add to test mocks:
// In src/test/mocks/store.ts
featureFlags: {
// ...
[ChainName]: false,
}
Step 4.5: CSP Headers
File: headers/csps/chains/[chainname].ts
const [chainLower]: Csp = {
'connect-src': [env.VITE_[CHAIN]_NODE_URL],
}
export default [chainLower]
Register CSP:
// In headers/csps/index.ts
import [chainLower] from './chains/[chainname]'
export default [
// ...
[chainLower],
]
Phase 5: Asset Generation
Step 5.1: CoinGecko Adapter Integration
CRITICAL: This step is required for asset discovery and pricing! See PR #11257 for Monad example.
File: packages/caip/src/adapters/coingecko/index.ts
Add your chain to the CoingeckoAssetPlatform enum and import the chain ID:
// Add import at top
import {
// ... existing imports
[chainLower]ChainId,
} from '../../constants'
// Add platform constant
export enum CoingeckoAssetPlatform {
// ... existing platforms
[ChainName] = '[coingecko-platform-id]', // e.g., 'hyperevm' for HyperEVM
}
File: packages/caip/src/adapters/coingecko/index.ts (3 touchpoints — enum + 2 switch statements)
CRITICAL: This file has 3 separate places to update. Missing any causes runtime failures.
Touchpoint 1 — Already shown above: CoingeckoAssetPlatform enum entry.
Touchpoint 2 — chainIdToCoingeckoAssetPlatform() forward mapping (CHAIN_REFERENCE → platform):
// For EVM chains, add to the EVM switch (inside chainNamespace Evm case)
case CHAIN_REFERENCE.[ChainName]Mainnet:
return CoingeckoAssetPlatform.[ChainName]
Touchpoint 3 — coingeckoAssetPlatformToChainId() reverse mapping (platform → chainId):
case CoingeckoAssetPlatform.[ChainName]:
return [chainLower]ChainId
NOTE: This reverse mapping requires importing [chainLower]ChainId from ../../constants. Only import what is used — chainIdToCoingeckoAssetPlatform uses CHAIN_REFERENCE not chainId constants.
File: packages/caip/src/adapters/coingecko/utils.ts (2 touchpoints)
Touchpoint 4 — Add chainId to buildByChainId loop in COINGECKO_ASSET_PLATFORM_TO_CHAIN_ID_MAP (~line 280-310):
// Import chainId + assetId from constants at top of file
import { [chainLower]AssetId, [chainLower]ChainId, ... } from '../../constants'
// Add to the switch/if chain inside the buildByChainId loop
prev[[chainLower]ChainId][assetId] = id
Touchpoint 5 — Add native asset to COINGECKO_NATIVE_ASSET_PLATFORM_TO_CHAIN_ID_MAP (~line 370-390):
[[chainLower]ChainId]: { [[chainLower]AssetId]: '[coingecko-native-coin-id]' },
// e.g., for Cronos: [cronosChainId]: { [cronosAssetId]: 'crypto-com-chain' }
// e.g., for ETH-native chains: [scrollChainId]: { [scrollAssetId]: 'ethereum' }
File: packages/caip/src/adapters/coingecko/utils.test.ts
Add test case for your chain:
it('returns correct platform for [ChainName]', () => {
expect(chainIdToCoingeckoAssetPlatform([chainLower]ChainId)).toEqual(
CoingeckoAssetPlatform.[ChainName]
)
})
File: packages/caip/src/adapters/coingecko/index.test.ts
Add test asset for your chain:
// Add example asset from your chain to test fixtures
const [chainLower]UsdcAssetId: AssetId = 'eip155:[CHAIN_ID]/erc20:[USDC_ADDRESS]'
// Update test expectations to include your chain's asset
Step 5.2: Create Asset Generator
File: scripts/generateAssetData/[chainname]/index.ts
Follow the pattern from monad/tron/sui:
import { [chainLower]ChainId } from '@shapeshiftoss/caip'
import type { Asset } from '@shapeshiftoss/types'
import { [chainLower], unfreeze } from '@shapeshiftoss/utils'
import * as coingecko from '../coingecko'
export const getAssets = async (): Promise<Asset[]> => {
const assets = await coingecko.getAssets([chainLower]ChainId)
return [...assets, unfreeze([chainLower])]
}
Wire into generator:
- Import in
scripts/generateAssetData/generateAssetData.ts:
import * as [chainLower] from './[chainname]'
- Fetch assets in the
generateAssetData()function:
const [chainLower]Assets = await [chainLower].getAssets()
- Add to unfilteredAssetData array:
...[chainLower]Assets,
Add chain to CoinGecko script:
File: scripts/generateAssetData/coingecko.ts
Import your chain:
import {
…(truncated)