@epoch-protocol/epoch-commons-sdk 0.1.20 → 0.2.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 CHANGED
@@ -1 +1,20 @@
1
1
  # Epoch Commons SDK
2
+
3
+ ### Ethereum and Robinhood mainnet graph
4
+
5
+ `mainnetGraph` is the canonical source for Ethereum (1) and Robinhood Chain
6
+ (4663) network, token, and protocol relationships. `getGraphChain`,
7
+ `getGraphToken`, and `supportsGraphProtocol` resolve chain ids and validate
8
+ path eligibility without treating a token on one network as available on another.
9
+
10
+ Native ETH uses the zero-address sentinel and 18 decimals. Generic filler and
11
+ external routes can select ETH; `uniswap-v3` relationships expose WETH instead,
12
+ because that solver currently requires ERC20 input. Ethereum USDG and USDe use
13
+ the issuer addresses documented by [Paxos](https://docs.paxos.com/guides/stablecoin/usdg/mainnet)
14
+ and [Ethena](https://docs.ethena.fi/api-documentation/overview). WBTC uses its
15
+ Ethereum token address and 8 decimals, distinct from the 18-decimal testnet mock.
16
+
17
+ Protocol router and custom Epoch/Safe address maps retain deployment
18
+ placeholders as requested. A graph relationship describes configured routing
19
+ eligibility; it does not verify deployment or pool liquidity. Replace placeholder
20
+ addresses with verified deployments before executing production operations.
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Canonical chain ids. Define new ones HERE and import them — `MIDEN_CHAIN_ID`
3
+ * is currently redefined in seven places across the monorepo.
4
+ *
5
+ * Ids MUST fit in uint32: `getEncodedIntent` and the deployed IntentRegistry
6
+ * both declare `uint32[] chainIds`, so anything larger throws at ABI encode
7
+ * time. That rules out Solana's CAIP-2 id (1151111081099710).
8
+ */
9
+ /**
10
+ * Solana mainnet-beta. Leading digits of the CAIP-2 id, so it stays traceable.
11
+ * PROVISIONAL — check chainlist for collisions before mainnet.
12
+ */
13
+ export declare const SOLANA_MAINNET_CHAIN_ID = 1151111;
14
+ /**
15
+ * Solana devnet. Separate id rather than one shared with mainnet, so devnet can
16
+ * pass the testnet-gated checks in dex-solver's candidate-evaluator and mainnet
17
+ * still can't skip them.
18
+ */
19
+ export declare const SOLANA_DEVNET_CHAIN_ID = 1151112;
20
+ /** Graph chain name. Load-bearing: `chains` and `token.contractAddress` key off it. */
21
+ export declare const SOLANA_CHAIN_NAME = "Solana";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Canonical chain ids. Define new ones HERE and import them — `MIDEN_CHAIN_ID`
3
+ * is currently redefined in seven places across the monorepo.
4
+ *
5
+ * Ids MUST fit in uint32: `getEncodedIntent` and the deployed IntentRegistry
6
+ * both declare `uint32[] chainIds`, so anything larger throws at ABI encode
7
+ * time. That rules out Solana's CAIP-2 id (1151111081099710).
8
+ */
9
+ /**
10
+ * Solana mainnet-beta. Leading digits of the CAIP-2 id, so it stays traceable.
11
+ * PROVISIONAL — check chainlist for collisions before mainnet.
12
+ */
13
+ export const SOLANA_MAINNET_CHAIN_ID = 1151111;
14
+ /**
15
+ * Solana devnet. Separate id rather than one shared with mainnet, so devnet can
16
+ * pass the testnet-gated checks in dex-solver's candidate-evaluator and mainnet
17
+ * still can't skip them.
18
+ */
19
+ export const SOLANA_DEVNET_CHAIN_ID = 1151112;
20
+ /** Graph chain name. Load-bearing: `chains` and `token.contractAddress` key off it. */
21
+ export const SOLANA_CHAIN_NAME = "Solana";
@@ -97,10 +97,13 @@ export const ARBITER_ADDRESS = {
97
97
  10: "0x9f1de3ca45b09c61792a6f649ae68f420aa8a43b",
98
98
  42161: "0x9f1de3ca45b09c61792a6f649ae68f420aa8a43b",
99
99
  8453: "0x9f1de3ca45b09c61792a6f649ae68f420aa8a43b",
100
+ // Robinhood placeholders — replace after deployment.
101
+ 4663: "0x9f1de3ca45b09c61792a6f649ae68f420aa8a43b",
100
102
  // Testnets
101
103
  11155111: "0x2F5eA866716A04e91a55099620F2e1B423eC7011",
102
104
  84532: "0x2F5eA866716A04e91a55099620F2e1B423eC7011",
103
105
  11155420: "0x2F5eA866716A04e91a55099620F2e1B423eC7011",
106
+ 46630: "0x2F5eA866716A04e91a55099620F2e1B423eC7011",
104
107
  123456789: "0x9f1de3ca45b09c61792a6f649ae68f420aa8a43b",
105
108
  999999999: "0x9f1de3ca45b09c61792a6f649ae68f420aa8a43b",
106
109
  };
@@ -111,10 +114,13 @@ export const ALLOCATOR_ADDRESS = {
111
114
  10: "0x444E8C0fC85BB47aBD3E97d00E2E2B0719034769",
112
115
  42161: "0x444E8C0fC85BB47aBD3E97d00E2E2B0719034769",
113
116
  8453: "0x444E8C0fC85BB47aBD3E97d00E2E2B0719034769",
117
+ // Robinhood placeholders — replace after deployment.
118
+ 4663: "0x444E8C0fC85BB47aBD3E97d00E2E2B0719034769",
114
119
  // Testnets
115
120
  11155111: "0xa46827635A7d329608085401a3b2Bf14a8a2bDF9",
116
121
  84532: "0xa46827635A7d329608085401a3b2Bf14a8a2bDF9",
117
122
  11155420: "0xa46827635A7d329608085401a3b2Bf14a8a2bDF9",
123
+ 46630: "0xa46827635A7d329608085401a3b2Bf14a8a2bDF9",
118
124
  123456789: "0x444E8C0fC85BB47aBD3E97d00E2E2B0719034769",
119
125
  999999999: "0x444E8C0fC85BB47aBD3E97d00E2E2B0719034769",
120
126
  };
@@ -134,6 +140,39 @@ export function getArbiterAddress(chainId) {
134
140
  }
135
141
  return address;
136
142
  }
143
+ // The Robinhood Testnet deployment uses the same deterministic/testnet
144
+ // addresses as the existing Epoch testnet configuration. Populate every
145
+ // shared contract map together so any consumer consulting one of these maps
146
+ // cannot silently treat 46630 as unsupported.
147
+ const TESTNET_CONTRACT_ADDRESS_MAPS = [
148
+ INTENT_REGISTRY_ADDRESS,
149
+ EPOCH_MODULE_SAFE_ADDRESS,
150
+ EPOCH_SAFE_INIT_SETUP,
151
+ SAFE_INIT_SETUP_FORWARDER,
152
+ SAFE_WEBAUTHN_SHARED_SIGNER,
153
+ SAFE_WEBAUTHN_SIGNER_FACTORY,
154
+ FCLP256Verifier,
155
+ SAFE_PROXY_FACTORY_ADDRESS,
156
+ SAFE_SINGLETON_ADDRESS,
157
+ SAFE_7702_SINGLETON_ADDRESS,
158
+ SAFE_7579_MODULE_ADDRESS,
159
+ SAFE_7579_REGISTRY_ADDRESS,
160
+ EIP7702_SAFE_PROXY_ADDRESS,
161
+ METAMASK_EXEC_MANAGER,
162
+ EPOCH_ESCROW_ADDRESS,
163
+ SAFE_MULTISEND_ADDRESS,
164
+ SAFE_MULTISEND_CALL_ONLY_ADDRESS,
165
+ ];
166
+ for (const addresses of TESTNET_CONTRACT_ADDRESS_MAPS) {
167
+ addresses[46630] = addresses[11155420];
168
+ }
169
+ // Ethereum and Robinhood mainnet deployment placeholders. These preserve the
170
+ // deterministic address configuration used by testnets; replace each entry
171
+ // with its verified mainnet deployment before executing production operations.
172
+ for (const addresses of TESTNET_CONTRACT_ADDRESS_MAPS) {
173
+ addresses[1] = addresses[11155420];
174
+ addresses[4663] = addresses[1];
175
+ }
137
176
  const CONTRACT_ADDRESSES = {
138
177
  METAMASK_EXEC_MANAGER,
139
178
  SAFE_PROXY_FACTORY_ADDRESS,
@@ -7,7 +7,7 @@ export declare const DUMMY_LENDING_PROTOCOL: "dummy-lending";
7
7
  export declare const DUMMY_LENDING_MARKET_ADDRESS = "0xa73c1ae0e4e9116020dba0a419f91585d73459ee";
8
8
  /** CREATE2 staking contract — same on all dummy-staking testnets. */
9
9
  export declare const DUMMY_STAKING_CONTRACT_ADDRESS = "0xA25A981bc027E1f13aC70882a323B8B8EE880B9C";
10
- export declare const DUMMY_LENDING_CHAIN_IDS: readonly [84532, 11155111, 11155420];
10
+ export declare const DUMMY_LENDING_CHAIN_IDS: readonly [84532, 11155111, 11155420, 46630];
11
11
  export type DummyLendingChainId = (typeof DUMMY_LENDING_CHAIN_IDS)[number];
12
12
  /** Unified CREATE2 token addresses — same on every dummy-lending chain. */
13
13
  export declare const DUMMY_LENDING_TOKENS: {
@@ -11,6 +11,7 @@ export const DUMMY_LENDING_CHAIN_IDS = [
11
11
  84532, // Base Sepolia
12
12
  11155111, // Ethereum Sepolia
13
13
  11155420, // Optimism Sepolia
14
+ 46630, // Robinhood Testnet
14
15
  ];
15
16
  /** Unified CREATE2 token addresses — same on every dummy-lending chain. */
16
17
  export const DUMMY_LENDING_TOKENS = {
@@ -5,7 +5,9 @@
5
5
  "Base": "0xMorphoBaseAddress",
6
6
  "Optimism": "0xMorphoOptimismAddress",
7
7
  "Polygon": "0xMorphoPolygonAddress",
8
- "Arbitrum": "0xMorphoArbitrumAddress"
8
+ "Arbitrum": "0xMorphoArbitrumAddress",
9
+ "Ethereum": "0xrafflesEthereumAddress",
10
+ "Robinhood": "0xrafflesRobinhoodAddress"
9
11
  },
10
12
  "type": ["protocol-interaction"]
11
13
  },
@@ -15,7 +17,8 @@
15
17
  "Base": "0x1deltaBaseAddress",
16
18
  "Optimism": "0x1deltaOptimismAddress",
17
19
  "Polygon": "0x1deltaPolygonAddress",
18
- "Arbitrum": "0x1deltaArbitrumAddress"
20
+ "Arbitrum": "0x1deltaArbitrumAddress",
21
+ "Robinhood": "0xOnedeltaRobinhoodAddress"
19
22
  },
20
23
  "type": ["protocol-interaction"]
21
24
  },
@@ -24,7 +27,9 @@
24
27
  "Base": "0xMorphoBaseAddress",
25
28
  "Optimism": "0xMorphoOptimismAddress",
26
29
  "Polygon": "0xMorphoPolygonAddress",
27
- "Arbitrum": "0xMorphoArbitrumAddress"
30
+ "Arbitrum": "0xMorphoArbitrumAddress",
31
+ "Ethereum": "0xuniswapv3EthereumAddress",
32
+ "Robinhood": "0xuniswapv3RobinhoodAddress"
28
33
  },
29
34
  "type": ["swap"]
30
35
  },
@@ -80,7 +85,22 @@
80
85
  "Base": "0x4200000000000000000000000000000000000006",
81
86
  "Optimism": "0x4200000000000000000000000000000000000006",
82
87
  "Polygon": "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",
83
- "Arbitrum": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
88
+ "Arbitrum": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
89
+ "Robinhood": "0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73"
90
+ },
91
+ "decimals": 18
92
+ },
93
+ "USDG": {
94
+ "contractAddress": {
95
+ "Robinhood": "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
96
+ "Ethereum": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D"
97
+ },
98
+ "decimals": 6
99
+ },
100
+ "USDe": {
101
+ "contractAddress": {
102
+ "Robinhood": "0x5d3a1Ff2b6BAb83B63cd9AD0787074081a52ef34",
103
+ "Ethereum": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"
84
104
  },
85
105
  "decimals": 18
86
106
  },
@@ -92,12 +112,30 @@
92
112
  "Arbitrum": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"
93
113
  },
94
114
  "decimals": 6
115
+ },
116
+ "ETH": {
117
+ "contractAddress": {
118
+ "Ethereum": "0x0000000000000000000000000000000000000000",
119
+ "Robinhood": "0x0000000000000000000000000000000000000000"
120
+ },
121
+ "decimals": 18
122
+ },
123
+ "WBTC": {
124
+ "contractAddress": {
125
+ "Ethereum": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
126
+ },
127
+ "decimals": 8
95
128
  }
96
129
  },
97
130
  "chains": {
98
131
  "Ethereum": {
99
132
  "chainId": 1,
100
- "explorer": "https://etherscan.io"
133
+ "explorer": "https://etherscan.io",
134
+ "nativeCurrency": {
135
+ "name": "Ether",
136
+ "symbol": "ETH",
137
+ "decimals": 18
138
+ }
101
139
  },
102
140
  "Base": {
103
141
  "chainId": 8453,
@@ -114,6 +152,15 @@
114
152
  "Arbitrum": {
115
153
  "chainId": 42161,
116
154
  "explorer": "https://arbiscan.io"
155
+ },
156
+ "Robinhood": {
157
+ "chainId": 4663,
158
+ "explorer": "https://robinhoodchain.blockscout.com",
159
+ "nativeCurrency": {
160
+ "name": "Ether",
161
+ "symbol": "ETH",
162
+ "decimals": 18
163
+ }
117
164
  }
118
165
  },
119
166
  "relationships": {
@@ -124,6 +171,21 @@
124
171
  },
125
172
  "Optimism": {
126
173
  "tokensInAndOut": ["WETH", "USDC", "DAI", "USDT"]
174
+ },
175
+ "Ethereum": {
176
+ "tokensInAndOut": [
177
+ "DAI",
178
+ "USDC",
179
+ "WETH",
180
+ "USDG",
181
+ "USDe",
182
+ "USDT",
183
+ "ETH",
184
+ "WBTC"
185
+ ]
186
+ },
187
+ "Robinhood": {
188
+ "tokensInAndOut": ["WETH", "USDG", "USDe", "ETH"]
127
189
  }
128
190
  },
129
191
  "uniswap-v3": {
@@ -138,6 +200,20 @@
138
200
  },
139
201
  "Arbitrum": {
140
202
  "tokensInAndOut": ["WETH", "USDC", "DAI", "USDT"]
203
+ },
204
+ "Ethereum": {
205
+ "tokensInAndOut": [
206
+ "DAI",
207
+ "USDC",
208
+ "WETH",
209
+ "USDG",
210
+ "USDe",
211
+ "USDT",
212
+ "WBTC"
213
+ ]
214
+ },
215
+ "Robinhood": {
216
+ "tokensInAndOut": ["WETH", "USDG", "USDe"]
141
217
  }
142
218
  },
143
219
  "FillerCrossChain": {
@@ -158,6 +234,21 @@
158
234
  "1delta": {
159
235
  "*": {
160
236
  "tokensInAndOut": ["*"]
237
+ },
238
+ "Ethereum": {
239
+ "tokensInAndOut": [
240
+ "DAI",
241
+ "USDC",
242
+ "WETH",
243
+ "USDG",
244
+ "USDe",
245
+ "USDT",
246
+ "ETH",
247
+ "WBTC"
248
+ ]
249
+ },
250
+ "Robinhood": {
251
+ "tokensInAndOut": ["WETH", "USDG", "USDe", "ETH"]
161
252
  }
162
253
  },
163
254
  "ExternalSwapBridge": {
@@ -169,16 +260,25 @@
169
260
  "tokenChains": {
170
261
  "DAI": ["Ethereum", "Base", "Optimism", "Polygon", "Arbitrum"],
171
262
  "USDC": ["Ethereum", "Base", "Optimism", "Polygon", "Arbitrum"],
172
- "WETH": ["Ethereum", "Base", "Optimism", "Polygon", "Arbitrum"],
263
+ "WETH": [
264
+ "Ethereum",
265
+ "Base",
266
+ "Optimism",
267
+ "Polygon",
268
+ "Arbitrum",
269
+ "Robinhood"
270
+ ],
271
+ "USDG": ["Ethereum", "Robinhood"],
173
272
  "USDT": ["Ethereum", "Optimism", "Polygon", "Arbitrum"],
174
- "WBTC": ["Ethereum", "Optimism", "Polygon", "Arbitrum"],
273
+ "WBTC": ["Ethereum"],
175
274
  "wstETH": ["Ethereum", "Base", "Optimism", "Arbitrum"],
176
275
  "GHO": ["Ethereum", "Arbitrum", "Base"],
177
- "USDe": ["Ethereum", "Arbitrum"],
276
+ "USDe": ["Ethereum", "Robinhood"],
178
277
  "cbBTC": ["Ethereum", "Base"],
179
278
  "PYUSD": ["Ethereum"],
180
279
  "LINK": ["Ethereum", "Base", "Optimism", "Polygon", "Arbitrum"],
181
- "weETH": ["Ethereum", "Base", "Arbitrum"]
280
+ "weETH": ["Ethereum", "Base", "Arbitrum"],
281
+ "ETH": ["Ethereum", "Robinhood"]
182
282
  }
183
283
  }
184
284
  }
@@ -71,7 +71,8 @@
71
71
  "Base": "0x2BB4FfD7E2c6D432b697554Efd77fA13bdbefd69",
72
72
  "Mainnet": "0x2BB4FfD7E2c6D432b697554Efd77fA13bdbefd69",
73
73
  "Miden": "0x537c15a622074e91188aa894456c52",
74
- "Optimism": "0x2BB4FfD7E2c6D432b697554Efd77fA13bdbefd69"
74
+ "Optimism": "0x2BB4FfD7E2c6D432b697554Efd77fA13bdbefd69",
75
+ "Robinhood": "0x2BB4FfD7E2c6D432b697554Efd77fA13bdbefd69"
75
76
  },
76
77
  "decimals": 18,
77
78
  "decimalsByChain": {
@@ -83,6 +84,7 @@
83
84
  "Base": "0xc30f1Ce05d1434d484E9A47283aA925fc8A8699a",
84
85
  "Mainnet": "0xc30f1Ce05d1434d484E9A47283aA925fc8A8699a",
85
86
  "Optimism": "0xc30f1Ce05d1434d484E9A47283aA925fc8A8699a",
87
+ "Robinhood": "0xc30f1Ce05d1434d484E9A47283aA925fc8A8699a",
86
88
  "Miden": "0xd17976f0809a8191412f2a126625df"
87
89
  },
88
90
  "decimals": 18,
@@ -95,7 +97,8 @@
95
97
  "Base": "0xc04d2869665Be874881133943523723Be5782720",
96
98
  "Mainnet": "0xc04d2869665Be874881133943523723Be5782720",
97
99
  "Miden": "0x6500ca8c2dd69e9147ab7eafad162c",
98
- "Optimism": "0xc04d2869665Be874881133943523723Be5782720"
100
+ "Optimism": "0xc04d2869665Be874881133943523723Be5782720",
101
+ "Robinhood": "0xc04d2869665Be874881133943523723Be5782720"
99
102
  },
100
103
  "decimals": 18,
101
104
  "decimalsByChain": {
@@ -107,6 +110,7 @@
107
110
  "Base": "0x7946dd86eE310D0aC16804A37787289Fa5b88A8A",
108
111
  "Mainnet": "0x7946dd86eE310D0aC16804A37787289Fa5b88A8A",
109
112
  "Optimism": "0x7946dd86eE310D0aC16804A37787289Fa5b88A8A",
113
+ "Robinhood": "0x7946dd86eE310D0aC16804A37787289Fa5b88A8A",
110
114
  "Miden": "0x4a09f13153d9cd114c078bfb62a7ec"
111
115
  },
112
116
  "decimals": 18,
@@ -119,6 +123,7 @@
119
123
  "Base": "0x9b2a2754a9182fD65360E23afCDf3BeFF51796E9",
120
124
  "Mainnet": "0x9b2a2754a9182fD65360E23afCDf3BeFF51796E9",
121
125
  "Optimism": "0x9b2a2754a9182fD65360E23afCDf3BeFF51796E9",
126
+ "Robinhood": "0x9b2a2754a9182fD65360E23afCDf3BeFF51796E9",
122
127
  "Miden": "0x5fd2e6fd17712c51404d09c2b847f7"
123
128
  },
124
129
  "decimals": 18,
@@ -130,7 +135,8 @@
130
135
  "contractAddress": {
131
136
  "Base": "0xEA7dC9849206Ce73b11c465d37b85eC06B11Cf2C",
132
137
  "Mainnet": "0xEA7dC9849206Ce73b11c465d37b85eC06B11Cf2C",
133
- "Optimism": "0xEA7dC9849206Ce73b11c465d37b85eC06B11Cf2C"
138
+ "Optimism": "0xEA7dC9849206Ce73b11c465d37b85eC06B11Cf2C",
139
+ "Robinhood": "0xEA7dC9849206Ce73b11c465d37b85eC06B11Cf2C"
134
140
  },
135
141
  "decimals": 18
136
142
  },
@@ -138,7 +144,8 @@
138
144
  "contractAddress": {
139
145
  "Base": "0xB588418c0f90F07Bc9587d0050845a90C23C7502",
140
146
  "Mainnet": "0xB588418c0f90F07Bc9587d0050845a90C23C7502",
141
- "Optimism": "0xB588418c0f90F07Bc9587d0050845a90C23C7502"
147
+ "Optimism": "0xB588418c0f90F07Bc9587d0050845a90C23C7502",
148
+ "Robinhood": "0xB588418c0f90F07Bc9587d0050845a90C23C7502"
142
149
  },
143
150
  "decimals": 18
144
151
  },
@@ -146,7 +153,8 @@
146
153
  "contractAddress": {
147
154
  "Base": "0x512Ee6Bd7A4be5Ba4796F15Df080c4D0F89a38eD",
148
155
  "Mainnet": "0x512Ee6Bd7A4be5Ba4796F15Df080c4D0F89a38eD",
149
- "Optimism": "0x512Ee6Bd7A4be5Ba4796F15Df080c4D0F89a38eD"
156
+ "Optimism": "0x512Ee6Bd7A4be5Ba4796F15Df080c4D0F89a38eD",
157
+ "Robinhood": "0x512Ee6Bd7A4be5Ba4796F15Df080c4D0F89a38eD"
150
158
  },
151
159
  "decimals": 18
152
160
  },
@@ -154,7 +162,8 @@
154
162
  "contractAddress": {
155
163
  "Base": "0x145e03A80c19ad1b9d0429d06b6d52707de724A0",
156
164
  "Mainnet": "0x145e03A80c19ad1b9d0429d06b6d52707de724A0",
157
- "Optimism": "0x145e03A80c19ad1b9d0429d06b6d52707de724A0"
165
+ "Optimism": "0x145e03A80c19ad1b9d0429d06b6d52707de724A0",
166
+ "Robinhood": "0x145e03A80c19ad1b9d0429d06b6d52707de724A0"
158
167
  },
159
168
  "decimals": 18
160
169
  }
@@ -175,6 +184,10 @@
175
184
  "Optimism": {
176
185
  "chainId": 11155420,
177
186
  "explorer": "https://sepolia-optimistic.etherscan.io/"
187
+ },
188
+ "Robinhood": {
189
+ "chainId": 46630,
190
+ "explorer": "https://explorer.testnet.chain.robinhood.com"
178
191
  }
179
192
  },
180
193
  "relationships": {
@@ -338,15 +351,15 @@
338
351
  }
339
352
  },
340
353
  "tokenChains": {
341
- "DAI": ["Base", "Mainnet", "Optimism"],
342
- "USDT": ["Base", "Mainnet", "Optimism"],
343
- "USDC": ["Base", "Optimism", "Mainnet", "Miden"],
344
- "WETH": ["Base", "Mainnet", "Optimism"],
345
- "WBTC": ["Base", "Mainnet", "Optimism"],
346
- "PENGU": ["Base", "Mainnet", "Optimism"],
347
- "OSWALD": ["Base", "Mainnet", "Optimism"],
348
- "KICK": ["Base", "Mainnet", "Optimism"],
349
- "FERB": ["Base", "Mainnet", "Optimism"]
354
+ "DAI": ["Base", "Mainnet", "Optimism", "Robinhood"],
355
+ "USDT": ["Base", "Mainnet", "Optimism", "Robinhood"],
356
+ "USDC": ["Base", "Optimism", "Mainnet", "Miden", "Robinhood"],
357
+ "WETH": ["Base", "Mainnet", "Optimism", "Robinhood"],
358
+ "WBTC": ["Base", "Mainnet", "Optimism", "Robinhood"],
359
+ "PENGU": ["Base", "Mainnet", "Optimism", "Robinhood"],
360
+ "OSWALD": ["Base", "Mainnet", "Optimism", "Robinhood"],
361
+ "KICK": ["Base", "Mainnet", "Optimism", "Robinhood"],
362
+ "FERB": ["Base", "Mainnet", "Optimism", "Robinhood"]
350
363
  }
351
364
  }
352
365
  }
@@ -1,3 +1,3 @@
1
- import mainnetGraph from "./epochgraphmainnet.json";
2
- import testnetGraph from "./epochgraphtestnet.json";
3
- export { mainnetGraph, testnetGraph };
1
+ import type { Graph } from "../types/index.js";
2
+ export declare const mainnetGraph: Graph;
3
+ export declare const testnetGraph: Graph;
@@ -1,3 +1,5 @@
1
- import mainnetGraph from "./epochgraphmainnet.json" with { type: "json" };
2
- import testnetGraph from "./epochgraphtestnet.json" with { type: "json" };
3
- export { mainnetGraph, testnetGraph };
1
+ import mainnetGraphData from "./epochgraphmainnet.json" with { type: "json" };
2
+ import testnetGraphData from "./epochgraphtestnet.json" with { type: "json" };
3
+ // Keep public graph exports compatible with graph utilities and consumers.
4
+ export const mainnetGraph = mainnetGraphData;
5
+ export const testnetGraph = testnetGraphData;
@@ -1,5 +1,6 @@
1
1
  import { ethers } from "ethers";
2
2
  import { Address, WalletClient } from "viem";
3
+ export type { DexPoolPreference, UniswapV3PoolPreference, UniswapV4PoolKey, UniswapV4PoolPreference, } from "../utils/dex-pools.js";
3
4
  export type ApiResponse<T> = {
4
5
  data: T;
5
6
  status: number;
@@ -110,8 +111,14 @@ export type Token = {
110
111
  export type Chain = {
111
112
  chainId: number;
112
113
  explorer: string;
114
+ nativeCurrency?: {
115
+ name: string;
116
+ symbol: string;
117
+ decimals: number;
118
+ };
113
119
  };
114
120
  export type Relationships = {
121
+ tokenChains?: Record<string, string[]>;
115
122
  protocolTokensByChain: {
116
123
  [protocolName: string]: {
117
124
  [chainName: string]: {
@@ -183,6 +190,8 @@ export type PathItem = {
183
190
  };
184
191
  calldata?: CalldataReturnInterface;
185
192
  fillerData?: FillerTokenData;
193
+ /** Client-side execution metadata retained when quote-only paths strip calldata. */
194
+ externalExecution?: ExternalExecutionPlan;
186
195
  };
187
196
  export type Path = PathItem[][][];
188
197
  export interface QuoteReturnInterface {
@@ -205,6 +214,21 @@ export type ExecutionTransaction = {
205
214
  value: string;
206
215
  callData: string;
207
216
  };
217
+ /** LI.FI's source bridge may be followed by an SDK-refreshed destination v4 swap. */
218
+ export type LifiExternalExecutionPlan = {
219
+ provider: "lifi";
220
+ sourceChainId: number;
221
+ externalSolverUrl: string;
222
+ sourceTransactions: ExecutionTransaction[];
223
+ submitTransactionIndex: number;
224
+ requiresSequentialReceipts: true;
225
+ lifi: {
226
+ destinationChainId: number;
227
+ bridgeToken: string;
228
+ };
229
+ destinationSwap?: Record<string, unknown>;
230
+ };
231
+ export type ExternalExecutionPlan = LifiExternalExecutionPlan;
208
232
  export type IntermediaryApprovals = {
209
233
  tokenAddress: string;
210
234
  spenderAddress: string;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Chain-aware address handling. Never call `.toLowerCase()` on an address.
3
+ *
4
+ * Lowercasing is safe for hex (EVM addresses, Miden faucet ids) and destroys
5
+ * base58 (Solana), where `A` and `a` are different characters. The failure is
6
+ * silent — graph lookups still match, because both sides get lowercased — and
7
+ * only surfaces once the value is used as a real address, by which point a
8
+ * solver has been told to deliver there.
9
+ */
10
+ /** Exactly 20-byte `0x` hex — a strict EVM address. */
11
+ export declare function isEvmAddress(value: string): boolean;
12
+ /** Solana's base58 alphabet (no `0`, `O`, `I`, `l`), 32-44 chars = 32-byte pubkey. */
13
+ export declare function isBase58Address(value: string): boolean;
14
+ /** Any identifier the protocol can route: EVM, Miden faucet, or Solana mint. */
15
+ export declare function isTokenIdentifier(value: string): boolean;
16
+ /** Canonical form for lookups and map keys. Hex is lowercased, base58 is not. */
17
+ export declare function normalizeTokenAddress(address: string): string;
18
+ /**
19
+ * Equality that respects each family's case rules. Replaces
20
+ * `a.toLowerCase() === b.toLowerCase()`, which returns true for two base58
21
+ * strings differing only in case — i.e. two different accounts.
22
+ */
23
+ export declare function sameTokenAddress(a?: string | null, b?: string | null): boolean;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Chain-aware address handling. Never call `.toLowerCase()` on an address.
3
+ *
4
+ * Lowercasing is safe for hex (EVM addresses, Miden faucet ids) and destroys
5
+ * base58 (Solana), where `A` and `a` are different characters. The failure is
6
+ * silent — graph lookups still match, because both sides get lowercased — and
7
+ * only surfaces once the value is used as a real address, by which point a
8
+ * solver has been told to deliver there.
9
+ */
10
+ /** `0x`-prefixed hex of any length: EVM addresses and Miden faucet ids. */
11
+ function isHexIdentifier(value) {
12
+ return /^0x[a-fA-F0-9]+$/.test(value);
13
+ }
14
+ /** Exactly 20-byte `0x` hex — a strict EVM address. */
15
+ export function isEvmAddress(value) {
16
+ return /^0x[a-fA-F0-9]{40}$/.test(value);
17
+ }
18
+ /** Solana's base58 alphabet (no `0`, `O`, `I`, `l`), 32-44 chars = 32-byte pubkey. */
19
+ export function isBase58Address(value) {
20
+ return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value);
21
+ }
22
+ /** Any identifier the protocol can route: EVM, Miden faucet, or Solana mint. */
23
+ export function isTokenIdentifier(value) {
24
+ return isHexIdentifier(value) || isBase58Address(value);
25
+ }
26
+ /** Canonical form for lookups and map keys. Hex is lowercased, base58 is not. */
27
+ export function normalizeTokenAddress(address) {
28
+ return isHexIdentifier(address) ? address.toLowerCase() : address;
29
+ }
30
+ /**
31
+ * Equality that respects each family's case rules. Replaces
32
+ * `a.toLowerCase() === b.toLowerCase()`, which returns true for two base58
33
+ * strings differing only in case — i.e. two different accounts.
34
+ */
35
+ export function sameTokenAddress(a, b) {
36
+ if (!a || !b)
37
+ return false;
38
+ return normalizeTokenAddress(a) === normalizeTokenAddress(b);
39
+ }
@@ -0,0 +1,6 @@
1
+ import type { ChainKind, Graph } from "../types/index.js";
2
+ /**
3
+ * `undefined` for an unknown chain — treat that as unsupported, never as EVM.
4
+ * Defaulting to EVM is how a Solana mint ends up in `ethers.getAddress()`.
5
+ */
6
+ export declare function getChainKind(graph: Graph | undefined | null, chainId: number): ChainKind | undefined;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `undefined` when the chain is not in the graph. Tolerates a missing or
3
+ * malformed graph rather than throwing — several suites stub the graph module
4
+ * out, and throwing there surfaces as a TypeError far from the cause.
5
+ */
6
+ function findChainById(graph, chainId) {
7
+ if (!graph?.chains)
8
+ return undefined;
9
+ const entry = Object.entries(graph.chains).find(([, chain]) => chain?.chainId === chainId);
10
+ return entry ? { name: entry[0], chain: entry[1] } : undefined;
11
+ }
12
+ /**
13
+ * `undefined` for an unknown chain — treat that as unsupported, never as EVM.
14
+ * Defaulting to EVM is how a Solana mint ends up in `ethers.getAddress()`.
15
+ */
16
+ export function getChainKind(graph, chainId) {
17
+ return findChainById(graph, chainId)?.chain.kind;
18
+ }
@@ -0,0 +1,24 @@
1
+ import type { Hex } from "viem";
2
+ export declare const DEX_POOLS_EXTRA_TYPESTRING = "string dexPools";
3
+ export declare const MAX_DEX_POOL_PREFERENCES = 16;
4
+ export type UniswapV3PoolPreference = {
5
+ protocol: "uniswap-v3";
6
+ chainId: number;
7
+ address: Hex;
8
+ };
9
+ export type UniswapV4PoolKey = {
10
+ currency0: Hex;
11
+ currency1: Hex;
12
+ fee: number;
13
+ tickSpacing: number;
14
+ hooks: Hex;
15
+ };
16
+ export type UniswapV4PoolPreference = {
17
+ protocol: "uniswap-v4";
18
+ chainId: number;
19
+ poolKey: UniswapV4PoolKey;
20
+ hookData?: Hex;
21
+ };
22
+ export type DexPoolPreference = UniswapV3PoolPreference | UniswapV4PoolPreference;
23
+ export declare function decodeDexPools(value: unknown): DexPoolPreference[];
24
+ export declare function encodeDexPools(pools: readonly DexPoolPreference[]): string;
@@ -0,0 +1,94 @@
1
+ export const DEX_POOLS_EXTRA_TYPESTRING = "string dexPools";
2
+ export const MAX_DEX_POOL_PREFERENCES = 16;
3
+ const DYNAMIC_FEE_FLAG = 0x800000;
4
+ function invalid(message) {
5
+ throw new Error(`Invalid dexPools: ${message}`);
6
+ }
7
+ function isHex(value) {
8
+ return typeof value === "string" && /^0x(?:[0-9a-fA-F]{2})*$/.test(value);
9
+ }
10
+ function address(value, field) {
11
+ if (typeof value !== "string" ||
12
+ !/^0x[0-9a-fA-F]{40}$/.test(value) ||
13
+ /^0x0{40}$/i.test(value)) {
14
+ return invalid(`${field} must be a non-zero address`);
15
+ }
16
+ return value.toLowerCase();
17
+ }
18
+ function integer(value, field, minimum, maximum) {
19
+ if (typeof value !== "number" ||
20
+ !Number.isSafeInteger(value) ||
21
+ value < minimum ||
22
+ value > maximum) {
23
+ return invalid(`${field} must be an integer between ${minimum} and ${maximum}`);
24
+ }
25
+ return value;
26
+ }
27
+ function decodeEntry(value) {
28
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
29
+ return invalid("each entry must be an object");
30
+ }
31
+ const entry = value;
32
+ const chainId = integer(entry.chainId, "chainId", 1, Number.MAX_SAFE_INTEGER);
33
+ if (entry.protocol === "uniswap-v3") {
34
+ return {
35
+ protocol: "uniswap-v3",
36
+ chainId,
37
+ address: address(entry.address, "address"),
38
+ };
39
+ }
40
+ if (entry.protocol !== "uniswap-v4") {
41
+ return invalid("protocol must be uniswap-v3 or uniswap-v4");
42
+ }
43
+ if (!entry.poolKey ||
44
+ typeof entry.poolKey !== "object" ||
45
+ Array.isArray(entry.poolKey)) {
46
+ return invalid("poolKey must be an object");
47
+ }
48
+ const key = entry.poolKey;
49
+ const currency0 = address(key.currency0, "poolKey.currency0");
50
+ const currency1 = address(key.currency1, "poolKey.currency1");
51
+ if (currency0 >= currency1) {
52
+ return invalid("poolKey.currency0 must sort before poolKey.currency1");
53
+ }
54
+ const fee = integer(key.fee, "poolKey.fee", 0, 0xffffff);
55
+ if (fee > 1_000_000 && fee !== DYNAMIC_FEE_FLAG) {
56
+ return invalid("poolKey.fee is outside the v4 fee range");
57
+ }
58
+ const tickSpacing = integer(key.tickSpacing, "poolKey.tickSpacing", 1, 32767);
59
+ const hooks = address(key.hooks, "poolKey.hooks");
60
+ const hookData = entry.hookData === undefined ? "0x" : entry.hookData;
61
+ if (!isHex(hookData))
62
+ return invalid("hookData must be even-length hex");
63
+ return {
64
+ protocol: "uniswap-v4",
65
+ chainId,
66
+ poolKey: { currency0, currency1, fee, tickSpacing, hooks },
67
+ hookData: hookData.toLowerCase(),
68
+ };
69
+ }
70
+ export function decodeDexPools(value) {
71
+ if (value === undefined || value === null || value === "")
72
+ return [];
73
+ if (typeof value !== "string")
74
+ return invalid("must be a JSON string");
75
+ let decoded;
76
+ try {
77
+ decoded = JSON.parse(value);
78
+ }
79
+ catch {
80
+ return invalid("must contain valid JSON");
81
+ }
82
+ if (!Array.isArray(decoded))
83
+ return invalid("must contain an array");
84
+ if (decoded.length > MAX_DEX_POOL_PREFERENCES) {
85
+ return invalid(`must contain at most ${MAX_DEX_POOL_PREFERENCES} entries`);
86
+ }
87
+ return decoded.map(decodeEntry);
88
+ }
89
+ export function encodeDexPools(pools) {
90
+ if (!Array.isArray(pools))
91
+ return invalid("must be an array");
92
+ // Reuse the decoder so encoding and decoding accept precisely the same schema.
93
+ return JSON.stringify(decodeDexPools(JSON.stringify(pools)));
94
+ }
@@ -0,0 +1,9 @@
1
+ /** Compact witness declaration for selecting an external quote provider. */
2
+ export declare const EXTERNAL_PROVIDER_EXTRA_TYPESTRING = "string provider";
3
+ export declare const EXTERNAL_QUOTE_PROVIDERS: readonly ["khalani", "near", "lifi"];
4
+ export type ExternalQuoteProvider = (typeof EXTERNAL_QUOTE_PROVIDERS)[number];
5
+ /**
6
+ * Validates an optional signed provider field without normalizing its value.
7
+ * The exact string is part of the Compact witness and must remain unchanged.
8
+ */
9
+ export declare function decodeExternalQuoteProvider(value: unknown): ExternalQuoteProvider | undefined;
@@ -0,0 +1,16 @@
1
+ /** Compact witness declaration for selecting an external quote provider. */
2
+ export const EXTERNAL_PROVIDER_EXTRA_TYPESTRING = "string provider";
3
+ export const EXTERNAL_QUOTE_PROVIDERS = ["khalani", "near", "lifi"];
4
+ /**
5
+ * Validates an optional signed provider field without normalizing its value.
6
+ * The exact string is part of the Compact witness and must remain unchanged.
7
+ */
8
+ export function decodeExternalQuoteProvider(value) {
9
+ if (value === undefined)
10
+ return undefined;
11
+ if (typeof value !== "string" ||
12
+ !EXTERNAL_QUOTE_PROVIDERS.includes(value)) {
13
+ throw new Error(`Invalid external provider; expected one of ${EXTERNAL_QUOTE_PROVIDERS.join(", ")}`);
14
+ }
15
+ return value;
16
+ }
@@ -0,0 +1,20 @@
1
+ import type { Graph } from "../types/index.js";
2
+ /** Resolve the canonical graph name and metadata from an EVM chain id. */
3
+ export declare function getGraphChain(graph: Graph, chainId: number): {
4
+ chainId: number;
5
+ explorer: string;
6
+ nativeCurrency?: {
7
+ name: string;
8
+ symbol: string;
9
+ decimals: number;
10
+ };
11
+ name: string;
12
+ } | undefined;
13
+ /** Resolve an asset only when its address exists on the requested chain. */
14
+ export declare function getGraphToken(graph: Graph, chainId: number, symbol: string): {
15
+ symbol: string;
16
+ address: string;
17
+ decimals: number;
18
+ } | undefined;
19
+ /** Check graph path eligibility, including wildcard filler protocols. */
20
+ export declare function supportsGraphProtocol(graph: Graph, chainId: number, protocol: string, symbol: string): boolean;
@@ -0,0 +1,34 @@
1
+ /** Resolve the canonical graph name and metadata from an EVM chain id. */
2
+ export function getGraphChain(graph, chainId) {
3
+ const entry = Object.entries(graph.chains).find(([, chain]) => chain.chainId === chainId);
4
+ return entry ? { name: entry[0], ...entry[1] } : undefined;
5
+ }
6
+ /** Resolve an asset only when its address exists on the requested chain. */
7
+ export function getGraphToken(graph, chainId, symbol) {
8
+ const chain = getGraphChain(graph, chainId);
9
+ const token = graph.tokens[symbol];
10
+ const address = chain && token?.contractAddress[chain.name];
11
+ return address && chain && token
12
+ ? {
13
+ symbol,
14
+ address,
15
+ decimals: token.decimalsByChain?.[chain.name] ?? token.decimals,
16
+ }
17
+ : undefined;
18
+ }
19
+ /** Check graph path eligibility, including wildcard filler protocols. */
20
+ export function supportsGraphProtocol(graph, chainId, protocol, symbol) {
21
+ const chain = getGraphChain(graph, chainId);
22
+ if (!chain || !getGraphToken(graph, chainId, symbol))
23
+ return false;
24
+ const addresses = graph.protocols[protocol]?.contractAddress;
25
+ if (!addresses?.[chain.name] && !addresses?.["*"])
26
+ return false;
27
+ const relationships = graph.relationships.protocolTokensByChain[protocol];
28
+ const relation = relationships?.[chain.name] ?? relationships?.["*"];
29
+ return [
30
+ relation?.tokensIn,
31
+ relation?.tokensOut,
32
+ relation?.tokensInAndOut,
33
+ ].some((tokens) => tokens?.includes(symbol) || tokens?.includes("*"));
34
+ }
@@ -1,4 +1,7 @@
1
1
  export * from "./compact.js";
2
+ export * from "./dex-pools.js";
3
+ export * from "./external-provider.js";
2
4
  export * from "./miden-intent.js";
3
5
  export * from "./path.js";
4
6
  export * from "./witness-typestring.js";
7
+ export * from "./graph.js";
@@ -1,4 +1,7 @@
1
1
  export * from "./compact.js";
2
+ export * from "./dex-pools.js";
3
+ export * from "./external-provider.js";
2
4
  export * from "./miden-intent.js";
3
5
  export * from "./path.js";
4
6
  export * from "./witness-typestring.js";
7
+ export * from "./graph.js";
@@ -0,0 +1,19 @@
1
+ /** Origin and destination are resolved independently and returned as a pair. */
2
+ export type IntentOrigin = "evm" | "miden" | "solana";
3
+ export type IntentDestination = "evm" | "miden" | "solana";
4
+ export interface IntentRoute {
5
+ origin: IntentOrigin;
6
+ destination: IntentDestination;
7
+ }
8
+ /** Table-dispatch key, e.g. `"miden->solana"`. */
9
+ export type RouteKey = `${IntentOrigin}->${IntentDestination}`;
10
+ export type IntentRouteResult = {
11
+ valid: true;
12
+ route: IntentRoute;
13
+ } | {
14
+ valid: false;
15
+ error: string;
16
+ };
17
+ export declare function routeKey(route: IntentRoute): RouteKey;
18
+ /** Resolve from witness markers; callers validate chain-ID consistency. */
19
+ export declare function resolveIntentRoute(data?: Record<string, unknown>): IntentRouteResult;
@@ -0,0 +1,84 @@
1
+ import { MIDEN_RECIPIENT_ACCOUNT_FIELD_NAME, MIDEN_SOURCE_ACCOUNT_FIELD_NAME, SOLANA_MINT_FIELD_NAME, SOLANA_RECIPIENT_ACCOUNT_FIELD_NAME, SOLANA_SOURCE_ACCOUNT_FIELD_NAME, SOLANA_SOURCE_MINT_FIELD_NAME, } from "./witness-typestring.js";
2
+ function hasTruthyField(data, fieldName) {
3
+ if (!data)
4
+ return false;
5
+ const value = data[fieldName];
6
+ return typeof value === "string" && value.trim() !== "";
7
+ }
8
+ export function routeKey(route) {
9
+ return `${route.origin}->${route.destination}`;
10
+ }
11
+ /** Resolve from witness markers; callers validate chain-ID consistency. */
12
+ export function resolveIntentRoute(data) {
13
+ for (const fieldName of [
14
+ MIDEN_SOURCE_ACCOUNT_FIELD_NAME,
15
+ MIDEN_RECIPIENT_ACCOUNT_FIELD_NAME,
16
+ SOLANA_RECIPIENT_ACCOUNT_FIELD_NAME,
17
+ SOLANA_MINT_FIELD_NAME,
18
+ SOLANA_SOURCE_ACCOUNT_FIELD_NAME,
19
+ SOLANA_SOURCE_MINT_FIELD_NAME,
20
+ ]) {
21
+ const value = data?.[fieldName];
22
+ if (value !== undefined &&
23
+ value !== null &&
24
+ (typeof value !== "string" || value.trim() === "")) {
25
+ return {
26
+ valid: false,
27
+ error: `Intent route marker "${fieldName}" must be a non-empty string`,
28
+ };
29
+ }
30
+ }
31
+ const hasMidenSource = hasTruthyField(data, MIDEN_SOURCE_ACCOUNT_FIELD_NAME);
32
+ const hasMidenRecipient = hasTruthyField(data, MIDEN_RECIPIENT_ACCOUNT_FIELD_NAME);
33
+ const hasSolanaRecipient = hasTruthyField(data, SOLANA_RECIPIENT_ACCOUNT_FIELD_NAME);
34
+ const hasSolanaMint = hasTruthyField(data, SOLANA_MINT_FIELD_NAME);
35
+ const hasSolanaSource = hasTruthyField(data, SOLANA_SOURCE_ACCOUNT_FIELD_NAME);
36
+ const hasSolanaSourceMint = hasTruthyField(data, SOLANA_SOURCE_MINT_FIELD_NAME);
37
+ if (hasMidenSource && hasMidenRecipient) {
38
+ return {
39
+ valid: false,
40
+ error: "Intent cannot include both midenSourceAccount and midenRecipientAccount",
41
+ };
42
+ }
43
+ if (hasSolanaRecipient !== hasSolanaMint) {
44
+ return {
45
+ valid: false,
46
+ error: "Solana intent must carry both solanaRecipientAccount and solanaMint",
47
+ };
48
+ }
49
+ if (hasSolanaSource !== hasSolanaSourceMint) {
50
+ return {
51
+ valid: false,
52
+ error: "Solana-origin intent must carry both solanaSourceAccount and solanaSourceMint",
53
+ };
54
+ }
55
+ if (hasMidenSource && hasSolanaSource) {
56
+ return {
57
+ valid: false,
58
+ error: "Intent declares two origins (midenSourceAccount and solanaSourceAccount)",
59
+ };
60
+ }
61
+ if (hasSolanaSource && hasSolanaRecipient) {
62
+ return {
63
+ valid: false,
64
+ error: "Solana→Solana intents are not supported",
65
+ };
66
+ }
67
+ if (hasMidenRecipient && hasSolanaRecipient) {
68
+ return {
69
+ valid: false,
70
+ error: "Intent declares two destinations (midenRecipientAccount and solanaRecipientAccount)",
71
+ };
72
+ }
73
+ const origin = hasMidenSource
74
+ ? "miden"
75
+ : hasSolanaSource
76
+ ? "solana"
77
+ : "evm";
78
+ const destination = hasSolanaRecipient
79
+ ? "solana"
80
+ : hasMidenRecipient
81
+ ? "miden"
82
+ : "evm";
83
+ return { valid: true, route: { origin, destination } };
84
+ }
@@ -0,0 +1,31 @@
1
+ import { EVM_TO_SOLANA_EXTRA_TYPESTRING, EVM_TO_SOLANA_FIELD_NAMES, SOLANA_MARKER_FIELD_NAMES, SOLANA_TO_EVM_EXTRA_TYPESTRING } from "./witness-typestring.js";
2
+ /** Detect Solana markers by exact field name in typestrings or mandate data. */
3
+ export interface SolanaExtraData {
4
+ /** Base58 account that receives the payout. Case-significant. */
5
+ solanaRecipientAccount?: string;
6
+ /** Base58 SPL mint being delivered. Case-significant. */
7
+ solanaMint?: string;
8
+ }
9
+ /** Solana-origin collateral: where the escrow deposit came from. */
10
+ export interface SolanaSourceData {
11
+ /** Base58 account that opened the escrow. Seeds its PDA. Case-significant. */
12
+ solanaSourceAccount: string;
13
+ /** Base58 SPL mint locked in the escrow. Case-significant. */
14
+ solanaSourceMint: string;
15
+ /** Nonce bound into the escrow PDA, preserved in its input representation. */
16
+ intentNonce: string | number | bigint;
17
+ }
18
+ /**
19
+ * True iff the intent is a Solana bridge intent.
20
+ * At least one of `typestring` or `data` must be provided.
21
+ */
22
+ export declare function isSolanaIntent(typestring?: string, data?: Record<string, unknown>): boolean;
23
+ /** True when both Solana destination identifiers are present. */
24
+ export declare function isEVMToSolanaIntent(data?: Record<string, unknown>): boolean;
25
+ /** True when both Solana source identifiers are present. */
26
+ export declare function isSolanaToEVMIntent(data?: Record<string, unknown>): boolean;
27
+ /** Extract complete Solana source metadata or return `null`. */
28
+ export declare function getSolanaSourceMetadata(data?: Record<string, unknown>): SolanaSourceData | null;
29
+ /** Extract Solana identifiers, or `null` when this is not a Solana intent. */
30
+ export declare function getSolanaMetadata(data?: Record<string, unknown>, typestring?: string): SolanaExtraData | null;
31
+ export { EVM_TO_SOLANA_EXTRA_TYPESTRING, EVM_TO_SOLANA_FIELD_NAMES, SOLANA_MARKER_FIELD_NAMES, SOLANA_TO_EVM_EXTRA_TYPESTRING, };
@@ -0,0 +1,75 @@
1
+ import { EVM_TO_SOLANA_EXTRA_TYPESTRING, EVM_TO_SOLANA_FIELD_NAMES, SOLANA_MARKER_FIELD_NAMES, SOLANA_MINT_FIELD_NAME, SOLANA_RECIPIENT_ACCOUNT_FIELD_NAME, SOLANA_SOURCE_ACCOUNT_FIELD_NAME, SOLANA_SOURCE_MINT_FIELD_NAME, SOLANA_SOURCE_NONCE_FIELD_NAME, SOLANA_TO_EVM_EXTRA_TYPESTRING, fieldNamesFromExtraTypestring, } from "./witness-typestring.js";
2
+ import { isUint256Value } from "./value-validation.js";
3
+ function hasTruthyField(data, fieldName) {
4
+ if (!data)
5
+ return false;
6
+ const value = data[fieldName];
7
+ return typeof value === "string" && value.trim() !== "";
8
+ }
9
+ /** True when a typestring DECLARES a field with exactly this name. */
10
+ function declaresField(typestring, fieldName) {
11
+ if (!typestring.trim())
12
+ return false;
13
+ try {
14
+ return fieldNamesFromExtraTypestring(typestring).includes(fieldName);
15
+ }
16
+ catch {
17
+ // Malformed typestring declares nothing. The caller's own typestring
18
+ // validation is responsible for rejecting it.
19
+ return false;
20
+ }
21
+ }
22
+ /** True when mandate/extra data carries any Solana bridge marker field. */
23
+ function hasSolanaMarkersInData(data) {
24
+ return SOLANA_MARKER_FIELD_NAMES.some((name) => hasTruthyField(data, name));
25
+ }
26
+ /** True when a witness/extra typestring declares any Solana bridge marker field. */
27
+ function hasSolanaMarkersInTypestring(typestring) {
28
+ return SOLANA_MARKER_FIELD_NAMES.some((name) => declaresField(typestring, name));
29
+ }
30
+ /**
31
+ * True iff the intent is a Solana bridge intent.
32
+ * At least one of `typestring` or `data` must be provided.
33
+ */
34
+ export function isSolanaIntent(typestring, data) {
35
+ const hasTypestring = typestring !== undefined && typestring.trim() !== "";
36
+ const hasData = data !== undefined;
37
+ if (!hasTypestring && !hasData) {
38
+ throw new Error("isSolanaIntent requires typestring and/or data");
39
+ }
40
+ return ((hasTypestring && hasSolanaMarkersInTypestring(typestring)) ||
41
+ hasSolanaMarkersInData(data));
42
+ }
43
+ /** True when both Solana destination identifiers are present. */
44
+ export function isEVMToSolanaIntent(data) {
45
+ return (hasTruthyField(data, SOLANA_RECIPIENT_ACCOUNT_FIELD_NAME) &&
46
+ hasTruthyField(data, SOLANA_MINT_FIELD_NAME));
47
+ }
48
+ /** True when both Solana source identifiers are present. */
49
+ export function isSolanaToEVMIntent(data) {
50
+ return (hasTruthyField(data, SOLANA_SOURCE_ACCOUNT_FIELD_NAME) &&
51
+ hasTruthyField(data, SOLANA_SOURCE_MINT_FIELD_NAME));
52
+ }
53
+ /** Extract complete Solana source metadata or return `null`. */
54
+ export function getSolanaSourceMetadata(data) {
55
+ if (!isSolanaToEVMIntent(data))
56
+ return null;
57
+ const intentNonce = data?.[SOLANA_SOURCE_NONCE_FIELD_NAME];
58
+ if (!isUint256Value(intentNonce))
59
+ return null;
60
+ return {
61
+ solanaSourceAccount: data[SOLANA_SOURCE_ACCOUNT_FIELD_NAME],
62
+ solanaSourceMint: data[SOLANA_SOURCE_MINT_FIELD_NAME],
63
+ intentNonce: intentNonce,
64
+ };
65
+ }
66
+ /** Extract Solana identifiers, or `null` when this is not a Solana intent. */
67
+ export function getSolanaMetadata(data, typestring) {
68
+ if (!data || !isSolanaIntent(typestring, data))
69
+ return null;
70
+ return {
71
+ solanaRecipientAccount: data[SOLANA_RECIPIENT_ACCOUNT_FIELD_NAME],
72
+ solanaMint: data[SOLANA_MINT_FIELD_NAME],
73
+ };
74
+ }
75
+ export { EVM_TO_SOLANA_EXTRA_TYPESTRING, EVM_TO_SOLANA_FIELD_NAMES, SOLANA_MARKER_FIELD_NAMES, SOLANA_TO_EVM_EXTRA_TYPESTRING, };
@@ -0,0 +1 @@
1
+ export declare function isUint256Value(value: unknown): value is string | number | bigint;
@@ -0,0 +1,18 @@
1
+ export function isUint256Value(value) {
2
+ if (typeof value !== "string" &&
3
+ typeof value !== "number" &&
4
+ typeof value !== "bigint") {
5
+ return false;
6
+ }
7
+ if (typeof value === "string" && value.trim() === "")
8
+ return false;
9
+ if (typeof value === "number" && !Number.isSafeInteger(value))
10
+ return false;
11
+ try {
12
+ const parsed = BigInt(value);
13
+ return parsed >= 0n && parsed < 1n << 256n;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epoch-protocol/epoch-commons-sdk",
3
- "version": "0.1.20",
3
+ "version": "0.2.0",
4
4
  "description": "SDK for interacting with Epoch Commons Protocol",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,6 +24,9 @@
24
24
  ],
25
25
  "author": "",
26
26
  "license": "MIT",
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
27
30
  "dependencies": {
28
31
  "ethers": "^6.0.0",
29
32
  "viem": "2.42.0"