Loading...
Loading...
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.
npx skill4agent add shapeshift/web chain-integrationpackages/hdwallet-*packages/hdwallet-ledgerAskUserQuestionDoes 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.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.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/0AskUserQuestionpackages/hdwallet-*_supportsChainName: booleansupportsChainName()yarn hdwallet:build# 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.tspackages/hdwallet-core/src/ethereum.ts// 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)
]packages/hdwallet-core/src/utils.ts// If your chain uses a different SLIP44 than Ethereum
{ slip44: YOUR_SLIP44, symbol: 'SYMBOL', name: 'ChainName' }packages/hdwallet-core/src/ethereum.tsexport interface ETHWalletInfo extends HDWalletInfo {
// ... existing flags
readonly _supportsMonad: boolean;
readonly _supportsHyperEvm: boolean; // ADD THIS
// ...
}packages/hdwallet-core/src/wallet.tssupportsMonadexport 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];
}readonly _supports[ChainName] = truereadonly _supports[ChainName] = falsereadonly _supports[ChainName] = truereadonly _supports[ChainName] = falsepackages/hdwallet-core/src/[chainname].tsimport { 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] }];
}// In packages/hdwallet-core/src/index.ts
export * from './[chainname]'// In packages/hdwallet-core/src/utils.ts
// Add to slip44Table
{ slip44: SLIP44, symbol: '[SYMBOL]', name: '[ChainName]' }packages/hdwallet-native/src/[chainname].tsimport * 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,
};
});
}
};
}// In packages/hdwallet-native/src/native.ts
// Add mixin to class hierarchy
// Add initialization in initialize() method
// Add wipe in wipe() methodpackages/hdwallet-native/src/crypto/isolation/adapters/[chainname].tstron.ts// In packages/hdwallet-native/src/crypto/isolation/adapters/index.ts
export * from './[chainname]'packages/hdwallet-core/src/wallet.tsexport function supports[Chain](wallet: HDWallet): wallet is [Chain]Wallet {
return !!(wallet as any)._supports[Chain]
}# Build hdwallet packages to verify
yarn hdwallet:build
# Run hdwallet tests
yarn hdwallet:testworkspace:^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
}packages/types/src/base.ts// Add to KnownChainIds enum (duplicate but required)
export enum KnownChainIds {
// ...
[ChainName]Mainnet = '[caip2-chain-id]',
}src/constants/chains.ts// Add to second-class chains array
export const SECOND_CLASS_CHAINS = [
// ...
KnownChainIds.[ChainName]Mainnet,
]
// Add to feature-flag gated chainspackages/chain-adapters/src/[adaptertype]/[chainname]/SecondClassEvmAdapterpackages/chain-adapters/src/evm/[chainname]/[ChainName]ChainAdapter.tsimport { 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 }IChainAdaptergetAccount()getAddress()getFeeData()broadcastTransaction()buildSendApiTransaction()signTransaction()parseTx()getTxHistory()getTxHistory()packages/chain-adapters/src/[chainname]/[ChainName]ChainAdapter.tsSuiChainAdapter.tsTronChainAdapter.ts// 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]'parseTx()SecondClassEvmAdapter.parseTx()SuiChainAdapter.parseTx()TronChainAdapter.parseTx()NearChainAdapter.parseTx()parseNep141Transfers()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, /* ... */ }
}// 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
}| Aspect | Native Asset | Tokens | Internal Transfers |
|---|---|---|---|
| Where to find | Transaction value field | Event logs / receipts | Nested calls / traces |
| Asset ID | | | Varies |
| pubkey comparison | Usually sender field | Event old_owner/new_owner | May be nested |
tx.valueethers.Interface.parseLog()// 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
}
}pubkeyalice.nearconst accountId = pubKeyToAddress(pubkey)"The parseTx implementation needs refinement for token transfers. Can you:
- Make a token send/swap
- Set a breakpoint in parseTx()
- Share the
variable from the RPC response This will help me see the actual data structure for token events."result
packages/utils/src/getAssetNamespaceFromChainId.tscase [chainLower]ChainId:
return ASSET_NAMESPACE.[tokenStandard]packages/utils/src/getChainShortName.tscase KnownChainIds.[ChainName]Mainnet:
return '[SHORT]'packages/utils/src/getNativeFeeAssetReference.tscase KnownChainIds.[ChainName]Mainnet:
return ASSET_REFERENCE.[ChainName]packages/utils/src/chainIdToFeeAssetId.ts[chainLower]ChainId: [chainLower]AssetId,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
}packages/utils/src/assetData/getBaseAsset.tscase [chainLower]ChainId:
return [chainLower]BaseAssetsrc/lib/utils/[chainname].tsimport { [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
}src/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsxcase KnownChainIds.[ChainName]Mainnet:
txStatus = await get[Chain]TransactionStatus(txHash, adapter)
breaksrc/lib/account/[chainname].tsimport { [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,
}
}// In src/lib/account/account.ts
case KnownChainIds.[ChainName]Mainnet:
return derive[Chain]AccountIdsAndMetadata(args)src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts// Add to switch statement
case KnownChainIds.[ChainName]Mainnet:
return supports[Chain](wallet) // from hdwallet-coresrc/state/slices/portfolioSlice/utils/index.tsisAssetSupportedByWallet// 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
}
}case hyperEvmChainId:
return supportsHyperEvm(wallet)src/plugins/[chainname]/index.tsximport { [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
})
},
],
],
},
},
],
]
}// In src/plugins/activePlugins.ts
import [chainLower] from './[chainname]'
export const activePlugins = [
// ...
[chainLower],
]// In src/context/PluginProvider/PluginProvider.tsx
// Add feature flag check for your chain.envVITE_[CHAIN]_NODE_URL=[default-public-rpc]
VITE_FEATURE_[CHAIN]=false.env.developmentVITE_[CHAIN]_NODE_URL=[dev-rpc]
VITE_FEATURE_[CHAIN]=true.env.productionVITE_[CHAIN]_NODE_URL=[prod-rpc]
VITE_FEATURE_[CHAIN]=falsesrc/config.tsconst validators = {
// ...
VITE_[CHAIN]_NODE_URL: url(),
VITE_FEATURE_[CHAIN]: bool({ default: false }),
}src/state/slices/preferencesSlice/preferencesSlice.tsexport type FeatureFlags = {
// ...
[ChainName]: boolean
}
const initialState: PreferencesState = {
featureFlags: {
// ...
[ChainName]: getConfig().VITE_FEATURE_[CHAIN],
},
}// In src/test/mocks/store.ts
featureFlags: {
// ...
[ChainName]: false,
}headers/csps/chains/[chainname].tsconst [chainLower]: Csp = {
'connect-src': [env.VITE_[CHAIN]_NODE_URL],
}
export default [chainLower]// In headers/csps/index.ts
import [chainLower] from './chains/[chainname]'
export default [
// ...
[chainLower],
]packages/caip/src/adapters/coingecko/index.ts// 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
}packages/caip/src/adapters/coingecko/utils.tschainIdToCoingeckoAssetPlatform// For EVM chains, add to the EVM switch statement
case CHAIN_REFERENCE.[ChainName]Mainnet:
return CoingeckoAssetPlatform.[ChainName]
// For non-EVM chains, add separate case in outer switchpackages/caip/src/adapters/coingecko/utils.test.tsit('returns correct platform for [ChainName]', () => {
expect(chainIdToCoingeckoAssetPlatform([chainLower]ChainId)).toEqual(
CoingeckoAssetPlatform.[ChainName]
)
})packages/caip/src/adapters/coingecko/index.test.ts// 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 assetscripts/generateAssetData/[chainname]/index.tsimport { [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])]
}scripts/generateAssetData/generateAssetData.tsimport * as [chainLower] from './[chainname]'generateAssetData()const [chainLower]Assets = await [chainLower].getAssets() ...[chainLower]Assets,scripts/generateAssetData/coingecko.tsimport {
// ... existing imports
[chainLower]ChainId,
} from '@shapeshiftoss/caip'
import {
// ... existing imports
[chainLower],
} from '@shapeshiftoss/utils'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,
}AskUserQuestion[chainname].tshyperEvm.tshyperEvmhyperliquid# Check current version
yarn why viem
# Update to latest pinned version
yarn up viem@latest
# Rebuild packages
yarn build:packagesdefineChain()packages/swapper/src/swappers/RelaySwapper/constant.tsimport {
// ... 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)
}packages/swapper/src/swappers/RelaySwapper/utils/relayTokenToAssetId.tsrelayTokenToAssetId// 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,
}chainIdToRelayChainIdconstant.tsrelayTokenToAssetId.tschainId 'XX' not supportedAskUserQuestionAskUserQuestiongenerate:all# Step 1: Generate CoinGecko CAIP adapters (JSON mappings from our code)
yarn 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)
yarn 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> yarn 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)
yarn 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)
yarn generate:thor-longtail-tokens
# ✓ Updates Thor longtail token list
# ✓ Takes ~5 secondsgenerate:allgit add src/assets/generated/ packages/caip/src/adapters/coingecko/generated/ scripts/generateAssetData/color-map.json
git commit -m "feat: generate [chainname] assets and mappings"AskUserQuestionWhich 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.plasmapackages/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
}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,
}swapper-integrationsrc/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)packages/hdwallet-ledger/src/ledger.tssrc/context/WalletProvider/Ledger/constants.tsimport { [chainLower]AssetId } from '@shapeshiftoss/caip'
export const availableLedgerAppAssetIds = [
// ...
[chainLower]AssetId,
]yarn type-check
# Fix any TypeScript errorsyarn lint --fixyarn build
# Verify no build errors
# Check bundle size didn't explodepackages/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..."walletSupportsChain()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)useDiscoverAccountswalletSupportsChain()deriveAccountIdsAndMetadata()yarn generate:asset-datascripts/generateAssetData/coingecko.ts[chainLower]ChainId[chainLower]AskUserQuestionZERION_API_KEY=<key> yarn generate:allsrc/lib/asset-service/service/AssetService.tsif (!config.VITE_FEATURE_[CHAIN] && asset.chainId === [chainLower]ChainId) return falseevmChainIdspackages/chain-adapters/src/evm/EvmBaseAdapter.tsevmChainIdsKnownChainIds.[Chain]MainnettargetNetworkpackages/chain-adapters/src/types.tsChainSpecificAccount[KnownChainIds.[Chain]Mainnet]: evm.AccountChainSpecificFeeData[KnownChainIds.[Chain]Mainnet]: evm.FeeDataChainSpecificBuildTxInput[KnownChainIds.[Chain]Mainnet]: evm.BuildTxInputChainSpecificGetFeeDataInput[KnownChainIds.[Chain]Mainnet]: evm.GetFeeDataInputaccountIdToLabel()src/state/slices/portfolioSlice/utils/index.tsimport { [chainLower]ChainId } from '@shapeshiftoss/caip'case [chainLower]ChainId:case baseChainId:
case hyperEvmChainId: // ← ADD THIS
case monadChainId:
case plasmaChainId:default''Error: Chain namespace [chain] on mainnet not supported.
at getNativeFeeAssetReference.ts:XX:XXgetNativeFeeAssetReference()packages/utils/src/getNativeFeeAssetReference.tscase 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.`)
}accountToPortfolio()src/state/slices/portfolioSlice/utils/index.tschainNamespacecase 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
}parseTx()SecondClassEvmAdapter.parseTx()SuiChainAdapter.parseTx()TronChainAdapter.parseTx()NearChainAdapter.parseNep141Transfers()alice.nearasync 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
}resultisToken()packages/utils/src/index.tscontractAddressOrUndefined(assetId)isToken()isToken()falsecontractAddressOrUndefined()undefinederc20erc721erc1155isToken()// 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
}
}console.log('[Send] assetId:', asset.assetId)
console.log('[Send] contractAddress:', contractAddressOrUndefined(asset.assetId))
// If contractAddress is undefined for a token, isToken() is missing the namespaceTypeError: cointranslateCoinAndMethoddefault: throw new TypeError("coin")translateCoinAndMethodpackages/hdwallet-ledger-webhid/src/transport.tspackages/hdwallet-ledger-webusb/src/transport.ts// 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>;
}@ledgerhq/hw-app-[chainname]LedgerTransportCoinTypepackages/hdwallet-ledger/src/transport.tscase "Eth"LedgerTransportCoinTypehdwallet-ledger/src/transport.tsLedgerTransportMethodMaphdwallet-ledger-webhid/src/transport.tshdwallet-ledger-webusb/src/transport.tspackages/hdwallet-core/src/[chainname].tspackages/hdwallet-core/src/index.tspackages/hdwallet-core/src/utils.tspackages/hdwallet-core/src/wallet.tspackages/hdwallet-native/src/[chainname].tspackages/hdwallet-native/src/native.tspackages/hdwallet-native/src/crypto/isolation/adapters/[chainname].tspackages/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.tssrc/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.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.tspackages/caip/src/adapters/coingecko/utils.tspackages/caip/src/adapters/coingecko/utils.test.tspackages/caip/src/adapters/coingecko/index.test.tsscripts/generateAssetData/coingecko.tsscripts/generateAssetData/[chainname]/index.tsscripts/generateAssetData/generateAssetData.tssrc/lib/asset-service/service/AssetService.tspackages/swapper/src/swappers/RelaySwapper/constant.tspackages/swapper/src/swappers/RelaySwapper/utils/relayTokenToAssetId.tssrc/context/WalletProvider/Ledger/constants.tspackages/hdwallet-*packages/hdwallet-corepackages/hdwallet-native