diff --git "a/docs/white-book/04-\346\234\215\345\212\241\347\257\207/02-\351\223\276\346\234\215\345\212\241/\345\256\236\347\216\260\350\247\204\350\214\203/02-web3-adapters.md" "b/docs/white-book/04-\346\234\215\345\212\241\347\257\207/02-\351\223\276\346\234\215\345\212\241/\345\256\236\347\216\260\350\247\204\350\214\203/02-web3-adapters.md"
new file mode 100644
index 00000000..e9ce4951
--- /dev/null
+++ "b/docs/white-book/04-\346\234\215\345\212\241\347\257\207/02-\351\223\276\346\234\215\345\212\241/\345\256\236\347\216\260\350\247\204\350\214\203/02-web3-adapters.md"
@@ -0,0 +1,209 @@
+# Web3 链适配器实现
+
+> 本文档描述 EVM、Tron、Bitcoin 链适配器的实现细节。
+
+## 概述
+
+Web3 适配器为非 Bioforest 链提供统一接口,包括:
+
+| 链类型 | 适配器 | API 端点 | 签名状态 |
+|-------|--------|---------|---------|
+| EVM (ETH/BSC) | `EvmAdapter` | PublicNode JSON-RPC | ✅ 完整 |
+| Tron | `TronAdapter` | PublicNode HTTP API | ✅ 完整 |
+| Bitcoin | `BitcoinAdapter` | mempool.space REST | ⚠️ 部分 |
+
+## 目录结构
+
+```
+src/services/chain-adapter/
+├── evm/
+│ ├── adapter.ts # 主适配器
+│ ├── identity-service.ts # 地址验证
+│ ├── asset-service.ts # 余额查询
+│ ├── chain-service.ts # 链信息、Gas 价格
+│ ├── transaction-service.ts # 交易构建、签名、广播
+│ └── types.ts
+├── tron/
+│ ├── adapter.ts
+│ ├── identity-service.ts # Base58Check 地址
+│ ├── asset-service.ts # TRX 余额
+│ ├── chain-service.ts # 带宽/能量
+│ ├── transaction-service.ts
+│ └── types.ts
+├── bitcoin/
+│ ├── adapter.ts
+│ ├── identity-service.ts # Bech32/Base58 地址
+│ ├── asset-service.ts # UTXO 余额
+│ ├── chain-service.ts # 费率估算
+│ ├── transaction-service.ts # UTXO 选择
+│ └── types.ts
+└── index.ts # 适配器注册
+```
+
+## EVM 适配器
+
+### 地址派生
+
+使用 BIP44 路径 `m/44'/60'/0'/0/index`:
+
+```typescript
+import { HDKey } from '@scure/bip32'
+import { keccak_256 } from '@noble/hashes/sha3.js'
+
+async deriveAddress(seed: Uint8Array, index = 0): Promise
{
+ const hdKey = HDKey.fromMasterSeed(seed)
+ const derived = hdKey.derive(`m/44'/60'/0'/0/${index}`)
+ const pubKey = secp256k1.getPublicKey(derived.privateKey!, false)
+ const hash = keccak_256(pubKey.slice(1))
+ return '0x' + bytesToHex(hash.slice(-20))
+}
+```
+
+### 交易签名
+
+使用 RLP 编码和 EIP-155 签名:
+
+```typescript
+// 1. 构建交易数据
+const txData = {
+ nonce: await this.rpc('eth_getTransactionCount', [from, 'pending']),
+ gasPrice: await this.rpc('eth_gasPrice'),
+ gasLimit: '0x5208', // 21000 for simple transfer
+ to, value, data: '0x',
+ chainId: this.evmChainId,
+}
+
+// 2. RLP 编码(EIP-155 预签名)
+const rawTx = this.rlpEncode([nonce, gasPrice, gasLimit, to, value, data, chainId, '0x', '0x'])
+
+// 3. 签名
+const msgHash = keccak_256(hexToBytes(rawTx.slice(2)))
+const sig = secp256k1.sign(msgHash, privateKey, { format: 'recovered' })
+const v = chainId * 2 + 35 + sig.recovery
+
+// 4. 编码签名交易
+const signedRaw = this.rlpEncode([nonce, gasPrice, gasLimit, to, value, data, v, r, s])
+```
+
+### API 调用
+
+使用标准 Ethereum JSON-RPC:
+
+```typescript
+private async rpc(method: string, params: unknown[] = []): Promise {
+ const response = await fetch(this.rpcUrl, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params }),
+ })
+ const json = await response.json()
+ return json.result
+}
+```
+
+## Tron 适配器
+
+### 地址格式
+
+Tron 使用 Base58Check 编码,前缀 `0x41`:
+
+```typescript
+// 公钥 → 地址
+const pubKeyHash = keccak_256(pubKey.slice(1)).slice(-20)
+const payload = new Uint8Array([0x41, ...pubKeyHash])
+const checksum = sha256(sha256(payload)).slice(0, 4)
+return base58.encode([...payload, ...checksum]) // 以 'T' 开头
+```
+
+### 交易流程
+
+1. **创建交易**:调用 `/wallet/createtransaction`
+2. **签名**:对 `txID`(已是 hash)进行 secp256k1 签名
+3. **广播**:调用 `/wallet/broadcasttransaction`
+
+```typescript
+// 创建交易
+const rawTx = await this.api('/wallet/createtransaction', {
+ owner_address: hexAddress,
+ to_address: toHexAddress,
+ amount: Number(amount.raw),
+})
+
+// 签名(Tron 的 txID 已经是 hash)
+const sig = secp256k1.sign(hexToBytes(rawTx.txID), privateKey, { format: 'recovered' })
+
+// 广播
+await this.api('/wallet/broadcasttransaction', { ...rawTx, signature: [bytesToHex(sig)] })
+```
+
+## Bitcoin 适配器
+
+### 地址类型支持
+
+| 类型 | 前缀 | BIP 路径 | 编码 |
+|-----|------|---------|------|
+| P2WPKH (SegWit) | bc1q | m/84'/0'/0'/0/x | Bech32 |
+| P2TR (Taproot) | bc1p | m/86'/0'/0'/0/x | Bech32m |
+| P2PKH (Legacy) | 1 | m/44'/0'/0'/0/x | Base58Check |
+
+默认使用 P2WPKH (Native SegWit):
+
+```typescript
+import { bech32 } from '@scure/base'
+
+async deriveAddress(seed: Uint8Array, index = 0): Promise {
+ const hdKey = HDKey.fromMasterSeed(seed)
+ const derived = hdKey.derive(`m/84'/0'/0'/0/${index}`)
+ const pubKeyHash = ripemd160(sha256(derived.publicKey!))
+ const words = bech32.toWords(pubKeyHash)
+ return bech32.encode('bc', [0, ...words])
+}
+```
+
+### UTXO 查询
+
+使用 mempool.space API:
+
+```typescript
+// 获取 UTXO 列表
+const utxos = await this.api(`/address/${address}/utxo`)
+
+// 计算余额
+const info = await this.api(`/address/${address}`)
+const balance = info.chain_stats.funded_txo_sum - info.chain_stats.spent_txo_sum
+```
+
+### 交易签名限制
+
+Bitcoin 交易签名比 EVM/Tron 复杂,需要:
+
+1. UTXO 选择算法
+2. 为每个输入构建 sighash
+3. 签名每个输入
+4. 构建完整的 witness 数据
+
+**当前状态**:余额查询、UTXO 列表、交易历史已实现。完整签名需要集成 `bitcoinjs-lib` 或类似库。
+
+## 依赖库
+
+| 库 | 用途 |
+|---|------|
+| `@noble/curves` | secp256k1 签名 |
+| `@noble/hashes` | sha256, keccak256, ripemd160 |
+| `@scure/bip32` | HD 密钥派生 |
+| `@scure/bip39` | 助记词处理 |
+| `@scure/base` | bech32, base58 编码 |
+| `viem` | EVM 工具(可选,用于进一步简化)|
+
+## 测试
+
+```bash
+# 运行适配器测试
+pnpm test -- --testPathPattern="chain-adapter"
+```
+
+## 相关文档
+
+- [链配置管理](../07-链配置管理/index.md)
+- [测试网络](../../08-测试篇/06-测试网络/index.md)
+- [ITransactionService](../ITransactionService.md)
diff --git "a/docs/white-book/08-\346\265\213\350\257\225\347\257\207/06-\346\265\213\350\257\225\347\275\221\347\273\234/index.md" "b/docs/white-book/08-\346\265\213\350\257\225\347\257\207/06-\346\265\213\350\257\225\347\275\221\347\273\234/index.md"
new file mode 100644
index 00000000..b31484fe
--- /dev/null
+++ "b/docs/white-book/08-\346\265\213\350\257\225\347\257\207/06-\346\265\213\350\257\225\347\275\221\347\273\234/index.md"
@@ -0,0 +1,337 @@
+# 公共 RPC 与测试网络接入指南
+
+> 本文档介绍如何接入公共 RPC 节点进行开发和测试。
+
+## 概述
+
+我们使用 **PublicNode** 作为统一的公共 RPC 提供商,它提供:
+
+- **免费**:无需 API Key,无请求限制
+- **全链支持**:Ethereum、BSC、Tron、Bitcoin 等
+- **主网+测试网**:同一提供商,API 一致
+
+## 公共 API 端点汇总
+
+### 主网
+
+| 链 | RPC 端点 | 协议 | 说明 |
+|---|---------|------|------|
+| Ethereum | `https://ethereum-rpc.publicnode.com` | JSON-RPC | PublicNode |
+| BSC | `https://bsc-rpc.publicnode.com` | JSON-RPC | PublicNode |
+| Tron | `https://tron-rpc.publicnode.com` | Tron HTTP API | PublicNode |
+| Bitcoin | `https://mempool.space/api` | REST API | mempool.space |
+
+### 测试网
+
+| 链 | RPC 端点 | 协议 | 说明 |
+|---|---------|------|------|
+| Ethereum Sepolia | `https://ethereum-sepolia-rpc.publicnode.com` | JSON-RPC | PublicNode |
+| BSC Testnet | `https://bsc-testnet-rpc.publicnode.com` | JSON-RPC | PublicNode |
+| Tron Nile | `https://nile.trongrid.io` | Tron HTTP API | TronGrid |
+| Bitcoin Testnet | `https://mempool.space/testnet/api` | REST API | mempool.space |
+| Bitcoin Signet | `https://mempool.space/signet/api` | REST API | mempool.space |
+
+### Bitcoin 为什么使用 mempool.space?
+
+PublicNode 提供标准 Bitcoin Core JSON-RPC,但存在以下限制:
+
+1. **地址查询慢**:`scantxoutset` 需要扫描整个 UTXO 集,约需 60 秒
+2. **无交易历史**:标准 RPC 不支持按地址查询交易记录
+3. **部分方法禁用**:`getaddressinfo` 等方法被限制
+
+mempool.space 提供专为钱包优化的 REST API:
+
+| 功能 | mempool.space | PublicNode |
+|------|--------------|------------|
+| 余额查询 | < 1秒 | ~60秒 |
+| UTXO 列表 | ✅ 支持 | ✅ 支持(慢)|
+| 交易历史 | ✅ 支持 | ❌ 不支持 |
+| 广播交易 | ✅ 支持 | ✅ 支持 |
+
+## 测试网络详情
+
+### Ethereum Sepolia
+
+Sepolia 是 Ethereum 官方推荐的测试网络。
+
+| 配置项 | 值 |
+|-------|-----|
+| 网络名称 | Sepolia Testnet |
+| Chain ID | 11155111 |
+| 货币符号 | SepoliaETH |
+| RPC 端点 | `https://ethereum-sepolia-rpc.publicnode.com` |
+| 区块浏览器 | https://sepolia.etherscan.io |
+| 水龙头 | https://sepoliafaucet.com |
+
+```typescript
+// chain-config 配置示例
+{
+ id: 'ethereum-sepolia',
+ version: '1.0',
+ type: 'evm',
+ name: 'Ethereum Sepolia',
+ symbol: 'SepoliaETH',
+ decimals: 18,
+ api: {
+ url: 'https://rpc.sepolia.org',
+ path: 'eth'
+ },
+ explorer: {
+ url: 'https://sepolia.etherscan.io',
+ queryTx: 'https://sepolia.etherscan.io/tx/:hash',
+ queryAddress: 'https://sepolia.etherscan.io/address/:address'
+ }
+}
+```
+
+#### BSC Testnet
+
+| 配置项 | 值 |
+|-------|-----|
+| 网络名称 | BSC Testnet |
+| Chain ID | 97 |
+| 货币符号 | tBNB |
+| RPC 端点 | `https://data-seed-prebsc-1-s1.binance.org:8545` |
+| 备用 RPC | `https://bsc-testnet-rpc.publicnode.com` |
+| 区块浏览器 | https://testnet.bscscan.com |
+| 水龙头 | https://testnet.binance.org/faucet-smart |
+
+### Bitcoin
+
+#### Bitcoin Testnet4
+
+Bitcoin Testnet4 是最新的测试网络,替代了经常被攻击的 Testnet3。
+
+| 配置项 | 值 |
+|-------|-----|
+| 网络名称 | Bitcoin Testnet4 |
+| 地址前缀 | tb1 (Bech32) / m/n (Legacy) |
+| RPC 端点 | 需要自建节点或使用 Blockstream API |
+| 区块浏览器 | https://mempool.space/testnet4 |
+| 水龙头 | https://testnet4.anyone.eu.org |
+
+#### Bitcoin Signet
+
+Signet 是一个受控的测试网络,区块由授权签名者产生,更稳定。
+
+| 配置项 | 值 |
+|-------|-----|
+| 网络名称 | Bitcoin Signet |
+| 地址前缀 | tb1 (Bech32) |
+| RPC 端点 | `https://mempool.space/signet/api` |
+| 区块浏览器 | https://mempool.space/signet |
+| 水龙头 | https://signetfaucet.com |
+
+### Tron
+
+#### Tron Nile Testnet
+
+Nile 是 Tron 官方维护的测试网络。
+
+| 配置项 | 值 |
+|-------|-----|
+| 网络名称 | Tron Nile Testnet |
+| 地址前缀 | T |
+| Full Node | `https://nile.trongrid.io` |
+| Solidity Node | `https://nile.trongrid.io` |
+| Event Server | `https://event.nileex.io` |
+| 区块浏览器 | https://nile.tronscan.org |
+| 水龙头 | https://nileex.io/join/getJoinPage |
+
+```typescript
+// chain-config 配置示例
+{
+ id: 'tron-nile',
+ version: '1.0',
+ type: 'bip39',
+ name: 'Tron Nile Testnet',
+ symbol: 'TRX',
+ decimals: 6,
+ api: {
+ url: 'https://nile.trongrid.io',
+ path: 'tron'
+ },
+ explorer: {
+ url: 'https://nile.tronscan.org',
+ queryTx: 'https://nile.tronscan.org/#/transaction/:hash',
+ queryAddress: 'https://nile.tronscan.org/#/address/:address'
+ }
+}
+```
+
+#### Tron Shasta Testnet
+
+Shasta 是另一个常用的 Tron 测试网络。
+
+| 配置项 | 值 |
+|-------|-----|
+| 网络名称 | Tron Shasta Testnet |
+| Full Node | `https://api.shasta.trongrid.io` |
+| 区块浏览器 | https://shasta.tronscan.org |
+| 水龙头 | https://www.trongrid.io/faucet |
+
+## 配置测试网络
+
+### 方式一:环境变量切换
+
+```bash
+# .env.development
+VITE_NETWORK_MODE=testnet
+
+# .env.production
+VITE_NETWORK_MODE=mainnet
+```
+
+### 方式二:运行时配置
+
+```typescript
+// src/services/chain-config/testnet-configs.ts
+import type { ChainConfig } from './types'
+
+export const TESTNET_CONFIGS: ChainConfig[] = [
+ {
+ id: 'ethereum-sepolia',
+ version: '1.0',
+ type: 'evm',
+ name: 'Ethereum Sepolia',
+ symbol: 'SepoliaETH',
+ decimals: 18,
+ api: { url: 'https://rpc.sepolia.org', path: 'eth' },
+ explorer: { url: 'https://sepolia.etherscan.io' },
+ enabled: true,
+ source: 'default',
+ },
+ {
+ id: 'bsc-testnet',
+ version: '1.0',
+ type: 'evm',
+ name: 'BSC Testnet',
+ symbol: 'tBNB',
+ decimals: 18,
+ api: { url: 'https://bsc-testnet-rpc.publicnode.com', path: 'bnb' },
+ explorer: { url: 'https://testnet.bscscan.com' },
+ enabled: true,
+ source: 'default',
+ },
+ {
+ id: 'tron-nile',
+ version: '1.0',
+ type: 'bip39',
+ name: 'Tron Nile',
+ symbol: 'TRX',
+ decimals: 6,
+ api: { url: 'https://nile.trongrid.io', path: 'tron' },
+ explorer: { url: 'https://nile.tronscan.org' },
+ enabled: true,
+ source: 'default',
+ },
+]
+```
+
+### 方式三:通过订阅 URL
+
+创建一个测试网络的链配置订阅文件:
+
+```json
+// https://example.com/testnet-chains.json
+[
+ {
+ "id": "ethereum-sepolia",
+ "version": "1.0",
+ "type": "evm",
+ "name": "Ethereum Sepolia",
+ "symbol": "SepoliaETH",
+ "decimals": 18,
+ "api": { "url": "https://rpc.sepolia.org", "path": "eth" }
+ }
+]
+```
+
+## 测试水龙头使用
+
+### 获取测试代币的步骤
+
+1. **生成测试钱包地址**
+ - 使用应用的钱包创建功能
+ - 或使用 `deriveAddressesForChains` 派生地址
+
+2. **访问水龙头网站**
+ - 输入钱包地址
+ - 完成人机验证
+ - 等待测试代币到账
+
+3. **验证余额**
+ ```typescript
+ const adapter = registry.getAdapter('ethereum-sepolia')
+ const balance = await adapter.asset.getNativeBalance(address)
+ console.log(`Balance: ${balance.amount.toFormatted()} ${balance.symbol}`)
+ ```
+
+### 自动化获取测试代币
+
+对于 CI/CD 环境,可以使用以下方式:
+
+```typescript
+// scripts/faucet.ts
+async function requestTestTokens(chain: string, address: string) {
+ switch (chain) {
+ case 'ethereum-sepolia':
+ // Alchemy Sepolia faucet API (需要 API key)
+ await fetch('https://api.alchemy.com/v2/faucet', {
+ method: 'POST',
+ body: JSON.stringify({ network: 'sepolia', address }),
+ })
+ break
+ case 'tron-nile':
+ // Tron Nile faucet (每日限额)
+ await fetch('https://nileex.io/api/v1/faucet', {
+ method: 'POST',
+ body: JSON.stringify({ address, amount: 1000 }),
+ })
+ break
+ }
+}
+```
+
+## 测试网络使用注意事项
+
+1. **测试代币无价值**
+ - 测试网代币仅用于测试,不具有真实价值
+ - 不要在测试网地址存入主网代币
+
+2. **网络不稳定**
+ - 测试网可能偶尔重置或中断
+ - 建议准备多个测试网备选
+
+3. **水龙头限制**
+ - 大多数水龙头有每日/每地址限额
+ - 建议提前储备足够的测试代币
+
+4. **地址格式一致**
+ - EVM 测试网地址与主网格式相同
+ - Bitcoin 测试网使用 `tb1` 前缀
+ - Tron 测试网使用 `T` 前缀(与主网相同)
+
+## 开发模式集成
+
+将测试网络集成到开发模式:
+
+```typescript
+// src/hooks/use-send.ts
+const { useMock = import.meta.env.DEV } = options
+
+// 开发模式下自动使用测试网配置
+const chainConfig = useMemo(() => {
+ if (import.meta.env.DEV && import.meta.env.VITE_USE_TESTNET) {
+ return getTestnetConfig(originalConfig.id)
+ }
+ return originalConfig
+}, [originalConfig])
+```
+
+## 相关资源
+
+- [Ethereum Sepolia 文档](https://ethereum.org/en/developers/docs/networks/#sepolia)
+- [BSC Testnet 文档](https://docs.bnbchain.org/docs/bsc-testnet/)
+- [Bitcoin Signet](https://en.bitcoin.it/wiki/Signet)
+- [Tron Nile 文档](https://developers.tron.network/docs/testnet)
diff --git "a/docs/white-book/08-\346\265\213\350\257\225\347\257\207/index.md" "b/docs/white-book/08-\346\265\213\350\257\225\347\257\207/index.md"
index bd1869e4..8f96eceb 100644
--- "a/docs/white-book/08-\346\265\213\350\257\225\347\257\207/index.md"
+++ "b/docs/white-book/08-\346\265\213\350\257\225\347\257\207/index.md"
@@ -10,6 +10,7 @@
- [第二十四章:Vitest 配置](./02-Vitest配置/) - 单元测试、组件测试
- [第二十五章:Playwright 配置](./03-Playwright配置/) - E2E、视觉回归
- [E2E 测试最佳实践](./03-Playwright配置/e2e-best-practices.md) - **必读:国际化项目的测试规范**
+- [第二十六章:测试网络](./06-测试网络/) - **新增:接入公开测试网络进行真实交易测试**
---
diff --git a/docs/white-book/index.md b/docs/white-book/index.md
index 963dfc40..1767286b 100644
--- a/docs/white-book/index.md
+++ b/docs/white-book/index.md
@@ -105,6 +105,7 @@
| [03 Playwright 配置](./08-测试篇/03-Playwright配置/) | E2E、截图、视觉回归 |
| [04 性能测试](./08-测试篇/04-性能测试/) | 性能指标、回归检测、自动化 |
| [05 安全测试](./08-测试篇/05-安全测试/) | SAST、SCA、渗透测试 |
+| [06 测试网络](./08-测试篇/06-测试网络/) | Sepolia、BSC Testnet、Nile、Signet |
### [第九篇:部署篇](./09-部署篇/)
diff --git a/package.json b/package.json
index 3e8fe25c..a810da7e 100644
--- a/package.json
+++ b/package.json
@@ -72,6 +72,7 @@
"@plaoc/plugins": "^1.1.9",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-slot": "^1.2.4",
+ "@scure/base": "^2.0.0",
"@scure/bip32": "^2.0.1",
"@scure/bip39": "^2.0.1",
"@stackflow/core": "^1.3.0",
@@ -103,6 +104,7 @@
"swiper": "^12.0.3",
"tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0",
+ "viem": "^2.43.3",
"yargs": "^18.0.0",
"zod": "^4.1.13"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6177f3ae..54e8b312 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -65,6 +65,9 @@ importers:
'@radix-ui/react-slot':
specifier: ^1.2.4
version: 1.2.4(@types/react@19.2.7)(react@19.2.3)
+ '@scure/base':
+ specifier: ^2.0.0
+ version: 2.0.0
'@scure/bip32':
specifier: ^2.0.1
version: 2.0.1
@@ -158,6 +161,9 @@ importers:
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
+ viem:
+ specifier: ^2.43.3
+ version: 2.43.3(typescript@5.9.3)(zod@4.2.1)
yargs:
specifier: ^18.0.0
version: 18.0.0
@@ -1666,6 +1672,10 @@ packages:
'@noble/curves@1.4.2':
resolution: {integrity: sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==}
+ '@noble/curves@1.9.1':
+ resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==}
+ engines: {node: ^14.21.3 || >=16}
+
'@noble/curves@1.9.7':
resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==}
engines: {node: ^14.21.3 || >=16}
@@ -2114,18 +2124,27 @@ packages:
'@scure/base@1.1.9':
resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==}
+ '@scure/base@1.2.6':
+ resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==}
+
'@scure/base@2.0.0':
resolution: {integrity: sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==}
'@scure/bip32@1.4.0':
resolution: {integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==}
+ '@scure/bip32@1.7.0':
+ resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==}
+
'@scure/bip32@2.0.1':
resolution: {integrity: sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==}
'@scure/bip39@1.3.0':
resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==}
+ '@scure/bip39@1.6.0':
+ resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==}
+
'@scure/bip39@2.0.1':
resolution: {integrity: sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==}
@@ -2823,6 +2842,17 @@ packages:
zod:
optional: true
+ abitype@1.2.3:
+ resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==}
+ peerDependencies:
+ typescript: '>=5.0.4'
+ zod: ^3.22.0 || ^4.0.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ zod:
+ optional: true
+
accepts@2.0.0:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'}
@@ -4178,6 +4208,11 @@ packages:
peerDependencies:
ws: '*'
+ isows@1.0.7:
+ resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==}
+ peerDependencies:
+ ws: '*'
+
isstream@0.1.2:
resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
@@ -4690,6 +4725,14 @@ packages:
outvariant@1.4.3:
resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
+ ox@0.11.1:
+ resolution: {integrity: sha512-1l1gOLAqg0S0xiN1dH5nkPna8PucrZgrIJOfS49MLNiMevxu07Iz4ZjuJS9N+xifvT+PsZyIptS7WHM8nC+0+A==}
+ peerDependencies:
+ typescript: '>=5.4.0'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
oxlint@1.35.0:
resolution: {integrity: sha512-QDX1aUgaiqznkGfTM2qHwva2wtKqhVoqPSVXrnPz+yLUhlNadikD3QRuRtppHl7WGuy3wG6nKAuR8lash3aWSg==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -5873,6 +5916,14 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+ viem@2.43.3:
+ resolution: {integrity: sha512-zM251fspfSjENCtfmT7cauuD+AA/YAlkFU7cksdEQJxj7wDuO0XFRWRH+RMvfmTFza88B9kug5cKU+Wk2nAjJg==}
+ peerDependencies:
+ typescript: '>=5.0.4'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
vite-plugin-commonjs@0.10.4:
resolution: {integrity: sha512-eWQuvQKCcx0QYB5e5xfxBNjQKyrjEWZIR9UOkOV6JAgxVhtbZvCOF+FNC2ZijBJ3U3Px04ZMMyyMyFBVWIJ5+g==}
@@ -7962,6 +8013,10 @@ snapshots:
dependencies:
'@noble/hashes': 1.4.0
+ '@noble/curves@1.9.1':
+ dependencies:
+ '@noble/hashes': 1.8.0
+
'@noble/curves@1.9.7':
dependencies:
'@noble/hashes': 1.8.0
@@ -8313,6 +8368,8 @@ snapshots:
'@scure/base@1.1.9': {}
+ '@scure/base@1.2.6': {}
+
'@scure/base@2.0.0': {}
'@scure/bip32@1.4.0':
@@ -8321,6 +8378,12 @@ snapshots:
'@noble/hashes': 1.4.0
'@scure/base': 1.1.9
+ '@scure/bip32@1.7.0':
+ dependencies:
+ '@noble/curves': 1.9.7
+ '@noble/hashes': 1.8.0
+ '@scure/base': 1.2.6
+
'@scure/bip32@2.0.1':
dependencies:
'@noble/curves': 2.0.1
@@ -8332,6 +8395,11 @@ snapshots:
'@noble/hashes': 1.4.0
'@scure/base': 1.1.9
+ '@scure/bip39@1.6.0':
+ dependencies:
+ '@noble/hashes': 1.8.0
+ '@scure/base': 1.2.6
+
'@scure/bip39@2.0.1':
dependencies:
'@noble/hashes': 2.0.1
@@ -9175,6 +9243,11 @@ snapshots:
optionalDependencies:
zod: 4.2.1
+ abitype@1.2.3(typescript@5.9.3)(zod@4.2.1):
+ optionalDependencies:
+ typescript: 5.9.3
+ zod: 4.2.1
+
accepts@2.0.0:
dependencies:
mime-types: 3.0.2
@@ -10622,6 +10695,10 @@ snapshots:
dependencies:
ws: 8.18.3
+ isows@1.0.7(ws@8.18.3):
+ dependencies:
+ ws: 8.18.3
+
isstream@0.1.2: {}
istanbul-lib-coverage@3.2.2: {}
@@ -11121,6 +11198,21 @@ snapshots:
outvariant@1.4.3: {}
+ ox@0.11.1(typescript@5.9.3)(zod@4.2.1):
+ dependencies:
+ '@adraffy/ens-normalize': 1.11.1
+ '@noble/ciphers': 1.3.0
+ '@noble/curves': 1.9.1
+ '@noble/hashes': 1.8.0
+ '@scure/bip32': 1.7.0
+ '@scure/bip39': 1.6.0
+ abitype: 1.2.3(typescript@5.9.3)(zod@4.2.1)
+ eventemitter3: 5.0.1
+ optionalDependencies:
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - zod
+
oxlint@1.35.0:
optionalDependencies:
'@oxlint/darwin-arm64': 1.35.0
@@ -12288,6 +12380,23 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
+ viem@2.43.3(typescript@5.9.3)(zod@4.2.1):
+ dependencies:
+ '@noble/curves': 1.9.1
+ '@noble/hashes': 1.8.0
+ '@scure/bip32': 1.7.0
+ '@scure/bip39': 1.6.0
+ abitype: 1.2.3(typescript@5.9.3)(zod@4.2.1)
+ isows: 1.0.7(ws@8.18.3)
+ ox: 0.11.1(typescript@5.9.3)(zod@4.2.1)
+ ws: 8.18.3
+ optionalDependencies:
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+ - zod
+
vite-plugin-commonjs@0.10.4:
dependencies:
acorn: 8.15.0
diff --git a/public/configs/default-chains.json b/public/configs/default-chains.json
index 629c9485..857c68eb 100644
--- a/public/configs/default-chains.json
+++ b/public/configs/default-chains.json
@@ -158,7 +158,7 @@
{
"id": "tron",
"version": "1.0",
- "type": "bip39",
+ "type": "tron",
"name": "Tron",
"symbol": "TRX",
"icon": "../icons/tron/chain.svg",
diff --git a/public/configs/testnet-chains.json b/public/configs/testnet-chains.json
new file mode 100644
index 00000000..383c901a
--- /dev/null
+++ b/public/configs/testnet-chains.json
@@ -0,0 +1,62 @@
+[
+ {
+ "id": "ethereum-sepolia",
+ "version": "1.0",
+ "type": "evm",
+ "name": "Ethereum Sepolia",
+ "symbol": "SepoliaETH",
+ "icon": "../icons/ethereum/chain.svg",
+ "decimals": 18,
+ "api": { "url": "https://rpc.sepolia.org", "path": "eth" },
+ "explorer": {
+ "url": "https://sepolia.etherscan.io",
+ "queryTx": "https://sepolia.etherscan.io/tx/:hash",
+ "queryAddress": "https://sepolia.etherscan.io/address/:address"
+ }
+ },
+ {
+ "id": "bsc-testnet",
+ "version": "1.0",
+ "type": "evm",
+ "name": "BSC Testnet",
+ "symbol": "tBNB",
+ "icon": "../icons/binance/chain.svg",
+ "decimals": 18,
+ "api": { "url": "https://bsc-testnet-rpc.publicnode.com", "path": "bnb" },
+ "explorer": {
+ "url": "https://testnet.bscscan.com",
+ "queryTx": "https://testnet.bscscan.com/tx/:hash",
+ "queryAddress": "https://testnet.bscscan.com/address/:address"
+ }
+ },
+ {
+ "id": "tron-nile",
+ "version": "1.0",
+ "type": "bip39",
+ "name": "Tron Nile Testnet",
+ "symbol": "TRX",
+ "icon": "../icons/tron/chain.svg",
+ "decimals": 6,
+ "api": { "url": "https://nile.trongrid.io", "path": "tron" },
+ "explorer": {
+ "url": "https://nile.tronscan.org",
+ "queryTx": "https://nile.tronscan.org/#/transaction/:hash",
+ "queryAddress": "https://nile.tronscan.org/#/address/:address"
+ }
+ },
+ {
+ "id": "bitcoin-signet",
+ "version": "1.0",
+ "type": "bip39",
+ "name": "Bitcoin Signet",
+ "symbol": "sBTC",
+ "icon": "../icons/bitcoin/chain.svg",
+ "decimals": 8,
+ "api": { "url": "https://mempool.space/signet/api", "path": "btc" },
+ "explorer": {
+ "url": "https://mempool.space/signet",
+ "queryTx": "https://mempool.space/signet/tx/:hash",
+ "queryAddress": "https://mempool.space/signet/address/:address"
+ }
+ }
+]
diff --git a/src/hooks/use-send.ts b/src/hooks/use-send.ts
index 77310c57..58f1d518 100644
--- a/src/hooks/use-send.ts
+++ b/src/hooks/use-send.ts
@@ -4,6 +4,7 @@ import { Amount } from '@/types/amount'
import { initialState, MOCK_FEES } from './use-send.constants'
import type { SendState, UseSendOptions, UseSendReturn } from './use-send.types'
import { fetchBioforestBalance, fetchBioforestFee, submitBioforestTransfer } from './use-send.bioforest'
+import { fetchWeb3Fee, submitWeb3Transfer, validateWeb3Address } from './use-send.web3'
import { adjustAmountForFee, canProceedToConfirm, validateAddressInput, validateAmountInput } from './use-send.logic'
import { submitMockTransfer } from './use-send.mock'
@@ -19,11 +20,16 @@ export function useSend(options: UseSendOptions = {}): UseSendReturn {
})
const isBioforestChain = chainConfig?.type === 'bioforest'
+ const isWeb3Chain = chainConfig?.type === 'evm' || chainConfig?.type === 'tron' || chainConfig?.type === 'bip39'
// Validate address
const validateAddress = useCallback((address: string): string | null => {
+ // Use chain adapter validation for Web3 chains
+ if (isWeb3Chain && chainConfig) {
+ return validateWeb3Address(chainConfig, address)
+ }
return validateAddressInput(address, isBioforestChain)
- }, [isBioforestChain])
+ }, [isBioforestChain, isWeb3Chain, chainConfig])
// Validate amount
const validateAmount = useCallback((amount: Amount | null, asset: AssetInfo | null): string | null => {
@@ -56,7 +62,9 @@ export function useSend(options: UseSendOptions = {}): UseSendReturn {
feeLoading: true,
}))
- if (useMock || !isBioforestChain || !chainConfig || !fromAddress) {
+ const shouldUseMock = useMock || (!isBioforestChain && !isWeb3Chain) || !chainConfig || !fromAddress
+
+ if (shouldUseMock) {
// Mock fee estimation delay
setTimeout(() => {
const fee = MOCK_FEES[asset.assetType] ?? { amount: '0.001', symbol: asset.assetType }
@@ -73,7 +81,11 @@ export function useSend(options: UseSendOptions = {}): UseSendReturn {
void (async () => {
try {
- const feeEstimate = await fetchBioforestFee(chainConfig, fromAddress)
+ // Use appropriate fee fetcher based on chain type
+ const feeEstimate = isWeb3Chain
+ ? await fetchWeb3Fee(chainConfig, fromAddress)
+ : await fetchBioforestFee(chainConfig, fromAddress)
+
setState((prev) => ({
...prev,
feeAmount: feeEstimate.amount,
@@ -88,7 +100,7 @@ export function useSend(options: UseSendOptions = {}): UseSendReturn {
}))
}
})()
- }, [chainConfig, fromAddress, isBioforestChain, useMock])
+ }, [chainConfig, fromAddress, isBioforestChain, isWeb3Chain, useMock])
useEffect(() => {
if (useMock || !isBioforestChain || !chainConfig || !fromAddress) return
@@ -183,15 +195,94 @@ export function useSend(options: UseSendOptions = {}): UseSendReturn {
return result.status === 'ok' ? { status: 'ok' as const } : { status: 'error' as const }
}
- if (!chainConfig || chainConfig.type !== 'bioforest') {
- console.log('[useSend.submit] Chain not supported:', chainConfig?.type)
+ if (!chainConfig) {
+ console.log('[useSend.submit] No chain config')
+ setState((prev) => ({
+ ...prev,
+ step: 'result',
+ isSubmitting: false,
+ resultStatus: 'failed',
+ txHash: null,
+ errorMessage: '链配置缺失',
+ }))
+ return { status: 'error' as const }
+ }
+
+ // Handle Web3 chains (EVM, Tron, Bitcoin)
+ if (chainConfig.type === 'evm' || chainConfig.type === 'tron' || chainConfig.type === 'bip39') {
+ console.log('[useSend.submit] Using Web3 transfer for:', chainConfig.type)
+
+ if (!walletId || !fromAddress || !state.asset || !state.amount) {
+ setState((prev) => ({
+ ...prev,
+ step: 'result',
+ isSubmitting: false,
+ resultStatus: 'failed',
+ txHash: null,
+ errorMessage: '钱包信息不完整',
+ }))
+ return { status: 'error' as const }
+ }
+
+ setState((prev) => ({
+ ...prev,
+ step: 'sending',
+ isSubmitting: true,
+ errorMessage: null,
+ }))
+
+ const result = await submitWeb3Transfer({
+ chainConfig,
+ walletId,
+ password,
+ fromAddress,
+ toAddress: state.toAddress,
+ amount: state.amount,
+ })
+
+ if (result.status === 'password') {
+ setState((prev) => ({
+ ...prev,
+ step: 'confirm',
+ isSubmitting: false,
+ }))
+ return { status: 'password' as const }
+ }
+
+ if (result.status === 'error') {
+ setState((prev) => ({
+ ...prev,
+ step: 'result',
+ isSubmitting: false,
+ resultStatus: 'failed',
+ txHash: null,
+ errorMessage: result.message,
+ }))
+ return { status: 'error' as const }
+ }
+
+ setState((prev) => ({
+ ...prev,
+ step: 'result',
+ isSubmitting: false,
+ resultStatus: 'success',
+ txHash: result.txHash,
+ errorMessage: null,
+ }))
+
+ return { status: 'ok' as const, txHash: result.txHash }
+ }
+
+ // Unsupported chain type
+ if (chainConfig.type !== 'bioforest') {
+ console.log('[useSend.submit] Chain type not supported:', chainConfig.type)
setState((prev) => ({
...prev,
step: 'result',
isSubmitting: false,
resultStatus: 'failed',
txHash: null,
- errorMessage: '当前链暂不支持真实转账',
+ errorMessage: `不支持的链类型: ${chainConfig.type}`,
}))
return { status: 'error' as const }
}
diff --git a/src/hooks/use-send.web3.ts b/src/hooks/use-send.web3.ts
new file mode 100644
index 00000000..0a07fde9
--- /dev/null
+++ b/src/hooks/use-send.web3.ts
@@ -0,0 +1,200 @@
+/**
+ * Web3 Transfer Implementation
+ *
+ * Handles transfers for EVM, Tron, and Bitcoin chains using chain adapters.
+ */
+
+import type { AssetInfo } from '@/types/asset'
+import type { ChainConfig } from '@/services/chain-config'
+import { Amount } from '@/types/amount'
+import { walletStorageService, WalletStorageError, WalletStorageErrorCode } from '@/services/wallet-storage'
+import { getAdapterRegistry, setupAdapters } from '@/services/chain-adapter'
+import { mnemonicToSeedSync } from '@scure/bip39'
+
+// Ensure adapters are registered
+let adaptersInitialized = false
+function ensureAdapters() {
+ if (!adaptersInitialized) {
+ setupAdapters()
+ adaptersInitialized = true
+ }
+}
+
+export interface Web3FeeResult {
+ amount: Amount
+ symbol: string
+}
+
+export async function fetchWeb3Fee(chainConfig: ChainConfig, fromAddress: string): Promise {
+ ensureAdapters()
+
+ const registry = getAdapterRegistry()
+ registry.setChainConfigs([chainConfig])
+
+ const adapter = registry.getAdapter(chainConfig.id)
+ if (!adapter) {
+ throw new Error(`No adapter found for chain: ${chainConfig.id}`)
+ }
+
+ const feeEstimate = await adapter.transaction.estimateFee({
+ from: fromAddress,
+ to: fromAddress,
+ amount: Amount.fromRaw('1', chainConfig.decimals, chainConfig.symbol),
+ })
+
+ return {
+ amount: feeEstimate.standard.amount,
+ symbol: chainConfig.symbol,
+ }
+}
+
+export async function fetchWeb3Balance(chainConfig: ChainConfig, fromAddress: string): Promise {
+ ensureAdapters()
+
+ const registry = getAdapterRegistry()
+ registry.setChainConfigs([chainConfig])
+
+ const adapter = registry.getAdapter(chainConfig.id)
+ if (!adapter) {
+ throw new Error(`No adapter found for chain: ${chainConfig.id}`)
+ }
+
+ const balance = await adapter.asset.getNativeBalance(fromAddress)
+
+ return {
+ assetType: balance.symbol,
+ name: chainConfig.name,
+ amount: balance.amount,
+ decimals: balance.amount.decimals,
+ }
+}
+
+export type SubmitWeb3Result =
+ | { status: 'ok'; txHash: string }
+ | { status: 'password' }
+ | { status: 'error'; message: string }
+
+export interface SubmitWeb3Params {
+ chainConfig: ChainConfig
+ walletId: string
+ password: string
+ fromAddress: string
+ toAddress: string
+ amount: Amount
+}
+
+export async function submitWeb3Transfer({
+ chainConfig,
+ walletId,
+ password,
+ fromAddress,
+ toAddress,
+ amount,
+}: SubmitWeb3Params): Promise {
+ ensureAdapters()
+
+ // Get mnemonic from wallet storage
+ let mnemonic: string
+ try {
+ mnemonic = await walletStorageService.getMnemonic(walletId, password)
+ } catch (error) {
+ if (error instanceof WalletStorageError && error.code === WalletStorageErrorCode.DECRYPTION_FAILED) {
+ return { status: 'password' }
+ }
+ return {
+ status: 'error',
+ message: error instanceof Error ? error.message : '未知错误',
+ }
+ }
+
+ if (!amount.isPositive()) {
+ return { status: 'error', message: '请输入有效金额' }
+ }
+
+ try {
+ const registry = getAdapterRegistry()
+ registry.setChainConfigs([chainConfig])
+
+ const adapter = registry.getAdapter(chainConfig.id)
+ if (!adapter) {
+ return { status: 'error', message: `不支持的链: ${chainConfig.id}` }
+ }
+
+ console.log('[submitWeb3Transfer] Starting transfer:', {
+ chain: chainConfig.id,
+ type: chainConfig.type,
+ fromAddress,
+ toAddress,
+ amount: amount.toRawString()
+ })
+
+ // Derive private key from mnemonic
+ const seed = mnemonicToSeedSync(mnemonic)
+
+ // Build unsigned transaction
+ console.log('[submitWeb3Transfer] Building transaction...')
+ const unsignedTx = await adapter.transaction.buildTransaction({
+ from: fromAddress,
+ to: toAddress,
+ amount,
+ })
+
+ // Sign transaction
+ console.log('[submitWeb3Transfer] Signing transaction...')
+ const signedTx = await adapter.transaction.signTransaction(unsignedTx, seed)
+
+ // Broadcast transaction
+ console.log('[submitWeb3Transfer] Broadcasting transaction...')
+ const txHash = await adapter.transaction.broadcastTransaction(signedTx)
+
+ console.log('[submitWeb3Transfer] SUCCESS! txHash:', txHash)
+ return { status: 'ok', txHash }
+ } catch (error) {
+ console.error('[submitWeb3Transfer] FAILED:', error)
+
+ const errorMessage = error instanceof Error ? error.message : String(error)
+
+ // Handle specific error cases
+ if (errorMessage.includes('insufficient') || errorMessage.includes('余额不足')) {
+ return { status: 'error', message: '余额不足' }
+ }
+
+ if (errorMessage.includes('fee') || errorMessage.includes('手续费') || errorMessage.includes('gas')) {
+ return { status: 'error', message: '手续费不足' }
+ }
+
+ if (errorMessage.includes('not yet implemented') || errorMessage.includes('not supported')) {
+ return { status: 'error', message: '该链转账功能尚未完全实现' }
+ }
+
+ return {
+ status: 'error',
+ message: errorMessage || '交易失败,请稍后重试',
+ }
+ }
+}
+
+/**
+ * Validate address for Web3 chains
+ */
+export function validateWeb3Address(chainConfig: ChainConfig, address: string): string | null {
+ ensureAdapters()
+
+ const registry = getAdapterRegistry()
+ registry.setChainConfigs([chainConfig])
+
+ const adapter = registry.getAdapter(chainConfig.id)
+ if (!adapter) {
+ return '不支持的链类型'
+ }
+
+ if (!address || address.trim() === '') {
+ return '请输入收款地址'
+ }
+
+ if (!adapter.identity.isValidAddress(address)) {
+ return '无效的地址格式'
+ }
+
+ return null
+}
diff --git a/src/services/chain-adapter/__tests__/bitcoin-adapter.test.ts b/src/services/chain-adapter/__tests__/bitcoin-adapter.test.ts
new file mode 100644
index 00000000..3f2b3ddf
--- /dev/null
+++ b/src/services/chain-adapter/__tests__/bitcoin-adapter.test.ts
@@ -0,0 +1,79 @@
+import { describe, it, expect } from 'vitest'
+import { BitcoinAdapter } from '../bitcoin'
+import type { ChainConfig } from '@/services/chain-config'
+import { mnemonicToSeedSync } from '@scure/bip39'
+
+const TEST_MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
+
+const btcConfig: ChainConfig = {
+ id: 'bitcoin',
+ version: '1.0',
+ type: 'bip39',
+ name: 'Bitcoin',
+ symbol: 'BTC',
+ decimals: 8,
+ enabled: true,
+ source: 'default',
+}
+
+describe('BitcoinAdapter', () => {
+ const adapter = new BitcoinAdapter(btcConfig)
+
+ describe('BitcoinIdentityService', () => {
+ it('derives correct P2WPKH address from mnemonic', async () => {
+ const seed = mnemonicToSeedSync(TEST_MNEMONIC)
+ const address = await adapter.identity.deriveAddress(seed, 0)
+
+ // P2WPKH (bc1q...) address for this mnemonic at BIP84 path m/84'/0'/0'/0/0
+ expect(address).toMatch(/^bc1q[a-z0-9]{38,}$/)
+ expect(address.startsWith('bc1q')).toBe(true)
+ })
+
+ it('derives different addresses for different indices', async () => {
+ const seed = mnemonicToSeedSync(TEST_MNEMONIC)
+ const addr0 = await adapter.identity.deriveAddress(seed, 0)
+ const addr1 = await adapter.identity.deriveAddress(seed, 1)
+
+ expect(addr0).not.toBe(addr1)
+ })
+
+ it('validates P2WPKH addresses correctly', () => {
+ // Valid bc1q addresses
+ expect(adapter.identity.isValidAddress('bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq')).toBe(true)
+ expect(adapter.identity.isValidAddress('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4')).toBe(true)
+
+ // Invalid addresses
+ expect(adapter.identity.isValidAddress('invalid')).toBe(false)
+ expect(adapter.identity.isValidAddress('bc1q123')).toBe(false)
+ expect(adapter.identity.isValidAddress('')).toBe(false)
+ })
+
+ it('validates P2TR (Taproot) addresses correctly', () => {
+ // Valid bc1p addresses
+ expect(adapter.identity.isValidAddress('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0')).toBe(true)
+ })
+
+ it('validates legacy P2PKH addresses correctly', () => {
+ // Valid legacy addresses starting with 1
+ expect(adapter.identity.isValidAddress('1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2')).toBe(true)
+ expect(adapter.identity.isValidAddress('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa')).toBe(true)
+ })
+
+ it('normalizes bech32 addresses to lowercase', () => {
+ const addr = 'BC1QAR0SRRR7XFKVY5L643LYDNW9RE59GTZZWF5MDQ'
+ expect(adapter.identity.normalizeAddress(addr)).toBe(addr.toLowerCase())
+ })
+ })
+
+ describe('BitcoinChainService', () => {
+ it('returns correct chain info', () => {
+ const info = adapter.chain.getChainInfo()
+
+ expect(info.chainId).toBe('bitcoin')
+ expect(info.symbol).toBe('BTC')
+ expect(info.decimals).toBe(8)
+ expect(info.blockTime).toBe(600) // ~10 minutes
+ expect(info.confirmations).toBe(6)
+ })
+ })
+})
diff --git a/src/services/chain-adapter/__tests__/evm-adapter.test.ts b/src/services/chain-adapter/__tests__/evm-adapter.test.ts
new file mode 100644
index 00000000..5202e8c5
--- /dev/null
+++ b/src/services/chain-adapter/__tests__/evm-adapter.test.ts
@@ -0,0 +1,47 @@
+import { describe, it, expect } from 'vitest'
+import { EvmAdapter } from '../evm'
+import type { ChainConfig } from '@/services/chain-config'
+
+const ethConfig: ChainConfig = {
+ id: 'ethereum',
+ version: '1.0',
+ type: 'evm',
+ name: 'Ethereum',
+ symbol: 'ETH',
+ decimals: 18,
+ enabled: true,
+ source: 'default',
+}
+
+describe('EvmAdapter', () => {
+ const adapter = new EvmAdapter(ethConfig)
+
+ describe('EvmIdentityService', () => {
+ it('validates Ethereum addresses correctly', () => {
+ expect(adapter.identity.isValidAddress('0x9858effd232b4033e47d90003d41ec34ecaeda94')).toBe(true)
+ expect(adapter.identity.isValidAddress('0x9858EfFD232B4033E47d90003D41EC34EcaEda94')).toBe(true) // checksummed
+ expect(adapter.identity.isValidAddress('invalid')).toBe(false)
+ expect(adapter.identity.isValidAddress('0x123')).toBe(false)
+ expect(adapter.identity.isValidAddress('')).toBe(false)
+ })
+
+ it('normalizes addresses to EIP-55 checksum format', () => {
+ const addr = '0x9858effd232b4033e47d90003d41ec34ecaeda94'
+ const normalized = adapter.identity.normalizeAddress(addr)
+ // EIP-55 checksum format preserves mixed case
+ expect(normalized.startsWith('0x')).toBe(true)
+ expect(normalized.length).toBe(42)
+ })
+ })
+
+ describe('EvmChainService', () => {
+ it('returns correct chain info', () => {
+ const info = adapter.chain.getChainInfo()
+
+ expect(info.chainId).toBe('ethereum')
+ expect(info.symbol).toBe('ETH')
+ expect(info.decimals).toBe(18)
+ expect(info.confirmations).toBe(12)
+ })
+ })
+})
diff --git a/src/services/chain-adapter/__tests__/tron-adapter.test.ts b/src/services/chain-adapter/__tests__/tron-adapter.test.ts
new file mode 100644
index 00000000..38806298
--- /dev/null
+++ b/src/services/chain-adapter/__tests__/tron-adapter.test.ts
@@ -0,0 +1,70 @@
+import { describe, it, expect } from 'vitest'
+import { TronAdapter } from '../tron'
+import type { ChainConfig } from '@/services/chain-config'
+import { mnemonicToSeedSync } from '@scure/bip39'
+
+const TEST_MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
+
+const tronConfig: ChainConfig = {
+ id: 'tron',
+ version: '1.0',
+ type: 'tron',
+ name: 'Tron',
+ symbol: 'TRX',
+ decimals: 6,
+ enabled: true,
+ source: 'default',
+}
+
+describe('TronAdapter', () => {
+ const adapter = new TronAdapter(tronConfig)
+
+ describe('TronIdentityService', () => {
+ it('derives correct Tron address from mnemonic', async () => {
+ const seed = mnemonicToSeedSync(TEST_MNEMONIC)
+ const address = await adapter.identity.deriveAddress(seed, 0)
+
+ // Tron addresses start with 'T' and are 34 characters
+ expect(address).toMatch(/^T[A-Za-z0-9]{33}$/)
+ expect(address.startsWith('T')).toBe(true)
+ expect(address.length).toBe(34)
+ })
+
+ it('derives different addresses for different indices', async () => {
+ const seed = mnemonicToSeedSync(TEST_MNEMONIC)
+ const addr0 = await adapter.identity.deriveAddress(seed, 0)
+ const addr1 = await adapter.identity.deriveAddress(seed, 1)
+
+ expect(addr0).not.toBe(addr1)
+ })
+
+ it('validates Tron addresses correctly', () => {
+ // Valid Tron addresses
+ expect(adapter.identity.isValidAddress('TZ4UXDV5ZhNW7fb2AMSbgfAEZ7hWsnYS2g')).toBe(true)
+ expect(adapter.identity.isValidAddress('TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t')).toBe(true)
+
+ // Invalid addresses
+ expect(adapter.identity.isValidAddress('invalid')).toBe(false)
+ expect(adapter.identity.isValidAddress('T123')).toBe(false)
+ expect(adapter.identity.isValidAddress('')).toBe(false)
+ expect(adapter.identity.isValidAddress('0x9858effd232b4033e47d90003d41ec34ecaeda94')).toBe(false) // Ethereum addr
+ })
+
+ it('does not modify valid Tron addresses', () => {
+ const addr = 'TZ4UXDV5ZhNW7fb2AMSbgfAEZ7hWsnYS2g'
+ expect(adapter.identity.normalizeAddress(addr)).toBe(addr)
+ })
+ })
+
+ describe('TronChainService', () => {
+ it('returns correct chain info', () => {
+ const info = adapter.chain.getChainInfo()
+
+ expect(info.chainId).toBe('tron')
+ expect(info.symbol).toBe('TRX')
+ expect(info.decimals).toBe(6)
+ expect(info.blockTime).toBe(3) // ~3 seconds
+ expect(info.confirmations).toBe(19)
+ })
+ })
+})
diff --git a/src/services/chain-adapter/bip39/adapter.ts b/src/services/chain-adapter/bip39/adapter.ts
new file mode 100644
index 00000000..bdf1c785
--- /dev/null
+++ b/src/services/chain-adapter/bip39/adapter.ts
@@ -0,0 +1,44 @@
+/**
+ * BIP39 Chain Adapter (Bitcoin, Tron)
+ */
+
+import type { ChainConfig, ChainConfigType } from '@/services/chain-config'
+import type { IChainAdapter, IStakingService } from '../types'
+import { Bip39IdentityService } from './identity-service'
+import { Bip39AssetService } from './asset-service'
+import { Bip39TransactionService } from './transaction-service'
+import { Bip39ChainService } from './chain-service'
+
+export class Bip39Adapter implements IChainAdapter {
+ readonly chainId: string
+ readonly chainType: ChainConfigType = 'bip39'
+
+ readonly identity: Bip39IdentityService
+ readonly asset: Bip39AssetService
+ readonly transaction: Bip39TransactionService
+ readonly chain: Bip39ChainService
+ readonly staking: IStakingService | null = null
+
+ private initialized = false
+
+ constructor(config: ChainConfig) {
+ this.chainId = config.id
+ this.identity = new Bip39IdentityService(config)
+ this.asset = new Bip39AssetService(config)
+ this.transaction = new Bip39TransactionService(config)
+ this.chain = new Bip39ChainService(config)
+ }
+
+ async initialize(_config: ChainConfig): Promise {
+ if (this.initialized) return
+ this.initialized = true
+ }
+
+ dispose(): void {
+ this.initialized = false
+ }
+}
+
+export function createBip39Adapter(config: ChainConfig): IChainAdapter {
+ return new Bip39Adapter(config)
+}
diff --git a/src/services/chain-adapter/bip39/asset-service.ts b/src/services/chain-adapter/bip39/asset-service.ts
new file mode 100644
index 00000000..079510fe
--- /dev/null
+++ b/src/services/chain-adapter/bip39/asset-service.ts
@@ -0,0 +1,100 @@
+/**
+ * BIP39 Asset Service (Bitcoin, Tron)
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+import type { IAssetService, Address, Balance, TokenMetadata } from '../types'
+import { Amount } from '@/types/amount'
+import { ChainServiceError, ChainErrorCodes } from '../types'
+
+export class Bip39AssetService implements IAssetService {
+ private readonly config: ChainConfig
+ private readonly apiUrl: string
+ private readonly apiPath: string
+
+ constructor(config: ChainConfig) {
+ this.config = config
+ this.apiUrl = config.api?.url ?? 'https://walletapi.bfmeta.info'
+ this.apiPath = config.api?.path ?? config.id
+ }
+
+ private async fetch(endpoint: string, body?: unknown): Promise {
+ const url = `${this.apiUrl}/wallet/${this.apiPath}${endpoint}`
+ const init: RequestInit = body
+ ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
+ : { method: 'GET' }
+ const response = await fetch(url, init)
+
+ if (!response.ok) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ `HTTP ${response.status}: ${response.statusText}`,
+ )
+ }
+
+ const json = await response.json() as { success: boolean; result?: T; error?: { message: string } }
+ if (!json.success) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ json.error?.message ?? 'API request failed',
+ )
+ }
+
+ return json.result as T
+ }
+
+ async getNativeBalance(address: Address): Promise {
+ try {
+ const result = await this.fetch<{ balance: string }>('/address/balance', { address })
+ return {
+ amount: Amount.fromRaw(result.balance, this.config.decimals, this.config.symbol),
+ symbol: this.config.symbol,
+ }
+ } catch {
+ return {
+ amount: Amount.fromRaw('0', this.config.decimals, this.config.symbol),
+ symbol: this.config.symbol,
+ }
+ }
+ }
+
+ async getTokenBalance(address: Address, tokenAddress: Address): Promise {
+ // Bitcoin doesn't have tokens, Tron has TRC20
+ if (this.config.id !== 'tron') {
+ return {
+ amount: Amount.fromRaw('0', 18, 'UNKNOWN'),
+ symbol: 'UNKNOWN',
+ }
+ }
+
+ try {
+ const result = await this.fetch<{ balance: string; decimals: number; symbol: string }>(
+ '/token/balance',
+ { address, tokenAddress },
+ )
+ return {
+ amount: Amount.fromRaw(result.balance, result.decimals, result.symbol),
+ symbol: result.symbol,
+ }
+ } catch {
+ return {
+ amount: Amount.fromRaw('0', 18, 'UNKNOWN'),
+ symbol: 'UNKNOWN',
+ }
+ }
+ }
+
+ async getTokenBalances(address: Address): Promise {
+ const nativeBalance = await this.getNativeBalance(address)
+ return [nativeBalance]
+ }
+
+ async getTokenMetadata(tokenAddress: Address): Promise {
+ return {
+ address: tokenAddress,
+ name: 'Unknown Token',
+ symbol: 'UNKNOWN',
+ decimals: 18,
+ }
+ }
+}
diff --git a/src/services/chain-adapter/bip39/chain-service.ts b/src/services/chain-adapter/bip39/chain-service.ts
new file mode 100644
index 00000000..7295fef0
--- /dev/null
+++ b/src/services/chain-adapter/bip39/chain-service.ts
@@ -0,0 +1,99 @@
+/**
+ * BIP39 Chain Service (Bitcoin, Tron)
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+import type { IChainService, ChainInfo, GasPrice, HealthStatus } from '../types'
+import { Amount } from '@/types/amount'
+import { ChainServiceError, ChainErrorCodes } from '../types'
+
+export class Bip39ChainService implements IChainService {
+ private readonly config: ChainConfig
+ private readonly apiUrl: string
+ private readonly apiPath: string
+
+ constructor(config: ChainConfig) {
+ this.config = config
+ this.apiUrl = config.api?.url ?? 'https://walletapi.bfmeta.info'
+ this.apiPath = config.api?.path ?? config.id
+ }
+
+ private async fetch(endpoint: string): Promise {
+ const url = `${this.apiUrl}/wallet/${this.apiPath}${endpoint}`
+ const response = await fetch(url)
+
+ if (!response.ok) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ `HTTP ${response.status}: ${response.statusText}`,
+ )
+ }
+
+ const json = await response.json() as { success: boolean; result?: T; error?: { message: string } }
+ if (!json.success) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ json.error?.message ?? 'API request failed',
+ )
+ }
+
+ return json.result as T
+ }
+
+ getChainInfo(): ChainInfo {
+ const blockTime = this.config.id === 'bitcoin' ? 600 : 3 // BTC ~10min, TRX ~3s
+ const confirmations = this.config.id === 'bitcoin' ? 6 : 19
+
+ return {
+ chainId: this.config.id,
+ name: this.config.name,
+ symbol: this.config.symbol,
+ decimals: this.config.decimals,
+ blockTime,
+ confirmations,
+ explorerUrl: this.config.explorer?.url,
+ }
+ }
+
+ async getBlockHeight(): Promise {
+ try {
+ const result = await this.fetch<{ height: number }>('/lastblock')
+ return BigInt(result.height)
+ } catch {
+ return 0n
+ }
+ }
+
+ async getGasPrice(): Promise {
+ // Bitcoin uses sat/vB, Tron uses bandwidth/energy
+ const defaultFee = Amount.fromRaw('1000', this.config.decimals, this.config.symbol)
+ return {
+ slow: defaultFee,
+ standard: defaultFee,
+ fast: defaultFee,
+ lastUpdated: Date.now(),
+ }
+ }
+
+ async healthCheck(): Promise {
+ const startTime = Date.now()
+ try {
+ const height = await this.getBlockHeight()
+ return {
+ isHealthy: true,
+ latency: Date.now() - startTime,
+ blockHeight: height,
+ isSyncing: false,
+ lastUpdated: Date.now(),
+ }
+ } catch {
+ return {
+ isHealthy: false,
+ latency: Date.now() - startTime,
+ blockHeight: 0n,
+ isSyncing: false,
+ lastUpdated: Date.now(),
+ }
+ }
+ }
+}
diff --git a/src/services/chain-adapter/bip39/identity-service.ts b/src/services/chain-adapter/bip39/identity-service.ts
new file mode 100644
index 00000000..fcf6ad99
--- /dev/null
+++ b/src/services/chain-adapter/bip39/identity-service.ts
@@ -0,0 +1,49 @@
+/**
+ * BIP39 Identity Service
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+import type { IIdentityService, Address, Signature } from '../types'
+import { isValidAddress } from '@/lib/crypto'
+
+export class Bip39IdentityService implements IIdentityService {
+ private readonly chainId: string
+
+ constructor(config: ChainConfig) {
+ this.chainId = config.id
+ }
+
+ async deriveAddress(_seed: Uint8Array, _index = 0): Promise {
+ throw new Error('Use deriveAddressesForChains from @/lib/crypto instead')
+ }
+
+ async deriveAddresses(_seed: Uint8Array, _startIndex: number, _count: number): Promise {
+ throw new Error('Use deriveAddressesForChains from @/lib/crypto instead')
+ }
+
+ isValidAddress(address: string): boolean {
+ if (this.chainId === 'bitcoin') {
+ return isValidAddress(address, 'bitcoin')
+ }
+ if (this.chainId === 'tron') {
+ return isValidAddress(address, 'tron')
+ }
+ return false
+ }
+
+ normalizeAddress(address: string): Address {
+ return address // BIP39 addresses are case-sensitive
+ }
+
+ async signMessage(_message: string | Uint8Array, _privateKey: Uint8Array): Promise {
+ throw new Error('Not implemented')
+ }
+
+ async verifyMessage(
+ _message: string | Uint8Array,
+ _signature: Signature,
+ _address: Address,
+ ): Promise {
+ throw new Error('Not implemented')
+ }
+}
diff --git a/src/services/chain-adapter/bip39/index.ts b/src/services/chain-adapter/bip39/index.ts
new file mode 100644
index 00000000..add77298
--- /dev/null
+++ b/src/services/chain-adapter/bip39/index.ts
@@ -0,0 +1,6 @@
+export { Bip39Adapter, createBip39Adapter } from './adapter'
+export { Bip39IdentityService } from './identity-service'
+export { Bip39AssetService } from './asset-service'
+export { Bip39TransactionService } from './transaction-service'
+export { Bip39ChainService } from './chain-service'
+export * from './types'
diff --git a/src/services/chain-adapter/bip39/transaction-service.ts b/src/services/chain-adapter/bip39/transaction-service.ts
new file mode 100644
index 00000000..cb18cd10
--- /dev/null
+++ b/src/services/chain-adapter/bip39/transaction-service.ts
@@ -0,0 +1,190 @@
+/**
+ * BIP39 Transaction Service (Bitcoin, Tron)
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+import type {
+ ITransactionService,
+ TransferParams,
+ UnsignedTransaction,
+ SignedTransaction,
+ TransactionHash,
+ TransactionStatus,
+ Transaction,
+ FeeEstimate,
+ Fee,
+} from '../types'
+import { Amount } from '@/types/amount'
+import { ChainServiceError, ChainErrorCodes } from '../types'
+
+export class Bip39TransactionService implements ITransactionService {
+ private readonly config: ChainConfig
+ private readonly apiUrl: string
+ private readonly apiPath: string
+
+ constructor(config: ChainConfig) {
+ this.config = config
+ this.apiUrl = config.api?.url ?? 'https://walletapi.bfmeta.info'
+ this.apiPath = config.api?.path ?? config.id
+ }
+
+ private async fetch(endpoint: string, body?: unknown): Promise {
+ const url = `${this.apiUrl}/wallet/${this.apiPath}${endpoint}`
+ const init: RequestInit = body
+ ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
+ : { method: 'GET' }
+ const response = await fetch(url, init)
+
+ if (!response.ok) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ `HTTP ${response.status}: ${response.statusText}`,
+ )
+ }
+
+ const json = await response.json() as { success: boolean; result?: T; error?: { message: string } }
+ if (!json.success) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ json.error?.message ?? 'API request failed',
+ )
+ }
+
+ return json.result as T
+ }
+
+ async estimateFee(_params: TransferParams): Promise {
+ // Default fee estimates
+ const feeAmount = this.config.id === 'bitcoin'
+ ? Amount.fromRaw('10000', this.config.decimals, this.config.symbol) // ~0.0001 BTC
+ : Amount.fromRaw('1000000', this.config.decimals, this.config.symbol) // 1 TRX
+
+ const fee: Fee = {
+ amount: feeAmount,
+ estimatedTime: this.config.id === 'bitcoin' ? 600 : 3,
+ }
+
+ return {
+ slow: { ...fee, estimatedTime: fee.estimatedTime * 2 },
+ standard: fee,
+ fast: { ...fee, estimatedTime: Math.ceil(fee.estimatedTime / 2) },
+ }
+ }
+
+ async buildTransaction(params: TransferParams): Promise {
+ return {
+ chainId: this.config.id,
+ data: {
+ from: params.from,
+ to: params.to,
+ amount: params.amount.raw.toString(),
+ },
+ }
+ }
+
+ async signTransaction(
+ _unsignedTx: UnsignedTransaction,
+ _privateKey: Uint8Array,
+ ): Promise {
+ throw new ChainServiceError(
+ ChainErrorCodes.CHAIN_NOT_SUPPORTED,
+ `${this.config.id} transaction signing not yet implemented`,
+ )
+ }
+
+ async broadcastTransaction(_signedTx: SignedTransaction): Promise {
+ throw new ChainServiceError(
+ ChainErrorCodes.CHAIN_NOT_SUPPORTED,
+ `${this.config.id} transaction broadcast not yet implemented`,
+ )
+ }
+
+ async getTransactionStatus(hash: TransactionHash): Promise {
+ try {
+ const result = await this.fetch<{
+ confirmations: number
+ status: string
+ }>('/transaction/status', { hash })
+
+ const requiredConfirmations = this.config.id === 'bitcoin' ? 6 : 19
+
+ return {
+ status: result.confirmations >= requiredConfirmations ? 'confirmed' : 'confirming',
+ confirmations: result.confirmations,
+ requiredConfirmations,
+ }
+ } catch {
+ return {
+ status: 'pending',
+ confirmations: 0,
+ requiredConfirmations: this.config.id === 'bitcoin' ? 6 : 19,
+ }
+ }
+ }
+
+ async getTransaction(hash: TransactionHash): Promise {
+ try {
+ const result = await this.fetch<{
+ hash: string
+ from: string
+ to: string
+ amount: string
+ fee: string
+ status: string
+ timestamp: number
+ blockNumber?: string
+ }>('/transaction', { hash })
+
+ return {
+ hash: result.hash,
+ from: result.from,
+ to: result.to,
+ amount: Amount.fromRaw(result.amount, this.config.decimals, this.config.symbol),
+ fee: Amount.fromRaw(result.fee, this.config.decimals, this.config.symbol),
+ status: {
+ status: result.status === 'confirmed' ? 'confirmed' : 'pending',
+ confirmations: result.status === 'confirmed' ? 6 : 0,
+ requiredConfirmations: this.config.id === 'bitcoin' ? 6 : 19,
+ },
+ timestamp: result.timestamp,
+ blockNumber: result.blockNumber ? BigInt(result.blockNumber) : undefined,
+ type: 'transfer',
+ }
+ } catch {
+ return null
+ }
+ }
+
+ async getTransactionHistory(address: string, limit = 20): Promise {
+ try {
+ const result = await this.fetch>('/transactions', { address, limit })
+
+ return result.map((tx) => ({
+ hash: tx.hash,
+ from: tx.from,
+ to: tx.to,
+ amount: Amount.fromRaw(tx.amount, this.config.decimals, this.config.symbol),
+ fee: Amount.fromRaw(tx.fee, this.config.decimals, this.config.symbol),
+ status: {
+ status: tx.status === 'confirmed' ? 'confirmed' as const : 'pending' as const,
+ confirmations: tx.status === 'confirmed' ? 6 : 0,
+ requiredConfirmations: this.config.id === 'bitcoin' ? 6 : 19,
+ },
+ timestamp: tx.timestamp,
+ blockNumber: tx.blockNumber ? BigInt(tx.blockNumber) : undefined,
+ type: 'transfer' as const,
+ }))
+ } catch {
+ return []
+ }
+ }
+}
diff --git a/src/services/chain-adapter/bip39/types.ts b/src/services/chain-adapter/bip39/types.ts
new file mode 100644
index 00000000..04f21b0b
--- /dev/null
+++ b/src/services/chain-adapter/bip39/types.ts
@@ -0,0 +1,15 @@
+/**
+ * BIP39 Chain Adapter Types (Bitcoin, Tron)
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+
+export interface Bip39ChainConfig extends ChainConfig {
+ type: 'bip39'
+}
+
+export type Bip39ChainId = 'bitcoin' | 'tron'
+
+export function isSupportedBip39Chain(chainId: string): chainId is Bip39ChainId {
+ return chainId === 'bitcoin' || chainId === 'tron'
+}
diff --git a/src/services/chain-adapter/bitcoin/adapter.ts b/src/services/chain-adapter/bitcoin/adapter.ts
new file mode 100644
index 00000000..aadb0e97
--- /dev/null
+++ b/src/services/chain-adapter/bitcoin/adapter.ts
@@ -0,0 +1,45 @@
+/**
+ * Bitcoin Chain Adapter
+ *
+ * Full adapter for Bitcoin network using mempool.space API
+ */
+
+import type { ChainConfig, ChainConfigType } from '@/services/chain-config'
+import type { IChainAdapter, IStakingService } from '../types'
+import { BitcoinIdentityService } from './identity-service'
+import { BitcoinAssetService } from './asset-service'
+import { BitcoinChainService } from './chain-service'
+import { BitcoinTransactionService } from './transaction-service'
+
+export class BitcoinAdapter implements IChainAdapter {
+ readonly config: ChainConfig
+ readonly identity: BitcoinIdentityService
+ readonly asset: BitcoinAssetService
+ readonly chain: BitcoinChainService
+ readonly transaction: BitcoinTransactionService
+ readonly staking: IStakingService | null = null
+
+ constructor(config: ChainConfig) {
+ this.config = config
+ this.identity = new BitcoinIdentityService(config)
+ this.asset = new BitcoinAssetService(config)
+ this.chain = new BitcoinChainService(config)
+ this.transaction = new BitcoinTransactionService(config)
+ }
+
+ get chainId(): string {
+ return this.config.id
+ }
+
+ get chainType(): ChainConfigType {
+ return 'bip39'
+ }
+
+ async initialize(_config: ChainConfig): Promise {
+ // No async initialization needed
+ }
+
+ dispose(): void {
+ // No cleanup needed
+ }
+}
diff --git a/src/services/chain-adapter/bitcoin/asset-service.ts b/src/services/chain-adapter/bitcoin/asset-service.ts
new file mode 100644
index 00000000..cd71de65
--- /dev/null
+++ b/src/services/chain-adapter/bitcoin/asset-service.ts
@@ -0,0 +1,89 @@
+/**
+ * Bitcoin Asset Service
+ *
+ * Uses mempool.space API for address balance and UTXO queries
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+import type { IAssetService, Balance, TokenMetadata, Address } from '../types'
+import { Amount } from '@/types/amount'
+import { ChainServiceError, ChainErrorCodes } from '../types'
+import type { BitcoinAddressInfo, BitcoinUtxo } from './types'
+
+/** mempool.space API endpoints */
+const API_URLS: Record = {
+ 'bitcoin': 'https://mempool.space/api',
+ 'bitcoin-testnet': 'https://mempool.space/testnet/api',
+ 'bitcoin-signet': 'https://mempool.space/signet/api',
+}
+
+export class BitcoinAssetService implements IAssetService {
+ private readonly config: ChainConfig
+ private readonly apiUrl: string
+
+ constructor(config: ChainConfig) {
+ this.config = config
+ this.apiUrl = API_URLS[config.id] ?? API_URLS['bitcoin']!
+ }
+
+ private async api(endpoint: string): Promise {
+ const url = `${this.apiUrl}${endpoint}`
+ const response = await fetch(url)
+
+ if (!response.ok) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ `Bitcoin API error: ${response.status}`,
+ )
+ }
+
+ return response.json() as Promise
+ }
+
+ async getNativeBalance(address: Address): Promise {
+ try {
+ const info = await this.api(`/address/${address}`)
+
+ // Balance = funded - spent (confirmed + mempool)
+ const confirmedBalance = info.chain_stats.funded_txo_sum - info.chain_stats.spent_txo_sum
+ const mempoolBalance = info.mempool_stats.funded_txo_sum - info.mempool_stats.spent_txo_sum
+ const totalBalance = confirmedBalance + mempoolBalance
+
+ return {
+ amount: Amount.fromRaw(totalBalance.toString(), this.config.decimals, this.config.symbol),
+ symbol: this.config.symbol,
+ }
+ } catch {
+ return {
+ amount: Amount.fromRaw('0', this.config.decimals, this.config.symbol),
+ symbol: this.config.symbol,
+ }
+ }
+ }
+
+ async getTokenBalance(_address: Address, _tokenAddress: Address): Promise {
+ // Bitcoin doesn't have native tokens (BRC-20 would require indexer)
+ return {
+ amount: Amount.fromRaw('0', 8, 'TOKEN'),
+ symbol: 'TOKEN',
+ }
+ }
+
+ async getTokenBalances(_address: Address): Promise {
+ return []
+ }
+
+ async getTokenMetadata(_tokenAddress: Address): Promise {
+ return {
+ address: _tokenAddress,
+ name: 'Unknown',
+ symbol: 'UNKNOWN',
+ decimals: 8,
+ }
+ }
+
+ /** Get UTXOs for an address (used for transaction building) */
+ async getUtxos(address: Address): Promise {
+ return this.api(`/address/${address}/utxo`)
+ }
+}
diff --git a/src/services/chain-adapter/bitcoin/chain-service.ts b/src/services/chain-adapter/bitcoin/chain-service.ts
new file mode 100644
index 00000000..e2f43f6b
--- /dev/null
+++ b/src/services/chain-adapter/bitcoin/chain-service.ts
@@ -0,0 +1,114 @@
+/**
+ * Bitcoin Chain Service
+ *
+ * Uses mempool.space API for fee estimates and block info
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+import type { IChainService, ChainInfo, GasPrice, HealthStatus } from '../types'
+import { Amount } from '@/types/amount'
+import { ChainServiceError, ChainErrorCodes } from '../types'
+import type { BitcoinFeeEstimates } from './types'
+
+/** mempool.space API endpoints */
+const API_URLS: Record = {
+ 'bitcoin': 'https://mempool.space/api',
+ 'bitcoin-testnet': 'https://mempool.space/testnet/api',
+ 'bitcoin-signet': 'https://mempool.space/signet/api',
+}
+
+export class BitcoinChainService implements IChainService {
+ private readonly config: ChainConfig
+ private readonly apiUrl: string
+
+ constructor(config: ChainConfig) {
+ this.config = config
+ this.apiUrl = API_URLS[config.id] ?? API_URLS['bitcoin']!
+ }
+
+ private async api(endpoint: string): Promise {
+ const url = `${this.apiUrl}${endpoint}`
+ const response = await fetch(url)
+
+ if (!response.ok) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ `Bitcoin API error: ${response.status}`,
+ )
+ }
+
+ return response.json() as Promise
+ }
+
+ getChainInfo(): ChainInfo {
+ return {
+ chainId: this.config.id,
+ name: this.config.name,
+ symbol: this.config.symbol,
+ decimals: this.config.decimals,
+ blockTime: 600, // ~10 minutes
+ confirmations: 6,
+ explorerUrl: this.config.explorer?.url,
+ }
+ }
+
+ async getBlockHeight(): Promise {
+ const height = await this.api('/blocks/tip/height')
+ return BigInt(height)
+ }
+
+ async getGasPrice(): Promise {
+ try {
+ const fees = await this.api('/v1/fees/recommended')
+
+ // Convert sat/vB to Amount (for a typical 250 vB transaction)
+ const typicalSize = 250n
+ const slow = Amount.fromRaw((BigInt(fees.hourFee) * typicalSize).toString(), this.config.decimals, this.config.symbol)
+ const standard = Amount.fromRaw((BigInt(fees.halfHourFee) * typicalSize).toString(), this.config.decimals, this.config.symbol)
+ const fast = Amount.fromRaw((BigInt(fees.fastestFee) * typicalSize).toString(), this.config.decimals, this.config.symbol)
+
+ return {
+ slow,
+ standard,
+ fast,
+ lastUpdated: Date.now(),
+ }
+ } catch {
+ // Default fees if API fails
+ const defaultFee = Amount.fromRaw('5000', this.config.decimals, this.config.symbol)
+ return {
+ slow: defaultFee,
+ standard: defaultFee,
+ fast: defaultFee,
+ lastUpdated: Date.now(),
+ }
+ }
+ }
+
+ async healthCheck(): Promise {
+ const start = Date.now()
+ try {
+ const height = await this.api('/blocks/tip/height')
+ return {
+ isHealthy: true,
+ latency: Date.now() - start,
+ blockHeight: BigInt(height),
+ isSyncing: false,
+ lastUpdated: Date.now(),
+ }
+ } catch {
+ return {
+ isHealthy: false,
+ latency: Date.now() - start,
+ blockHeight: 0n,
+ isSyncing: false,
+ lastUpdated: Date.now(),
+ }
+ }
+ }
+
+ /** Get recommended fee rates in sat/vB */
+ async getFeeRates(): Promise {
+ return this.api('/v1/fees/recommended')
+ }
+}
diff --git a/src/services/chain-adapter/bitcoin/identity-service.ts b/src/services/chain-adapter/bitcoin/identity-service.ts
new file mode 100644
index 00000000..62632627
--- /dev/null
+++ b/src/services/chain-adapter/bitcoin/identity-service.ts
@@ -0,0 +1,102 @@
+/**
+ * Bitcoin Identity Service
+ *
+ * Supports multiple address types:
+ * - P2WPKH (Native SegWit, bc1q...) - Default, BIP84
+ * - P2TR (Taproot, bc1p...) - BIP86
+ * - P2PKH (Legacy, 1...) - BIP44
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+import type { IIdentityService, Address, Signature } from '../types'
+import { sha256 } from '@noble/hashes/sha2.js'
+import { ripemd160 } from '@noble/hashes/legacy.js'
+import { secp256k1 } from '@noble/curves/secp256k1.js'
+import { bytesToHex } from '@noble/hashes/utils.js'
+import { HDKey } from '@scure/bip32'
+import { bech32, bech32m, base58check } from '@scure/base'
+
+export class BitcoinIdentityService implements IIdentityService {
+ constructor(_config: ChainConfig) {}
+
+ async deriveAddress(seed: Uint8Array, index = 0): Promise {
+ // Default to Native SegWit (P2WPKH) - BIP84: m/84'/0'/0'/0/index
+ const hdKey = HDKey.fromMasterSeed(seed)
+ const derived = hdKey.derive(`m/84'/0'/0'/0/${index}`)
+
+ if (!derived.publicKey) {
+ throw new Error('Failed to derive public key')
+ }
+
+ // P2WPKH: HASH160(compressed pubkey)
+ const pubKeyHash = ripemd160(sha256(derived.publicKey))
+
+ // Encode as bech32 (bc1q...)
+ const words = bech32.toWords(pubKeyHash)
+ return bech32.encode('bc', [0, ...words])
+ }
+
+ async deriveAddresses(seed: Uint8Array, startIndex: number, count: number): Promise {
+ const addresses: Address[] = []
+ for (let i = 0; i < count; i++) {
+ addresses.push(await this.deriveAddress(seed, startIndex + i))
+ }
+ return addresses
+ }
+
+ isValidAddress(address: string): boolean {
+ try {
+ // Legacy P2PKH (starts with 1) or P2SH (starts with 3)
+ if (address.startsWith('1') || address.startsWith('3')) {
+ const decoded = base58check(sha256).decode(address)
+ return decoded.length === 21 // 1 version + 20 hash
+ }
+
+ // Native SegWit P2WPKH (bc1q...)
+ if (address.toLowerCase().startsWith('bc1q')) {
+ const addr = address.toLowerCase() as `${string}1${string}`
+ const decoded = bech32.decode(addr)
+ const data = bech32.fromWords(decoded.words.slice(1))
+ return decoded.prefix === 'bc' && decoded.words[0] === 0 && data.length === 20
+ }
+
+ // Taproot P2TR (bc1p...)
+ if (address.toLowerCase().startsWith('bc1p')) {
+ const addr = address.toLowerCase() as `${string}1${string}`
+ const decoded = bech32m.decode(addr)
+ const data = bech32m.fromWords(decoded.words.slice(1))
+ return decoded.prefix === 'bc' && decoded.words[0] === 1 && data.length === 32
+ }
+
+ return false
+ } catch {
+ return false
+ }
+ }
+
+ normalizeAddress(address: string): Address {
+ if (address.startsWith('bc1') || address.startsWith('BC1')) {
+ return address.toLowerCase()
+ }
+ return address
+ }
+
+ async signMessage(message: string | Uint8Array, privateKey: Uint8Array): Promise {
+ const msgBytes = typeof message === 'string'
+ ? new TextEncoder().encode(message)
+ : message
+
+ // Bitcoin message signing format
+ const prefix = new TextEncoder().encode('\x18Bitcoin Signed Message:\n')
+ const msgLen = new Uint8Array([msgBytes.length])
+ const fullMsg = new Uint8Array([...prefix, ...msgLen, ...msgBytes])
+ const hash = sha256(sha256(fullMsg))
+
+ const sig = secp256k1.sign(hash, privateKey, { prehash: false, format: 'recovered' })
+ return bytesToHex(sig)
+ }
+
+ async verifyMessage(_message: string | Uint8Array, _signature: Signature, _address: Address): Promise {
+ return false
+ }
+}
diff --git a/src/services/chain-adapter/bitcoin/index.ts b/src/services/chain-adapter/bitcoin/index.ts
new file mode 100644
index 00000000..14866cfb
--- /dev/null
+++ b/src/services/chain-adapter/bitcoin/index.ts
@@ -0,0 +1,10 @@
+/**
+ * Bitcoin Chain Adapter exports
+ */
+
+export { BitcoinAdapter } from './adapter'
+export { BitcoinIdentityService } from './identity-service'
+export { BitcoinAssetService } from './asset-service'
+export { BitcoinChainService } from './chain-service'
+export { BitcoinTransactionService } from './transaction-service'
+export * from './types'
diff --git a/src/services/chain-adapter/bitcoin/transaction-service.ts b/src/services/chain-adapter/bitcoin/transaction-service.ts
new file mode 100644
index 00000000..a8b809b1
--- /dev/null
+++ b/src/services/chain-adapter/bitcoin/transaction-service.ts
@@ -0,0 +1,260 @@
+/**
+ * Bitcoin Transaction Service
+ *
+ * Handles UTXO selection, transaction building, signing, and broadcasting.
+ * Uses mempool.space API for UTXO queries and transaction broadcast.
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+import type {
+ ITransactionService,
+ TransferParams,
+ UnsignedTransaction,
+ SignedTransaction,
+ TransactionHash,
+ TransactionStatus,
+ Transaction,
+ FeeEstimate,
+ Fee,
+} from '../types'
+import { Amount } from '@/types/amount'
+import { ChainServiceError, ChainErrorCodes } from '../types'
+import type { BitcoinUtxo, BitcoinTransaction, BitcoinUnsignedTx, BitcoinFeeEstimates } from './types'
+
+/** mempool.space API endpoints */
+const API_URLS: Record = {
+ 'bitcoin': 'https://mempool.space/api',
+ 'bitcoin-testnet': 'https://mempool.space/testnet/api',
+ 'bitcoin-signet': 'https://mempool.space/signet/api',
+}
+
+export class BitcoinTransactionService implements ITransactionService {
+ private readonly config: ChainConfig
+ private readonly apiUrl: string
+
+ constructor(config: ChainConfig) {
+ this.config = config
+ this.apiUrl = API_URLS[config.id] ?? API_URLS['bitcoin']!
+ }
+
+ private async api(endpoint: string, options?: RequestInit): Promise {
+ const url = `${this.apiUrl}${endpoint}`
+ const response = await fetch(url, options)
+
+ if (!response.ok) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ `Bitcoin API error: ${response.status}`,
+ )
+ }
+
+ const text = await response.text()
+ try {
+ return JSON.parse(text) as T
+ } catch {
+ return text as T
+ }
+ }
+
+ async estimateFee(_params: TransferParams): Promise {
+ try {
+ const fees = await this.api('/v1/fees/recommended')
+
+ // Estimate for typical P2WPKH transaction (~140 vB for 1-in-2-out)
+ const typicalVsize = 140
+
+ const slow: Fee = {
+ amount: Amount.fromRaw((fees.hourFee * typicalVsize).toString(), this.config.decimals, this.config.symbol),
+ estimatedTime: 3600, // 1 hour
+ }
+
+ const standard: Fee = {
+ amount: Amount.fromRaw((fees.halfHourFee * typicalVsize).toString(), this.config.decimals, this.config.symbol),
+ estimatedTime: 1800, // 30 minutes
+ }
+
+ const fast: Fee = {
+ amount: Amount.fromRaw((fees.fastestFee * typicalVsize).toString(), this.config.decimals, this.config.symbol),
+ estimatedTime: 600, // 10 minutes
+ }
+
+ return { slow, standard, fast }
+ } catch {
+ // Default fee estimate
+ const defaultFee: Fee = {
+ amount: Amount.fromRaw('2000', this.config.decimals, this.config.symbol),
+ estimatedTime: 1800,
+ }
+ return { slow: defaultFee, standard: defaultFee, fast: defaultFee }
+ }
+ }
+
+ async buildTransaction(params: TransferParams): Promise {
+ // Get UTXOs for the sender
+ const utxos = await this.api(`/address/${params.from}/utxo`)
+
+ if (utxos.length === 0) {
+ throw new ChainServiceError(ChainErrorCodes.INSUFFICIENT_BALANCE, 'No UTXOs available')
+ }
+
+ // Get fee rate
+ const fees = await this.api('/v1/fees/recommended')
+ const feeRate = fees.halfHourFee // sat/vB
+
+ // Simple UTXO selection: use all available UTXOs
+ const totalInput = utxos.reduce((sum, u) => sum + u.value, 0)
+ const sendAmount = Number(params.amount.raw)
+
+ // Estimate transaction size (simplified)
+ const estimatedVsize = 10 + utxos.length * 68 + 2 * 31 // header + inputs + outputs
+ const fee = feeRate * estimatedVsize
+
+ if (totalInput < sendAmount + fee) {
+ throw new ChainServiceError(
+ ChainErrorCodes.INSUFFICIENT_BALANCE,
+ `Insufficient balance: need ${sendAmount + fee}, have ${totalInput}`,
+ )
+ }
+
+ const change = totalInput - sendAmount - fee
+
+ const txData: BitcoinUnsignedTx = {
+ inputs: utxos.map(u => ({
+ txid: u.txid,
+ vout: u.vout,
+ value: u.value,
+ scriptPubKey: '', // Would need to fetch from previous tx
+ })),
+ outputs: [
+ { address: params.to, value: sendAmount },
+ ],
+ fee,
+ changeAddress: params.from,
+ }
+
+ // Add change output if significant
+ if (change > 546) { // Dust threshold
+ txData.outputs.push({ address: params.from, value: change })
+ }
+
+ return {
+ chainId: this.config.id,
+ data: txData,
+ }
+ }
+
+ async signTransaction(
+ _unsignedTx: UnsignedTransaction,
+ _privateKey: Uint8Array,
+ ): Promise {
+ // Bitcoin transaction signing is complex - requires:
+ // 1. Serializing the transaction for signing
+ // 2. Creating sighash for each input
+ // 3. Signing each input with the private key
+ // 4. Building the final transaction with signatures
+
+ // For now, throw an error - full implementation would require
+ // a proper Bitcoin transaction library
+ throw new ChainServiceError(
+ ChainErrorCodes.CHAIN_NOT_SUPPORTED,
+ 'Bitcoin transaction signing requires specialized library (bitcoinjs-lib or similar)',
+ )
+ }
+
+ async broadcastTransaction(signedTx: SignedTransaction): Promise {
+ const txHex = signedTx.data as string
+
+ const txid = await this.api('/tx', {
+ method: 'POST',
+ body: txHex,
+ })
+
+ return txid
+ }
+
+ async getTransactionStatus(hash: TransactionHash): Promise {
+ try {
+ const tx = await this.api(`/tx/${hash}`)
+
+ if (!tx.status.confirmed) {
+ return { status: 'pending', confirmations: 0, requiredConfirmations: 6 }
+ }
+
+ // Get current block height for confirmation count
+ const currentHeight = await this.api('/blocks/tip/height')
+ const confirmations = currentHeight - (tx.status.block_height ?? currentHeight) + 1
+
+ return {
+ status: confirmations >= 6 ? 'confirmed' : 'confirming',
+ confirmations: Math.max(0, confirmations),
+ requiredConfirmations: 6,
+ }
+ } catch {
+ return { status: 'pending', confirmations: 0, requiredConfirmations: 6 }
+ }
+ }
+
+ async getTransaction(hash: TransactionHash): Promise {
+ try {
+ const tx = await this.api(`/tx/${hash}`)
+
+ // Simplify: assume first input is sender, first output is recipient
+ const from = tx.vin[0]?.prevout?.scriptpubkey_address ?? ''
+ const to = tx.vout[0]?.scriptpubkey_address ?? ''
+ const amount = tx.vout[0]?.value ?? 0
+
+ return {
+ hash: tx.txid,
+ from,
+ to,
+ amount: Amount.fromRaw(amount.toString(), this.config.decimals, this.config.symbol),
+ fee: Amount.fromRaw(tx.fee.toString(), this.config.decimals, this.config.symbol),
+ status: {
+ status: tx.status.confirmed ? 'confirmed' : 'pending',
+ confirmations: tx.status.confirmed ? 6 : 0,
+ requiredConfirmations: 6,
+ },
+ timestamp: (tx.status.block_time ?? Math.floor(Date.now() / 1000)) * 1000,
+ blockNumber: tx.status.block_height ? BigInt(tx.status.block_height) : undefined,
+ type: 'transfer',
+ }
+ } catch {
+ return null
+ }
+ }
+
+ async getTransactionHistory(address: string, limit = 20): Promise {
+ try {
+ const txs = await this.api(`/address/${address}/txs`)
+
+ return txs.slice(0, limit).map(tx => {
+ const isOutgoing = tx.vin.some(v => v.prevout?.scriptpubkey_address === address)
+ const from = isOutgoing ? address : (tx.vin[0]?.prevout?.scriptpubkey_address ?? '')
+ const to = isOutgoing
+ ? (tx.vout.find(v => v.scriptpubkey_address !== address)?.scriptpubkey_address ?? '')
+ : address
+ const amount = isOutgoing
+ ? tx.vout.filter(v => v.scriptpubkey_address !== address).reduce((s, v) => s + v.value, 0)
+ : tx.vout.filter(v => v.scriptpubkey_address === address).reduce((s, v) => s + v.value, 0)
+
+ return {
+ hash: tx.txid,
+ from,
+ to,
+ amount: Amount.fromRaw(amount.toString(), this.config.decimals, this.config.symbol),
+ fee: Amount.fromRaw(tx.fee.toString(), this.config.decimals, this.config.symbol),
+ status: {
+ status: tx.status.confirmed ? 'confirmed' as const : 'pending' as const,
+ confirmations: tx.status.confirmed ? 6 : 0,
+ requiredConfirmations: 6,
+ },
+ timestamp: (tx.status.block_time ?? Math.floor(Date.now() / 1000)) * 1000,
+ blockNumber: tx.status.block_height ? BigInt(tx.status.block_height) : undefined,
+ type: 'transfer' as const,
+ }
+ })
+ } catch {
+ return []
+ }
+ }
+}
diff --git a/src/services/chain-adapter/bitcoin/types.ts b/src/services/chain-adapter/bitcoin/types.ts
new file mode 100644
index 00000000..0ecf1127
--- /dev/null
+++ b/src/services/chain-adapter/bitcoin/types.ts
@@ -0,0 +1,95 @@
+/**
+ * Bitcoin-specific types
+ */
+
+/** UTXO from mempool.space API */
+export interface BitcoinUtxo {
+ txid: string
+ vout: number
+ status: {
+ confirmed: boolean
+ block_height?: number
+ block_hash?: string
+ block_time?: number
+ }
+ value: number
+}
+
+/** Address info from mempool.space API */
+export interface BitcoinAddressInfo {
+ address: string
+ chain_stats: {
+ funded_txo_count: number
+ funded_txo_sum: number
+ spent_txo_count: number
+ spent_txo_sum: number
+ tx_count: number
+ }
+ mempool_stats: {
+ funded_txo_count: number
+ funded_txo_sum: number
+ spent_txo_count: number
+ spent_txo_sum: number
+ tx_count: number
+ }
+}
+
+/** Transaction from mempool.space API */
+export interface BitcoinTransaction {
+ txid: string
+ version: number
+ locktime: number
+ vin: Array<{
+ txid: string
+ vout: number
+ prevout: {
+ scriptpubkey: string
+ scriptpubkey_address: string
+ value: number
+ }
+ scriptsig: string
+ sequence: number
+ }>
+ vout: Array<{
+ scriptpubkey: string
+ scriptpubkey_address: string
+ value: number
+ }>
+ size: number
+ weight: number
+ fee: number
+ status: {
+ confirmed: boolean
+ block_height?: number
+ block_hash?: string
+ block_time?: number
+ }
+}
+
+/** Fee estimates from mempool.space API */
+export interface BitcoinFeeEstimates {
+ fastestFee: number
+ halfHourFee: number
+ hourFee: number
+ economyFee: number
+ minimumFee: number
+}
+
+/** Bitcoin address types */
+export type BitcoinAddressType = 'p2pkh' | 'p2sh' | 'p2wpkh' | 'p2tr'
+
+/** Unsigned Bitcoin transaction data */
+export interface BitcoinUnsignedTx {
+ inputs: Array<{
+ txid: string
+ vout: number
+ value: number
+ scriptPubKey: string
+ }>
+ outputs: Array<{
+ address: string
+ value: number
+ }>
+ fee: number
+ changeAddress: string
+}
diff --git a/src/services/chain-adapter/evm/adapter.ts b/src/services/chain-adapter/evm/adapter.ts
new file mode 100644
index 00000000..8b263620
--- /dev/null
+++ b/src/services/chain-adapter/evm/adapter.ts
@@ -0,0 +1,44 @@
+/**
+ * EVM Chain Adapter
+ */
+
+import type { ChainConfig, ChainConfigType } from '@/services/chain-config'
+import type { IChainAdapter, IStakingService } from '../types'
+import { EvmIdentityService } from './identity-service'
+import { EvmAssetService } from './asset-service'
+import { EvmTransactionService } from './transaction-service'
+import { EvmChainService } from './chain-service'
+
+export class EvmAdapter implements IChainAdapter {
+ readonly chainId: string
+ readonly chainType: ChainConfigType = 'evm'
+
+ readonly identity: EvmIdentityService
+ readonly asset: EvmAssetService
+ readonly transaction: EvmTransactionService
+ readonly chain: EvmChainService
+ readonly staking: IStakingService | null = null
+
+ private initialized = false
+
+ constructor(config: ChainConfig) {
+ this.chainId = config.id
+ this.identity = new EvmIdentityService(config)
+ this.asset = new EvmAssetService(config)
+ this.transaction = new EvmTransactionService(config)
+ this.chain = new EvmChainService(config)
+ }
+
+ async initialize(_config: ChainConfig): Promise {
+ if (this.initialized) return
+ this.initialized = true
+ }
+
+ dispose(): void {
+ this.initialized = false
+ }
+}
+
+export function createEvmAdapter(config: ChainConfig): IChainAdapter {
+ return new EvmAdapter(config)
+}
diff --git a/src/services/chain-adapter/evm/asset-service.ts b/src/services/chain-adapter/evm/asset-service.ts
new file mode 100644
index 00000000..06700fa0
--- /dev/null
+++ b/src/services/chain-adapter/evm/asset-service.ts
@@ -0,0 +1,109 @@
+/**
+ * EVM Asset Service
+ *
+ * Provides balance queries for EVM chains via walletapi.
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+import type { IAssetService, Address, Balance, TokenMetadata } from '../types'
+import { Amount } from '@/types/amount'
+import { ChainServiceError, ChainErrorCodes } from '../types'
+
+export class EvmAssetService implements IAssetService {
+ private readonly config: ChainConfig
+ private readonly apiUrl: string
+ private readonly apiPath: string
+
+ constructor(config: ChainConfig) {
+ this.config = config
+ this.apiUrl = config.api?.url ?? 'https://walletapi.bfmeta.info'
+ this.apiPath = config.api?.path ?? config.id
+ }
+
+ private async fetch(endpoint: string, body?: unknown): Promise {
+ const url = `${this.apiUrl}/wallet/${this.apiPath}${endpoint}`
+ const init: RequestInit = body
+ ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
+ : { method: 'GET' }
+ const response = await fetch(url, init)
+
+ if (!response.ok) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ `HTTP ${response.status}: ${response.statusText}`,
+ )
+ }
+
+ const json = await response.json() as { success: boolean; result?: T; error?: { message: string } }
+ if (!json.success) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ json.error?.message ?? 'API request failed',
+ )
+ }
+
+ return json.result as T
+ }
+
+ async getNativeBalance(address: Address): Promise {
+ try {
+ const result = await this.fetch<{ balance: string }>('/address/balance', { address })
+ return {
+ amount: Amount.fromRaw(result.balance, this.config.decimals, this.config.symbol),
+ symbol: this.config.symbol,
+ }
+ } catch (error) {
+ if (error instanceof ChainServiceError) throw error
+ return {
+ amount: Amount.fromRaw('0', this.config.decimals, this.config.symbol),
+ symbol: this.config.symbol,
+ }
+ }
+ }
+
+ async getTokenBalance(address: Address, tokenAddress: Address): Promise {
+ try {
+ const result = await this.fetch<{ balance: string; decimals: number; symbol: string }>(
+ '/token/balance',
+ { address, tokenAddress },
+ )
+ return {
+ amount: Amount.fromRaw(result.balance, result.decimals, result.symbol),
+ symbol: result.symbol,
+ }
+ } catch {
+ return {
+ amount: Amount.fromRaw('0', 18, 'UNKNOWN'),
+ symbol: 'UNKNOWN',
+ }
+ }
+ }
+
+ async getTokenBalances(address: Address): Promise {
+ // Return only native balance for now
+ const nativeBalance = await this.getNativeBalance(address)
+ return [nativeBalance]
+ }
+
+ async getTokenMetadata(tokenAddress: Address): Promise {
+ try {
+ const result = await this.fetch<{ name: string; symbol: string; decimals: number }>(
+ '/token/info',
+ { tokenAddress },
+ )
+ return {
+ address: tokenAddress,
+ name: result.name,
+ symbol: result.symbol,
+ decimals: result.decimals,
+ }
+ } catch {
+ return {
+ address: tokenAddress,
+ name: 'Unknown Token',
+ symbol: 'UNKNOWN',
+ decimals: 18,
+ }
+ }
+ }
+}
diff --git a/src/services/chain-adapter/evm/chain-service.ts b/src/services/chain-adapter/evm/chain-service.ts
new file mode 100644
index 00000000..c9ff1eb2
--- /dev/null
+++ b/src/services/chain-adapter/evm/chain-service.ts
@@ -0,0 +1,109 @@
+/**
+ * EVM Chain Service
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+import type { IChainService, ChainInfo, GasPrice, HealthStatus } from '../types'
+import { Amount } from '@/types/amount'
+import { ChainServiceError, ChainErrorCodes } from '../types'
+
+export class EvmChainService implements IChainService {
+ private readonly config: ChainConfig
+ private readonly apiUrl: string
+ private readonly apiPath: string
+
+ constructor(config: ChainConfig) {
+ this.config = config
+ this.apiUrl = config.api?.url ?? 'https://walletapi.bfmeta.info'
+ this.apiPath = config.api?.path ?? config.id
+ }
+
+ private async fetch(endpoint: string): Promise {
+ const url = `${this.apiUrl}/wallet/${this.apiPath}${endpoint}`
+ const response = await fetch(url)
+
+ if (!response.ok) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ `HTTP ${response.status}: ${response.statusText}`,
+ )
+ }
+
+ const json = await response.json() as { success: boolean; result?: T; error?: { message: string } }
+ if (!json.success) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ json.error?.message ?? 'API request failed',
+ )
+ }
+
+ return json.result as T
+ }
+
+ getChainInfo(): ChainInfo {
+ return {
+ chainId: this.config.id,
+ name: this.config.name,
+ symbol: this.config.symbol,
+ decimals: this.config.decimals,
+ blockTime: 12, // ~12 seconds for Ethereum
+ confirmations: 12,
+ explorerUrl: this.config.explorer?.url,
+ }
+ }
+
+ async getBlockHeight(): Promise {
+ try {
+ const result = await this.fetch<{ height: number }>('/lastblock')
+ return BigInt(result.height)
+ } catch {
+ return 0n
+ }
+ }
+
+ async getGasPrice(): Promise {
+ try {
+ const result = await this.fetch<{ gasPrice: string; baseFee?: string }>('/gasprice')
+ const gasPrice = Amount.fromRaw(result.gasPrice, 9, 'Gwei') // Gas price in Gwei
+
+ return {
+ slow: gasPrice,
+ standard: gasPrice,
+ fast: gasPrice,
+ baseFee: result.baseFee ? Amount.fromRaw(result.baseFee, 9, 'Gwei') : undefined,
+ lastUpdated: Date.now(),
+ }
+ } catch {
+ // Return default gas prices
+ const defaultGas = Amount.fromRaw('20000000000', 9, 'Gwei') // 20 Gwei
+ return {
+ slow: defaultGas,
+ standard: defaultGas,
+ fast: defaultGas,
+ lastUpdated: Date.now(),
+ }
+ }
+ }
+
+ async healthCheck(): Promise {
+ const startTime = Date.now()
+ try {
+ const height = await this.getBlockHeight()
+ return {
+ isHealthy: true,
+ latency: Date.now() - startTime,
+ blockHeight: height,
+ isSyncing: false,
+ lastUpdated: Date.now(),
+ }
+ } catch {
+ return {
+ isHealthy: false,
+ latency: Date.now() - startTime,
+ blockHeight: 0n,
+ isSyncing: false,
+ lastUpdated: Date.now(),
+ }
+ }
+ }
+}
diff --git a/src/services/chain-adapter/evm/identity-service.ts b/src/services/chain-adapter/evm/identity-service.ts
new file mode 100644
index 00000000..c76128e3
--- /dev/null
+++ b/src/services/chain-adapter/evm/identity-service.ts
@@ -0,0 +1,41 @@
+/**
+ * EVM Identity Service
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+import type { IIdentityService, Address, Signature } from '../types'
+import { toChecksumAddress, isValidAddress } from '@/lib/crypto'
+
+export class EvmIdentityService implements IIdentityService {
+ constructor(_config: ChainConfig) {}
+
+ async deriveAddress(_seed: Uint8Array, _index = 0): Promise {
+ throw new Error('Use deriveAddressesForChains from @/lib/crypto instead')
+ }
+
+ async deriveAddresses(_seed: Uint8Array, _startIndex: number, _count: number): Promise {
+ throw new Error('Use deriveAddressesForChains from @/lib/crypto instead')
+ }
+
+ isValidAddress(address: string): boolean {
+ return isValidAddress(address, 'ethereum')
+ }
+
+ normalizeAddress(address: string): Address {
+ return toChecksumAddress(address)
+ }
+
+ async signMessage(_message: string | Uint8Array, _privateKey: Uint8Array): Promise {
+ // TODO: Implement EIP-191 personal_sign
+ throw new Error('Not implemented')
+ }
+
+ async verifyMessage(
+ _message: string | Uint8Array,
+ _signature: Signature,
+ _address: Address,
+ ): Promise {
+ // TODO: Implement signature verification
+ throw new Error('Not implemented')
+ }
+}
diff --git a/src/services/chain-adapter/evm/index.ts b/src/services/chain-adapter/evm/index.ts
new file mode 100644
index 00000000..6359ec05
--- /dev/null
+++ b/src/services/chain-adapter/evm/index.ts
@@ -0,0 +1,6 @@
+export { EvmAdapter, createEvmAdapter } from './adapter'
+export { EvmIdentityService } from './identity-service'
+export { EvmAssetService } from './asset-service'
+export { EvmTransactionService } from './transaction-service'
+export { EvmChainService } from './chain-service'
+export type * from './types'
diff --git a/src/services/chain-adapter/evm/transaction-service.ts b/src/services/chain-adapter/evm/transaction-service.ts
new file mode 100644
index 00000000..9485cb79
--- /dev/null
+++ b/src/services/chain-adapter/evm/transaction-service.ts
@@ -0,0 +1,324 @@
+/**
+ * EVM Transaction Service
+ *
+ * Handles transaction building, signing, and broadcasting for EVM chains.
+ * Uses standard Ethereum JSON-RPC API (compatible with PublicNode, Infura, etc.)
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+import type {
+ ITransactionService,
+ TransferParams,
+ UnsignedTransaction,
+ SignedTransaction,
+ TransactionHash,
+ TransactionStatus,
+ Transaction,
+ FeeEstimate,
+} from '../types'
+import { Amount } from '@/types/amount'
+import { ChainServiceError, ChainErrorCodes } from '../types'
+import { secp256k1 } from '@noble/curves/secp256k1.js'
+import { keccak_256 } from '@noble/hashes/sha3.js'
+import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js'
+
+/** EVM Chain IDs */
+const EVM_CHAIN_IDS: Record = {
+ 'ethereum': 1,
+ 'ethereum-sepolia': 11155111,
+ 'binance': 56,
+ 'bsc-testnet': 97,
+}
+
+/** Default RPC endpoints (PublicNode - free, no API key required) */
+const DEFAULT_RPC_URLS: Record = {
+ 'ethereum': 'https://ethereum-rpc.publicnode.com',
+ 'ethereum-sepolia': 'https://ethereum-sepolia-rpc.publicnode.com',
+ 'binance': 'https://bsc-rpc.publicnode.com',
+ 'bsc-testnet': 'https://bsc-testnet-rpc.publicnode.com',
+}
+
+export class EvmTransactionService implements ITransactionService {
+ private readonly config: ChainConfig
+ private readonly rpcUrl: string
+ private readonly evmChainId: number
+
+ constructor(config: ChainConfig) {
+ this.config = config
+ this.rpcUrl = config.api?.url ?? DEFAULT_RPC_URLS[config.id] ?? 'https://ethereum-rpc.publicnode.com'
+ this.evmChainId = EVM_CHAIN_IDS[config.id] ?? 1
+ }
+
+ /** Make a JSON-RPC call */
+ private async rpc(method: string, params: unknown[] = []): Promise {
+ const response = await fetch(this.rpcUrl, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: Date.now(),
+ method,
+ params,
+ }),
+ })
+
+ if (!response.ok) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ `HTTP ${response.status}: ${response.statusText}`,
+ )
+ }
+
+ const json = await response.json() as { result?: T; error?: { code: number; message: string } }
+ if (json.error) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ json.error.message,
+ { code: json.error.code },
+ )
+ }
+
+ return json.result as T
+ }
+
+ async estimateFee(_params: TransferParams): Promise {
+ // Get current gas price from network
+ const gasPriceHex = await this.rpc('eth_gasPrice')
+ const gasPrice = BigInt(gasPriceHex)
+
+ // Estimate gas (21000 for simple ETH transfer)
+ const gasLimit = 21000n
+ const baseFee = gasLimit * gasPrice
+
+ // Calculate slow/standard/fast with multipliers
+ const slow = Amount.fromRaw((baseFee * 80n / 100n).toString(), this.config.decimals, this.config.symbol)
+ const standard = Amount.fromRaw(baseFee.toString(), this.config.decimals, this.config.symbol)
+ const fast = Amount.fromRaw((baseFee * 120n / 100n).toString(), this.config.decimals, this.config.symbol)
+
+ return {
+ slow: { amount: slow, estimatedTime: 60 },
+ standard: { amount: standard, estimatedTime: 15 },
+ fast: { amount: fast, estimatedTime: 5 },
+ }
+ }
+
+ async buildTransaction(params: TransferParams): Promise {
+ // Get nonce for the sender
+ const nonceHex = await this.rpc('eth_getTransactionCount', [params.from, 'pending'])
+ const nonce = parseInt(nonceHex, 16)
+
+ // Get current gas price
+ const gasPriceHex = await this.rpc('eth_gasPrice')
+
+ return {
+ chainId: this.config.id,
+ data: {
+ nonce,
+ gasPrice: gasPriceHex,
+ gasLimit: '0x5208', // 21000 in hex
+ to: params.to,
+ value: '0x' + params.amount.raw.toString(16),
+ data: '0x',
+ chainId: this.evmChainId,
+ },
+ }
+ }
+
+ async signTransaction(
+ unsignedTx: UnsignedTransaction,
+ privateKey: Uint8Array,
+ ): Promise {
+ const txData = unsignedTx.data as {
+ nonce: number
+ gasPrice: string
+ gasLimit: string
+ to: string
+ value: string
+ data: string
+ chainId: number
+ }
+
+ // RLP encode the transaction for signing (EIP-155)
+ const rawTx = this.rlpEncode([
+ this.toRlpHex(txData.nonce),
+ txData.gasPrice,
+ txData.gasLimit,
+ txData.to.toLowerCase(),
+ txData.value,
+ txData.data,
+ this.toRlpHex(txData.chainId),
+ '0x',
+ '0x',
+ ])
+
+ // Hash and sign (use recovered format to get recovery bit)
+ const msgHash = keccak_256(hexToBytes(rawTx.slice(2)))
+ const sigBytes = secp256k1.sign(msgHash, privateKey, { prehash: false, format: 'recovered' })
+
+ // Parse signature: recovered format is 65 bytes (r[32] + s[32] + recovery[1])
+ const r = BigInt('0x' + bytesToHex(sigBytes.slice(0, 32)))
+ const s = BigInt('0x' + bytesToHex(sigBytes.slice(32, 64)))
+ const recovery = sigBytes[64]!
+
+ // Calculate v value (EIP-155)
+ const v = txData.chainId * 2 + 35 + recovery
+
+ // Get r and s as hex strings
+ const rHex = r.toString(16).padStart(64, '0')
+ const sHex = s.toString(16).padStart(64, '0')
+
+ // RLP encode signed transaction
+ const signedRaw = this.rlpEncode([
+ this.toRlpHex(txData.nonce),
+ txData.gasPrice,
+ txData.gasLimit,
+ txData.to.toLowerCase(),
+ txData.value,
+ txData.data,
+ this.toRlpHex(v),
+ '0x' + rHex,
+ '0x' + sHex,
+ ])
+
+ return {
+ chainId: this.config.id,
+ data: signedRaw,
+ signature: '0x' + rHex + sHex,
+ }
+ }
+
+ async broadcastTransaction(signedTx: SignedTransaction): Promise {
+ const rawTx = signedTx.data as string
+ const txHash = await this.rpc('eth_sendRawTransaction', [rawTx])
+ return txHash
+ }
+
+ /** Convert number to RLP hex format */
+ private toRlpHex(n: number): string {
+ if (n === 0) return '0x'
+ return '0x' + n.toString(16)
+ }
+
+ /** Simple RLP encoding for transaction */
+ private rlpEncode(items: string[]): string {
+ const encoded = items.map(item => {
+ if (item === '0x' || item === '') {
+ return new Uint8Array([0x80])
+ }
+ const bytes = hexToBytes(item.startsWith('0x') ? item.slice(2) : item)
+ if (bytes.length === 1 && bytes[0]! < 0x80) {
+ return bytes
+ }
+ if (bytes.length <= 55) {
+ return new Uint8Array([0x80 + bytes.length, ...bytes])
+ }
+ const lenBytes = this.numberToBytes(bytes.length)
+ return new Uint8Array([0xb7 + lenBytes.length, ...lenBytes, ...bytes])
+ })
+
+ const totalLen = encoded.reduce((sum, e) => sum + e.length, 0)
+ let prefix: Uint8Array
+ if (totalLen <= 55) {
+ prefix = new Uint8Array([0xc0 + totalLen])
+ } else {
+ const lenBytes = this.numberToBytes(totalLen)
+ prefix = new Uint8Array([0xf7 + lenBytes.length, ...lenBytes])
+ }
+
+ const result = new Uint8Array(prefix.length + totalLen)
+ result.set(prefix)
+ let offset = prefix.length
+ for (const e of encoded) {
+ result.set(e, offset)
+ offset += e.length
+ }
+ return '0x' + bytesToHex(result)
+ }
+
+ private numberToBytes(n: number): Uint8Array {
+ const hex = n.toString(16)
+ const padded = hex.length % 2 ? '0' + hex : hex
+ return hexToBytes(padded)
+ }
+
+ async getTransactionStatus(hash: TransactionHash): Promise {
+ try {
+ const receipt = await this.rpc<{
+ status: string
+ blockNumber: string
+ } | null>('eth_getTransactionReceipt', [hash])
+
+ if (!receipt) {
+ return { status: 'pending', confirmations: 0, requiredConfirmations: 12 }
+ }
+
+ const currentBlock = await this.rpc('eth_blockNumber')
+ const confirmations = parseInt(currentBlock, 16) - parseInt(receipt.blockNumber, 16)
+
+ return {
+ status: confirmations >= 12 ? 'confirmed' : 'confirming',
+ confirmations: Math.max(0, confirmations),
+ requiredConfirmations: 12,
+ }
+ } catch {
+ return { status: 'pending', confirmations: 0, requiredConfirmations: 12 }
+ }
+ }
+
+ async getTransaction(hash: TransactionHash): Promise {
+ try {
+ const [tx, receipt] = await Promise.all([
+ this.rpc<{
+ hash: string
+ from: string
+ to: string
+ value: string
+ gasPrice: string
+ blockNumber: string | null
+ } | null>('eth_getTransactionByHash', [hash]),
+ this.rpc<{
+ status: string
+ gasUsed: string
+ blockNumber: string
+ } | null>('eth_getTransactionReceipt', [hash]),
+ ])
+
+ if (!tx) return null
+
+ const block = tx.blockNumber
+ ? await this.rpc<{ timestamp: string }>('eth_getBlockByNumber', [tx.blockNumber, false])
+ : null
+
+ return {
+ hash: tx.hash,
+ from: tx.from,
+ to: tx.to ?? '',
+ amount: Amount.fromRaw(BigInt(tx.value).toString(), this.config.decimals, this.config.symbol),
+ fee: receipt
+ ? Amount.fromRaw(
+ (BigInt(receipt.gasUsed) * BigInt(tx.gasPrice)).toString(),
+ this.config.decimals,
+ this.config.symbol,
+ )
+ : Amount.fromRaw('0', this.config.decimals, this.config.symbol),
+ status: {
+ status: receipt?.status === '0x1' ? 'confirmed' : receipt ? 'failed' : 'pending',
+ confirmations: receipt ? 12 : 0,
+ requiredConfirmations: 12,
+ },
+ timestamp: block ? parseInt(block.timestamp, 16) * 1000 : Date.now(),
+ blockNumber: tx.blockNumber ? BigInt(tx.blockNumber) : undefined,
+ type: 'transfer',
+ }
+ } catch {
+ return null
+ }
+ }
+
+ async getTransactionHistory(_address: string, _limit = 20): Promise {
+ // Note: Standard JSON-RPC doesn't support transaction history queries
+ // This would require an indexer service like Etherscan API
+ // For now, return empty array - can be extended with Etherscan/BlockScout API
+ return []
+ }
+}
diff --git a/src/services/chain-adapter/evm/types.ts b/src/services/chain-adapter/evm/types.ts
new file mode 100644
index 00000000..98cc6cba
--- /dev/null
+++ b/src/services/chain-adapter/evm/types.ts
@@ -0,0 +1,34 @@
+/**
+ * EVM Chain Adapter Types
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+
+export interface EvmChainConfig extends ChainConfig {
+ type: 'evm'
+ /** EVM Chain ID (e.g., 1 for Ethereum mainnet, 56 for BSC) */
+ chainId?: number
+ /** RPC endpoint URL */
+ rpcUrl?: string
+}
+
+export interface EvmTransactionReceipt {
+ transactionHash: string
+ blockNumber: bigint
+ blockHash: string
+ status: 'success' | 'reverted'
+ gasUsed: bigint
+ effectiveGasPrice: bigint
+}
+
+export interface EvmTransactionRequest {
+ from: string
+ to: string
+ value: bigint
+ data?: string
+ nonce?: number
+ gasLimit?: bigint
+ gasPrice?: bigint
+ maxFeePerGas?: bigint
+ maxPriorityFeePerGas?: bigint
+}
diff --git a/src/services/chain-adapter/index.ts b/src/services/chain-adapter/index.ts
index 188e8f5e..319c0ac1 100644
--- a/src/services/chain-adapter/index.ts
+++ b/src/services/chain-adapter/index.ts
@@ -40,15 +40,31 @@ export { getAdapterRegistry, resetAdapterRegistry } from './registry'
// Adapters
export { BioforestAdapter, createBioforestAdapter } from './bioforest'
+export { EvmAdapter, createEvmAdapter } from './evm'
+export { Bip39Adapter, createBip39Adapter } from './bip39'
+export { TronAdapter } from './tron'
+export { BitcoinAdapter } from './bitcoin'
// Setup function to register all adapters
import { getAdapterRegistry } from './registry'
import { createBioforestAdapter } from './bioforest'
+import { createEvmAdapter } from './evm'
+import { TronAdapter } from './tron'
+import { BitcoinAdapter } from './bitcoin'
+import type { ChainConfig } from '@/services/chain-config'
+
+function createTronAdapter(config: ChainConfig) {
+ return new TronAdapter(config)
+}
+
+function createBitcoinAdapter(config: ChainConfig) {
+ return new BitcoinAdapter(config)
+}
export function setupAdapters(): void {
const registry = getAdapterRegistry()
registry.register('bioforest', createBioforestAdapter)
- // TODO: Register other adapters
- // registry.register('evm', createEvmAdapter)
- // registry.register('bip39', createBip39Adapter)
+ registry.register('evm', createEvmAdapter)
+ registry.register('tron', createTronAdapter)
+ registry.register('bip39', createBitcoinAdapter) // Bitcoin uses bip39 type
}
diff --git a/src/services/chain-adapter/tron/adapter.ts b/src/services/chain-adapter/tron/adapter.ts
new file mode 100644
index 00000000..8575fec0
--- /dev/null
+++ b/src/services/chain-adapter/tron/adapter.ts
@@ -0,0 +1,45 @@
+/**
+ * Tron Chain Adapter
+ *
+ * Full adapter for Tron network using PublicNode HTTP API
+ */
+
+import type { ChainConfig, ChainConfigType } from '@/services/chain-config'
+import type { IChainAdapter, IStakingService } from '../types'
+import { TronIdentityService } from './identity-service'
+import { TronAssetService } from './asset-service'
+import { TronChainService } from './chain-service'
+import { TronTransactionService } from './transaction-service'
+
+export class TronAdapter implements IChainAdapter {
+ readonly config: ChainConfig
+ readonly identity: TronIdentityService
+ readonly asset: TronAssetService
+ readonly chain: TronChainService
+ readonly transaction: TronTransactionService
+ readonly staking: IStakingService | null = null
+
+ constructor(config: ChainConfig) {
+ this.config = config
+ this.identity = new TronIdentityService(config)
+ this.asset = new TronAssetService(config)
+ this.chain = new TronChainService(config)
+ this.transaction = new TronTransactionService(config)
+ }
+
+ get chainId(): string {
+ return this.config.id
+ }
+
+ get chainType(): ChainConfigType {
+ return 'tron'
+ }
+
+ async initialize(_config: ChainConfig): Promise {
+ // No async initialization needed
+ }
+
+ dispose(): void {
+ // No cleanup needed
+ }
+}
diff --git a/src/services/chain-adapter/tron/asset-service.ts b/src/services/chain-adapter/tron/asset-service.ts
new file mode 100644
index 00000000..10380398
--- /dev/null
+++ b/src/services/chain-adapter/tron/asset-service.ts
@@ -0,0 +1,93 @@
+/**
+ * Tron Asset Service
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+import type { IAssetService, Balance, TokenMetadata, Address } from '../types'
+import { Amount } from '@/types/amount'
+import { ChainServiceError, ChainErrorCodes } from '../types'
+import type { TronAccount } from './types'
+
+/** Default Tron RPC endpoints */
+const DEFAULT_RPC_URLS: Record = {
+ 'tron': 'https://tron-rpc.publicnode.com',
+ 'tron-nile': 'https://nile.trongrid.io',
+ 'tron-shasta': 'https://api.shasta.trongrid.io',
+}
+
+export class TronAssetService implements IAssetService {
+ private readonly config: ChainConfig
+ private readonly rpcUrl: string
+
+ constructor(config: ChainConfig) {
+ this.config = config
+ this.rpcUrl = DEFAULT_RPC_URLS[config.id] ?? config.api?.url ?? DEFAULT_RPC_URLS['tron']!
+ }
+
+ private async api(endpoint: string, body?: unknown): Promise {
+ const url = `${this.rpcUrl}${endpoint}`
+ const init: RequestInit = body
+ ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
+ : { method: 'GET' }
+
+ const response = await fetch(url, init)
+ if (!response.ok) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ `Tron API error: ${response.status}`,
+ )
+ }
+
+ return response.json() as Promise
+ }
+
+ async getNativeBalance(address: Address): Promise {
+ try {
+ const account = await this.api>('/wallet/getaccount', {
+ address,
+ visible: true,
+ })
+
+ // Empty object means account doesn't exist yet (0 balance)
+ if (!account || !('balance' in account)) {
+ return {
+ amount: Amount.fromRaw('0', this.config.decimals, this.config.symbol),
+ symbol: this.config.symbol,
+ }
+ }
+
+ return {
+ amount: Amount.fromRaw(account.balance.toString(), this.config.decimals, this.config.symbol),
+ symbol: this.config.symbol,
+ }
+ } catch {
+ return {
+ amount: Amount.fromRaw('0', this.config.decimals, this.config.symbol),
+ symbol: this.config.symbol,
+ }
+ }
+ }
+
+ async getTokenBalance(_address: Address, _tokenAddress: Address): Promise {
+ // TRC20 token balance requires contract calls - not implemented
+ return {
+ amount: Amount.fromRaw('0', 18, 'TOKEN'),
+ symbol: 'TOKEN',
+ }
+ }
+
+ async getTokenBalances(_address: Address): Promise {
+ // Would require TronGrid API for full token list
+ return []
+ }
+
+ async getTokenMetadata(_tokenAddress: Address): Promise {
+ // TRC20 token metadata requires contract calls
+ return {
+ address: _tokenAddress,
+ name: 'Unknown',
+ symbol: 'UNKNOWN',
+ decimals: 18,
+ }
+ }
+}
diff --git a/src/services/chain-adapter/tron/chain-service.ts b/src/services/chain-adapter/tron/chain-service.ts
new file mode 100644
index 00000000..2a6f9c4c
--- /dev/null
+++ b/src/services/chain-adapter/tron/chain-service.ts
@@ -0,0 +1,101 @@
+/**
+ * Tron Chain Service
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+import type { IChainService, ChainInfo, GasPrice, HealthStatus } from '../types'
+import { Amount } from '@/types/amount'
+import { ChainServiceError, ChainErrorCodes } from '../types'
+import type { TronBlock, TronAccountResource } from './types'
+
+/** Default Tron RPC endpoints */
+const DEFAULT_RPC_URLS: Record = {
+ 'tron': 'https://tron-rpc.publicnode.com',
+ 'tron-nile': 'https://nile.trongrid.io',
+ 'tron-shasta': 'https://api.shasta.trongrid.io',
+}
+
+export class TronChainService implements IChainService {
+ private readonly config: ChainConfig
+ private readonly rpcUrl: string
+
+ constructor(config: ChainConfig) {
+ this.config = config
+ this.rpcUrl = DEFAULT_RPC_URLS[config.id] ?? config.api?.url ?? DEFAULT_RPC_URLS['tron']!
+ }
+
+ private async api(endpoint: string, body?: unknown): Promise {
+ const url = `${this.rpcUrl}${endpoint}`
+ const init: RequestInit = body
+ ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
+ : { method: 'GET' }
+
+ const response = await fetch(url, init)
+ if (!response.ok) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ `Tron API error: ${response.status}`,
+ )
+ }
+
+ return response.json() as Promise
+ }
+
+ getChainInfo(): ChainInfo {
+ return {
+ chainId: this.config.id,
+ name: this.config.name,
+ symbol: this.config.symbol,
+ decimals: this.config.decimals,
+ blockTime: 3,
+ confirmations: 19,
+ explorerUrl: this.config.explorer?.url,
+ }
+ }
+
+ async getBlockHeight(): Promise {
+ const block = await this.api('/wallet/getnowblock')
+ return BigInt(block.block_header.raw_data.number)
+ }
+
+ async getGasPrice(): Promise {
+ // Tron uses bandwidth/energy, not gas
+ // 1000 SUN per bandwidth unit
+ const price = Amount.fromRaw('1000', this.config.decimals, this.config.symbol)
+ return {
+ slow: price,
+ standard: price,
+ fast: price,
+ lastUpdated: Date.now(),
+ }
+ }
+
+ async healthCheck(): Promise {
+ const start = Date.now()
+ try {
+ const block = await this.api('/wallet/getnowblock')
+ return {
+ isHealthy: true,
+ latency: Date.now() - start,
+ blockHeight: BigInt(block.block_header.raw_data.number),
+ isSyncing: false,
+ lastUpdated: Date.now(),
+ }
+ } catch {
+ return {
+ isHealthy: false,
+ latency: Date.now() - start,
+ blockHeight: 0n,
+ isSyncing: false,
+ lastUpdated: Date.now(),
+ }
+ }
+ }
+
+ async getAccountResources(address: string): Promise {
+ return this.api('/wallet/getaccountresource', {
+ address,
+ visible: true,
+ })
+ }
+}
diff --git a/src/services/chain-adapter/tron/identity-service.ts b/src/services/chain-adapter/tron/identity-service.ts
new file mode 100644
index 00000000..e6a3880c
--- /dev/null
+++ b/src/services/chain-adapter/tron/identity-service.ts
@@ -0,0 +1,164 @@
+/**
+ * Tron Identity Service
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+import type { IIdentityService, Address, Signature } from '../types'
+import { sha256 } from '@noble/hashes/sha2.js'
+import { secp256k1 } from '@noble/curves/secp256k1.js'
+import { keccak_256 } from '@noble/hashes/sha3.js'
+import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js'
+import { HDKey } from '@scure/bip32'
+
+/** Base58 alphabet used by Tron */
+const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
+
+export class TronIdentityService implements IIdentityService {
+ constructor(_config: ChainConfig) {}
+
+ async deriveAddress(seed: Uint8Array, index = 0): Promise {
+ // Tron uses BIP44 path: m/44'/195'/0'/0/index
+ const hdKey = HDKey.fromMasterSeed(seed)
+ const derived = hdKey.derive(`m/44'/195'/0'/0/${index}`)
+
+ if (!derived.privateKey) {
+ throw new Error('Failed to derive private key')
+ }
+
+ // Get public key and derive address
+ const pubKey = secp256k1.getPublicKey(derived.privateKey, false)
+ const pubKeyHash = keccak_256(pubKey.slice(1))
+ const addressBytes = pubKeyHash.slice(-20)
+
+ // Add Tron prefix (0x41) and encode to base58check
+ const payload = new Uint8Array([0x41, ...addressBytes])
+ const checksum = sha256(sha256(payload)).slice(0, 4)
+ const full = new Uint8Array([...payload, ...checksum])
+
+ return this.encodeBase58(full)
+ }
+
+ async deriveAddresses(seed: Uint8Array, startIndex: number, count: number): Promise {
+ const addresses: Address[] = []
+ for (let i = 0; i < count; i++) {
+ addresses.push(await this.deriveAddress(seed, startIndex + i))
+ }
+ return addresses
+ }
+
+ isValidAddress(address: string): boolean {
+ // Tron addresses start with 'T' and are 34 characters
+ if (!address.startsWith('T') || address.length !== 34) {
+ return false
+ }
+
+ // Validate base58 characters
+ for (const char of address) {
+ if (!ALPHABET.includes(char)) {
+ return false
+ }
+ }
+
+ // Validate checksum
+ try {
+ const decoded = this.decodeBase58(address)
+ if (decoded.length !== 25) return false
+
+ const payload = decoded.slice(0, 21)
+ const checksum = decoded.slice(21)
+ const hash = sha256(sha256(payload))
+
+ return checksum.every((byte, i) => byte === hash[i])
+ } catch {
+ return false
+ }
+ }
+
+ private decodeBase58(str: string): Uint8Array {
+ let num = BigInt(0)
+ for (const char of str) {
+ const index = ALPHABET.indexOf(char)
+ if (index === -1) throw new Error(`Invalid base58 character: ${char}`)
+ num = num * 58n + BigInt(index)
+ }
+
+ const hex = num.toString(16).padStart(50, '0')
+ const bytes = new Uint8Array(25)
+ for (let i = 0; i < 25; i++) {
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
+ }
+ return bytes
+ }
+
+ normalizeAddress(address: string): Address {
+ return address
+ }
+
+ async signMessage(message: string | Uint8Array, privateKey: Uint8Array): Promise {
+ const msgBytes = typeof message === 'string'
+ ? new TextEncoder().encode(message)
+ : message
+
+ // Tron uses keccak256 for message hashing
+ const prefix = new TextEncoder().encode('\x19TRON Signed Message:\n' + msgBytes.length)
+ const hash = keccak_256(new Uint8Array([...prefix, ...msgBytes]))
+
+ const sig = secp256k1.sign(hash, privateKey, { prehash: false, format: 'recovered' })
+ return bytesToHex(sig)
+ }
+
+ async verifyMessage(message: string | Uint8Array, signature: Signature, address: Address): Promise {
+ try {
+ const msgBytes = typeof message === 'string'
+ ? new TextEncoder().encode(message)
+ : message
+
+ const prefix = new TextEncoder().encode('\x19TRON Signed Message:\n' + msgBytes.length)
+ const hash = keccak_256(new Uint8Array([...prefix, ...msgBytes]))
+
+ // Parse signature bytes (65 bytes: r[32] + s[32] + v[1])
+ const sigBytes = hexToBytes(signature)
+ const recoveredPubKey = secp256k1.recoverPublicKey(hash, sigBytes)
+ if (!recoveredPubKey) return false
+
+ // Derive address from recovered public key and compare
+ const pubKeyHash = keccak_256(recoveredPubKey.slice(1))
+ const addressBytes = pubKeyHash.slice(-20)
+
+ // Add Tron prefix (0x41) and encode to base58check
+ const payload = new Uint8Array([0x41, ...addressBytes])
+ const checksum = sha256(sha256(payload)).slice(0, 4)
+ const full = new Uint8Array([...payload, ...checksum])
+ const recoveredAddress = this.encodeBase58(full)
+
+ return recoveredAddress === address
+ } catch {
+ return false
+ }
+ }
+
+ private encodeBase58(bytes: Uint8Array): string {
+ let num = BigInt(0)
+ for (const byte of bytes) {
+ num = num * 256n + BigInt(byte)
+ }
+
+ let result = ''
+ while (num > 0n) {
+ const rem = Number(num % 58n)
+ num = num / 58n
+ result = ALPHABET[rem] + result
+ }
+
+ // Handle leading zeros
+ for (const byte of bytes) {
+ if (byte === 0) {
+ result = ALPHABET[0] + result
+ } else {
+ break
+ }
+ }
+
+ return result
+ }
+}
diff --git a/src/services/chain-adapter/tron/index.ts b/src/services/chain-adapter/tron/index.ts
new file mode 100644
index 00000000..53673858
--- /dev/null
+++ b/src/services/chain-adapter/tron/index.ts
@@ -0,0 +1,10 @@
+/**
+ * Tron Chain Adapter exports
+ */
+
+export { TronAdapter } from './adapter'
+export { TronIdentityService } from './identity-service'
+export { TronAssetService } from './asset-service'
+export { TronChainService } from './chain-service'
+export { TronTransactionService } from './transaction-service'
+export * from './types'
diff --git a/src/services/chain-adapter/tron/transaction-service.ts b/src/services/chain-adapter/tron/transaction-service.ts
new file mode 100644
index 00000000..76da3961
--- /dev/null
+++ b/src/services/chain-adapter/tron/transaction-service.ts
@@ -0,0 +1,238 @@
+/**
+ * Tron Transaction Service
+ *
+ * Uses Tron HTTP API via PublicNode (tron-rpc.publicnode.com)
+ */
+
+import type { ChainConfig } from '@/services/chain-config'
+import type {
+ ITransactionService,
+ TransferParams,
+ UnsignedTransaction,
+ SignedTransaction,
+ TransactionHash,
+ TransactionStatus,
+ Transaction,
+ FeeEstimate,
+ Fee,
+} from '../types'
+import { Amount } from '@/types/amount'
+import { ChainServiceError, ChainErrorCodes } from '../types'
+import { secp256k1 } from '@noble/curves/secp256k1.js'
+import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js'
+import type {
+ TronRawTransaction,
+ TronSignedTransaction,
+ TronTransactionInfo,
+ TronBlock,
+} from './types'
+
+/** Default Tron RPC endpoints */
+const DEFAULT_RPC_URLS: Record = {
+ 'tron': 'https://tron-rpc.publicnode.com',
+ 'tron-nile': 'https://nile.trongrid.io',
+ 'tron-shasta': 'https://api.shasta.trongrid.io',
+}
+
+export class TronTransactionService implements ITransactionService {
+ private readonly config: ChainConfig
+ private readonly rpcUrl: string
+
+ constructor(config: ChainConfig) {
+ this.config = config
+ this.rpcUrl = DEFAULT_RPC_URLS[config.id] ?? config.api?.url ?? DEFAULT_RPC_URLS['tron']!
+ }
+
+ private async api(endpoint: string, body?: unknown): Promise {
+ const url = `${this.rpcUrl}${endpoint}`
+ const init: RequestInit = body
+ ? {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body)
+ }
+ : { method: 'GET' }
+
+ const response = await fetch(url, init)
+ if (!response.ok) {
+ throw new ChainServiceError(
+ ChainErrorCodes.NETWORK_ERROR,
+ `Tron API error: ${response.status} ${response.statusText}`,
+ )
+ }
+
+ return response.json() as Promise
+ }
+
+ /** Convert base58 Tron address to hex format */
+ private base58ToHex(address: string): string {
+ const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
+ let num = BigInt(0)
+ for (const char of address) {
+ const index = ALPHABET.indexOf(char)
+ if (index === -1) throw new Error(`Invalid base58 character: ${char}`)
+ num = num * 58n + BigInt(index)
+ }
+ // Convert to hex, remove checksum (last 4 bytes)
+ const hex = num.toString(16).padStart(50, '0')
+ return hex.slice(0, 42) // 21 bytes = 42 hex chars
+ }
+
+ async estimateFee(_params: TransferParams): Promise {
+ // TRX transfers typically cost bandwidth (free up to limit)
+ // If bandwidth exhausted, burns TRX at 1000 SUN per bandwidth unit
+ const feeAmount = Amount.fromRaw('0', this.config.decimals, this.config.symbol)
+
+ const fee: Fee = {
+ amount: feeAmount,
+ estimatedTime: 3, // ~3 seconds
+ }
+
+ return {
+ slow: fee,
+ standard: fee,
+ fast: fee,
+ }
+ }
+
+ async buildTransaction(params: TransferParams): Promise {
+ // Convert addresses to hex format for API
+ const ownerAddressHex = this.base58ToHex(params.from)
+ const toAddressHex = this.base58ToHex(params.to)
+
+ // Create transaction via Tron API
+ const rawTx = await this.api('/wallet/createtransaction', {
+ owner_address: ownerAddressHex,
+ to_address: toAddressHex,
+ amount: Number(params.amount.raw),
+ visible: false,
+ })
+
+ if (!rawTx.txID) {
+ throw new ChainServiceError(
+ ChainErrorCodes.TX_BUILD_FAILED,
+ 'Failed to create Tron transaction',
+ )
+ }
+
+ return {
+ chainId: this.config.id,
+ data: rawTx,
+ }
+ }
+
+ async signTransaction(
+ unsignedTx: UnsignedTransaction,
+ privateKey: Uint8Array,
+ ): Promise {
+ const rawTx = unsignedTx.data as TronRawTransaction
+
+ // Sign the transaction ID (which is already a hash)
+ const txIdBytes = hexToBytes(rawTx.txID)
+ const sigBytes = secp256k1.sign(txIdBytes, privateKey, { prehash: false, format: 'recovered' })
+
+ // Tron signature format: r (32) + s (32) + recovery (1)
+ const signature = bytesToHex(sigBytes)
+
+ const signedTx: TronSignedTransaction = {
+ ...rawTx,
+ signature: [signature],
+ }
+
+ return {
+ chainId: this.config.id,
+ data: signedTx,
+ signature: signature,
+ }
+ }
+
+ async broadcastTransaction(signedTx: SignedTransaction): Promise {
+ const tx = signedTx.data as TronSignedTransaction
+
+ const result = await this.api<{ result?: boolean; txid?: string; code?: string; message?: string }>(
+ '/wallet/broadcasttransaction',
+ tx,
+ )
+
+ if (!result.result) {
+ const errorMsg = result.message
+ ? Buffer.from(result.message, 'hex').toString('utf8')
+ : result.code ?? 'Unknown error'
+ throw new ChainServiceError(ChainErrorCodes.TX_BROADCAST_FAILED, `Broadcast failed: ${errorMsg}`)
+ }
+
+ return tx.txID
+ }
+
+ async getTransactionStatus(hash: TransactionHash): Promise {
+ try {
+ const info = await this.api>(
+ '/wallet/gettransactioninfobyid',
+ { value: hash },
+ )
+
+ // Empty object means transaction not found/pending
+ if (!info || !('blockNumber' in info)) {
+ return { status: 'pending', confirmations: 0, requiredConfirmations: 19 }
+ }
+
+ // Get current block for confirmation count
+ const block = await this.api('/wallet/getnowblock')
+ const currentBlock = block.block_header.raw_data.number
+ const confirmations = currentBlock - info.blockNumber
+
+ return {
+ status: confirmations >= 19 ? 'confirmed' : 'confirming',
+ confirmations: Math.max(0, confirmations),
+ requiredConfirmations: 19,
+ }
+ } catch {
+ return { status: 'pending', confirmations: 0, requiredConfirmations: 19 }
+ }
+ }
+
+ async getTransaction(hash: TransactionHash): Promise {
+ try {
+ const [tx, info] = await Promise.all([
+ this.api>('/wallet/gettransactionbyid', { value: hash }),
+ this.api>('/wallet/gettransactioninfobyid', { value: hash }),
+ ])
+
+ if (!tx || !('txID' in tx)) return null
+
+ const contract = tx.raw_data.contract[0]
+ if (!contract || contract.type !== 'TransferContract') return null
+
+ const { amount, owner_address, to_address } = contract.parameter.value
+ const isConfirmed = 'blockNumber' in info
+
+ return {
+ hash: tx.txID,
+ from: owner_address,
+ to: to_address,
+ amount: Amount.fromRaw(amount.toString(), this.config.decimals, this.config.symbol),
+ fee: Amount.fromRaw(
+ ((info as TronTransactionInfo).receipt?.net_usage ?? 0).toString(),
+ this.config.decimals,
+ this.config.symbol,
+ ),
+ status: {
+ status: isConfirmed ? 'confirmed' : 'pending',
+ confirmations: isConfirmed ? 19 : 0,
+ requiredConfirmations: 19,
+ },
+ timestamp: tx.raw_data.timestamp,
+ blockNumber: isConfirmed ? BigInt((info as TronTransactionInfo).blockNumber) : undefined,
+ type: 'transfer',
+ }
+ } catch {
+ return null
+ }
+ }
+
+ async getTransactionHistory(_address: string, _limit = 20): Promise {
+ // Tron HTTP API doesn't support transaction history queries
+ // Would need TronGrid API or similar indexer service
+ return []
+ }
+}
diff --git a/src/services/chain-adapter/tron/types.ts b/src/services/chain-adapter/tron/types.ts
new file mode 100644
index 00000000..d5dc17f1
--- /dev/null
+++ b/src/services/chain-adapter/tron/types.ts
@@ -0,0 +1,79 @@
+/**
+ * Tron-specific types
+ */
+
+/** Tron account info from API */
+export interface TronAccount {
+ address: string
+ balance: number
+ create_time: number
+ latest_opration_time?: number
+ account_resource?: {
+ energy_usage?: number
+ frozen_balance_for_energy?: number
+ }
+}
+
+/** Tron block info */
+export interface TronBlock {
+ blockID: string
+ block_header: {
+ raw_data: {
+ number: number
+ txTrieRoot: string
+ witness_address: string
+ parentHash: string
+ timestamp: number
+ }
+ }
+}
+
+/** Raw transaction from Tron API */
+export interface TronRawTransaction {
+ txID: string
+ raw_data: {
+ contract: Array<{
+ parameter: {
+ value: {
+ amount: number
+ owner_address: string
+ to_address: string
+ }
+ type_url: string
+ }
+ type: string
+ }>
+ ref_block_bytes: string
+ ref_block_hash: string
+ expiration: number
+ timestamp: number
+ }
+ raw_data_hex: string
+}
+
+/** Signed transaction ready for broadcast */
+export interface TronSignedTransaction extends TronRawTransaction {
+ signature: string[]
+}
+
+/** Transaction info from API */
+export interface TronTransactionInfo {
+ id: string
+ blockNumber: number
+ blockTimeStamp: number
+ contractResult: string[]
+ receipt: {
+ net_usage?: number
+ energy_usage?: number
+ }
+}
+
+/** Account resource info */
+export interface TronAccountResource {
+ freeNetLimit: number
+ freeNetUsed?: number
+ NetLimit?: number
+ NetUsed?: number
+ EnergyLimit?: number
+ EnergyUsed?: number
+}
diff --git a/src/services/chain-adapter/types.ts b/src/services/chain-adapter/types.ts
index 7d938841..1063f434 100644
--- a/src/services/chain-adapter/types.ts
+++ b/src/services/chain-adapter/types.ts
@@ -210,6 +210,9 @@ export const ChainErrorCodes = {
INVALID_ADDRESS: 'INVALID_ADDRESS',
TRANSACTION_REJECTED: 'TRANSACTION_REJECTED',
TRANSACTION_TIMEOUT: 'TRANSACTION_TIMEOUT',
+ TX_BUILD_FAILED: 'TX_BUILD_FAILED',
+ TX_BROADCAST_FAILED: 'TX_BROADCAST_FAILED',
+ SIGNATURE_FAILED: 'SIGNATURE_FAILED',
// BioForest
ADDRESS_FROZEN: 'ADDRESS_FROZEN',
diff --git a/src/services/chain-config/__tests__/schema.test.ts b/src/services/chain-config/__tests__/schema.test.ts
index 9fa85baf..c22c5c3e 100644
--- a/src/services/chain-config/__tests__/schema.test.ts
+++ b/src/services/chain-config/__tests__/schema.test.ts
@@ -64,11 +64,13 @@ describe('default-chains.json', () => {
// Verify chain types
const bioforestChains = chains.filter(c => c.type === 'bioforest')
const evmChains = chains.filter(c => c.type === 'evm')
+ const tronChains = chains.filter(c => c.type === 'tron')
const bip39Chains = chains.filter(c => c.type === 'bip39')
expect(bioforestChains).toHaveLength(7)
expect(evmChains).toHaveLength(2)
- expect(bip39Chains).toHaveLength(2)
+ expect(tronChains).toHaveLength(1)
+ expect(bip39Chains).toHaveLength(1)
})
})
diff --git a/src/services/chain-config/index.ts b/src/services/chain-config/index.ts
index efe3131b..07a775fd 100644
--- a/src/services/chain-config/index.ts
+++ b/src/services/chain-config/index.ts
@@ -27,11 +27,12 @@ export interface ChainConfigSnapshot {
warnings: ChainConfigWarning[]
}
-const KNOWN_TYPES: ReadonlySet = new Set(['bioforest', 'evm', 'bip39', 'custom'])
+const KNOWN_TYPES: ReadonlySet = new Set(['bioforest', 'evm', 'tron', 'bip39', 'custom'])
const SUPPORTED_MAJOR_BY_TYPE: Record = {
bioforest: 1,
evm: 1,
+ tron: 1,
bip39: 1,
custom: 1,
}
diff --git a/src/services/chain-config/schema.ts b/src/services/chain-config/schema.ts
index 79f55b97..5e9ee263 100644
--- a/src/services/chain-config/schema.ts
+++ b/src/services/chain-config/schema.ts
@@ -17,7 +17,7 @@ export const ChainConfigVersionSchema = z
.string()
.regex(/^\d+\.\d+$/, 'version must be "major.minor" (e.g. "1.0")')
-export const ChainConfigTypeSchema = z.enum(['bioforest', 'evm', 'bip39', 'custom'])
+export const ChainConfigTypeSchema = z.enum(['bioforest', 'evm', 'tron', 'bip39', 'custom'])
export const ChainConfigSourceSchema = z.enum(['default', 'subscription', 'manual'])