Integrate a new blockchain as a second-class citizen in ShapeShift Web. HDWallet packages live in the monorepo under packages/hdwallet-*. Covers everything from HDWallet native/Ledger support to Web chain adapter, asset generation, and feature flags. Activates when user wants to add basic support for a new blockchain.
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.
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.
Use this skill when the user wants to:
Recent examples: Monad (EVM), Tron (UTXO-like), Sui (non-EVM)
What it includes:
What it DOESN'T include:
ALWAYS follow this order:
packages/hdwallet-*)packages/hdwallet-ledger - if chain is supported by Ledger)CRITICAL: This phase determines the entire integration strategy. Take time to research thoroughly.
First, search for basic chain information:
Search for official chain website and docs
Determine chain architecture
Find RPC endpoints
Why this matters:
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
After determining chain type (EVM or non-EVM), collect remaining details:
Use AskUserQuestion to ask:
For ALL chains:
Chain Basic Info
Documentation Links
Asset Information
For EVM chains only:
For non-EVM chains only:
Chain Architecture Details
Ledger Hardware Wallet Support
Action: Don't proceed until you have:
Pro Tips:
Based on Phase 0 research, choose your path:
Examples: Monad, Base, Arbitrum, Optimism
Characteristics:
What you'll do:
Time estimate: 2-4 hours for basic integration
Examples: Tron, Sui, Cosmos, Solana
Characteristics:
What you'll do:
Time estimate: 1-2 days for basic integration
Working Directory: Same monorepo — hdwallet packages are at packages/hdwallet-*
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)
For EVM-compatible chains (like Monad, HyperEVM, Base), you need MINIMAL changes to hdwallet:
What EVM chains DON'T need:
What EVM chains DO need:
_supportsChainName: boolean)supportsChainName())pnpm run hdwallet:buildWhy? 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:
Time estimate: 30 minutes for hdwallet changes (vs 1-2 days for non-EVM)
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.
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).
For EVM chains only (like Monad, HyperEVM). Follow these PRs as reference:
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:
Set readonly _supports[ChainName] = false on:
For non-EVM chains:
Set readonly _supports[ChainName] = true for Native only:
Set readonly _supports[ChainName] = false on all other wallet types listed above.
Then: Skip to Step 1.6 (Build & Verify)
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]' }
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
File: packages/hdwallet-native/src/crypto/isolation/adapters/[chainname].ts
Implement chain-specific cryptography:
Reference: See tron.ts adapter for a complete example
Export adapter:
// In packages/hdwallet-native/src/crypto/isolation/adapters/index.ts
export * from './[chainname]'
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]
}
# 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:^).
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
Directory: packages/chain-adapters/src/[adaptertype]/[chainname]/
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:
Just follow the pattern from HyperEVM, Monad, or Plasma adapters.
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:
getTxHistory() to return empty arrayFile: 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]'
CRITICAL: The parseTx() method parses transaction data after broadcast. This determines:
Reference Implementations (use these as patterns):
SecondClassEvmAdapter.parseTx() - handles ERC-20 Transfer events automaticallySuiChainAdapter.parseTx() - parses SUI native and coin transfersTronChainAdapter.parseTx() - parses TRC-20 transfersNearChainAdapter.parseTx() + parseNep141Transfers() - parses NEP-141 token logsIterative Development Flow:
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:
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):
tx.valueethers.Interface.parseLog() to decode eventsNon-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:
pubkey as hex public keyalice.near, base58, etc.)const accountId = pubKeyToAddress(pubkey)When to ask user for debugger scope:
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."
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
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
}
File: src/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsx
Add case for your chain:
case KnownChainIds.[ChainName]Mainnet:
txStatus = await get[Chain]TransactionStatus(txHash, adapter)
break
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)
File: src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts
// Add to switch statement
case KnownChainIds.[ChainName]Mainnet:
return supports[Chain](wallet) // from hdwallet-core
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)
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
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
File: src/config.ts
const validators = {
// ...
VITE_[CHAIN]_NODE_URL: url(),
VITE_FEATURE_[CHAIN]: bool({ default: false }),
}
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,
}
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],
]
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
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:
scripts/generateAssetData/generateAssetData.ts:import * as [chainLower] from './[chainname]'
generateAssetData() function:const [chainLower]Assets = await [chainLower].getAssets()
...[chainLower]Assets,
Add chain to CoinGecko script:
File: scripts/generateAssetData/coingecko.ts
Import your chain:
import {
// ... existing imports
[chainLower]ChainId,
} from '@shapeshiftoss/caip'
import {
// ... existing imports
[chainLower],
} from '@shapeshiftoss/utils'
Add case in the switch statement (around line 133+):
case [chainLower]ChainId:
return {
assetNamespace: ASSET_NAMESPACE.erc20, // or trc20, suiCoin, etc.
category: adapters.chainIdToCoingeckoAssetPlatform(chainId),
explorer: [chainLower].explorer,
explorerAddressLink: [chainLower].explorerAddressLink,
explorerTxLink: [chainLower].explorerTxLink,
}
CRITICAL: Add your chain to supported swappers so users can actually trade!
Use AskUserQuestion to ask:
Question: Which swappers support [ChainName]?
Options:
Context: Different swappers support different chains. We need to add your chain to each swapper that supports it.
Search for swapper support:
Common patterns:
For Relay Swapper (supports most chains):
IMPORTANT - Check viem chain definitions first:
Search viem's chain definitions: Check if your chain exists in viem:
[chainname].ts file (e.g., hyperEvm.ts for HyperEVM)hyperEvm not hyperliquid)Update viem if needed: For new/recent chains, update viem to latest version first:
# Check current version
pnpm why viem
# Update to latest pinned version
pnpm update viem@latest
# Rebuild packages
pnpm run build:packages
Only define manually if unavailable: If the chain doesn't exist in viem, use defineChain() pattern (see viem docs)
File: packages/swapper/src/swappers/RelaySwapper/constant.ts
Add your chain to the Relay chain ID mapping:
import {
// ... existing imports
[chainLower]ChainId,
} from '@shapeshiftoss/caip'
// Add to viem chain imports if EVM (check viem/chains for exact export name!)
import {
// ... existing chains
[chainLower], // e.g., hyperEvm from viem/chains (note: hyperEvm not hyperliquid!)
} from 'viem/chains'
export const chainIdToRelayChainId = {
// ... existing mappings
[[chainLower]ChainId]: [chainLower].id, // For EVM chains using viem
// OR
[[chainLower]ChainId]: [RELAY_CHAIN_ID], // For non-EVM (get from Relay docs)
}
File: packages/swapper/src/swappers/RelaySwapper/utils/relayTokenToAssetId.ts
Add native asset case in the relayTokenToAssetId function:
// Add to the switch statement for native assets (around line 100+)
case CHAIN_REFERENCE.[ChainName]Mainnet:
return {
assetReference: ASSET_REFERENCE.[ChainName],
assetNamespace: ASSET_NAMESPACE.slip44,
}
IMPORTANT: Make sure ALL chains that are in the chainIdToRelayChainId mapping in constant.ts have a corresponding case in the switch statement in relayTokenToAssetId.ts. Missing cases will cause runtime errors like chainId 'XX' not supported.
Check Relay docs for your chain:
IMPORTANT: Asset generation requires a Zerion API key for related asset indexing.
Ask user for Zerion API key using AskUserQuestion:
Question: Do you have a Zerion API key to run asset generation?
Options:
Context: Asset generation fetches token metadata and requires a Zerion API key. The key is passed via environment variable and should NEVER be committed to VCS.
Ask user how they want to run generation using AskUserQuestion:
Question: How do you want to run the asset generation pipeline?
Options:
Context: Asset generation has 5 steps (caip-adapters, color-map, asset-data, tradable-asset-map, thor-longtail). Running manually gives full visibility of progress (you'll see "chain_id: hyperevm" tokens being processed). Claude running it is hands-off but you won't see detailed progress, and it may appear stuck for several minutes while processing thousands of tokens.
Run generation scripts ONE AT A TIME (better visibility than generate:all):
# Step 1: Generate CoinGecko CAIP adapters (JSON mappings from our code)
pnpm run generate:caip-adapters
# ✓ Generates packages/caip/src/adapters/coingecko/generated/eip155_999/adapter.json
# ✓ Takes ~10 seconds
# Step 2: Generate color map (picks up new assets)
pnpm run generate:color-map
# ✓ Updates scripts/generateAssetData/color-map.json
# ✓ Takes ~5 seconds
# Step 3: Generate asset data (fetches tokens from CoinGecko)
ZERION_API_KEY=<user-provided-key> pnpm run generate:asset-data
# ✓ Fetches all HyperEVM ERC20 tokens from CoinGecko platform 'hyperevm'
# ✓ Updates src/assets/generated/
# ✓ Takes 2-5 minutes - YOU SHOULD SEE:
# - "Total Portals tokens fetched for ethereum: XXXX"
# - "Total Portals tokens fetched for base: XXXX"
# - "chain_id": "hyperevm" appearing in output (means HyperEVM tokens found!)
# - "Generated CoinGecko AssetId adapter data."
# - "Asset data generated successfully"
# Step 4: Generate tradable asset map (for swapper support)
pnpm run generate:tradable-asset-map
# ✓ Generates src/lib/swapper/constants.ts mappings
# ✓ Takes ~10 seconds
# Step 5: Generate Thor longtail tokens (Thor-specific, optional for most chains)
pnpm run generate:thor-longtail-tokens
# ✓ Updates Thor longtail token list
# ✓ Takes ~5 seconds
Why step-by-step is better than generate:all:
Commit generated assets:
git add src/assets/generated/ packages/caip/src/adapters/coingecko/generated/ scripts/generateAssetData/color-map.json
git commit -m "feat: generate [chainname] assets and mappings"
⚠️ CRITICAL: NEVER commit the Zerion API key. Only use it in the command line.
IMPORTANT: After assets are generated, check which swappers support your new chain!
Use AskUserQuestion to determine swapper support:
Which swappers support [ChainName]?
Options:
1. "I know which swappers support it" → User provides list
2. "Research it for me" → AI will search swapper docs
3. "Skip for now" → Can add swapper support later
Context: Different DEX aggregators support different chains. We need to add your chain to each swapper that supports it so users can trade.
If user chooses "Research it for me", check these sources:
Relay (most common, supports most chains):
plasma from 'viem/chains')Other swappers to check:
If Relay supports your chain:
File: packages/swapper/src/swappers/RelaySwapper/constant.ts
// 1. Add imports
import {
// ... existing imports
plasmaChainId,
} from '@shapeshiftoss/caip'
import {
// ... existing chains
plasma, // Check if viem/chains exports your chain
} from 'viem/chains'
// 2. Add to chainIdToRelayChainId mapping
export const chainIdToRelayChainId = {
// ... existing mappings
[plasmaChainId]: plasma.id, // Uses viem chain ID
}
File: packages/swapper/src/swappers/RelaySwapper/utils/relayTokenToAssetId.ts
// Add native asset case in switch statement (around line 124):
case CHAIN_REFERENCE.PlasmaMainnet:
return {
assetReference: ASSET_REFERENCE.Plasma,
assetNamespace: ASSET_NAMESPACE.slip44,
}
Follow similar patterns for other swappers (CowSwap, 0x, etc.) - see swapper-integration skill for detailed guidance.
Reference: Plasma added to Relay swapper for swap support
CRITICAL: Second-class citizen chains are not in CoinGecko's top 100 by market cap, so they won't appear in the popular assets list by default. This causes the native asset to be missing when users filter by that chain.
File: src/components/TradeAssetSearch/hooks/useGetPopularAssetsQuery.tsx
// Add import at the top
import {
hyperEvmAssetId,
mayachainAssetId,
monadAssetId,
plasmaAssetId, // example for Plasma
[chainLower]AssetId, // Add your chain's asset ID
thorchainAssetId,
tronAssetId,
suiAssetId,
} from '@shapeshiftoss/caip'
// Add to the queryFn, after the mayachain check (around line 37)
// add second-class citizen chains to popular assets for discoverability
if (enabledFlags.HyperEvm) assetIds.push(hyperEvmAssetId)
if (enabledFlags.Monad) assetIds.push(monadAssetId)
if (enabledFlags.Plasma) assetIds.push(plasmaAssetId)
if (enabledFlags.[ChainName]) assetIds.push([chainLower]AssetId) // Add your chain
if (enabledFlags.Tron) assetIds.push(tronAssetId)
if (enabledFlags.Sui) assetIds.push(suiAssetId)
Why this is needed:
Reference PRs:
ONLY for chains where ETH is the native gas token (e.g., Optimism, Arbitrum, Base, Katana, MegaETH). SKIP for chains with their own native token (e.g., Berachain/BERA, Monad/MON, Tron/TRX, Sui/SUI).
Why this matters: The related asset index groups the same asset across different chains (e.g., ETH on Ethereum, ETH on Optimism, ETH on Arbitrum). When a user views ETH, they can see all the chain variants. If your chain uses ETH as its native gas token and you don't add it here, the chain's ETH won't appear as a related asset in the UI.
File: scripts/generateAssetData/generateRelatedAssetIndex/generateRelatedAssetIndex.ts
import {
adapters,
arbitrumAssetId,
baseAssetId,
ethAssetId,
FEE_ASSET_IDS,
foxAssetId,
foxOnArbitrumOneAssetId,
fromAssetId,
katanaAssetId,
megaethAssetId,
[chainLower]AssetId, // ADD THIS - only if native token IS ETH
optimismAssetId,
starknetAssetId,
} from '@shapeshiftoss/caip'
manualRelatedAssetIndex under ethAssetId:const manualRelatedAssetIndex: Record<AssetId, AssetId[]> = {
[ethAssetId]: [optimismAssetId, arbitrumAssetId, baseAssetId, katanaAssetId, megaethAssetId, [chainLower]AssetId],
// ^^^^^^^^^^^^^^^^ ADD
[foxAssetId]: [foxOnArbitrumOneAssetId],
// ...
}
How to determine if this step applies:
Reference: Look at which assetIds are already in the array - they're all L2s/chains where the native token is ETH.
Only if chain is supported by Ledger hardware
For EVM chains: Automatically supported via Ethereum app For non-EVM: Needs chain-specific Ledger app
File: packages/hdwallet-ledger/src/ledger.ts
Add chain support following existing patterns.
For EVM: Just add to supported chains list For non-EVM: Implement chain-specific Ledger transport
File: src/context/WalletProvider/Ledger/constants.ts
import { [chainLower]AssetId } from '@shapeshiftoss/caip'
export const availableLedgerAppAssetIds = [
// ...
[chainLower]AssetId,
]
CRITICAL: After adding a new chain, TWO test files in packages/caip/src/adapters/coingecko/ will almost always fail. Fix them BEFORE running the full test suite.
IMPORTANT: Always run pnpm run build:packages FIRST so TypeScript can resolve workspace package exports. Without this, type-check shows false errors like '"@shapeshiftoss/caip"' has no exported member named '[chainLower]ChainId'.
packages/caip/src/adapters/coingecko/utils.test.tsThe parseData test at line ~100 has a hardcoded expected output with every chain's entry. When you add a new chain to parseData() in utils.ts, you MUST add the matching expected entry:
// Add your chain's expected entry to the `expected` object in the test
'eip155:[CHAIN_ID]': {
'eip155:[CHAIN_ID]/slip44:60': '[coingecko-native-id]',
},
How to find the CoinGecko native ID: Look at what you set in the parseData() function's initial data (the object at the bottom of parseData() that maps chainId → { assetId: coingeckoId }).
packages/caip/src/adapters/coingecko/index.test.tsONLY for ETH-native chains (where CoinGecko maps the native asset to 'ethereum'):
The coingeckoToAssetIds('ethereum') test expects a specific list of all chain asset IDs that map to 'ethereum' in CoinGecko. Add your chain:
const ethOn[ChainName] = toAssetId({
chainNamespace,
chainReference: CHAIN_REFERENCE.[ChainName]Mainnet,
assetNamespace: 'slip44',
assetReference: ASSET_REFERENCE.[ChainName],
})
expect(coingeckoToAssetIds('ethereum')).toEqual([
ethOnEthereum,
ethOnOptimism,
ethOnArbitrum,
ethOnBase,
ethOnMegaEth,
ethOn[ChainName], // ADD THIS
])
Skip this if your chain's native asset has its own CoinGecko ID (e.g., 'mantle', 'monad', 'berachain-bera').
packages/chain-adapters/src/evm/EvmBaseAdapter.ts (targetNetwork)CRITICAL for EVM chains: The targetNetwork object in EvmBaseAdapter.ts (around line ~230) maps chain IDs to network display info for ethSwitchChain. If you add your chain to evmChainIds but forget targetNetwork, the build will fail with:
error TS2339: Property 'eip155:XXX' does not exist on type...
error TS18048: 'targetNetwork' is possibly 'undefined'.
Fix: Add your chain's entry:
[KnownChainIds.[ChainName]Mainnet]: {
name: '[ChainName]',
symbol: '[SYMBOL]',
explorer: 'https://[explorer-url]',
},
# 1. Build packages first (REQUIRED before type-check)
pnpm run build:packages
# 2. Run CAIP tests specifically
pnpm exec vitest run packages/caip/ --reporter=verbose
# 3. Type-check
pnpm run type-check
# 4. Lint
pnpm run lint --fix
Common patterns by chain native token:
| Native Token | CoinGecko ID | Update index.test.ts? | Update utils.test.ts? |
|---|---|---|---|
| ETH (L2s like Base, Optimism, Ink) | 'ethereum' | YES - add to coingeckoToAssetIds('ethereum') | YES - add chain entry |
| Own token (BERA, MON, MNT, CRO) | 'berachain-bera', 'monad', 'mantle', 'cronos' | NO | YES - add chain entry |
pnpm run type-check
# Fix any TypeScript errors
pnpm run lint --fix
pnpm run build
# Verify no build errors
# Check bundle size didn't explode
If Ledger supported:
Everything is in the same monorepo now — hdwallet packages are workspace packages under packages/hdwallet-*.
# Commit everything together
git add -A
git commit -m "feat: implement [chainname]
- Add [ChainName] hdwallet core interfaces and native wallet support
- Add [ChainName] chain adapter with poor man's RPC
- Support native asset sends/receives
- Add account derivation
- Add feature flag VITE_FEATURE_[CHAIN]
- Add asset generation for [chain] from CoinGecko
- Wire transaction status polling
- Add [chain] plugin with feature flag gating
Behind feature flag for now."
git push origin HEAD
# Open PR to develop
gh pr create --title "feat: implement [chainname]" \
--body "Adds basic [ChainName] support as second-class citizen..."
Problem: Token balances display incorrectly Solution: Verify decimals/precision match chain metadata Example: Tron TRC20 tokens hardcoded precision caused display issues (#11222)
Problem: Invalid addresses accepted or valid ones rejected Solution: Use chain-specific validation (checksumming for EVM, base58 for Tron, etc.) Example: Tron address parsing issues (#11229)
Problem: Signed transactions fail to broadcast Solution: Check serialization format matches chain expectations Example: Ensure proper hex encoding, network byte for Tron, etc.
Problem: Build size explodes after adding chain SDK Solution: Extract large dependencies to separate chunk Example: Sui SDK needed code splitting (#11238 comments)
Problem: Small swaps fail without clear error Solution: Add minimum amount validation in swapper Example: Tron tokens need minimum amounts (#11253)
Problem: Tokens don't group with related assets in UI Solution: Check asset namespace and ID generation Example: Tron/Sui tokens grouping issues (#11252)
Problem: Ledger transactions fail with unclear error Solution: Verify correct Ledger app is mapped Example: Use Ethereum app for EVM chains, not chain-specific app
Problem: Assets appear but no accounts are derived/discovered for the chain Symptoms:
walletSupportsChain() switch statement
Solution: Add your chain to the switch statement in src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts:// 1. Import chain ID
import { [chainLower]ChainId } from '@shapeshiftoss/caip'
// 2. Import support function from hdwallet-core
import { supports[ChainName] } from '@shapeshiftoss/hdwallet-core'
// 3. Add to switch statement (around line 186+)
case [chainLower]ChainId:
return supports[ChainName](wallet)
Example: HyperEVM was missing this - caused account discovery to skip it entirely
Why it matters: useDiscoverAccounts filters chains using walletSupportsChain(). If it returns false, the chain is never passed to deriveAccountIdsAndMetadata() → no accounts!
Reference: Same issue as Plasma PR #11361 but for wallet support instead of feature flag
Problem: Chain doesn't appear even with flag enabled Solution: Check ALL places flags are checked:
Problem: Balances don't update after transactions Solution: Implement polling in tx status subscriber Example: Add chain case in useSendActionSubscriber
Problem: Requests fail intermittently Solution: Add retry logic, use multiple RPC endpoints Example: Implement fallback RPC URLs
Problem: pnpm run generate:asset-data fails with "no coingecko token support for chainId"
Solution: Add your chain case to scripts/generateAssetData/coingecko.ts
Files to update:
[chainLower]ChainId from caip[chainLower] base asset from utilsProblem: Asset generation fails with "Missing Zerion API key"
Solution: Get key from user via AskUserQuestion, pass as env var
Command: ZERION_API_KEY=<key> pnpm run generate:all
CRITICAL: NEVER commit the Zerion API key to VCS!
Example: Always pass key via command line only
Problem: Assets for your chain appear even when feature flag is disabled
Solution: Add feature flag filter to AssetService
File: src/lib/asset-service/service/AssetService.ts
Code: if (!config.VITE_FEATURE_[CHAIN] && asset.chainId === [chainLower]ChainId) return false
Example: See line ~53 for Monad/Tron/Sui pattern
Reference: Fixed in PR #11241 (Monad) - was initially forgotten
Problem: TypeScript errors "Type 'KnownChainIds.[Chain]Mainnet' is not assignable to type EvmChainId"
Solution: Add your chain to the evmChainIds array in EvmBaseAdapter
Files to update:
packages/chain-adapters/src/evm/EvmBaseAdapter.ts (line ~70)evmChainIds array: KnownChainIds.[Chain]MainnettargetNetwork object (line ~210): network name, symbol, explorer
Example: HyperEVM added at lines 81 and 262-266
Why: The array defines which chains are EVM-compatible for type checkingProblem: TypeScript errors like:
Solution: Add your chain to FOUR type mapping objects in chain-adapters/src/types.ts
File: packages/chain-adapters/src/types.ts
ALL FOUR mappings required:
ChainSpecificAccount → [KnownChainIds.[Chain]Mainnet]: evm.AccountChainSpecificFeeData → [KnownChainIds.[Chain]Mainnet]: evm.FeeDataChainSpecificBuildTxInput → [KnownChainIds.[Chain]Mainnet]: evm.BuildTxInputChainSpecificGetFeeDataInput → [KnownChainIds.[Chain]Mainnet]: evm.GetFeeDataInputExample: HyperEVM added at lines 45, 91, 219, 320
Why: TypeScript uses these to determine chain-specific data structures
CRITICAL: Missing even ONE of these causes cryptic type errors! All 4 are required for ALL chains (EVM and non-EVM).
Problem: Addresses don't display in:
Root Cause: Missing chainId case in accountIdToLabel() function
File: src/state/slices/portfolioSlice/utils/index.ts (around line 80-125)
Solution:
import { [chainLower]ChainId } from '@shapeshiftoss/caip'case [chainLower]ChainId:Example:
case baseChainId:
case hyperEvmChainId: // ← ADD THIS
case monadChainId:
case plasmaChainId:
Why: This function converts accountId to human-readable label. Without the case, it hits the default and returns '' (empty string), causing blank addresses everywhere in the UI.
Note: This affects ALL wallet types (Native, Ledger, Trezor, MetaMask), not just one wallet.
Problem: App crashes with error:
Error: Chain namespace [chain] on mainnet not supported.
at getNativeFeeAssetReference.ts:XX:XX
Root Cause: Missing chainNamespace case in getNativeFeeAssetReference() function
File: packages/utils/src/getNativeFeeAssetReference.ts
Solution: Add case to the switch statement:
case CHAIN_NAMESPACE.[ChainName]:
switch (chainReference) {
case CHAIN_REFERENCE.[ChainName]Mainnet:
return ASSET_REFERENCE.[ChainName]
default:
throw new Error(`Chain namespace ${chainNamespace} on ${chainReference} not supported.`)
}
Why: This function maps chainId to the native fee asset reference. Without it, any selector that needs the fee asset for your chain will throw and crash the app.
Note: This is called early in the app initialization, so it will crash immediately when the app tries to load accounts for your chain.
Problem: Account discovery succeeds (getAccount returns valid data) but account doesn't appear in the UI anywhere.
Root Cause: Missing or placeholder implementation in accountToPortfolio() function
File: src/state/slices/portfolioSlice/utils/index.ts
Symptom: You can verify getAccount is returning correct data with console.log, but the account just doesn't show up in the portfolio UI.
Solution:
Find the switch statement for chainNamespace and implement your chain's case:
case CHAIN_NAMESPACE.[ChainName]: {
const chainAccount = account as Account<KnownChainIds.[ChainName]Mainnet>
const { chainId, assetId, pubkey } = account
const accountId = toAccountId({ chainId, account: pubkey })
portfolio.accounts.ids.push(accountId)
portfolio.accounts.byId[accountId] = { assetIds: [assetId], hasActivity }
portfolio.accountBalances.ids.push(accountId)
portfolio.accountBalances.byId[accountId] = { [assetId]: account.balance }
// Add token support if applicable
chainAccount.chainSpecific.tokens?.forEach(token => {
if (!assetIds.includes(token.assetId)) return
if (bnOrZero(token.balance).gt(0)) portfolio.accounts.byId[accountId].hasActivity = true
portfolio.accounts.byId[accountId].assetIds.push(token.assetId)
portfolio.accountBalances.byId[accountId][token.assetId] = token.balance
})
break
}
Why: This function converts chain adapter account data into the Redux portfolio state structure. Without it, the account data is fetched but never stored, making the account invisible.
Problem:
Root Cause: parseTx() only parses native transfers, not token events
Symptom: User reports swap worked but execution price shows as incorrect or zero.
Solution: Implement token parsing in parseTx. This is an iterative process:
Reference implementations:
SecondClassEvmAdapter.parseTx() - ERC-20 Transfer eventsSuiChainAdapter.parseTx() - SUI coin object changesTronChainAdapter.parseTx() - TRC-20 logsNearChainAdapter.parseNep141Transfers() - NEP-141 EVENT_JSON logsCommon issues:
alice.near) but function receives hex pubkey
How to debug:
async parseTx(txHash: string, pubkey: string): Promise<Transaction> {
const result = await this.rpcCall('tx', [txHash, 'dontcare'])
console.log('parseTx result:', JSON.stringify(result, null, 2))
console.log('pubkey:', pubkey)
// ... rest of implementation
}
Ask user for: Breakpoint in parseTx, share result variable to see actual token event structure.
Problem:
Root Cause: isToken() in packages/utils/src/index.ts doesn't include the new chain's token namespace.
Why it matters: contractAddressOrUndefined(assetId) uses isToken() to determine if an asset is a token. If isToken() returns false, contractAddressOrUndefined() returns undefined, and the chain adapter builds a native transfer instead of a token transfer.
EVM chains: Not affected - EVM abstraction already handles erc20, erc721, erc1155.
Non-EVM chains: MUST add their token namespace to isToken():
// packages/utils/src/index.ts
export const isToken = (assetId: AssetId) => {
switch (fromAssetId(assetId).assetNamespace) {
case ASSET_NAMESPACE.erc20:
case ASSET_NAMESPACE.erc721:
case ASSET_NAMESPACE.erc1155:
case ASSET_NAMESPACE.splToken: // Solana
case ASSET_NAMESPACE.trc20: // Tron
case ASSET_NAMESPACE.suiCoin: // Sui
case ASSET_NAMESPACE.nep141: // NEAR <-- ADD YOUR NAMESPACE HERE
return true
default:
return false
}
}
Symptom: User sends token, tx succeeds, but token balance unchanged. Tx on block explorer shows native transfer to self.
Debug: Add logs in Send flow:
console.log('[Send] assetId:', asset.assetId)
console.log('[Send] contractAddress:', contractAddressOrUndefined(asset.assetId))
// If contractAddress is undefined for a token, isToken() is missing the namespace
Problem:
TypeError: coin when deriving addresstranslateCoinAndMethod hitting default: throw new TypeError("coin")Root Cause: For non-EVM chains, translateCoinAndMethod in Ledger WebHID/WebUSB transport needs a case for the new coin type.
Files to update:
packages/hdwallet-ledger-webhid/src/transport.tspackages/hdwallet-ledger-webusb/src/transport.tsSolution: Add import and case for the new chain's Ledger app:
// 1. Add import at top
import Near from "@ledgerhq/hw-app-near";
// 2. Add case in translateCoinAndMethod switch
case "Near": {
const near = new Near(transport as Transport);
const methodInstance = near[method as LedgerTransportMethodName<"Near">].bind(near);
return methodInstance as LedgerTransportMethod<T, U>;
}
Prerequisites:
@ledgerhq/hw-app-[chainname] must exist as npm packageLedgerTransportCoinType union in packages/hdwallet-ledger/src/transport.tsEVM chains: Not affected - they use the Ethereum app via case "Eth".
Checklist for new non-EVM Ledger chain:
LedgerTransportCoinType in hdwallet-ledger/src/transport.tsLedgerTransportMethodMap type mapping in same filehdwallet-ledger-webhid/src/transport.tshdwallet-ledger-webusb/src/transport.tspackages/hdwallet-core/src/[chainname].tspackages/hdwallet-core/src/index.ts (export)packages/hdwallet-core/src/utils.ts (SLIP44)packages/hdwallet-core/src/wallet.ts (support function)packages/hdwallet-native/src/[chainname].tspackages/hdwallet-native/src/native.ts (integrate mixin)packages/hdwallet-native/src/crypto/isolation/adapters/[chainname].ts (if non-EVM)packages/hdwallet-ledger/src/ledger.tspackages/caip/src/constants.tspackages/types/src/base.tssrc/constants/chains.tspackages/chain-adapters/src/[type]/[chainname]/[ChainName]ChainAdapter.tspackages/chain-adapters/src/[type]/[chainname]/types.tspackages/chain-adapters/src/[type]/[chainname]/index.tssrc/lib/utils/[chainname].tssrc/lib/account/[chainname].tssrc/lib/account/account.ts (wire into dispatcher)src/plugins/[chainname]/index.tsxsrc/plugins/activePlugins.tssrc/context/PluginProvider/PluginProvider.tsxsrc/hooks/useWalletSupportsChain/useWalletSupportsChain.tssrc/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsxsrc/state/slices/portfolioSlice/utils/index.ts (isAssetSupportedByWallet function).env.env.development.env.productionsrc/config.tssrc/state/slices/preferencesSlice/preferencesSlice.tssrc/test/mocks/store.tsheaders/csps/chains/[chainname].tsheaders/csps/index.tspackages/utils/src/getAssetNamespaceFromChainId.tspackages/utils/src/getChainShortName.tspackages/utils/src/getNativeFeeAssetReference.tspackages/utils/src/chainIdToFeeAssetId.tspackages/utils/src/assetData/baseAssets.tspackages/utils/src/assetData/getBaseAsset.tspackages/caip/src/adapters/coingecko/index.ts (add platform enum)packages/caip/src/adapters/coingecko/utils.ts (add chainId mapping and native asset)packages/caip/src/adapters/coingecko/utils.test.ts (add test)packages/caip/src/adapters/coingecko/index.test.ts (add asset fixture)scripts/generateAssetData/coingecko.ts (add chain case for token fetching)scripts/generateAssetData/[chainname]/index.ts (create asset generator)scripts/generateAssetData/generateAssetData.ts (wire in generator)src/lib/asset-service/service/AssetService.ts (add feature flag filter)packages/swapper/src/swappers/RelaySwapper/constant.ts (add chain mapping)packages/swapper/src/swappers/RelaySwapper/utils/relayTokenToAssetId.ts (add native asset case)src/context/WalletProvider/Ledger/constants.tsThis skill covers the COMPLETE process for adding a new blockchain as a second-class citizen:
packages/hdwallet-*)Key Principles:
packages/hdwallet-core + packages/hdwallet-native)Remember: Second-class citizen = basic support only. No fancy features, no microservices, just enough to send/receive and swap.