clanker-wallet 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/SKILL.md +14 -1
- package/dist/chain-data.d.ts +31 -0
- package/dist/chain-data.js +174 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/mcp.d.ts +152 -1
- package/dist/mcp.js +74 -7
- package/dist/reads.d.ts +51 -0
- package/dist/reads.js +210 -0
- package/dist/tokens.d.ts +9 -0
- package/dist/tokens.js +142 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -178,11 +178,24 @@ Claude Desktop (`claude_desktop_config.json`), Cursor (`~/.cursor/mcp.json`), an
|
|
|
178
178
|
|
|
179
179
|
| Tool | Arguments | Returns |
|
|
180
180
|
|------|-----------|---------|
|
|
181
|
-
| `request_evm_tx` | `chain_id`, `to`, `value` (wei, integer string), `data` (default `0x`), `reason`, optional `urgency` / `expected_outcome` / `from` (the paired wallet to send from, needed when several wallets of the same family are paired) | `{"request_id":"req_...","status":"pending","expires_at":"..."}` |
|
|
181
|
+
| `request_evm_tx` | `chain_id`, `to`, `value` (wei, integer string), `data` (default `0x`), `reason`, optional `urgency` / `expected_outcome` / `from` (the paired wallet to send from, needed when several wallets of the same family are paired) | `{"request_id":"req_...","review_url":"https://clanker-wallet.xyz/?request=req_...","status":"pending","expires_at":"..."}` |
|
|
182
182
|
| `request_transfer` | `family` (`bitcoin` or `chia`), `network` (bitcoin: `mainnet`, `testnet`, `signet`; chia: `mainnet`, `testnet11`), `to`, `amount` (sats, integer string) or mojos for chia, `reason`, optional `fee` (chia only: mojos, default `"0"`) / `urgency` / `expected_outcome` / `from` (the paired wallet to send from, needed when several wallets of the same family are paired) | as above |
|
|
183
|
+
| `request_token_transfer` | `chain_id`, `to`, `amount` (positive human decimal string), `reason`, optional `token` (listed symbol or contract address; omitted/native symbol = native coin), `from`, `urgency`, `expected_outcome` | `request_id`, `review_url`, pending status and expiry; existing EVM request, no signing here |
|
|
183
184
|
| `get_request` | `request_id`, optional `wait_seconds` (at most 50) | `status`: `pending`, `approved` (with `tx_hash`; a chia send may have none), `rejected`, `failed` (with `error`), `unknown` (with `error`) or `expired` |
|
|
185
|
+
| `get_balance` | `family`, `address` (any recipient), EVM `chain_id`, bitcoin/chia `network`, optional EVM `token` (listed symbol or address) | exact `raw_balance`, human `balance`, `asset`, `decimals`; bitcoin also confirmed/mempool deltas; chia recipient puzzle-hash unspent coins only |
|
|
186
|
+
| `get_tx_status` | `family` (evm/bitcoin), `tx_hash`, EVM `chain_id` or bitcoin `network` | receipt `status` and `confirmations`; missing EVM receipt is `pending_or_unknown`, missing bitcoin hash is `unknown` |
|
|
184
187
|
| `list_pairings` | none | `agent_id`, `fingerprint`, and the paired families (`evm`, `bitcoin`, `chia`) with network, wallet and mode |
|
|
185
188
|
|
|
189
|
+
`request_token_transfer` uses the browser-safe `clanker-wallet/tokens` list (chain id, symbol, address, decimals). Unlisted symbols are refused: pass the contract address; never search by symbol. Unlisted contracts must answer both `decimals()` and `symbol()` via RPC. Excess fraction digits are refused, never rounded. The review card reads unlisted token metadata independently and warns with the full contract address; a reported symbol matching a listed token at another address is a danger flag, not an identity check.
|
|
190
|
+
|
|
191
|
+
`get_balance` needs no pairing and can check any recipient: EVM native/ERC-20 through RPC; bitcoin via `mempool.space` on mainnet/testnet/signet (includes mempool deltas, which can be negative); chia via keyless `api.coinset.org` / `testnet11.api.coinset.org` coin records on mainnet/testnet11. Chia addresses are decoded with the package's bech32 implementation; only unspent coins at that puzzle hash are summed, not every address in a wallet, not mempool or guaranteed spendability. Coin uint64 values stay exact; older runtimes without JSON source context refuse unsafe numeric amounts rather than round. Providers may lag or be unavailable. After a chia approval without a hash, call `get_balance` on the recipient; compare against a prior balance where possible, since a balance alone cannot prove which payment arrived.
|
|
192
|
+
|
|
193
|
+
`get_tx_status` reports EVM receipt success/failure plus confirmations, or bitcoin confirmations. No receipt is `pending_or_unknown`, not proof of submission; bitcoin can be `pending`, `confirmed` or `unknown`. Reads never sign or send transactions. Chain response bodies are stream-limited to 1 MiB (1,048,576 bytes); Chia replies also allow at most 10,000 coin records. Oversized responses are canceled and refused explicitly, never truncated into a partial balance. Addresses with larger coin histories must be checked using a provider/wallet with appropriate limits; this tool does not paginate or return a partial sum. These bounds supplement provider limits and the request timeout, not a guarantee of provider availability. The hosted connector shares a 60-per-minute per-grant chain-read budget across both read tools and token preparation; local servers use the provider's limits. Full multi-chain transaction history remains out of scope: it would need indexed transfer/transaction providers (typically keyed EVM APIs), pagination/backfills and wallet/address indexing; no keyed API is introduced here.
|
|
194
|
+
|
|
195
|
+
Chain RPC URLs are keyless public defaults for Ethereum, Base, Arbitrum, Optimism, Polygon and their configured testnets. Override per chain with the `rpcUrls` option or `CLANKER_WALLET_RPC_URLS` (JSON object of chain id to HTTP(S) URL); the hosted connector uses `RPC_URLS` in the same format. Keep keyed URLs in private deployment configuration. RPC services see queried contracts; token metadata is untrusted.
|
|
196
|
+
|
|
197
|
+
Show the user the `review_url` returned by request tools (also present on pending `get_request` results). It opens `https://clanker-wallet.xyz/?request=<request_id>` and highlights the card after the wallet loads its waiting requests; unknown ids are silently ignored. The link contains only the request id, not pairing secrets. Local MCP servers can override the origin with `webOrigin` or `CLANKER_WALLET_WEB_ORIGIN`.
|
|
198
|
+
|
|
186
199
|
Requests are asynchronous: a request tool returns once the relay has the request, then the assistant calls `get_request` (with `wait_seconds` to wait for an answer) until the status is no longer `pending`. Invalid input or a missing pairing is a tool error, and nothing is sent. Pending requests live in the server process: after 1 h the server stops waiting and reports `expired`, and a restarted server no longer knows them (`get_request` then reports an unknown `request_id`). In both cases the human may still approve the request in their wallet, so ask them before sending it again. `unknown` means the request was delivered but the server lost its relay connection: the human may still approve it, so ask them before sending it again. A chia approval may have no `tx_hash`: the Sage wallet sends without reporting a transaction id, so check the recipient's balance.
|
|
187
200
|
|
|
188
201
|
---
|
package/SKILL.md
CHANGED
|
@@ -215,11 +215,24 @@ Claude Desktop (`claude_desktop_config.json`), Cursor (`~/.cursor/mcp.json`), an
|
|
|
215
215
|
|
|
216
216
|
| Tool | Arguments | Returns |
|
|
217
217
|
|------|-----------|---------|
|
|
218
|
-
| `request_evm_tx` | `chain_id`, `to`, `value` (wei, integer string), `data` (default `0x`), `reason`, optional `urgency` / `expected_outcome` / `from` (the paired wallet to send from, needed when several wallets of the same family are paired) | `{"request_id":"req_...","status":"pending","expires_at":"..."}` |
|
|
218
|
+
| `request_evm_tx` | `chain_id`, `to`, `value` (wei, integer string), `data` (default `0x`), `reason`, optional `urgency` / `expected_outcome` / `from` (the paired wallet to send from, needed when several wallets of the same family are paired) | `{"request_id":"req_...","review_url":"https://clanker-wallet.xyz/?request=req_...","status":"pending","expires_at":"..."}` |
|
|
219
219
|
| `request_transfer` | `family` (`bitcoin` or `chia`), `network` (bitcoin: `mainnet`, `testnet`, `signet`; chia: `mainnet`, `testnet11`), `to`, `amount` (sats, integer string) or mojos for chia, `reason`, optional `fee` (chia only: mojos, default `"0"`) / `urgency` / `expected_outcome` / `from` (the paired wallet to send from, needed when several wallets of the same family are paired) | as above |
|
|
220
|
+
| `request_token_transfer` | `chain_id`, `to`, `amount` (positive human decimal string), `reason`, optional `token` (listed symbol or contract address; omitted/native symbol = native coin), `from`, `urgency`, `expected_outcome` | `request_id`, `review_url`, pending status and expiry; existing EVM request, no signing here |
|
|
220
221
|
| `get_request` | `request_id`, optional `wait_seconds` (at most 50) | `status`: `pending`, `approved` (with `tx_hash`; a chia send may have none), `rejected`, `failed` (with `error`), `unknown` (with `error`) or `expired` |
|
|
222
|
+
| `get_balance` | `family`, `address` (any recipient), EVM `chain_id`, bitcoin/chia `network`, optional EVM `token` (listed symbol or address) | exact `raw_balance`, human `balance`, `asset`, `decimals`; bitcoin also confirmed/mempool deltas; chia recipient puzzle-hash unspent coins only |
|
|
223
|
+
| `get_tx_status` | `family` (evm/bitcoin), `tx_hash`, EVM `chain_id` or bitcoin `network` | receipt `status` and `confirmations`; missing EVM receipt is `pending_or_unknown`, missing bitcoin hash is `unknown` |
|
|
221
224
|
| `list_pairings` | none | `agent_id`, `fingerprint`, and the paired families (`evm`, `bitcoin`, `chia`) with network, wallet and mode |
|
|
222
225
|
|
|
226
|
+
`request_token_transfer` uses the browser-safe `clanker-wallet/tokens` list (chain id, symbol, address, decimals). Unlisted symbols are refused: pass the contract address; never search by symbol. Unlisted contracts must answer both `decimals()` and `symbol()` via RPC. Excess fraction digits are refused, never rounded. The review card reads unlisted token metadata independently and warns with the full contract address; a reported symbol matching a listed token at another address is a danger flag, not an identity check.
|
|
227
|
+
|
|
228
|
+
`get_balance` needs no pairing and can check any recipient: EVM native/ERC-20 through RPC; bitcoin via `mempool.space` on mainnet/testnet/signet (includes mempool deltas, which can be negative); chia via keyless `api.coinset.org` / `testnet11.api.coinset.org` coin records on mainnet/testnet11. Chia addresses are decoded with the package's bech32 implementation; only unspent coins at that puzzle hash are summed, not every address in a wallet, not mempool or guaranteed spendability. Coin uint64 values stay exact; older runtimes without JSON source context refuse unsafe numeric amounts rather than round. Providers may lag or be unavailable. After a chia approval without a hash, call `get_balance` on the recipient; compare against a prior balance where possible, since a balance alone cannot prove which payment arrived.
|
|
229
|
+
|
|
230
|
+
`get_tx_status` reports EVM receipt success/failure plus confirmations, or bitcoin confirmations. No receipt is `pending_or_unknown`, not proof of submission; bitcoin can be `pending`, `confirmed` or `unknown`. Reads never sign or send transactions. Chain response bodies are stream-limited to 1 MiB (1,048,576 bytes); Chia replies also allow at most 10,000 coin records. Oversized responses are canceled and refused explicitly, never truncated into a partial balance. Addresses with larger coin histories must be checked using a provider/wallet with appropriate limits; this tool does not paginate or return a partial sum. These bounds supplement provider limits and the request timeout, not a guarantee of provider availability. The hosted connector shares a 60-per-minute per-grant chain-read budget across both read tools and token preparation; local servers use the provider's limits. Full multi-chain transaction history remains out of scope: it would need indexed transfer/transaction providers (typically keyed EVM APIs), pagination/backfills and wallet/address indexing; no keyed API is introduced here.
|
|
231
|
+
|
|
232
|
+
Chain RPC URLs are keyless public defaults for Ethereum, Base, Arbitrum, Optimism, Polygon and their configured testnets. Override per chain with the `rpcUrls` option or `CLANKER_WALLET_RPC_URLS` (JSON object of chain id to HTTP(S) URL); the hosted connector uses `RPC_URLS` in the same format. Keep keyed URLs in private deployment configuration. RPC services see queried contracts; token metadata is untrusted.
|
|
233
|
+
|
|
234
|
+
Show the user the `review_url` returned by request tools (also present on pending `get_request` results). It opens `https://clanker-wallet.xyz/?request=<request_id>` and highlights the card after the wallet loads its waiting requests; unknown ids are silently ignored. The link contains only the request id, not pairing secrets. Local MCP servers can override the origin with `webOrigin` or `CLANKER_WALLET_WEB_ORIGIN`.
|
|
235
|
+
|
|
223
236
|
Flow: call `list_pairings` to see which chains you can use, call `request_evm_tx` or `request_transfer` (same rules as `tx` / `send`: wei, sats and mojos as integer strings, real addresses), tell the human a request is waiting (see "Transaction notifications"), then call `get_request` with `wait_seconds` until the status is no longer `pending`. Invalid input or a missing pairing comes back as a tool error and nothing is sent. After 1 h the server stops waiting and reports `expired`, and a restarted server no longer knows its earlier requests (`get_request` then reports an unknown `request_id`). In both cases the human may still approve the request in their wallet, so ask them before sending it again. `unknown` means the request was delivered but the server lost its relay connection: the human may still approve it, so ask them before sending it again. A chia approval may have no `tx_hash`: the Sage wallet sends without reporting a transaction id, so check the recipient's balance.
|
|
224
237
|
|
|
225
238
|
## Troubleshooting
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Keyless public defaults; operators can supply keyed URLs without exposing them in tool results. */
|
|
2
|
+
export declare const DEFAULT_RPC_URLS: Readonly<Record<number, string>>;
|
|
3
|
+
export interface ChainDataOptions {
|
|
4
|
+
rpcUrls?: Readonly<Record<number, string>>;
|
|
5
|
+
fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
6
|
+
}
|
|
7
|
+
export declare class ChainDataError extends Error {
|
|
8
|
+
}
|
|
9
|
+
/** Bound decoded chain-response buffering by bytes actually read, not an untrusted Content-Length. */
|
|
10
|
+
export declare function readChainResponse(response: Response): Promise<string>;
|
|
11
|
+
export declare function rpcUrlsFromJson(value: string | undefined): Record<number, string> | undefined;
|
|
12
|
+
export declare function evmRpc(chainId: number, method: string, params: unknown[], options?: ChainDataOptions): Promise<unknown>;
|
|
13
|
+
export interface TokenMetadata {
|
|
14
|
+
address: string;
|
|
15
|
+
symbol: string;
|
|
16
|
+
decimals: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function nativeSymbol(chainId: number): string;
|
|
19
|
+
/** Read both fields for unlisted contracts. Never search a symbol or guess its precision. */
|
|
20
|
+
export declare function resolveToken(chainId: number, token: unknown, options?: ChainDataOptions): Promise<TokenMetadata | null>;
|
|
21
|
+
export declare function abiUint(value: unknown): bigint;
|
|
22
|
+
export declare function decimalUnits(amount: unknown, decimals: number): string;
|
|
23
|
+
export declare function buildTokenTransfer(args: {
|
|
24
|
+
chain_id: unknown;
|
|
25
|
+
to: unknown;
|
|
26
|
+
token?: unknown;
|
|
27
|
+
amount: unknown;
|
|
28
|
+
}, options?: ChainDataOptions): Promise<{
|
|
29
|
+
chain_id: number;
|
|
30
|
+
transaction: import("./types.js").Transaction;
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { TOKENS } from './tokens.js';
|
|
2
|
+
import { validateTx } from './validate.js';
|
|
3
|
+
/** Keyless public defaults; operators can supply keyed URLs without exposing them in tool results. */
|
|
4
|
+
export const DEFAULT_RPC_URLS = {
|
|
5
|
+
1: 'https://ethereum-rpc.publicnode.com',
|
|
6
|
+
11155111: 'https://ethereum-sepolia-rpc.publicnode.com',
|
|
7
|
+
8453: 'https://mainnet.base.org',
|
|
8
|
+
84532: 'https://sepolia.base.org',
|
|
9
|
+
42161: 'https://arb1.arbitrum.io/rpc',
|
|
10
|
+
421614: 'https://sepolia-rollup.arbitrum.io/rpc',
|
|
11
|
+
10: 'https://mainnet.optimism.io',
|
|
12
|
+
11155420: 'https://sepolia.optimism.io',
|
|
13
|
+
137: 'https://polygon-bor-rpc.publicnode.com',
|
|
14
|
+
80002: 'https://polygon-amoy-bor-rpc.publicnode.com',
|
|
15
|
+
};
|
|
16
|
+
export class ChainDataError extends Error {
|
|
17
|
+
}
|
|
18
|
+
const MAX_CHAIN_RESPONSE_BYTES = 1024 * 1024;
|
|
19
|
+
/** Bound decoded chain-response buffering by bytes actually read, not an untrusted Content-Length. */
|
|
20
|
+
export async function readChainResponse(response) {
|
|
21
|
+
const reader = response.body?.getReader();
|
|
22
|
+
if (!reader)
|
|
23
|
+
return '';
|
|
24
|
+
const decoder = new TextDecoder();
|
|
25
|
+
let bytes = 0;
|
|
26
|
+
let text = '';
|
|
27
|
+
try {
|
|
28
|
+
while (true) {
|
|
29
|
+
const chunk = await reader.read();
|
|
30
|
+
if (chunk.done)
|
|
31
|
+
return text + decoder.decode();
|
|
32
|
+
bytes += chunk.value.byteLength;
|
|
33
|
+
if (bytes > MAX_CHAIN_RESPONSE_BYTES)
|
|
34
|
+
throw new ChainDataError('chain response too large: limit 1048576 bytes; no partial result returned');
|
|
35
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
try {
|
|
40
|
+
await reader.cancel();
|
|
41
|
+
}
|
|
42
|
+
catch { /* an already-errored body may refuse cancellation; retain the original sanitized failure */ }
|
|
43
|
+
if (err instanceof ChainDataError)
|
|
44
|
+
throw err;
|
|
45
|
+
throw new ChainDataError('chain response body read failed');
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
reader.releaseLock();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function rpcUrlsFromJson(value) {
|
|
52
|
+
if (!value)
|
|
53
|
+
return undefined;
|
|
54
|
+
try {
|
|
55
|
+
const urls = JSON.parse(value);
|
|
56
|
+
if (!urls || typeof urls !== 'object' || Array.isArray(urls))
|
|
57
|
+
throw new Error();
|
|
58
|
+
for (const [chain, url] of Object.entries(urls)) {
|
|
59
|
+
if (!/^[1-9][0-9]*$/.test(chain) || typeof url !== 'string' || !/^https?:$/.test(new URL(url).protocol))
|
|
60
|
+
throw new Error();
|
|
61
|
+
}
|
|
62
|
+
return urls;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
throw new ChainDataError('invalid RPC URL map: expected JSON mapping chain ids to http(s) URLs');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export async function evmRpc(chainId, method, params, options = {}) {
|
|
69
|
+
const url = options.rpcUrls?.[chainId] ?? DEFAULT_RPC_URLS[chainId];
|
|
70
|
+
if (!url)
|
|
71
|
+
throw new ChainDataError(`no RPC configured for chain ${chainId}`);
|
|
72
|
+
try {
|
|
73
|
+
if (!/^https?:$/.test(new URL(url).protocol))
|
|
74
|
+
throw new Error();
|
|
75
|
+
const response = await (options.fetch ?? globalThis.fetch)(url, {
|
|
76
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
77
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), signal: AbortSignal.timeout(10_000),
|
|
78
|
+
});
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
await response.body?.cancel();
|
|
81
|
+
throw new Error();
|
|
82
|
+
}
|
|
83
|
+
const body = JSON.parse(await readChainResponse(response));
|
|
84
|
+
if (body.error || !Object.hasOwn(body, 'result'))
|
|
85
|
+
throw new Error();
|
|
86
|
+
return body.result;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
if (err instanceof ChainDataError)
|
|
90
|
+
throw err;
|
|
91
|
+
throw new ChainDataError(`chain ${chainId} RPC read failed (${method})`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function nativeSymbol(chainId) {
|
|
95
|
+
return chainId === 137 || chainId === 80002 ? 'POL' : 'ETH';
|
|
96
|
+
}
|
|
97
|
+
/** Read both fields for unlisted contracts. Never search a symbol or guess its precision. */
|
|
98
|
+
export async function resolveToken(chainId, token, options = {}) {
|
|
99
|
+
validateTx(chainId, { to: '0x' + '0'.repeat(40), value: '0' });
|
|
100
|
+
if (token === undefined || (typeof token === 'string' && token.toUpperCase() === nativeSymbol(chainId)))
|
|
101
|
+
return null;
|
|
102
|
+
if (typeof token !== 'string' || !token)
|
|
103
|
+
throw new ChainDataError('invalid token: use a listed symbol or pass the contract address');
|
|
104
|
+
const listed = TOKENS.find(t => t.chain_id === chainId && (t.address === token.toLowerCase() || t.symbol.toUpperCase() === token.toUpperCase()));
|
|
105
|
+
if (listed)
|
|
106
|
+
return listed;
|
|
107
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(token))
|
|
108
|
+
throw new ChainDataError('unlisted symbol: pass the contract address; never search by symbol');
|
|
109
|
+
const { transaction } = validateTx(chainId, { to: token, value: '0' });
|
|
110
|
+
try {
|
|
111
|
+
const call = (data) => evmRpc(chainId, 'eth_call', [{ to: transaction.to, data }, 'latest'], options);
|
|
112
|
+
const [rawDecimals, rawSymbol] = await Promise.all([call('0x313ce567'), call('0x95d89b41')]);
|
|
113
|
+
const decimals = Number(abiUint(rawDecimals));
|
|
114
|
+
if (decimals > 255)
|
|
115
|
+
throw new Error();
|
|
116
|
+
const symbol = abiSymbol(rawSymbol);
|
|
117
|
+
return { address: transaction.to, decimals, symbol };
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const detail = err instanceof ChainDataError ? ` (${err.message})` : '';
|
|
121
|
+
throw new ChainDataError(`token metadata unavailable${detail}: decimals() and symbol() must both answer; nothing was sent`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function abiUint(value) {
|
|
125
|
+
if (typeof value !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(value))
|
|
126
|
+
throw new ChainDataError('invalid uint256 RPC result');
|
|
127
|
+
return BigInt(value);
|
|
128
|
+
}
|
|
129
|
+
function abiSymbol(value) {
|
|
130
|
+
if (typeof value !== 'string' || !/^0x(?:[0-9a-fA-F]{2})+$/.test(value) || value.length > 1026)
|
|
131
|
+
throw new Error();
|
|
132
|
+
const hex = value.slice(2);
|
|
133
|
+
let bytes;
|
|
134
|
+
if (hex.length === 64)
|
|
135
|
+
bytes = hex.replace(/(?:00)+$/, ''); // legacy bytes32 symbol
|
|
136
|
+
else {
|
|
137
|
+
if (BigInt('0x' + hex.slice(0, 64)) !== 32n)
|
|
138
|
+
throw new Error();
|
|
139
|
+
const size = Number(BigInt('0x' + hex.slice(64, 128)));
|
|
140
|
+
if (size < 1 || size > 128 || hex.length < 128 + Math.ceil(size / 32) * 64)
|
|
141
|
+
throw new Error();
|
|
142
|
+
bytes = hex.slice(128, 128 + size * 2);
|
|
143
|
+
}
|
|
144
|
+
const symbol = new TextDecoder('utf-8', { fatal: true }).decode(Uint8Array.from(bytes.match(/../g) ?? [], b => parseInt(b, 16)));
|
|
145
|
+
if (!symbol.trim() || /[\u0000-\u001f\u007f-\u009f]/.test(symbol))
|
|
146
|
+
throw new Error();
|
|
147
|
+
return symbol;
|
|
148
|
+
}
|
|
149
|
+
function decimalParts(amount) {
|
|
150
|
+
if (typeof amount !== 'string' || amount.length > 400 || !/^(0|[1-9][0-9]*)(\.[0-9]+)?$/.test(amount))
|
|
151
|
+
throw new ChainDataError('invalid amount: use a positive human decimal string, not exponent notation');
|
|
152
|
+
const [whole, fraction = ''] = amount.split('.');
|
|
153
|
+
if (BigInt(whole + fraction) === 0n)
|
|
154
|
+
throw new ChainDataError('amount must be positive');
|
|
155
|
+
return [whole, fraction];
|
|
156
|
+
}
|
|
157
|
+
export function decimalUnits(amount, decimals) {
|
|
158
|
+
const [whole, fraction] = decimalParts(amount);
|
|
159
|
+
if (fraction.length > decimals)
|
|
160
|
+
throw new ChainDataError(`amount has more fraction digits than ${decimals}; refusing to round`);
|
|
161
|
+
const raw = BigInt(whole + fraction.padEnd(decimals, '0'));
|
|
162
|
+
if (raw <= 0n || raw >= 2n ** 256n)
|
|
163
|
+
throw new ChainDataError('amount must be positive and fit uint256');
|
|
164
|
+
return raw.toString();
|
|
165
|
+
}
|
|
166
|
+
export async function buildTokenTransfer(args, options = {}) {
|
|
167
|
+
const { chain_id, transaction } = validateTx(args.chain_id, { to: args.to, value: '0' });
|
|
168
|
+
decimalParts(args.amount); // refuse invalid/zero amounts before metadata I/O
|
|
169
|
+
const token = await resolveToken(chain_id, args.token, options);
|
|
170
|
+
const amount = decimalUnits(args.amount, token?.decimals ?? 18);
|
|
171
|
+
return validateTx(chain_id, token ? {
|
|
172
|
+
to: token.address, value: '0', data: '0xa9059cbb' + transaction.to.slice(2).toLowerCase().padStart(64, '0') + BigInt(amount).toString(16).padStart(64, '0'),
|
|
173
|
+
} : { to: transaction.to, value: amount, data: '0x' });
|
|
174
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export { getBalance, getTxStatus } from './reads.js';
|
|
2
|
+
export { buildTokenTransfer, resolveToken, rpcUrlsFromJson, evmRpc, ChainDataError, DEFAULT_RPC_URLS } from './chain-data.js';
|
|
3
|
+
export type { ChainDataOptions, TokenMetadata } from './chain-data.js';
|
|
1
4
|
export { ClankerWallet, RelayError } from './client.js';
|
|
2
5
|
export type { ClankerWalletOptions } from './client.js';
|
|
3
6
|
export { base64ToBytes, bytesToBase64, decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex, verifySignature } from './crypto.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { getBalance, getTxStatus } from './reads.js';
|
|
2
|
+
export { buildTokenTransfer, resolveToken, rpcUrlsFromJson, evmRpc, ChainDataError, DEFAULT_RPC_URLS } from './chain-data.js';
|
|
1
3
|
export { ClankerWallet, RelayError } from './client.js';
|
|
2
4
|
export { base64ToBytes, bytesToBase64, decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex, verifySignature } from './crypto.js';
|
|
3
5
|
export { TypedEmitter } from './events.js';
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ChainDataOptions } from './chain-data.js';
|
|
1
2
|
import { type ClankerWallet } from './client.js';
|
|
2
3
|
/**
|
|
3
4
|
* MCP server for `clanker-wallet mcp`: newline-delimited JSON-RPC 2.0 over stdio, hand-written so
|
|
@@ -19,9 +20,11 @@ export interface SavedPairing {
|
|
|
19
20
|
}
|
|
20
21
|
/** the families the tools request on */
|
|
21
22
|
type RequestFamily = 'evm' | 'bitcoin' | 'chia';
|
|
22
|
-
export interface McpServerOptions {
|
|
23
|
+
export interface McpServerOptions extends ChainDataOptions {
|
|
23
24
|
/** the package version, reported in initialize */
|
|
24
25
|
version: string;
|
|
26
|
+
/** review app origin (or CLANKER_WALLET_WEB_ORIGIN; default https://clanker-wallet.xyz) */
|
|
27
|
+
webOrigin?: string;
|
|
25
28
|
identity: {
|
|
26
29
|
agent_id: string;
|
|
27
30
|
fingerprint: string;
|
|
@@ -76,6 +79,11 @@ export declare const MCP_TOOLS: ({
|
|
|
76
79
|
};
|
|
77
80
|
request_id?: undefined;
|
|
78
81
|
wait_seconds?: undefined;
|
|
82
|
+
family?: undefined;
|
|
83
|
+
network?: undefined;
|
|
84
|
+
address?: undefined;
|
|
85
|
+
token?: undefined;
|
|
86
|
+
tx_hash?: undefined;
|
|
79
87
|
};
|
|
80
88
|
required: string[];
|
|
81
89
|
};
|
|
@@ -132,6 +140,64 @@ export declare const MCP_TOOLS: ({
|
|
|
132
140
|
};
|
|
133
141
|
request_id?: undefined;
|
|
134
142
|
wait_seconds?: undefined;
|
|
143
|
+
chain_id?: undefined;
|
|
144
|
+
address?: undefined;
|
|
145
|
+
token?: undefined;
|
|
146
|
+
tx_hash?: undefined;
|
|
147
|
+
};
|
|
148
|
+
required: string[];
|
|
149
|
+
};
|
|
150
|
+
annotations: {
|
|
151
|
+
readOnlyHint: boolean;
|
|
152
|
+
destructiveHint: boolean;
|
|
153
|
+
idempotentHint: boolean;
|
|
154
|
+
openWorldHint: boolean;
|
|
155
|
+
};
|
|
156
|
+
} | {
|
|
157
|
+
name: string;
|
|
158
|
+
description: string;
|
|
159
|
+
inputSchema: {
|
|
160
|
+
type: string;
|
|
161
|
+
properties: {
|
|
162
|
+
from: {
|
|
163
|
+
type: string;
|
|
164
|
+
description: string;
|
|
165
|
+
};
|
|
166
|
+
reason: {
|
|
167
|
+
type: string;
|
|
168
|
+
description: string;
|
|
169
|
+
};
|
|
170
|
+
urgency: {
|
|
171
|
+
type: string;
|
|
172
|
+
enum: string[];
|
|
173
|
+
description: string;
|
|
174
|
+
};
|
|
175
|
+
expected_outcome: {
|
|
176
|
+
type: string;
|
|
177
|
+
description: string;
|
|
178
|
+
};
|
|
179
|
+
chain_id: {
|
|
180
|
+
type: string;
|
|
181
|
+
description: string;
|
|
182
|
+
};
|
|
183
|
+
to: {
|
|
184
|
+
type: string;
|
|
185
|
+
description: string;
|
|
186
|
+
};
|
|
187
|
+
token: {
|
|
188
|
+
type: string;
|
|
189
|
+
description: string;
|
|
190
|
+
};
|
|
191
|
+
amount: {
|
|
192
|
+
type: string;
|
|
193
|
+
description: string;
|
|
194
|
+
};
|
|
195
|
+
request_id?: undefined;
|
|
196
|
+
wait_seconds?: undefined;
|
|
197
|
+
family?: undefined;
|
|
198
|
+
network?: undefined;
|
|
199
|
+
address?: undefined;
|
|
200
|
+
tx_hash?: undefined;
|
|
135
201
|
};
|
|
136
202
|
required: string[];
|
|
137
203
|
};
|
|
@@ -157,12 +223,91 @@ export declare const MCP_TOOLS: ({
|
|
|
157
223
|
maximum: number;
|
|
158
224
|
description: string;
|
|
159
225
|
};
|
|
226
|
+
family?: undefined;
|
|
227
|
+
chain_id?: undefined;
|
|
228
|
+
network?: undefined;
|
|
229
|
+
address?: undefined;
|
|
230
|
+
token?: undefined;
|
|
231
|
+
tx_hash?: undefined;
|
|
232
|
+
};
|
|
233
|
+
required: string[];
|
|
234
|
+
};
|
|
235
|
+
annotations: {
|
|
236
|
+
readOnlyHint: boolean;
|
|
237
|
+
openWorldHint: boolean;
|
|
238
|
+
};
|
|
239
|
+
} | {
|
|
240
|
+
name: string;
|
|
241
|
+
description: string;
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: string;
|
|
244
|
+
properties: {
|
|
245
|
+
family: {
|
|
246
|
+
type: string;
|
|
247
|
+
enum: string[];
|
|
248
|
+
description: string;
|
|
249
|
+
};
|
|
250
|
+
chain_id: {
|
|
251
|
+
type: string;
|
|
252
|
+
description: string;
|
|
253
|
+
};
|
|
254
|
+
network: {
|
|
255
|
+
type: string;
|
|
256
|
+
enum: string[];
|
|
257
|
+
description: string;
|
|
258
|
+
};
|
|
259
|
+
address: {
|
|
260
|
+
type: string;
|
|
261
|
+
description: string;
|
|
262
|
+
};
|
|
263
|
+
token: {
|
|
264
|
+
type: string;
|
|
265
|
+
description: string;
|
|
266
|
+
};
|
|
267
|
+
request_id?: undefined;
|
|
268
|
+
wait_seconds?: undefined;
|
|
269
|
+
tx_hash?: undefined;
|
|
160
270
|
};
|
|
161
271
|
required: string[];
|
|
162
272
|
};
|
|
163
273
|
annotations: {
|
|
274
|
+
openWorldHint: boolean;
|
|
164
275
|
readOnlyHint: boolean;
|
|
276
|
+
};
|
|
277
|
+
} | {
|
|
278
|
+
name: string;
|
|
279
|
+
description: string;
|
|
280
|
+
inputSchema: {
|
|
281
|
+
type: string;
|
|
282
|
+
properties: {
|
|
283
|
+
family: {
|
|
284
|
+
type: string;
|
|
285
|
+
enum: string[];
|
|
286
|
+
description: string;
|
|
287
|
+
};
|
|
288
|
+
chain_id: {
|
|
289
|
+
type: string;
|
|
290
|
+
description: string;
|
|
291
|
+
};
|
|
292
|
+
network: {
|
|
293
|
+
type: string;
|
|
294
|
+
enum: string[];
|
|
295
|
+
description: string;
|
|
296
|
+
};
|
|
297
|
+
tx_hash: {
|
|
298
|
+
type: string;
|
|
299
|
+
description: string;
|
|
300
|
+
};
|
|
301
|
+
request_id?: undefined;
|
|
302
|
+
wait_seconds?: undefined;
|
|
303
|
+
address?: undefined;
|
|
304
|
+
token?: undefined;
|
|
305
|
+
};
|
|
306
|
+
required: string[];
|
|
307
|
+
};
|
|
308
|
+
annotations: {
|
|
165
309
|
openWorldHint: boolean;
|
|
310
|
+
readOnlyHint: boolean;
|
|
166
311
|
};
|
|
167
312
|
} | {
|
|
168
313
|
name: string;
|
|
@@ -172,6 +317,12 @@ export declare const MCP_TOOLS: ({
|
|
|
172
317
|
properties: {
|
|
173
318
|
request_id?: undefined;
|
|
174
319
|
wait_seconds?: undefined;
|
|
320
|
+
family?: undefined;
|
|
321
|
+
chain_id?: undefined;
|
|
322
|
+
network?: undefined;
|
|
323
|
+
address?: undefined;
|
|
324
|
+
token?: undefined;
|
|
325
|
+
tx_hash?: undefined;
|
|
175
326
|
};
|
|
176
327
|
required?: undefined;
|
|
177
328
|
};
|
package/dist/mcp.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getBalance, getTxStatus } from './reads.js';
|
|
2
|
+
import { buildTokenTransfer, ChainDataError, rpcUrlsFromJson } from './chain-data.js';
|
|
1
3
|
import { RelayError } from './client.js';
|
|
2
4
|
import { InvalidTxError, validateTransfer, validateTx } from './validate.js';
|
|
3
5
|
/**
|
|
@@ -36,7 +38,7 @@ export const MCP_TOOLS = [
|
|
|
36
38
|
name: 'request_evm_tx',
|
|
37
39
|
description: 'Ask the human to send an EVM transaction (ETH or a contract call) from their own wallet. ' +
|
|
38
40
|
'Nothing is signed here: the human approves every request on their own device, or rejects it. ' +
|
|
39
|
-
'Returns { request_id } once
|
|
41
|
+
'Returns { request_id, review_url } once delivered; show review_url to the user, then call get_request for the outcome. ' +
|
|
40
42
|
'value is an integer string in wei (1 ETH = "1000000000000000000"), no decimals.',
|
|
41
43
|
inputSchema: {
|
|
42
44
|
type: 'object',
|
|
@@ -56,7 +58,7 @@ export const MCP_TOOLS = [
|
|
|
56
58
|
name: 'request_transfer',
|
|
57
59
|
description: 'Ask the human to send bitcoin or chia (XCH) from their own wallet, which builds, signs and broadcasts it. ' +
|
|
58
60
|
'Nothing is signed here: the human approves every request on their own device, or rejects it. ' +
|
|
59
|
-
'Returns { request_id } once
|
|
61
|
+
'Returns { request_id, review_url } once delivered; show review_url to the user, then call get_request for the outcome. ' +
|
|
60
62
|
'amount is an integer string, no decimals: sats for bitcoin (1 BTC = "100000000"), where the wallet sets the fee; ' +
|
|
61
63
|
'mojos for chia (1 XCH = "1000000000000"), with an optional fee in mojos (default "0").',
|
|
62
64
|
inputSchema: {
|
|
@@ -78,6 +80,26 @@ export const MCP_TOOLS = [
|
|
|
78
80
|
},
|
|
79
81
|
annotations: REQUEST_HINTS,
|
|
80
82
|
},
|
|
83
|
+
{
|
|
84
|
+
name: 'request_token_transfer',
|
|
85
|
+
description: 'Ask the human to send an EVM token or native coin from their own wallet. ' +
|
|
86
|
+
'The human approves every request on their own device. amount is a human decimal string; never rounded. ' +
|
|
87
|
+
'Use a listed symbol for that chain or pass the contract address; unlisted symbols are refused, never searched. ' +
|
|
88
|
+
'Omit token (or use the native symbol) for native coin. Returns { request_id, review_url }; show review_url to the user, then call get_request.',
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
chain_id: { type: 'integer', description: 'EVM chain id' },
|
|
93
|
+
to: { type: 'string', description: 'recipient EVM address' },
|
|
94
|
+
token: { type: 'string', description: 'listed symbol on this chain or token contract address; omitted or native symbol sends native coin' },
|
|
95
|
+
amount: { type: 'string', description: 'positive human decimal string, e.g. 1.25; excess fraction digits are refused' },
|
|
96
|
+
...CONTEXT_PROPERTIES,
|
|
97
|
+
...FROM_PROPERTY,
|
|
98
|
+
},
|
|
99
|
+
required: ['chain_id', 'to', 'amount', 'reason'],
|
|
100
|
+
},
|
|
101
|
+
annotations: REQUEST_HINTS,
|
|
102
|
+
},
|
|
81
103
|
{
|
|
82
104
|
name: 'get_request',
|
|
83
105
|
description: 'Get the outcome of a request: pending, approved (with tx_hash; a chia send may have none, so check the recipient\'s balance), rejected, failed (with error), unknown (with error) or expired. ' +
|
|
@@ -88,13 +110,44 @@ export const MCP_TOOLS = [
|
|
|
88
110
|
inputSchema: {
|
|
89
111
|
type: 'object',
|
|
90
112
|
properties: {
|
|
91
|
-
request_id: { type: 'string', description: 'from request_evm_tx or
|
|
113
|
+
request_id: { type: 'string', description: 'from request_evm_tx, request_transfer or request_token_transfer' },
|
|
92
114
|
wait_seconds: { type: 'number', minimum: 0, maximum: MAX_WAIT_SECONDS, description: 'wait up to this long for an answer (default 0)' },
|
|
93
115
|
},
|
|
94
116
|
required: ['request_id'],
|
|
95
117
|
},
|
|
96
118
|
annotations: READ_HINTS,
|
|
97
119
|
},
|
|
120
|
+
{
|
|
121
|
+
name: 'get_balance',
|
|
122
|
+
description: 'Read the balance of any address, including a recipient not paired here. EVM native or ERC-20 (listed symbol or contract address), bitcoin confirmed/mempool, or chia unspent coins at the recipient puzzle hash (not the whole wallet; excludes mempool). After a chia send approved without a hash, check the recipient balance; a balance alone does not prove which payment arrived.',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
family: { type: 'string', enum: ['evm', 'bitcoin', 'chia'], description: 'chain family' },
|
|
127
|
+
chain_id: { type: 'integer', description: 'required for EVM' },
|
|
128
|
+
network: { type: 'string', enum: ['mainnet', 'testnet', 'signet', 'testnet11'], description: 'required for bitcoin or chia; EVM uses chain_id' },
|
|
129
|
+
address: { type: 'string', description: 'any address on that network, including a recipient' },
|
|
130
|
+
token: { type: 'string', description: 'EVM only: listed symbol or contract address; omitted/native symbol = native coin' },
|
|
131
|
+
},
|
|
132
|
+
required: ['family', 'address'],
|
|
133
|
+
},
|
|
134
|
+
annotations: { ...READ_HINTS, openWorldHint: true },
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'get_tx_status',
|
|
138
|
+
description: 'Read an EVM transaction receipt status and confirmations, or bitcoin confirmations. An EVM missing receipt means pending_or_unknown, not proof of submission. After a chia approval without a hash, use get_balance on the recipient instead.',
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: {
|
|
142
|
+
family: { type: 'string', enum: ['evm', 'bitcoin'], description: 'chain family' },
|
|
143
|
+
chain_id: { type: 'integer', description: 'required for EVM' },
|
|
144
|
+
network: { type: 'string', enum: ['mainnet', 'testnet', 'signet'], description: 'required for bitcoin' },
|
|
145
|
+
tx_hash: { type: 'string', description: 'EVM 0x + 64 hex digits; bitcoin 64 hex digits' },
|
|
146
|
+
},
|
|
147
|
+
required: ['family', 'tx_hash'],
|
|
148
|
+
},
|
|
149
|
+
annotations: { ...READ_HINTS, openWorldHint: true },
|
|
150
|
+
},
|
|
98
151
|
{
|
|
99
152
|
name: 'list_pairings',
|
|
100
153
|
description: 'List the wallets you can request on (chain family, network, wallet address, mode) and your agent fingerprint, ' +
|
|
@@ -104,7 +157,7 @@ export const MCP_TOOLS = [
|
|
|
104
157
|
},
|
|
105
158
|
];
|
|
106
159
|
const INSTRUCTIONS = 'clanker-wallet sends transaction requests to a human, who approves or rejects each one in their own wallet; ' +
|
|
107
|
-
'you never hold keys. Call list_pairings to see which chains you can use, request_evm_tx or
|
|
160
|
+
'you never hold keys. Call list_pairings to see which chains you can use, request_evm_tx, request_transfer or request_token_transfer to ask, ' +
|
|
108
161
|
'and get_request (with wait_seconds) until the request is no longer pending.';
|
|
109
162
|
/** what get_request reports, in the order the docs list them */
|
|
110
163
|
export const REQUEST_STATUSES = ['pending', 'approved', 'rejected', 'failed', 'unknown', 'expired'];
|
|
@@ -169,8 +222,11 @@ const NOT_PAIRED = {
|
|
|
169
222
|
chia: 'not paired with a chia wallet: ask the human to open the XCH tab at https://clanker-wallet.xyz, copy its pairing JSON, and run: clanker-wallet pair \'<JSON>\'',
|
|
170
223
|
};
|
|
171
224
|
export function createMcpServer(options) {
|
|
225
|
+
const webOrigin = new URL(options.webOrigin ?? process.env.CLANKER_WALLET_WEB_ORIGIN ?? 'https://clanker-wallet.xyz').origin;
|
|
226
|
+
const reviewUrl = (id) => `${webOrigin}/?request=${encodeURIComponent(id)}`;
|
|
172
227
|
const ttl = options.requestTtlMs ?? DEFAULT_REQUEST_TTL_MS;
|
|
173
228
|
const joinTimeout = options.joinTimeoutMs ?? DEFAULT_JOIN_TIMEOUT_MS;
|
|
229
|
+
const chainOptions = { ...options, rpcUrls: options.rpcUrls ?? rpcUrlsFromJson(process.env.CLANKER_WALLET_RPC_URLS) };
|
|
174
230
|
const records = new Map();
|
|
175
231
|
/** tools/call requests still being answered, by JSON-RPC id, for notifications/cancelled */
|
|
176
232
|
const inFlight = new Map();
|
|
@@ -180,6 +236,8 @@ export function createMcpServer(options) {
|
|
|
180
236
|
* join timeout, cancelled)
|
|
181
237
|
*/
|
|
182
238
|
function submit(family, from, cancelled, send) {
|
|
239
|
+
if (cancelled.aborted)
|
|
240
|
+
return Promise.resolve(toolError('request cancelled before sending; nothing was sent'));
|
|
183
241
|
const pairing = options.loadPairing(family);
|
|
184
242
|
if (!pairing)
|
|
185
243
|
return Promise.resolve(toolError(NOT_PAIRED[family]));
|
|
@@ -203,7 +261,7 @@ export function createMcpServer(options) {
|
|
|
203
261
|
record = newRecord(request_id, expiresAt);
|
|
204
262
|
records.set(request_id, record);
|
|
205
263
|
record.settled.then(() => clearTimeout(expiry));
|
|
206
|
-
resolve(toolResult({ request_id, status: 'pending', expires_at: new Date(expiresAt).toISOString() }));
|
|
264
|
+
resolve(toolResult({ request_id, review_url: reviewUrl(request_id), status: 'pending', expires_at: new Date(expiresAt).toISOString() }));
|
|
207
265
|
});
|
|
208
266
|
wallet.on('response', (response) => { answered = response; });
|
|
209
267
|
send(wallet, { timeout_ms: ttl, join_timeout_ms: joinTimeout, signal: unsent.signal }).then((txHash) => record.settle('approved', { tx_hash: txHash }), (err) => {
|
|
@@ -232,6 +290,12 @@ export function createMcpServer(options) {
|
|
|
232
290
|
const { chain_id, transaction } = validateTx(args.chain_id, { to: args.to, value: args.value, data: args.data ?? '0x' });
|
|
233
291
|
return submit('evm', from, cancelled, (wallet, limits) => wallet.requestTx({ chain_id, transaction, context, ...limits }));
|
|
234
292
|
}
|
|
293
|
+
async function requestTokenTransfer(args, cancelled) {
|
|
294
|
+
const context = contextFrom(args);
|
|
295
|
+
const from = fromArg(args);
|
|
296
|
+
const { chain_id, transaction } = await buildTokenTransfer({ chain_id: args.chain_id, to: args.to, token: args.token, amount: args.amount }, chainOptions);
|
|
297
|
+
return submit('evm', from, cancelled, (wallet, limits) => wallet.requestTx({ chain_id, transaction, context, ...limits }));
|
|
298
|
+
}
|
|
235
299
|
async function requestTransfer(args, cancelled) {
|
|
236
300
|
const family = args.family;
|
|
237
301
|
if (family !== 'bitcoin' && family !== 'chia')
|
|
@@ -289,7 +353,7 @@ export function createMcpServer(options) {
|
|
|
289
353
|
record.settle('expired');
|
|
290
354
|
const { status, tx_hash, error } = record;
|
|
291
355
|
if (status === 'pending')
|
|
292
|
-
return toolResult({ request_id, status, expires_at: new Date(record.expiresAt).toISOString() });
|
|
356
|
+
return toolResult({ request_id, review_url: reviewUrl(request_id), status, expires_at: new Date(record.expiresAt).toISOString() });
|
|
293
357
|
return toolResult({ request_id, status, ...(tx_hash !== undefined && { tx_hash }), ...(error !== undefined && { error }) });
|
|
294
358
|
}
|
|
295
359
|
function listPairings() {
|
|
@@ -302,7 +366,10 @@ export function createMcpServer(options) {
|
|
|
302
366
|
const TOOL_HANDLERS = {
|
|
303
367
|
request_evm_tx: requestEvmTx,
|
|
304
368
|
request_transfer: requestTransfer,
|
|
369
|
+
request_token_transfer: requestTokenTransfer,
|
|
305
370
|
get_request: getRequest,
|
|
371
|
+
get_balance: async (args) => toolResult(await getBalance(args, chainOptions)),
|
|
372
|
+
get_tx_status: async (args) => toolResult(await getTxStatus(args, chainOptions)),
|
|
306
373
|
list_pairings: listPairings,
|
|
307
374
|
};
|
|
308
375
|
async function callTool(params, cancelled) {
|
|
@@ -318,7 +385,7 @@ export function createMcpServer(options) {
|
|
|
318
385
|
return await handler(args, cancelled);
|
|
319
386
|
}
|
|
320
387
|
catch (err) {
|
|
321
|
-
if (err instanceof ToolInputError || err instanceof InvalidTxError)
|
|
388
|
+
if (err instanceof ToolInputError || err instanceof InvalidTxError || err instanceof ChainDataError)
|
|
322
389
|
return toolError(err.message);
|
|
323
390
|
throw err;
|
|
324
391
|
}
|
package/dist/reads.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type ChainDataOptions } from './chain-data.js';
|
|
2
|
+
export declare function getBalance(args: Record<string, unknown>, options?: ChainDataOptions): Promise<{
|
|
3
|
+
decimals: number;
|
|
4
|
+
raw_balance: string;
|
|
5
|
+
balance: string;
|
|
6
|
+
token_address?: string | undefined;
|
|
7
|
+
family: string;
|
|
8
|
+
chain_id: number;
|
|
9
|
+
address: string;
|
|
10
|
+
asset: string;
|
|
11
|
+
network?: undefined;
|
|
12
|
+
confirmed_balance?: undefined;
|
|
13
|
+
unconfirmed_balance?: undefined;
|
|
14
|
+
scope?: undefined;
|
|
15
|
+
} | {
|
|
16
|
+
family: string;
|
|
17
|
+
network: string;
|
|
18
|
+
address: string;
|
|
19
|
+
asset: string;
|
|
20
|
+
decimals: number;
|
|
21
|
+
raw_balance: string;
|
|
22
|
+
balance: string;
|
|
23
|
+
confirmed_balance: string;
|
|
24
|
+
unconfirmed_balance: string;
|
|
25
|
+
scope?: undefined;
|
|
26
|
+
} | {
|
|
27
|
+
family: string;
|
|
28
|
+
network: string;
|
|
29
|
+
address: unknown;
|
|
30
|
+
asset: string;
|
|
31
|
+
decimals: number;
|
|
32
|
+
raw_balance: string;
|
|
33
|
+
balance: string;
|
|
34
|
+
scope: string;
|
|
35
|
+
confirmed_balance?: undefined;
|
|
36
|
+
unconfirmed_balance?: undefined;
|
|
37
|
+
}>;
|
|
38
|
+
export declare function getTxStatus(args: Record<string, unknown>, options?: ChainDataOptions): Promise<{
|
|
39
|
+
family: string;
|
|
40
|
+
chain_id: number;
|
|
41
|
+
tx_hash: string;
|
|
42
|
+
status: string;
|
|
43
|
+
confirmations: number;
|
|
44
|
+
} | {
|
|
45
|
+
status: string;
|
|
46
|
+
confirmations: number;
|
|
47
|
+
family: string;
|
|
48
|
+
network: string;
|
|
49
|
+
tx_hash: string;
|
|
50
|
+
chain_id?: undefined;
|
|
51
|
+
}>;
|
package/dist/reads.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { decodeChiaAddress, decodeSegwitAddress } from './bech32.js';
|
|
2
|
+
import { abiUint, ChainDataError, evmRpc, nativeSymbol, readChainResponse, resolveToken } from './chain-data.js';
|
|
3
|
+
import { validateTx } from './validate.js';
|
|
4
|
+
const BITCOIN_API = {
|
|
5
|
+
mainnet: 'https://mempool.space/api', testnet: 'https://mempool.space/testnet/api', signet: 'https://mempool.space/signet/api',
|
|
6
|
+
};
|
|
7
|
+
const MAX_CHIA_COIN_RECORDS = 10_000;
|
|
8
|
+
const CHIA_API = { mainnet: 'https://api.coinset.org', testnet11: 'https://testnet11.api.coinset.org' };
|
|
9
|
+
const object = (value) => {
|
|
10
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
11
|
+
throw new ChainDataError('malformed chain response');
|
|
12
|
+
return value;
|
|
13
|
+
};
|
|
14
|
+
const nonnegativeInteger = (value) => {
|
|
15
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
|
|
16
|
+
throw new ChainDataError('invalid chain integer');
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
function quantity(value) {
|
|
20
|
+
if (typeof value !== 'string' || !/^0x(?:0|[1-9a-fA-F][0-9a-fA-F]*)$/.test(value) || value.length > 66)
|
|
21
|
+
throw new ChainDataError('invalid RPC quantity');
|
|
22
|
+
return BigInt(value);
|
|
23
|
+
}
|
|
24
|
+
function formatUnits(raw, decimals) {
|
|
25
|
+
const negative = raw < 0n;
|
|
26
|
+
const digits = (negative ? -raw : raw).toString().padStart(decimals + 1, '0');
|
|
27
|
+
const fraction = decimals ? digits.slice(-decimals).replace(/0+$/, '') : '';
|
|
28
|
+
return (negative ? '-' : '') + (decimals ? digits.slice(0, -decimals) : digits) + (fraction ? '.' + fraction : '');
|
|
29
|
+
}
|
|
30
|
+
function network(args, endpoints) {
|
|
31
|
+
if (typeof args.network !== 'string' || !Object.hasOwn(endpoints, args.network))
|
|
32
|
+
throw new ChainDataError(`invalid network: must be ${Object.keys(endpoints).join(', ')}`);
|
|
33
|
+
if (args.chain_id !== undefined || args.token !== undefined)
|
|
34
|
+
throw new ChainDataError('chain_id and token are EVM-only');
|
|
35
|
+
return args.network;
|
|
36
|
+
}
|
|
37
|
+
async function http(url, options, init, allowMissing = false) {
|
|
38
|
+
try {
|
|
39
|
+
const result = await (options.fetch ?? globalThis.fetch)(url, { ...init, signal: AbortSignal.timeout(10_000) });
|
|
40
|
+
if (!result.ok) {
|
|
41
|
+
await result.body?.cancel();
|
|
42
|
+
if (!(allowMissing && result.status === 404))
|
|
43
|
+
throw new Error();
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new ChainDataError('public chain API read failed; retry later');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function json(response) {
|
|
52
|
+
const text = await readChainResponse(response);
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(text);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new ChainDataError('malformed chain JSON response');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Validate legacy Base58Check as well as segwit recipients; no URL/path comes from the caller. */
|
|
61
|
+
async function bitcoinAddress(address, chainNetwork) {
|
|
62
|
+
if (typeof address !== 'string' || address.length > 90)
|
|
63
|
+
throw new ChainDataError('invalid bitcoin address');
|
|
64
|
+
if (decodeSegwitAddress(chainNetwork === 'mainnet' ? 'bc' : 'tb', address))
|
|
65
|
+
return address.toLowerCase();
|
|
66
|
+
const alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
67
|
+
if (address.length < 26 || address.length > 35 || [...address].some(c => !alphabet.includes(c)))
|
|
68
|
+
throw new ChainDataError('invalid bitcoin address for network');
|
|
69
|
+
let value = 0n;
|
|
70
|
+
for (const c of address)
|
|
71
|
+
value = value * 58n + BigInt(alphabet.indexOf(c));
|
|
72
|
+
let hex = value.toString(16);
|
|
73
|
+
if (hex.length % 2)
|
|
74
|
+
hex = '0' + hex;
|
|
75
|
+
const bytes = Uint8Array.from([...Array(address.match(/^1*/)?.[0].length ?? 0).fill(0), ...hex.match(/../g).map(b => parseInt(b, 16))]);
|
|
76
|
+
const versions = chainNetwork === 'mainnet' ? [0, 5] : [111, 196];
|
|
77
|
+
if (bytes.length !== 25 || !versions.includes(bytes[0]))
|
|
78
|
+
throw new ChainDataError('invalid bitcoin address for network');
|
|
79
|
+
const first = await crypto.subtle.digest('SHA-256', bytes.slice(0, 21));
|
|
80
|
+
const checksum = new Uint8Array(await crypto.subtle.digest('SHA-256', first));
|
|
81
|
+
if (!bytes.slice(21).every((b, i) => b === checksum[i]))
|
|
82
|
+
throw new ChainDataError('invalid bitcoin address checksum');
|
|
83
|
+
return address;
|
|
84
|
+
}
|
|
85
|
+
/** Preserve uint64 coin amounts using native JSON source context; older runtimes refuse unsafe numbers. */
|
|
86
|
+
function coinJson(text) {
|
|
87
|
+
try {
|
|
88
|
+
return object(JSON.parse(text, (_key, value, context) => {
|
|
89
|
+
if (typeof value !== 'number')
|
|
90
|
+
return value;
|
|
91
|
+
if (context?.source)
|
|
92
|
+
return context.source;
|
|
93
|
+
if (Number.isSafeInteger(value))
|
|
94
|
+
return String(value);
|
|
95
|
+
throw new Error('unsafe coin amount');
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new ChainDataError('malformed or imprecise coin-record response');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export async function getBalance(args, options = {}) {
|
|
103
|
+
if (args.family === 'evm') {
|
|
104
|
+
if (args.network !== undefined)
|
|
105
|
+
throw new ChainDataError('EVM reads use chain_id, not network');
|
|
106
|
+
const { chain_id, transaction } = validateTx(args.chain_id, { to: args.address, value: '0' });
|
|
107
|
+
const token = await resolveToken(chain_id, args.token, options);
|
|
108
|
+
const result = token
|
|
109
|
+
? await evmRpc(chain_id, 'eth_call', [{ to: token.address, data: '0x70a08231' + transaction.to.slice(2).toLowerCase().padStart(64, '0') }, 'latest'], options)
|
|
110
|
+
: await evmRpc(chain_id, 'eth_getBalance', [transaction.to, 'latest'], options);
|
|
111
|
+
const raw = token ? abiUint(result) : quantity(result);
|
|
112
|
+
const decimals = token?.decimals ?? 18;
|
|
113
|
+
return { family: 'evm', chain_id, address: transaction.to, asset: token?.symbol ?? nativeSymbol(chain_id), ...(token && { token_address: token.address }), decimals, raw_balance: raw.toString(), balance: formatUnits(raw, decimals) };
|
|
114
|
+
}
|
|
115
|
+
if (args.family === 'bitcoin') {
|
|
116
|
+
const chainNetwork = network(args, BITCOIN_API);
|
|
117
|
+
const address = await bitcoinAddress(args.address, chainNetwork);
|
|
118
|
+
const body = object(await json(await http(`${BITCOIN_API[chainNetwork]}/address/${encodeURIComponent(address)}`, options)));
|
|
119
|
+
const stats = (field) => {
|
|
120
|
+
const row = object(body[field]);
|
|
121
|
+
return BigInt(nonnegativeInteger(row.funded_txo_sum)) - BigInt(nonnegativeInteger(row.spent_txo_sum));
|
|
122
|
+
};
|
|
123
|
+
const confirmed = stats('chain_stats');
|
|
124
|
+
const pending = stats('mempool_stats');
|
|
125
|
+
if (confirmed < 0n || confirmed + pending < 0n)
|
|
126
|
+
throw new ChainDataError('invalid bitcoin balance response');
|
|
127
|
+
return { family: 'bitcoin', network: chainNetwork, address, asset: 'BTC', decimals: 8, raw_balance: String(confirmed + pending), balance: formatUnits(confirmed + pending, 8), confirmed_balance: formatUnits(confirmed, 8), unconfirmed_balance: formatUnits(pending, 8) };
|
|
128
|
+
}
|
|
129
|
+
if (args.family === 'chia') {
|
|
130
|
+
const chainNetwork = network(args, CHIA_API);
|
|
131
|
+
const puzzle = typeof args.address === 'string' && decodeChiaAddress(chainNetwork === 'mainnet' ? 'xch' : 'txch', args.address);
|
|
132
|
+
if (!puzzle)
|
|
133
|
+
throw new ChainDataError('invalid chia address for network');
|
|
134
|
+
const puzzleHash = '0x' + puzzle.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
135
|
+
const response = await http(`${CHIA_API[chainNetwork]}/get_coin_records_by_puzzle_hash`, options, {
|
|
136
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ puzzle_hash: puzzleHash, include_spent_coins: false }),
|
|
137
|
+
});
|
|
138
|
+
const body = coinJson(await readChainResponse(response));
|
|
139
|
+
if (body.success !== true || !Array.isArray(body.coin_records))
|
|
140
|
+
throw new ChainDataError('coin-record API did not return a successful balance');
|
|
141
|
+
if (body.coin_records.length > MAX_CHIA_COIN_RECORDS)
|
|
142
|
+
throw new ChainDataError('too many coin records: limit 10000; no partial balance returned');
|
|
143
|
+
let raw = 0n;
|
|
144
|
+
const seen = new Set();
|
|
145
|
+
for (const item of body.coin_records) {
|
|
146
|
+
const record = object(item);
|
|
147
|
+
if (record.spent === true)
|
|
148
|
+
continue;
|
|
149
|
+
if (record.spent !== false)
|
|
150
|
+
throw new ChainDataError('invalid coin-record spent flag');
|
|
151
|
+
const coin = object(record.coin);
|
|
152
|
+
if (coin.puzzle_hash !== puzzleHash || typeof coin.parent_coin_info !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(coin.parent_coin_info) || typeof coin.amount !== 'string' || !/^(0|[1-9][0-9]*)$/.test(coin.amount) || coin.amount.length > 20)
|
|
153
|
+
throw new ChainDataError('invalid coin-record identity or amount');
|
|
154
|
+
const amount = BigInt(coin.amount);
|
|
155
|
+
if (amount >= 2n ** 64n)
|
|
156
|
+
throw new ChainDataError('coin amount exceeds uint64');
|
|
157
|
+
const id = coin.parent_coin_info.toLowerCase() + ':' + coin.amount;
|
|
158
|
+
if (seen.has(id))
|
|
159
|
+
throw new ChainDataError('duplicate coin record');
|
|
160
|
+
seen.add(id);
|
|
161
|
+
raw += amount;
|
|
162
|
+
}
|
|
163
|
+
return { family: 'chia', network: chainNetwork, address: args.address, asset: 'XCH', decimals: 12, raw_balance: raw.toString(), balance: formatUnits(raw, 12), scope: 'unspent coins at this puzzle hash; not the whole wallet; excludes mempool' };
|
|
164
|
+
}
|
|
165
|
+
throw new ChainDataError('invalid family: must be evm, bitcoin or chia');
|
|
166
|
+
}
|
|
167
|
+
export async function getTxStatus(args, options = {}) {
|
|
168
|
+
if (args.family === 'evm') {
|
|
169
|
+
if (args.network !== undefined)
|
|
170
|
+
throw new ChainDataError('EVM reads use chain_id, not network');
|
|
171
|
+
const { chain_id } = validateTx(args.chain_id, { to: '0x' + '0'.repeat(40), value: '0' });
|
|
172
|
+
if (typeof args.tx_hash !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(args.tx_hash))
|
|
173
|
+
throw new ChainDataError('invalid EVM tx_hash');
|
|
174
|
+
const receipt = await evmRpc(chain_id, 'eth_getTransactionReceipt', [args.tx_hash], options);
|
|
175
|
+
if (receipt === null)
|
|
176
|
+
return { family: 'evm', chain_id, tx_hash: args.tx_hash, status: 'pending_or_unknown', confirmations: 0 };
|
|
177
|
+
const row = object(receipt);
|
|
178
|
+
if (row.status !== '0x0' && row.status !== '0x1')
|
|
179
|
+
throw new ChainDataError('invalid receipt status');
|
|
180
|
+
const height = quantity(row.blockNumber);
|
|
181
|
+
const tip = quantity(await evmRpc(chain_id, 'eth_blockNumber', [], options));
|
|
182
|
+
if (tip < height || tip - height >= BigInt(Number.MAX_SAFE_INTEGER))
|
|
183
|
+
throw new ChainDataError('inconsistent receipt/block height; retry after chain reorganization');
|
|
184
|
+
return { family: 'evm', chain_id, tx_hash: args.tx_hash, status: row.status === '0x1' ? 'confirmed' : 'failed', confirmations: Number(tip - height + 1n) };
|
|
185
|
+
}
|
|
186
|
+
if (args.family === 'bitcoin') {
|
|
187
|
+
const chainNetwork = network(args, BITCOIN_API);
|
|
188
|
+
if (typeof args.tx_hash !== 'string' || !/^[0-9a-fA-F]{64}$/.test(args.tx_hash))
|
|
189
|
+
throw new ChainDataError('invalid bitcoin tx_hash');
|
|
190
|
+
const base = BITCOIN_API[chainNetwork];
|
|
191
|
+
const response = await http(`${base}/tx/${args.tx_hash}/status`, options, undefined, true);
|
|
192
|
+
const common = { family: 'bitcoin', network: chainNetwork, tx_hash: args.tx_hash };
|
|
193
|
+
if (response.status === 404)
|
|
194
|
+
return { ...common, status: 'unknown', confirmations: 0 };
|
|
195
|
+
const row = object(await json(response));
|
|
196
|
+
if (row.confirmed === false)
|
|
197
|
+
return { ...common, status: 'pending', confirmations: 0 };
|
|
198
|
+
if (row.confirmed !== true)
|
|
199
|
+
throw new ChainDataError('invalid bitcoin transaction status');
|
|
200
|
+
const height = nonnegativeInteger(row.block_height);
|
|
201
|
+
const tipText = await readChainResponse(await http(`${base}/blocks/tip/height`, options));
|
|
202
|
+
if (!/^(0|[1-9][0-9]*)$/.test(tipText))
|
|
203
|
+
throw new ChainDataError('invalid bitcoin tip height');
|
|
204
|
+
const tip = nonnegativeInteger(Number(tipText));
|
|
205
|
+
if (tip < height)
|
|
206
|
+
throw new ChainDataError('inconsistent bitcoin block height; retry');
|
|
207
|
+
return { ...common, status: 'confirmed', confirmations: tip - height + 1 };
|
|
208
|
+
}
|
|
209
|
+
throw new ChainDataError('transaction status supports evm or bitcoin; after a chia send without a hash, use get_balance on the recipient');
|
|
210
|
+
}
|
package/dist/tokens.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Browser-safe, data-only token list. Addresses, not symbols, identify assets. */
|
|
2
|
+
export interface ListedToken {
|
|
3
|
+
readonly chain_id: number;
|
|
4
|
+
readonly address: string;
|
|
5
|
+
readonly symbol: string;
|
|
6
|
+
readonly decimals: number;
|
|
7
|
+
readonly label: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const TOKENS: readonly ListedToken[];
|
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export const TOKENS = [
|
|
2
|
+
{
|
|
3
|
+
"chain_id": 1,
|
|
4
|
+
"address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
|
5
|
+
"symbol": "USDC",
|
|
6
|
+
"decimals": 6,
|
|
7
|
+
"label": "USDC"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"chain_id": 1,
|
|
11
|
+
"address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
|
|
12
|
+
"symbol": "USDT",
|
|
13
|
+
"decimals": 6,
|
|
14
|
+
"label": "USDT"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"chain_id": 1,
|
|
18
|
+
"address": "0x6b175474e89094c44da98b954eedeac495271d0f",
|
|
19
|
+
"symbol": "DAI",
|
|
20
|
+
"decimals": 18,
|
|
21
|
+
"label": "DAI"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"chain_id": 1,
|
|
25
|
+
"address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
|
26
|
+
"symbol": "WETH",
|
|
27
|
+
"decimals": 18,
|
|
28
|
+
"label": "WETH"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"chain_id": 1,
|
|
32
|
+
"address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",
|
|
33
|
+
"symbol": "WBTC",
|
|
34
|
+
"decimals": 8,
|
|
35
|
+
"label": "WBTC"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"chain_id": 1,
|
|
39
|
+
"address": "0x514910771af9ca656af840dff83e8264ecf986ca",
|
|
40
|
+
"symbol": "LINK",
|
|
41
|
+
"decimals": 18,
|
|
42
|
+
"label": "LINK"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"chain_id": 1,
|
|
46
|
+
"address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
|
|
47
|
+
"symbol": "UNI",
|
|
48
|
+
"decimals": 18,
|
|
49
|
+
"label": "UNI"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"chain_id": 1,
|
|
53
|
+
"address": "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9",
|
|
54
|
+
"symbol": "AAVE",
|
|
55
|
+
"decimals": 18,
|
|
56
|
+
"label": "AAVE"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"chain_id": 8453,
|
|
60
|
+
"address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
|
61
|
+
"symbol": "USDC",
|
|
62
|
+
"decimals": 6,
|
|
63
|
+
"label": "USDC (Base)"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"chain_id": 8453,
|
|
67
|
+
"address": "0x4200000000000000000000000000000000000006",
|
|
68
|
+
"symbol": "WETH",
|
|
69
|
+
"decimals": 18,
|
|
70
|
+
"label": "WETH (Base)"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"chain_id": 84532,
|
|
74
|
+
"address": "0x036cbd53842c5426634e7929541ec2318f3dcf7e",
|
|
75
|
+
"symbol": "USDC",
|
|
76
|
+
"decimals": 6,
|
|
77
|
+
"label": "USDC (Base Sepolia)"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"chain_id": 84532,
|
|
81
|
+
"address": "0x4200000000000000000000000000000000000006",
|
|
82
|
+
"symbol": "WETH",
|
|
83
|
+
"decimals": 18,
|
|
84
|
+
"label": "WETH (Base Sepolia)"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"chain_id": 42161,
|
|
88
|
+
"address": "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
|
|
89
|
+
"symbol": "USDC",
|
|
90
|
+
"decimals": 6,
|
|
91
|
+
"label": "USDC (Arbitrum)"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"chain_id": 42161,
|
|
95
|
+
"address": "0x82af49447d8a07e3bd95bd0d56f35241523fbab1",
|
|
96
|
+
"symbol": "WETH",
|
|
97
|
+
"decimals": 18,
|
|
98
|
+
"label": "WETH (Arbitrum)"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"chain_id": 137,
|
|
102
|
+
"address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
|
|
103
|
+
"symbol": "USDC",
|
|
104
|
+
"decimals": 6,
|
|
105
|
+
"label": "USDC (Polygon)"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"chain_id": 137,
|
|
109
|
+
"address": "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270",
|
|
110
|
+
"symbol": "WMATIC",
|
|
111
|
+
"decimals": 18,
|
|
112
|
+
"label": "WMATIC"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"chain_id": 10,
|
|
116
|
+
"address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85",
|
|
117
|
+
"symbol": "USDC",
|
|
118
|
+
"decimals": 6,
|
|
119
|
+
"label": "USDC (Optimism)"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"chain_id": 10,
|
|
123
|
+
"address": "0x4200000000000000000000000000000000000006",
|
|
124
|
+
"symbol": "WETH",
|
|
125
|
+
"decimals": 18,
|
|
126
|
+
"label": "WETH (Optimism)"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"chain_id": 1,
|
|
130
|
+
"address": "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0",
|
|
131
|
+
"symbol": "wstETH",
|
|
132
|
+
"decimals": 18,
|
|
133
|
+
"label": "Lido wstETH"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"chain_id": 1,
|
|
137
|
+
"address": "0xae7ab96520de3a18e5e111b5eaab095312d7fe84",
|
|
138
|
+
"symbol": "stETH",
|
|
139
|
+
"decimals": 18,
|
|
140
|
+
"label": "Lido stETH"
|
|
141
|
+
}
|
|
142
|
+
];
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clanker-wallet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
|
+
"./tokens": {
|
|
9
|
+
"types": "./dist/tokens.d.ts",
|
|
10
|
+
"import": "./dist/tokens.js"
|
|
11
|
+
},
|
|
8
12
|
".": {
|
|
9
13
|
"import": "./dist/index.js",
|
|
10
14
|
"types": "./dist/index.d.ts"
|