@continuumdao/ctm-mpc-defi 0.2.56 → 0.2.58

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.
Files changed (66) hide show
  1. package/dist/agent/catalog.cjs +749 -171
  2. package/dist/agent/catalog.cjs.map +1 -1
  3. package/dist/agent/catalog.d.ts +373 -44
  4. package/dist/agent/catalog.js +738 -173
  5. package/dist/agent/catalog.js.map +1 -1
  6. package/dist/agent/skills/aave-v4/SKILL.md +26 -3
  7. package/dist/agent/skills/aerodrome/SKILL.md +3 -1
  8. package/dist/agent/skills/circle-cctp/SKILL.md +1 -1
  9. package/dist/agent/skills/curve-dao/SKILL.md +2 -2
  10. package/dist/agent/skills/railgun/SKILL.md +39 -0
  11. package/dist/agent/skills/uniswap-v4/SKILL.md +2 -2
  12. package/dist/index.cjs +165 -19
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.js +166 -20
  15. package/dist/index.js.map +1 -1
  16. package/dist/protocols/evm/aave-v4/index.cjs +198 -15
  17. package/dist/protocols/evm/aave-v4/index.cjs.map +1 -1
  18. package/dist/protocols/evm/aave-v4/index.d.ts +62 -7
  19. package/dist/protocols/evm/aave-v4/index.js +189 -16
  20. package/dist/protocols/evm/aave-v4/index.js.map +1 -1
  21. package/dist/protocols/evm/aerodrome/index.cjs +152 -63
  22. package/dist/protocols/evm/aerodrome/index.cjs.map +1 -1
  23. package/dist/protocols/evm/aerodrome/index.d.ts +30 -58
  24. package/dist/protocols/evm/aerodrome/index.js +151 -64
  25. package/dist/protocols/evm/aerodrome/index.js.map +1 -1
  26. package/dist/protocols/evm/circle-cctp/index.cjs +5 -3
  27. package/dist/protocols/evm/circle-cctp/index.cjs.map +1 -1
  28. package/dist/protocols/evm/circle-cctp/index.d.ts +3 -1
  29. package/dist/protocols/evm/circle-cctp/index.js +5 -4
  30. package/dist/protocols/evm/circle-cctp/index.js.map +1 -1
  31. package/dist/protocols/evm/compound-v3/index.cjs +115 -2
  32. package/dist/protocols/evm/compound-v3/index.cjs.map +1 -1
  33. package/dist/protocols/evm/compound-v3/index.js +116 -3
  34. package/dist/protocols/evm/compound-v3/index.js.map +1 -1
  35. package/dist/protocols/evm/continuum-dao/index.cjs +1 -1
  36. package/dist/protocols/evm/continuum-dao/index.cjs.map +1 -1
  37. package/dist/protocols/evm/continuum-dao/index.d.ts +1 -1
  38. package/dist/protocols/evm/continuum-dao/index.js +1 -1
  39. package/dist/protocols/evm/continuum-dao/index.js.map +1 -1
  40. package/dist/protocols/evm/curve-dao/index.cjs +120 -5
  41. package/dist/protocols/evm/curve-dao/index.cjs.map +1 -1
  42. package/dist/protocols/evm/curve-dao/index.js +120 -5
  43. package/dist/protocols/evm/curve-dao/index.js.map +1 -1
  44. package/dist/protocols/evm/euler-v2/index.cjs +121 -6
  45. package/dist/protocols/evm/euler-v2/index.cjs.map +1 -1
  46. package/dist/protocols/evm/euler-v2/index.js +121 -6
  47. package/dist/protocols/evm/euler-v2/index.js.map +1 -1
  48. package/dist/protocols/evm/morpho/index.cjs +129 -57
  49. package/dist/protocols/evm/morpho/index.cjs.map +1 -1
  50. package/dist/protocols/evm/morpho/index.d.ts +2 -1
  51. package/dist/protocols/evm/morpho/index.js +129 -58
  52. package/dist/protocols/evm/morpho/index.js.map +1 -1
  53. package/dist/protocols/evm/pendle/index.cjs +129 -10
  54. package/dist/protocols/evm/pendle/index.cjs.map +1 -1
  55. package/dist/protocols/evm/pendle/index.js +129 -10
  56. package/dist/protocols/evm/pendle/index.js.map +1 -1
  57. package/dist/protocols/evm/railgun/index.cjs +1261 -0
  58. package/dist/protocols/evm/railgun/index.cjs.map +1 -0
  59. package/dist/protocols/evm/railgun/index.d.ts +294 -0
  60. package/dist/protocols/evm/railgun/index.js +1218 -0
  61. package/dist/protocols/evm/railgun/index.js.map +1 -0
  62. package/dist/protocols/evm/uniswap-v4/index.cjs +164 -17
  63. package/dist/protocols/evm/uniswap-v4/index.cjs.map +1 -1
  64. package/dist/protocols/evm/uniswap-v4/index.js +165 -18
  65. package/dist/protocols/evm/uniswap-v4/index.js.map +1 -1
  66. package/package.json +17 -1
@@ -11,7 +11,7 @@ Aave v4 organizes liquidity into **hubs** (market segments) and **spokes** (on-c
11
11
 
12
12
  | Concept | What it is | Agent needs to know |
13
13
  |---------|------------|---------------------|
14
- | **Hub** | Off-chain market segment (Core, Plus, Prime) | Maps to UI `marketId`: `main` → Plus, `core` → Core, `bluechip` → Prime |
14
+ | **Hub** | Off-chain market segment (Core, Plus, Prime, Coinbase Stocks) | Maps to UI `marketId`: `main` → Plus, `core` → Core, `bluechip` → Prime, `bstocks` → Coinbase Stocks (Base tokenized equities) |
15
15
  | **Spoke** | On-chain contract (`supply`, `withdraw`, `borrow`, `repay`) | **Do not guess.** The MCP server resolves spoke from the Aave v4 API using `chainId` + `underlying` + `marketId` |
16
16
  | **Reserve** | One asset in one hub (underlying + APY + spoke) | From `ctm_aave_v4_fetch_markets` — do not web-search rates |
17
17
  | **Position** | User’s supplies/debt on a spoke | Withdraw/borrow/repay risk depends on health factor on that spoke |
@@ -33,6 +33,7 @@ Do **not** web-search Aave APRs, APYs, or reserves. Load the protocol and use th
33
33
  | User intent | MCP tool |
34
34
  |-------------|----------|
35
35
  | List reserves / lending APRs / compare rates | `ctm_aave_v4_fetch_markets` |
36
+ | Base tokenized stocks (bStocks) | `ctm_aave_v4_fetch_stock_markets` |
36
37
  | One asset’s APY (optionally one hub) | `ctm_aave_v4_fetch_market` |
37
38
  | Supply / deposit (lend) | `ctm_aave_v4_build_deposit_multisign` |
38
39
  | Withdraw supplied balance | `ctm_aave_v4_build_withdraw_multisign` |
@@ -50,7 +51,7 @@ Do **not** web-search Aave APRs, APYs, or reserves. Load the protocol and use th
50
51
  | `purposeText` | yes | Human-readable reason for the sign request |
51
52
  | `underlying` | yes | Token address for the action (see per-action below) |
52
53
  | `amountHuman` | yes | Human amount (e.g. `"0.5"`, `"100"`) |
53
- | `marketId` | no | `main` (default), `core`, or `bluechip` |
54
+ | `marketId` | no | `main` (default), `core`, `bluechip`, or `bstocks` (aliases: `stocks`, `equities`) |
54
55
  | `useCustomGas` | no | `true` to use chain registry gas config |
55
56
  | `spoke` | no | **Omit** — server resolves from Aave API |
56
57
 
@@ -87,7 +88,7 @@ Do **not** web-search Aave APRs, APYs, or reserves. Load the protocol and use th
87
88
  Example: “How do Aave lending APRs compare to Compound on Ethereum?”
88
89
 
89
90
  1. `load_defi_protocol({ protocolId: "aave-v4" })` then `get_defi_protocol_skill`.
90
- 2. `ctm_aave_v4_fetch_markets` with `chainId` 1. Optional `marketId` (`main` / `core` / `bluechip`) or `underlying` to filter.
91
+ 2. `ctm_aave_v4_fetch_markets` with `chainId` 1. Optional `marketId` (`main` / `core` / `bluechip` / `bstocks`) or `underlying` to filter. On Base, `ctm_aave_v4_fetch_stock_markets` lists the Coinbase Stocks hub.
91
92
  3. Each row is one live spoke on a hub (unused zero-liquidity listings omitted): `supplyApyPercent`, `borrowApyPercent`, `utilizationPercent`, `availableLiquidity`. The same asset can appear on more than one spoke.
92
93
  4. Aave reports **APY** (compounded). Compound v3 reports **APR** — say so when comparing.
93
94
  5. Copy `underlying` + `marketId` for a later deposit/borrow. Do not invent rates from the web.
@@ -158,6 +159,28 @@ After stETH is supplied:
158
159
  }
159
160
  ```
160
161
 
162
+ ## bStocks (Base tokenized equities)
163
+
164
+ Aave v4 on Base (chainId **8453**) has a **Coinbase Stocks** hub. UI and MCP `marketId` is **`bstocks`**. The same hub resolver is used by the node app and by `ctm_aave_v4_fetch_stock_markets` / `build_*_multisign`.
165
+
166
+ - Collateral is whatever the Coinbase Stocks hub currently lists (`ctm_aave_v4_fetch_stock_markets`). Pass a ticker such as `AAPLc`; it resolves from the live Coinbase catalog. Equity reserves are **not borrowable**.
167
+ - Borrow asset: **USDC** only. Supply the stock with `marketId: "bstocks"`, then borrow USDC. `collateralUnderlying` may be the stock ticker.
168
+ - Chainlink equity feeds publish 24/5 (Sunday 20:00 ET through Friday 20:00 ET) and hold the last price over weekends and US market holidays. Health factor can still fall from interest while the feed is stale.
169
+ - Coinbase Tokenized Stocks are not for U.S. persons.
170
+ - Do not treat `main` as this hub. Base has no Plus hub; `main` does not select Coinbase Stocks.
171
+
172
+ ```json
173
+ {
174
+ "keyGenId": "<keygen-id>",
175
+ "chainId": 8453,
176
+ "purposeText": "Supply 1 AAPLc to Aave v4 bStocks",
177
+ "underlying": "AAPLc",
178
+ "marketId": "bstocks",
179
+ "amountHuman": "1",
180
+ "enableAsCollateralAfterSupply": true
181
+ }
182
+ ```
183
+
161
184
  ## Notes
162
185
 
163
186
  - Native supply uses wrapped native from `get_defi_protocol_supported_tokens` (`nativeWrapped`).
@@ -86,7 +86,9 @@ Skip emissions when `claimEmissions` is `null` (no gauge). Slipstream unstaked f
86
86
 
87
87
  Loaded from official GitHub (`velodrome-finance/sugar` `deployments/base.env` and `aerodrome-finance/docs` security.mdx) with baked fallbacks. Do not invent addresses.
88
88
 
89
- Coinbase B20 stocks (AAPLc, NVDAc, METAc, GOOGLc, SPCXc, …) trade on **Slipstream 3** (`clFactory3`) vs USDC. Stock-backed **loans** (lend/borrow USDC against B20 collateral) are Morpho — `load_defi_protocol morpho` and `ctm_morpho_fetch_stock_markets`, not Aerodrome LP. New official names are picked up from the [Base tokenized-stocks docs](https://docs.base.org/base-chain/asset-issuance/tokenized-stocks-on-base) and from Sugar tokens whose address uses the B20 ASSET prefix `0xb200…`. The docs fetch is optional: if that page is gone or unusable, discovery continues from the seed list + Sugar. `issued` is live `totalSupply() > 0` — do not hard-code unissued tickers. Quotes use factory `getPool` discovery + MixedRouteQuoterV3 (path bitmasks). Sugar `forSwaps` alone misses these pools. Native ETH → stock is typically WETH → USDC → stock.
89
+ Aero (the Aerodrome rename) is scheduled for **2026-10-22 00:00 UTC** on Base, Ethereum, OP Mainnet, Arc, Ink, Robinhood Chain, and Arbitrum. Those contract addresses are not published yet, so quotes and writes stay on the live Base deployment (`chainId` 8453). Tokenized stocks stay on Base even after that launch.
90
+
91
+ Coinbase B20 stocks (AAPLc, NVDAc, PYPLc, AMDc, ORCLc, BRK.Bc, …) trade on **Slipstream 3** (`clFactory3`) vs USDC. Tickers resolve from the live Coinbase tokenized-stocks API. There is no baked-in address list. Pass the ticker — do not ask for a contract address. Stock-backed **loans** (lend/borrow USDC against B20 collateral) are Morpho — `load_defi_protocol morpho` and `ctm_morpho_fetch_stock_markets`, not Aerodrome LP. Sugar tokens whose address uses the B20 ASSET prefix `0xb200…` are also treated as stocks. `issued` is live `totalSupply() > 0`. Quotes use factory `getPool` discovery + MixedRouteQuoterV3 (path bitmasks). Sugar `forSwaps` alone misses these pools. Native ETH → stock is typically WETH → USDC → stock.
90
92
 
91
93
  ## Out of scope
92
94
 
@@ -33,7 +33,7 @@ After `{ requestId }`: `sign_request_agree` → `trigger_sign_result` → `broad
33
33
  | `sourceChainId` | yes | EIP-155 id where burn executes |
34
34
  | `destChainId` | yes | EIP-155 destination (mint chain) |
35
35
  | `transferAmountHuman` | yes for a usable maxFee | USDC the recipient receives (6 decimals). Protocol fee is this amount times Iris `minimumFee` bps. Omit only to inspect raw forwarding tiers. |
36
- | `feeTier` | no | `low` \| `med` (default) \| `high` |
36
+ | `feeTier` | no | `low` \| `med` \| `high` (default) |
37
37
  | `minFinalityThreshold` | no | `1000` fast (default, has protocol fee) or `2000` standard |
38
38
 
39
39
  `maxFee = forwardFee[tier] + protocolFee`. `forwardFee*` is the Forwarding Service component only. `maxFeeLow` / `Med` / `High` are the combined caps. Returns `fees` plus the same fields at the top level, including `protocolFeeHuman`, `minimumFeeBps`, and `totalBurnHuman` when an amount was passed. Pass that output as `feeSnapshot` to build and balance tools. Do not add `minimumFee` again on top of `maxFee`.
@@ -55,8 +55,8 @@ Always quote first unless the user already has a verified route. Explain quote o
55
55
  | Field | Required | Notes |
56
56
  |-------|----------|-------|
57
57
  | `chainId` | yes | Must be Curve-supported (see supported chains below) |
58
- | `tokenIn` | yes | ERC-20 address, or native: `0xeeee…`, `0x0`, `eth` |
59
- | `tokenOut` | yes | ERC-20 address or `0xeeee…` for native out |
58
+ | `tokenIn` | yes | ERC-20 address, a Coinbase stock ticker (`AAPLc`) from the live catalog, or native: `0xeeee…`, `0x0`, `eth` |
59
+ | `tokenOut` | yes | ERC-20 address, the same Coinbase stock ticker, or `0xeeee…` for native out |
60
60
  | `amountHuman` | yes | Human decimal string of **tokenIn** (e.g. `"100"`, `"0.5"`) |
61
61
  | `tokenInDecimals` | no | Omit to read decimals on-chain (native = 18) |
62
62
  | `rpcUrl` | no | **Do not pass** — server injects from chain registry |
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: railgun
3
+ description: Shield public ERC-20s into the Railgun wallet stored on the KeyGen. Private transfer and unshield need the KeyGen signing quorum.
4
+ ---
5
+
6
+ # Railgun (MCP)
7
+
8
+ Railgun does not publish a token allowlist. Any standard ERC-20 on a deployed chain can be shielded, except CTM. CTM is off until a broadcaster quotes it: do not shield, privately transfer, or unshield CTM, and do not treat it as a supported Railgun token. The asset-row icon is the top 5 ERC-20s in the latest indexed Shield events. That read does not scan chain logs. Railgun is not deployed on Linea.
9
+
10
+ Use `get_defi_protocol_supported_chains` for deployed chains. Contract addresses come from Railgun `NETWORK_CONFIG`. Do not invent chain ids or token lists.
11
+
12
+ The 0zk address and its balances are stored on the KeyGen record. Each node holds a viewing key, so the node agent and Telegram can scan balances. The spending key is split into shares at the KeyGen signing quorum. No node can spend the 0zk wallet by itself. Do not ask the user to unlock a browser wallet, and do not pass a mnemonic, viewing key, or encryption key.
13
+
14
+ ## Tools
15
+
16
+ | Intent | Tool |
17
+ |--------|------|
18
+ | Create the KeyGen Railgun wallet, or read its 0zk address | `ctm_railgun_ensure_wallet` |
19
+ | Public tokens that show the Railgun icon | `ctm_railgun_fetch_popular_assets` |
20
+ | This KeyGen's shielded balances | `ctm_railgun_fetch_private_balances` |
21
+ | Shield a public ERC-20 into the 0zk address | `ctm_railgun_build_shield_multisign` |
22
+ | Start a private transfer | `ctm_railgun_private_transfer` |
23
+ | Start an unshield to a public address | `ctm_railgun_unshield` |
24
+ | Agree this node's share of a pending spend | `ctm_railgun_agree_spend` |
25
+
26
+ Call `ctm_railgun_ensure_wallet` first. Use the returned `railgunAddress` as the shield recipient. Call `ctm_railgun_fetch_private_balances` before an unshield or private transfer. Pass `refresh: true` when the stored scan may be stale. If `walletConfigured` is false, call `ctm_railgun_ensure_wallet` and then fetch again.
27
+
28
+ `ctm_railgun_fetch_popular_assets` is the public icon list, not the shielded holdings.
29
+
30
+ ## Shield
31
+
32
+ 1. Call `ctm_railgun_build_shield_multisign` without `shieldSignature`, with `railgunAddress` from `ctm_railgun_ensure_wallet`. Submit that personal_sign request.
33
+ 2. Call the same tool again with `shieldSignature` set to the completed signature. Submit the resulting approve-and-shield transaction.
34
+
35
+ The first call signs Railgun's shield message. The second call builds the EVM shield. If allowance is short, the second call includes an approve, and a zero-approve reset when a non-zero allowance is still too small. Shielding spends the KeyGen threshold key, not the 0zk spending key.
36
+
37
+ ## Private transfer and unshield
38
+
39
+ These start a pending spend and agree this node's share. The response is `pending` until `approvals` reaches `quorum`. Tell the user the `spendId` and ask an operator on each other KeyGen node to call `ctm_railgun_agree_spend` with that id. When the quorum is reached, the node that started the spend builds the proof and discards the spending key. A later balance scan shows the result. Unshield defaults the recipient to the KeyGen Ethereum address.
@@ -41,8 +41,8 @@ Do not skip steps. Pass the **full quote JSON** into `create_swap`, then pass **
41
41
  |-------|----------|-------|
42
42
  | `type` | yes | `EXACT_INPUT` (sell exact tokenIn) or `EXACT_OUTPUT` (receive exact tokenOut) |
43
43
  | `amount` | yes | **Base units** string (wei for 18-decimal tokens). For human amounts, convert with token decimals first |
44
- | `tokenIn` | yes | Input token; native ETH: `0x0000000000000000000000000000000000000000` |
45
- | `tokenOut` | yes | Output token address (same chain) |
44
+ | `tokenIn` | yes | Input token address, or a Coinbase stock ticker (`AAPLc`) from the live catalog. Native ETH: `0x0000000000000000000000000000000000000000` |
45
+ | `tokenOut` | yes | Output token address, or the same Coinbase stock ticker |
46
46
  | `chainId` | yes | Must be Uniswap-supported |
47
47
  | `keyGenId` | recommended | Server resolves `swapper` (MPC executor) |
48
48
  | `swapper` | optional | MPC wallet address if `keyGenId` omitted |
package/dist/index.cjs CHANGED
@@ -51,6 +51,124 @@ var init_expiry = __esm({
51
51
  ]);
52
52
  }
53
53
  });
54
+ function isCoinbaseB20AssetToken(address) {
55
+ try {
56
+ return viem.getAddress(address).toLowerCase().startsWith(COINBASE_B20_ASSET_PREFIX);
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+ function looksLikeCoinbaseStockSymbol(symbol) {
62
+ return /^[A-Za-z][A-Za-z0-9]{0,8}(?:\.[A-Za-z][A-Za-z0-9]{0,4})?c$/.test((symbol ?? "").trim());
63
+ }
64
+ function indexCatalog(rows) {
65
+ catalogRows = rows;
66
+ catalogAt = Date.now();
67
+ catalogBySymbol.clear();
68
+ catalogByAddress.clear();
69
+ for (const row of rows) {
70
+ catalogBySymbol.set(row.symbol.toLowerCase(), row);
71
+ catalogByAddress.set(row.address.toLowerCase(), row);
72
+ }
73
+ }
74
+ function parseCoinbaseTokenizedStocksPayload(payload) {
75
+ const tokens = Array.isArray(payload) ? payload : payload && typeof payload === "object" && Array.isArray(payload.tokens) ? payload.tokens : [];
76
+ const out = [];
77
+ const seen = /* @__PURE__ */ new Set();
78
+ for (const row of tokens) {
79
+ if (!row || typeof row !== "object") continue;
80
+ const rec = row;
81
+ const symbol = typeof rec.symbol === "string" ? rec.symbol.trim() : "";
82
+ const addressRaw = typeof rec.contract_address === "string" ? rec.contract_address.trim() : "";
83
+ if (!looksLikeCoinbaseStockSymbol(symbol) || !isCoinbaseB20AssetToken(addressRaw)) continue;
84
+ const address = viem.getAddress(addressRaw);
85
+ const key = address.toLowerCase();
86
+ if (seen.has(key)) continue;
87
+ seen.add(key);
88
+ const name = typeof rec.name === "string" && rec.name.trim() ? rec.name.trim() : symbol;
89
+ const iconUrl = typeof rec.icon_url === "string" && rec.icon_url.trim() ? rec.icon_url.trim() : void 0;
90
+ const decimals = typeof rec.decimals === "number" && Number.isInteger(rec.decimals) && rec.decimals >= 0 && rec.decimals <= 18 ? rec.decimals : COINBASE_B20_STOCK_DECIMALS;
91
+ out.push({ address, symbol, name, decimals, iconUrl });
92
+ if (out.length >= CATALOG_MAX) break;
93
+ }
94
+ return out;
95
+ }
96
+ async function fetchCoinbaseB20StockCatalog() {
97
+ const merged = [];
98
+ const seen = /* @__PURE__ */ new Set();
99
+ let pageToken = "";
100
+ for (let page = 0; page < 4; page++) {
101
+ const url = new URL(COINBASE_TOKENIZED_STOCKS_API_URL);
102
+ url.searchParams.set("page_size", "500");
103
+ if (pageToken) url.searchParams.set("page_token", pageToken);
104
+ const res = await fetch(url, { cache: "no-store", signal: AbortSignal.timeout(8e3) });
105
+ if (!res.ok) break;
106
+ const json = await res.json();
107
+ for (const row of parseCoinbaseTokenizedStocksPayload(json)) {
108
+ const key = row.address.toLowerCase();
109
+ if (seen.has(key)) continue;
110
+ seen.add(key);
111
+ merged.push(row);
112
+ if (merged.length >= CATALOG_MAX) break;
113
+ }
114
+ pageToken = typeof json.next_page_token === "string" ? json.next_page_token.trim() : "";
115
+ if (!pageToken || merged.length >= CATALOG_MAX) break;
116
+ }
117
+ if (merged.length > 0) indexCatalog(merged);
118
+ return catalogRows ?? [];
119
+ }
120
+ async function ensureCoinbaseB20StockCatalog() {
121
+ if (catalogRows && catalogRows.length > 0 && Date.now() - catalogAt < CATALOG_TTL_MS) return catalogRows;
122
+ if (!catalogInflight) {
123
+ catalogInflight = fetchCoinbaseB20StockCatalog().finally(() => {
124
+ catalogInflight = null;
125
+ });
126
+ }
127
+ try {
128
+ return await catalogInflight;
129
+ } catch {
130
+ return catalogRows ?? [];
131
+ }
132
+ }
133
+ function lookupCoinbaseB20StockToken(token) {
134
+ const key = (token ?? "").trim();
135
+ if (!key) return null;
136
+ const fromCatalog = catalogBySymbol.get(key.toLowerCase());
137
+ if (fromCatalog) return fromCatalog;
138
+ try {
139
+ return catalogByAddress.get(viem.getAddress(key).toLowerCase()) ?? null;
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+ async function canonicalizeEvmTokenInput(token) {
145
+ const raw = (token ?? "").trim();
146
+ if (!raw || viem.isAddress(raw)) return raw;
147
+ if (!looksLikeCoinbaseStockSymbol(raw)) return raw;
148
+ await ensureCoinbaseB20StockCatalog().catch(() => []);
149
+ const hit = lookupCoinbaseB20StockToken(raw);
150
+ if (!hit) {
151
+ throw new Error(
152
+ `Unknown Coinbase stock ticker "${raw}". It is not in the live tokenized-stocks catalog.`
153
+ );
154
+ }
155
+ return hit.address;
156
+ }
157
+ var COINBASE_B20_STOCK_DECIMALS, COINBASE_B20_ASSET_PREFIX, COINBASE_TOKENIZED_STOCKS_API_URL, CATALOG_TTL_MS, CATALOG_MAX, catalogRows, catalogAt, catalogInflight, catalogBySymbol, catalogByAddress;
158
+ var init_coinbaseB20 = __esm({
159
+ "src/protocols/evm/shared/coinbaseB20.ts"() {
160
+ COINBASE_B20_STOCK_DECIMALS = 8;
161
+ COINBASE_B20_ASSET_PREFIX = "0xb200000000000000000000";
162
+ COINBASE_TOKENIZED_STOCKS_API_URL = "https://api.coinbase.com/v1/tokenized-stocks";
163
+ CATALOG_TTL_MS = 15 * 60 * 1e3;
164
+ CATALOG_MAX = 500;
165
+ catalogRows = null;
166
+ catalogAt = 0;
167
+ catalogInflight = null;
168
+ catalogBySymbol = /* @__PURE__ */ new Map();
169
+ catalogByAddress = /* @__PURE__ */ new Map();
170
+ }
171
+ });
54
172
  function isAerodromeChainSupported(chainId) {
55
173
  const n = typeof chainId === "number" ? chainId : Number.parseInt(String(chainId), 10);
56
174
  return n === AERODROME_CHAIN_ID;
@@ -946,6 +1064,9 @@ var UNISWAP_V4_LP_COLLECT_DEFAULT_GAS_UNITS = 900000n;
946
1064
  var UNISWAP_V4_LP_ERC20_APPROVE_FALLBACK = 100000n;
947
1065
  var UNISWAP_V4_LP_WETH_DEPOSIT_FALLBACK = 120000n;
948
1066
  var UNISWAP_V4_LP_PERMIT2_APPROVE_FALLBACK = 100000n;
1067
+
1068
+ // src/protocols/evm/uniswap-v4/quote.ts
1069
+ init_coinbaseB20();
949
1070
  viem.parseAbi([
950
1071
  "function isAllowed(address account, uint8 permission) view returns (bool)",
951
1072
  "function PERMISSIONED_TOKEN() view returns (address)"
@@ -1312,6 +1433,11 @@ function messageFromUniswapHttpResponseBody(text, status, statusText = "") {
1312
1433
  return short;
1313
1434
  }
1314
1435
  async function uniswapTradeQuote(args) {
1436
+ args = {
1437
+ ...args,
1438
+ tokenIn: await canonicalizeEvmTokenInput(args.tokenIn),
1439
+ tokenOut: await canonicalizeEvmTokenInput(args.tokenOut)
1440
+ };
1315
1441
  const apiKey = (args.uniswapApiKey || "").trim();
1316
1442
  if (!apiKey) {
1317
1443
  throw new Error("uniswapApiKey (x-api-key) is required");
@@ -1431,6 +1557,7 @@ function parseUniswapQuoteClassicInOut(res) {
1431
1557
  }
1432
1558
 
1433
1559
  // src/protocols/evm/uniswap-v4/swapMultisign.ts
1560
+ init_coinbaseB20();
1434
1561
  init_defiEvmClient();
1435
1562
  function parseOptionalGasLimitString(raw) {
1436
1563
  if (raw == null) return null;
@@ -1763,7 +1890,7 @@ async function buildEvmMultisignBodyUniswapV4NativeInOnly(args, quoteInputWei) {
1763
1890
  });
1764
1891
  }
1765
1892
  async function buildEvmMultisignBodyUniswapV4SkipPermit2Batch(args) {
1766
- const tokenIn = viem.getAddress(args.tokenIn);
1893
+ const tokenIn = viem.getAddress(await canonicalizeEvmTokenInput(args.tokenIn));
1767
1894
  const parsedInOut = parseUniswapQuoteClassicInOut(args.fullQuoteSnapshot);
1768
1895
  const quoteInputWei = parsedInOut?.inputWei;
1769
1896
  if (quoteInputWei == null || quoteInputWei <= 0n) {
@@ -2038,6 +2165,7 @@ async function buildEvmMultisignBodyUniswapV4SkipPermit2Batch(args) {
2038
2165
  var UNISWAP_V4_LP_POOL_LOOKUP_HINT = "Pool may not be initialized on this chain. Call ctm_uniswap_v4_list_lp_pools for other presets, pass a custom existingPool.poolReference, or use newPool to initialize a pool.";
2039
2166
 
2040
2167
  // src/protocols/evm/uniswap-v4/liquidityApi.ts
2168
+ init_coinbaseB20();
2041
2169
  var LP_HEADERS_BASE = {
2042
2170
  "Content-Type": "application/json",
2043
2171
  Accept: "application/json",
@@ -2142,36 +2270,47 @@ function extractUniswapLpTokenAmounts(response) {
2142
2270
  return { token0: read("token0"), token1: read("token1") };
2143
2271
  }
2144
2272
  async function uniswapLpCreatePosition(args) {
2273
+ const independentAddress = await canonicalizeEvmTokenInput(args.independentToken.tokenAddress);
2274
+ const existingPool = args.existingPool ? {
2275
+ ...args.existingPool,
2276
+ token0Address: await canonicalizeEvmTokenInput(args.existingPool.token0Address),
2277
+ token1Address: await canonicalizeEvmTokenInput(args.existingPool.token1Address)
2278
+ } : void 0;
2279
+ const newPool = args.newPool ? {
2280
+ ...args.newPool,
2281
+ token0Address: await canonicalizeEvmTokenInput(args.newPool.token0Address),
2282
+ token1Address: await canonicalizeEvmTokenInput(args.newPool.token1Address)
2283
+ } : void 0;
2145
2284
  const body = {
2146
2285
  protocol: "V4",
2147
2286
  walletAddress: trimAddr2(args.walletAddress),
2148
2287
  chainId: args.chainId,
2149
2288
  independentToken: {
2150
- tokenAddress: trimAddr2(args.independentToken.tokenAddress),
2289
+ tokenAddress: trimAddr2(independentAddress),
2151
2290
  amount: String(args.independentToken.amount).trim()
2152
2291
  },
2153
2292
  slippageTolerance: args.slippageTolerance ?? UNISWAP_LP_DEFAULT_SLIPPAGE_PERCENT,
2154
2293
  deadline: args.deadline ?? lpDeadlineUnix(),
2155
2294
  simulateTransaction: args.simulateTransaction ?? false
2156
2295
  };
2157
- if (args.existingPool) {
2296
+ if (existingPool) {
2158
2297
  body.existingPool = {
2159
- token0Address: trimAddr2(args.existingPool.token0Address),
2160
- token1Address: trimAddr2(args.existingPool.token1Address),
2161
- poolReference: trimAddr2(args.existingPool.poolReference)
2298
+ token0Address: trimAddr2(existingPool.token0Address),
2299
+ token1Address: trimAddr2(existingPool.token1Address),
2300
+ poolReference: trimAddr2(existingPool.poolReference)
2162
2301
  };
2163
2302
  }
2164
- if (args.newPool) {
2303
+ if (newPool) {
2165
2304
  body.newPool = {
2166
- token0Address: trimAddr2(args.newPool.token0Address),
2167
- token1Address: trimAddr2(args.newPool.token1Address),
2168
- fee: args.newPool.fee,
2169
- tickSpacing: args.newPool.tickSpacing,
2170
- initialPrice: String(args.newPool.initialPrice).trim(),
2171
- ...args.newPool.hooks ? { hooks: trimAddr2(args.newPool.hooks) } : {}
2305
+ token0Address: trimAddr2(newPool.token0Address),
2306
+ token1Address: trimAddr2(newPool.token1Address),
2307
+ fee: newPool.fee,
2308
+ tickSpacing: newPool.tickSpacing,
2309
+ initialPrice: String(newPool.initialPrice).trim(),
2310
+ ...newPool.hooks ? { hooks: trimAddr2(newPool.hooks) } : {}
2172
2311
  };
2173
2312
  }
2174
- if (!args.existingPool && !args.newPool) {
2313
+ if (!existingPool && !newPool) {
2175
2314
  throw new Error("Provide existingPool or newPool for LP create.");
2176
2315
  }
2177
2316
  if (args.priceBounds) {
@@ -2206,11 +2345,11 @@ async function uniswapLpIncreasePosition(args) {
2206
2345
  protocol: "V4",
2207
2346
  walletAddress: trimAddr2(args.walletAddress),
2208
2347
  chainId: args.chainId,
2209
- token0Address: trimAddr2(args.token0Address),
2210
- token1Address: trimAddr2(args.token1Address),
2348
+ token0Address: trimAddr2(await canonicalizeEvmTokenInput(args.token0Address)),
2349
+ token1Address: trimAddr2(await canonicalizeEvmTokenInput(args.token1Address)),
2211
2350
  nftTokenId: String(args.nftTokenId),
2212
2351
  independentToken: {
2213
- tokenAddress: trimAddr2(args.independentToken.tokenAddress),
2352
+ tokenAddress: trimAddr2(await canonicalizeEvmTokenInput(args.independentToken.tokenAddress)),
2214
2353
  amount: String(args.independentToken.amount).trim()
2215
2354
  },
2216
2355
  slippageTolerance: args.slippageTolerance ?? UNISWAP_LP_DEFAULT_SLIPPAGE_PERCENT,
@@ -2820,6 +2959,7 @@ async function swapExactInput(args) {
2820
2959
  }
2821
2960
 
2822
2961
  // src/protocols/evm/uniswap-v4/limitOrder.ts
2962
+ init_coinbaseB20();
2823
2963
  function trimAddr3(a) {
2824
2964
  return a.trim();
2825
2965
  }
@@ -2920,6 +3060,11 @@ async function uniswapLimitOrderQuote(args) {
2920
3060
  args.nodeReadAuth ?? { bearerOnGet: true, jwt: null }
2921
3061
  );
2922
3062
  }
3063
+ args = {
3064
+ ...args,
3065
+ tokenIn: await canonicalizeEvmTokenInput(args.tokenIn),
3066
+ tokenOut: await canonicalizeEvmTokenInput(args.tokenOut)
3067
+ };
2923
3068
  const chainIdForGuard = parseUniswapChainId(resolveInputChainId(args));
2924
3069
  assertUniswapLimitOrderChain(chainIdForGuard);
2925
3070
  const base = args.baseUrl && args.baseUrl.trim() ? args.baseUrl.trim().replace(/\/$/, "") : "https://trade-api.gateway.uniswap.org/v1";
@@ -3372,6 +3517,7 @@ async function loadFullCurveSessionForRpc(rpcUrl) {
3372
3517
  var CURVE_ROUTER_EXCHANGE_DEFAULT_GAS_UNITS = 1200000n;
3373
3518
 
3374
3519
  // src/protocols/evm/curve-dao/multisign.ts
3520
+ init_coinbaseB20();
3375
3521
  init_defiEvmClient();
3376
3522
  var ETH_PLACEHOLDER2 = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
3377
3523
  function txToViemStep(tx) {
@@ -3387,7 +3533,7 @@ function txToViemStep(tx) {
3387
3533
  return { to, data, value };
3388
3534
  }
3389
3535
  async function buildEvmMultisignBodyCurveDaoBatch(args) {
3390
- const tokenIn = viem.getAddress(args.tokenIn);
3536
+ const tokenIn = viem.getAddress(await canonicalizeEvmTokenInput(args.tokenIn));
3391
3537
  const executor = viem.getAddress(args.executorAddress);
3392
3538
  const session = await loadFullCurveSessionForRpc(args.rpcUrl);
3393
3539
  if (!session?.curve?.router?.populateSwap) {
@@ -3396,7 +3542,7 @@ async function buildEvmMultisignBodyCurveDaoBatch(args) {
3396
3542
  const { curve, wrappedNative } = session;
3397
3543
  const wn = wrappedNative;
3398
3544
  const tokenInId = toCurveRouterTokenId(tokenIn, wn);
3399
- const outTrim = (args.tokenOut ?? "").trim();
3545
+ const outTrim = (await canonicalizeEvmTokenInput(args.tokenOut ?? "")).trim();
3400
3546
  const tokenOutId = toCurveRouterTokenId(
3401
3547
  outTrim.toLowerCase() === ETH_PLACEHOLDER2 ? ETH_PLACEHOLDER2 : viem.getAddress(outTrim),
3402
3548
  wn