@switch-win/sdk 1.2.1 → 1.2.3

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.
@@ -1,321 +1,384 @@
1
- import { API_BASE } from "../constants.js";
2
-
3
- /** API network value used by Switch endpoints. */
4
- export const ROBINHOOD_NETWORK = "robinhood" as const;
5
-
6
- /** Robinhood Chain mainnet metadata. */
7
- export const ROBINHOOD_CHAIN = {
8
- id: 4663,
9
- name: "Robinhood Chain",
10
- nativeCurrency: {
11
- name: "Ether",
12
- symbol: "ETH",
13
- decimals: 18,
14
- },
15
- rpcUrls: ["https://rpc.mainnet.chain.robinhood.com"],
16
- blockExplorerUrl: "https://robinhoodchain.blockscout.com",
17
- } as const;
18
-
19
- /** Native-ETH sentinel accepted by the Switch quote API. */
20
- export const ROBINHOOD_NATIVE_ETH =
21
- "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
22
-
23
- /** Switch contracts deployed on Robinhood Chain. */
1
+ import { API_BASE } from "../constants.js";
2
+
3
+ /** API network value used by Switch endpoints. */
4
+ export const ROBINHOOD_NETWORK = "robinhood" as const;
5
+
6
+ /** Robinhood Chain mainnet metadata. */
7
+ export const ROBINHOOD_CHAIN = {
8
+ id: 4663,
9
+ name: "Robinhood Chain",
10
+ nativeCurrency: {
11
+ name: "Ether",
12
+ symbol: "ETH",
13
+ decimals: 18,
14
+ },
15
+ rpcUrls: ["https://rpc.mainnet.chain.robinhood.com"],
16
+ blockExplorerUrl: "https://robinhoodchain.blockscout.com",
17
+ } as const;
18
+
19
+ /** Native-ETH sentinel accepted by the Switch quote API. */
20
+ export const ROBINHOOD_NATIVE_ETH =
21
+ "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
22
+
23
+ /** Switch contracts deployed on Robinhood Chain. */
24
24
  export const ROBINHOOD_SWITCH_CONTRACTS = {
25
- /** ERC-20 approval target. Use quote.tx.to when submitting a swap. */
26
- router: "0x8730C3e2cF2c8CDa8E6166837A1Ed26f46aa9E59",
27
- routerView: "0xFF6b56d3F444eB5b7FA1db047F57140C84810376",
28
- uniswapV2Adapter: "0x7a14d7A8509a66209D4332843b983b29bF5604A4",
29
- uniswapV3Adapter: "0xbcA08f296d9Ba0dc19Aa0E05D355365cE29A3205",
25
+ /** ERC-20 approval target. Use quote.tx.to when submitting a swap. */
26
+ router: "0x8730C3e2cF2c8CDa8E6166837A1Ed26f46aa9E59",
27
+ routerView: "0xFF6b56d3F444eB5b7FA1db047F57140C84810376",
28
+ uniswapV2Adapter: "0x7a14d7A8509a66209D4332843b983b29bF5604A4",
29
+ uniswapV3Adapter: "0xbcA08f296d9Ba0dc19Aa0E05D355365cE29A3205",
30
30
  uniswapV4Adapter: "0x754dDCD05aFbAd1cc7Bc42B9268EB586F579E7F6",
31
+ limitOrder: "0x752c50DDd3B426cAE3D7A995F313Ac74ac6B0230",
32
+ nativeEthFlow: "0x029FfC6aF9112eA078f1D6f4a98826DDB2136cf6",
33
+ limitOrderAdapter: "0x412F625072c10e58C619D1e0b3C95cd3d5689871",
34
+ limitOrderAdapterIndex: 3,
31
35
  } as const;
32
36
 
33
- /** Uniswap contracts used by the Robinhood Switch deployment. */
34
- export const ROBINHOOD_UNISWAP_CONTRACTS = {
35
- v2Factory: "0x8bcEaA40B9AcdfAedF85AdF4FF01F5Ad6517937f",
36
- v3Factory: "0x1f7d7550B1b028f7571E69A784071F0205FD2EfA",
37
- v3QuoterV2: "0x33e885eD0Ec9bF04EcfB19341582aADCb4c8A9E7",
38
- v3SwapRouter02: "0xCaf681a66D020601342297493863E78C959E5cb2",
39
- /** Canonical Uniswap V4 singleton and periphery deployments. */
40
- v4PoolManager: "0x8366a39cc670b4001a1121b8f6a443a643e40951",
41
- v4PositionDescriptor: "0x9639443158e8c5efa35bd45287bf2effd3d8dc06",
42
- v4PositionManager: "0x58daec3116aae6d93017baaea7749052e8a04fa7",
43
- v4Quoter: "0x8dc178efb8111bb0973dd9d722ebeff267c98f94",
44
- v4StateView: "0xf3334192d15450cdd385c8b70e03f9a6bd9e673b",
45
- v4UniversalRouter: "0x8876789976decbfcbbbe364623c63652db8c0904",
46
- permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
37
+ /** EIP-712 domain used for Robinhood ERC-20 limit-order signatures. */
38
+ export const ROBINHOOD_LIMIT_ORDER_EIP712_DOMAIN = {
39
+ name: "SwitchLimitOrder",
40
+ version: "2",
41
+ chainId: ROBINHOOD_CHAIN.id,
42
+ verifyingContract: ROBINHOOD_SWITCH_CONTRACTS.limitOrder,
47
43
  } as const;
48
-
49
- export interface RobinhoodToken {
50
- readonly address: string;
51
- readonly symbol: string;
52
- readonly name: string;
53
- readonly decimals: number;
54
- }
55
-
56
- /**
57
- * Tokens in the Switch Robinhood frontend's curated list.
58
- *
59
- * Community-token addresses must never be selected by symbol alone; clones
60
- * can share the same symbol. The addresses, symbols, and decimals below were
61
- * checked against Robinhood Chain on 2026-07-11.
62
- */
63
- export const ROBINHOOD_TOKENS = {
64
- WETH: {
65
- address: "0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73",
66
- symbol: "WETH",
67
- name: "Wrapped Ether",
68
- decimals: 18,
69
- },
70
- USDG: {
71
- address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
72
- symbol: "USDG",
73
- name: "Global Dollar",
74
- decimals: 6,
75
- },
76
- VIRTUAL: {
77
- address: "0xc6911796042b15d7Fa4F6CDe69e245DdCd3d9c31",
78
- symbol: "VIRTUAL",
79
- name: "Virtuals Protocol",
80
- decimals: 18,
81
- },
82
- CASHCAT: {
83
- address: "0x020bfC650A365f8BB26819deAAbF3E21291018b4",
84
- symbol: "CASHCAT",
85
- name: "Cash Cat",
86
- decimals: 18,
87
- },
88
- WALLET: {
89
- address: "0x0339f5459FC690aC85F1782e15782A151b4A9E1b",
90
- symbol: "WALLET",
91
- name: "Robinhood Wallet",
92
- decimals: 18,
93
- },
44
+
45
+ /** Uniswap contracts used by the Robinhood Switch deployment. */
46
+ export const ROBINHOOD_UNISWAP_CONTRACTS = {
47
+ v2Factory: "0x8bcEaA40B9AcdfAedF85AdF4FF01F5Ad6517937f",
48
+ v3Factory: "0x1f7d7550B1b028f7571E69A784071F0205FD2EfA",
49
+ v3QuoterV2: "0x33e885eD0Ec9bF04EcfB19341582aADCb4c8A9E7",
50
+ v3SwapRouter02: "0xCaf681a66D020601342297493863E78C959E5cb2",
51
+ /** Canonical Uniswap V4 singleton and periphery deployments. */
52
+ v4PoolManager: "0x8366a39cc670b4001a1121b8f6a443a643e40951",
53
+ v4PositionDescriptor: "0x9639443158e8c5efa35bd45287bf2effd3d8dc06",
54
+ v4PositionManager: "0x58daec3116aae6d93017baaea7749052e8a04fa7",
55
+ v4Quoter: "0x8dc178efb8111bb0973dd9d722ebeff267c98f94",
56
+ v4StateView: "0xf3334192d15450cdd385c8b70e03f9a6bd9e673b",
57
+ v4UniversalRouter: "0x8876789976decbfcbbbe364623c63652db8c0904",
58
+ permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
59
+ } as const;
60
+
61
+ export interface RobinhoodToken {
62
+ readonly address: string;
63
+ readonly symbol: string;
64
+ readonly name: string;
65
+ readonly decimals: number;
66
+ }
67
+
68
+ /**
69
+ * Tokens in the Switch Robinhood frontend's curated list.
70
+ *
71
+ * Community-token addresses must never be selected by symbol alone; clones
72
+ * can share the same symbol. The addresses, symbols, and decimals below were
73
+ * checked against Robinhood Chain on 2026-07-11.
74
+ */
75
+ export const ROBINHOOD_TOKENS = {
76
+ WETH: {
77
+ address: "0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73",
78
+ symbol: "WETH",
79
+ name: "Wrapped Ether",
80
+ decimals: 18,
81
+ },
82
+ USDG: {
83
+ address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
84
+ symbol: "USDG",
85
+ name: "Global Dollar",
86
+ decimals: 6,
87
+ },
88
+ VIRTUAL: {
89
+ address: "0xc6911796042b15d7Fa4F6CDe69e245DdCd3d9c31",
90
+ symbol: "VIRTUAL",
91
+ name: "Virtuals Protocol",
92
+ decimals: 18,
93
+ },
94
+ CASHCAT: {
95
+ address: "0x020bfC650A365f8BB26819deAAbF3E21291018b4",
96
+ symbol: "CASHCAT",
97
+ name: "Cash Cat",
98
+ decimals: 18,
99
+ },
100
+ WALLET: {
101
+ address: "0x0339f5459FC690aC85F1782e15782A151b4A9E1b",
102
+ symbol: "WALLET",
103
+ name: "Robinhood Wallet",
104
+ decimals: 18,
105
+ },
94
106
  SEEDCOIN: {
95
- address: "0xD8d1C08A8bA4fc64BAC744f74290B89ADcb6Bf25",
96
- symbol: "seedcoin",
97
- name: "watch it grow",
98
- decimals: 18,
99
- },
100
- JUGGERNAUT: {
101
- address: "0xD7321801CAae694090694Ff55A9323139F043B88",
102
- symbol: "JUGGERNAUT",
103
- name: "The Juggernaut",
104
- decimals: 18,
105
- },
106
- HOODRAT: {
107
- address: "0x8e62F281f282686fCa6dCB39288069a93fC23F1c",
108
- symbol: "HOODRAT",
109
- name: "Hoodrat",
110
- decimals: 18,
111
- },
112
- DIH: {
113
- address: "0x17bb0C898254406b1Ea2e8E99B0C263e26c9E4a4",
114
- symbol: "DIH",
115
- name: "Dog In Hood",
116
- decimals: 18,
117
- },
118
- KITSU: {
119
- address: "0x8d4dFaaA4198b6486E0293Fec914C2B6a821D4DC",
120
- symbol: "KITSU",
121
- name: "KITSU",
107
+ address: "0x58f693A30F124E59b125F7c7b837b0F6bbAF5a45",
108
+ symbol: "SEEDCOIN",
109
+ name: "Seedcoin",
110
+ decimals: 18,
111
+ },
112
+ JUGGERNAUT: {
113
+ address: "0xD7321801CAae694090694Ff55A9323139F043B88",
114
+ symbol: "JUGGERNAUT",
115
+ name: "The Juggernaut",
116
+ decimals: 18,
117
+ },
118
+ HOODRAT: {
119
+ address: "0x8e62F281f282686fCa6dCB39288069a93fC23F1c",
120
+ symbol: "HOODRAT",
121
+ name: "Hoodrat",
122
+ decimals: 18,
123
+ },
124
+ DIH: {
125
+ address: "0x17bb0C898254406b1Ea2e8E99B0C263e26c9E4a4",
126
+ symbol: "DIH",
127
+ name: "Dog In Hood",
128
+ decimals: 18,
129
+ },
130
+ KITSU: {
131
+ address: "0x8d4dFaaA4198b6486E0293Fec914C2B6a821D4DC",
132
+ symbol: "KITSU",
133
+ name: "KITSU",
134
+ decimals: 18,
135
+ },
136
+ WEN: {
137
+ address: "0xA80eb66b3E0CF66ccB46f8b8C9e7ff5803eEb820",
138
+ symbol: "WEN",
139
+ name: "Wen Lambo",
140
+ decimals: 18,
141
+ },
142
+ REPE: {
143
+ address: "0x5266eeafF092D6136AB63D18B975A60a0Cc0C8f7",
144
+ symbol: "REPE",
145
+ name: "Robinhood Pepe",
146
+ decimals: 18,
147
+ },
148
+ TENDIES: {
149
+ address: "0x45242320DBB855EeA8Fd36804C6487E10E97FCF9",
150
+ symbol: "TENDIES",
151
+ name: "TENDIES",
152
+ decimals: 18,
153
+ },
154
+ GME: {
155
+ address: "0x7e86381A763F0Ecca2bDF27C54eAC403ddD48123",
156
+ symbol: "GME",
157
+ name: "GameStop",
158
+ decimals: 18,
159
+ },
160
+ TOKEN_4663: {
161
+ address: "0xd4052415613B34Af236024B895574c467f65b6dD",
162
+ symbol: "4663",
163
+ name: "4663",
164
+ decimals: 18,
165
+ },
166
+ MARIAN: {
167
+ address: "0x01637b14B7378B99dE75A64d50656d98488D9a4d",
168
+ symbol: "MARIAN",
169
+ name: "Lady Marian",
122
170
  decimals: 18,
123
171
  },
124
- WEN: {
125
- address: "0xA80eb66b3E0CF66ccB46f8b8C9e7ff5803eEb820",
126
- symbol: "WEN",
127
- name: "Wen Lambo",
172
+ INDEX: {
173
+ address: "0x56910D4409F3a0C78C64DD8D0545FF0705389870",
174
+ symbol: "Index",
175
+ name: "The Index",
128
176
  decimals: 18,
129
177
  },
130
- REPE: {
131
- address: "0x5266eeafF092D6136AB63D18B975A60a0Cc0C8f7",
132
- symbol: "REPE",
133
- name: "Robinhood Pepe",
178
+ VEX: {
179
+ address: "0x8Ff92566f2e81BDd68EDfAa8cde73942A723796b",
180
+ symbol: "VEX",
181
+ name: "ProjectVex",
134
182
  decimals: 18,
135
183
  },
136
- TENDIES: {
137
- address: "0x45242320DBB855EeA8Fd36804C6487E10E97FCF9",
138
- symbol: "TENDIES",
139
- name: "TENDIES",
184
+ HOODIE: {
185
+ address: "0xC72c01AAB5f5678dc1d6f5C6d2B417d91D402Ba3",
186
+ symbol: "HOODIE",
187
+ name: "HOODIE",
140
188
  decimals: 18,
141
189
  },
142
- GME: {
143
- address: "0x7e86381A763F0Ecca2bDF27C54eAC403ddD48123",
144
- symbol: "GME",
145
- name: "GameStop",
190
+ WISHBONE: {
191
+ address: "0x77581054581B9c525E7dd7a0155DE43867532d03",
192
+ symbol: "WISHBONE",
193
+ name: "WISHBONE",
146
194
  decimals: 18,
147
195
  },
148
- TOKEN_4663: {
149
- address: "0xd4052415613B34Af236024B895574c467f65b6dD",
150
- symbol: "4663",
151
- name: "4663",
196
+ VLAD: {
197
+ address: "0x31BE8f7485e36928C9De86566c62da82d4B6BF81",
198
+ symbol: "VLAD",
199
+ name: "The Green Bull",
152
200
  decimals: 18,
153
201
  },
154
- MARIAN: {
155
- address: "0x01637b14B7378B99dE75A64d50656d98488D9a4d",
156
- symbol: "MARIAN",
157
- name: "Lady Marian",
202
+ AEON: {
203
+ address: "0xd4c93eD1843606f92CccA078941f3d52A585982f",
204
+ symbol: "AEON",
205
+ name: "Aeon",
158
206
  decimals: 18,
159
207
  },
160
208
  } as const satisfies Readonly<Record<string, RobinhoodToken>>;
161
-
162
- /** Exact ERC-20 ordering used by the Switch Robinhood token dropdown. */
163
- export const ROBINHOOD_FRONTEND_TOKEN_LIST = [
164
- ROBINHOOD_TOKENS.WETH,
165
- ROBINHOOD_TOKENS.USDG,
166
- ROBINHOOD_TOKENS.VIRTUAL,
167
- ROBINHOOD_TOKENS.CASHCAT,
168
- ROBINHOOD_TOKENS.WALLET,
169
- ROBINHOOD_TOKENS.SEEDCOIN,
170
- ROBINHOOD_TOKENS.JUGGERNAUT,
171
- ROBINHOOD_TOKENS.HOODRAT,
172
- ROBINHOOD_TOKENS.DIH,
173
- ROBINHOOD_TOKENS.KITSU,
174
- ROBINHOOD_TOKENS.WEN,
175
- ROBINHOOD_TOKENS.REPE,
176
- ROBINHOOD_TOKENS.TENDIES,
177
- ROBINHOOD_TOKENS.GME,
209
+
210
+ /** Exact ERC-20 ordering used by the Switch Robinhood token dropdown. */
211
+ export const ROBINHOOD_FRONTEND_TOKEN_LIST = [
212
+ ROBINHOOD_TOKENS.WETH,
213
+ ROBINHOOD_TOKENS.USDG,
214
+ ROBINHOOD_TOKENS.VIRTUAL,
215
+ ROBINHOOD_TOKENS.CASHCAT,
216
+ ROBINHOOD_TOKENS.WALLET,
217
+ ROBINHOOD_TOKENS.SEEDCOIN,
218
+ ROBINHOOD_TOKENS.JUGGERNAUT,
219
+ ROBINHOOD_TOKENS.HOODRAT,
220
+ ROBINHOOD_TOKENS.DIH,
221
+ ROBINHOOD_TOKENS.KITSU,
222
+ ROBINHOOD_TOKENS.WEN,
223
+ ROBINHOOD_TOKENS.REPE,
224
+ ROBINHOOD_TOKENS.TENDIES,
225
+ ROBINHOOD_TOKENS.GME,
178
226
  ROBINHOOD_TOKENS.TOKEN_4663,
179
227
  ROBINHOOD_TOKENS.MARIAN,
180
- ] as const satisfies readonly RobinhoodToken[];
181
-
182
- /**
183
- * Tokens used to initialize the Robinhood swap UI.
184
- *
185
- * The current default pair is native ETH -> USDG. WETH remains in this set so
186
- * native/wrapped lookup and token selection use the canonical deployment.
187
- */
188
- export const ROBINHOOD_FRONTEND_DEFAULT_TOKENS = [
189
- {
190
- address: ROBINHOOD_NATIVE_ETH,
191
- symbol: "ETH",
192
- name: "Ether",
193
- decimals: 18,
194
- },
228
+ ROBINHOOD_TOKENS.INDEX,
229
+ ROBINHOOD_TOKENS.VEX,
230
+ ROBINHOOD_TOKENS.HOODIE,
231
+ ROBINHOOD_TOKENS.WISHBONE,
232
+ ROBINHOOD_TOKENS.VLAD,
233
+ ROBINHOOD_TOKENS.AEON,
234
+ ] as const satisfies readonly RobinhoodToken[];
235
+
236
+ /**
237
+ * Tokens used to initialize the Robinhood swap UI.
238
+ *
239
+ * The current default pair is native ETH -> USDG. WETH remains in this set so
240
+ * native/wrapped lookup and token selection use the canonical deployment.
241
+ */
242
+ export const ROBINHOOD_FRONTEND_DEFAULT_TOKENS = [
243
+ {
244
+ address: ROBINHOOD_NATIVE_ETH,
245
+ symbol: "ETH",
246
+ name: "Ether",
247
+ decimals: 18,
248
+ },
195
249
  ROBINHOOD_TOKENS.WETH,
196
250
  ROBINHOOD_TOKENS.USDG,
197
- ] as const satisfies readonly RobinhoodToken[];
198
-
199
- /**
200
- * Production trusted-hop addresses, ordered by routing usefulness.
201
- *
202
- * The 2026-07-11 audit enumerated every pool creation from the Robinhood
203
- * Uniswap V2/V3 factories, then required multiple meaningful counterparties
204
- * and a successful zero-tax simulation. Keep this list synchronized with the
205
- * Robinhood backend routing configuration.
206
- */
207
- export const ROBINHOOD_TRUSTED_INTERMEDIATES = [
208
- ROBINHOOD_TOKENS.WETH.address,
209
- ROBINHOOD_TOKENS.USDG.address,
251
+ ROBINHOOD_TOKENS.VIRTUAL,
252
+ ROBINHOOD_TOKENS.CASHCAT,
253
+ ROBINHOOD_TOKENS.INDEX,
254
+ ] as const satisfies readonly RobinhoodToken[];
255
+
256
+ /**
257
+ * Production trusted-hop addresses, ordered by routing usefulness.
258
+ *
259
+ * The 2026-07-16 audit combined Dexscreener pool connectivity with Switch tax
260
+ * simulation, then required multiple meaningful liquidity corridors. VEX is
261
+ * selectable but excluded because it is taxed; INDEX is excluded because its
262
+ * dominant liquidity currently depends on unsupported V4 hooks. Keep this
263
+ * list synchronized with the Robinhood backend and operator bot.
264
+ */
265
+ export const ROBINHOOD_TRUSTED_INTERMEDIATES = [
266
+ ROBINHOOD_TOKENS.WETH.address,
267
+ ROBINHOOD_TOKENS.USDG.address,
210
268
  ROBINHOOD_TOKENS.VIRTUAL.address,
211
269
  ROBINHOOD_TOKENS.CASHCAT.address,
212
- ] as const;
213
-
214
- /** Preferred Robinhood fee tokens, from highest to lowest priority. */
215
- export const ROBINHOOD_FEE_TOKEN_PRIORITY = [
216
- ROBINHOOD_TOKENS.WETH.address,
217
- ROBINHOOD_TOKENS.USDG.address,
270
+ ROBINHOOD_TOKENS.HOODRAT.address,
271
+ ROBINHOOD_TOKENS.TENDIES.address,
272
+ ROBINHOOD_TOKENS.JUGGERNAUT.address,
273
+ ROBINHOOD_TOKENS.MARIAN.address,
218
274
  ROBINHOOD_TOKENS.WALLET.address,
219
- ROBINHOOD_TOKENS.SEEDCOIN.address,
220
- ROBINHOOD_TOKENS.CASHCAT.address,
221
275
  ] as const;
222
-
223
- const ROBINHOOD_FEE_PRIORITY_BY_ADDRESS = new Map<string, number>(
224
- ROBINHOOD_FEE_TOKEN_PRIORITY.map((address, index) => [
225
- address.toLowerCase(),
226
- index + 1,
227
- ]),
228
- );
229
- ROBINHOOD_FEE_PRIORITY_BY_ADDRESS.set(
230
- ROBINHOOD_NATIVE_ETH.toLowerCase(),
231
- 1,
232
- );
233
-
234
- /** Minimum directional tax data required for fee-mode selection. */
235
- export interface RobinhoodTaxInfo {
236
- isTaxToken: boolean;
237
- buyTaxBps: number;
238
- sellTaxBps: number;
239
- }
240
-
241
- /**
242
- * Select the safer/preferred fee side for a Robinhood swap.
243
- *
244
- * Tax safety takes precedence over fee-token preference. Native ETH is treated
245
- * as equivalent to WETH. `false` means fee on input; `true` means fee on output.
246
- */
247
- export function selectRobinhoodFeeOnOutput(
248
- from: string,
249
- to: string,
250
- fromTax: RobinhoodTaxInfo,
251
- toTax: RobinhoodTaxInfo,
252
- ): boolean {
253
- const inputHasSellTax = fromTax.isTaxToken && fromTax.sellTaxBps > 0;
254
- const outputHasBuyTax = toTax.isTaxToken && toTax.buyTaxBps > 0;
255
-
256
- if (inputHasSellTax && outputHasBuyTax) return false;
257
- if (outputHasBuyTax) return false;
258
- if (inputHasSellTax) return true;
259
-
260
- const inputPriority =
261
- ROBINHOOD_FEE_PRIORITY_BY_ADDRESS.get(from.toLowerCase()) ?? Infinity;
262
- const outputPriority =
263
- ROBINHOOD_FEE_PRIORITY_BY_ADDRESS.get(to.toLowerCase()) ?? Infinity;
264
-
265
- if (outputPriority < inputPriority) return true;
266
- if (inputPriority < outputPriority) return false;
267
- if (inputPriority !== Infinity) return true;
268
- return false;
269
- }
270
-
271
- export interface RobinhoodQuoteParams {
272
- from: string;
273
- to: string;
274
- /** Raw integer input amount in the input token's smallest unit. */
275
- amount: string | bigint;
276
- sender?: string;
277
- receiver?: string;
278
- /** Slippage in basis points. */
279
- slippage?: number;
280
- /** Partner fee in basis points. */
281
- fee?: number;
282
- partnerAddress?: string;
283
- feeOnOutput?: boolean;
284
- adapters?: string | readonly number[];
285
- }
286
-
287
- /** Build a Robinhood quote URL with the required network value included. */
288
- export function buildRobinhoodQuoteUrl(
289
- params: RobinhoodQuoteParams,
290
- apiBase: string = API_BASE,
291
- ): string {
292
- const query = new URLSearchParams({
293
- network: ROBINHOOD_NETWORK,
294
- from: params.from,
295
- to: params.to,
296
- amount: params.amount.toString(),
297
- });
298
-
299
- if (params.sender) query.set("sender", params.sender);
300
- if (params.receiver) query.set("receiver", params.receiver);
301
- if (params.slippage !== undefined) {
302
- query.set("slippage", params.slippage.toString());
303
- }
304
- if (params.fee !== undefined) query.set("fee", params.fee.toString());
305
- if (params.partnerAddress) {
306
- query.set("partnerAddress", params.partnerAddress);
307
- }
308
- if (params.feeOnOutput !== undefined) {
309
- query.set("feeOnOutput", params.feeOnOutput.toString());
310
- }
311
- if (params.adapters !== undefined) {
312
- query.set(
313
- "adapters",
314
- Array.isArray(params.adapters)
315
- ? params.adapters.join(",")
316
- : String(params.adapters),
317
- );
318
- }
319
-
320
- return `${apiBase.replace(/\/$/, "")}/swap/quote?${query.toString()}`;
321
- }
276
+
277
+ /** Preferred Robinhood fee tokens, from highest to lowest priority. */
278
+ export const ROBINHOOD_FEE_TOKEN_PRIORITY = [
279
+ ROBINHOOD_TOKENS.WETH.address,
280
+ ROBINHOOD_TOKENS.USDG.address,
281
+ ROBINHOOD_TOKENS.WALLET.address,
282
+ ROBINHOOD_TOKENS.SEEDCOIN.address,
283
+ ROBINHOOD_TOKENS.CASHCAT.address,
284
+ ] as const;
285
+
286
+ const ROBINHOOD_FEE_PRIORITY_BY_ADDRESS = new Map<string, number>(
287
+ ROBINHOOD_FEE_TOKEN_PRIORITY.map((address, index) => [
288
+ address.toLowerCase(),
289
+ index + 1,
290
+ ]),
291
+ );
292
+ ROBINHOOD_FEE_PRIORITY_BY_ADDRESS.set(
293
+ ROBINHOOD_NATIVE_ETH.toLowerCase(),
294
+ 1,
295
+ );
296
+
297
+ /** Minimum directional tax data required for fee-mode selection. */
298
+ export interface RobinhoodTaxInfo {
299
+ isTaxToken: boolean;
300
+ buyTaxBps: number;
301
+ sellTaxBps: number;
302
+ }
303
+
304
+ /**
305
+ * Select the safer/preferred fee side for a Robinhood swap.
306
+ *
307
+ * Tax safety takes precedence over fee-token preference. Native ETH is treated
308
+ * as equivalent to WETH. `false` means fee on input; `true` means fee on output.
309
+ */
310
+ export function selectRobinhoodFeeOnOutput(
311
+ from: string,
312
+ to: string,
313
+ fromTax: RobinhoodTaxInfo,
314
+ toTax: RobinhoodTaxInfo,
315
+ ): boolean {
316
+ const inputHasSellTax = fromTax.isTaxToken && fromTax.sellTaxBps > 0;
317
+ const outputHasBuyTax = toTax.isTaxToken && toTax.buyTaxBps > 0;
318
+
319
+ if (inputHasSellTax && outputHasBuyTax) return false;
320
+ if (outputHasBuyTax) return false;
321
+ if (inputHasSellTax) return true;
322
+
323
+ const inputPriority =
324
+ ROBINHOOD_FEE_PRIORITY_BY_ADDRESS.get(from.toLowerCase()) ?? Infinity;
325
+ const outputPriority =
326
+ ROBINHOOD_FEE_PRIORITY_BY_ADDRESS.get(to.toLowerCase()) ?? Infinity;
327
+
328
+ if (outputPriority < inputPriority) return true;
329
+ if (inputPriority < outputPriority) return false;
330
+ if (inputPriority !== Infinity) return true;
331
+ return false;
332
+ }
333
+
334
+ export interface RobinhoodQuoteParams {
335
+ from: string;
336
+ to: string;
337
+ /** Raw integer input amount in the input token's smallest unit. */
338
+ amount: string | bigint;
339
+ sender?: string;
340
+ receiver?: string;
341
+ /** Slippage in basis points. */
342
+ slippage?: number;
343
+ /** Partner fee in basis points. */
344
+ fee?: number;
345
+ partnerAddress?: string;
346
+ feeOnOutput?: boolean;
347
+ adapters?: string | readonly number[];
348
+ }
349
+
350
+ /** Build a Robinhood quote URL with the required network value included. */
351
+ export function buildRobinhoodQuoteUrl(
352
+ params: RobinhoodQuoteParams,
353
+ apiBase: string = API_BASE,
354
+ ): string {
355
+ const query = new URLSearchParams({
356
+ network: ROBINHOOD_NETWORK,
357
+ from: params.from,
358
+ to: params.to,
359
+ amount: params.amount.toString(),
360
+ });
361
+
362
+ if (params.sender) query.set("sender", params.sender);
363
+ if (params.receiver) query.set("receiver", params.receiver);
364
+ if (params.slippage !== undefined) {
365
+ query.set("slippage", params.slippage.toString());
366
+ }
367
+ if (params.fee !== undefined) query.set("fee", params.fee.toString());
368
+ if (params.partnerAddress) {
369
+ query.set("partnerAddress", params.partnerAddress);
370
+ }
371
+ if (params.feeOnOutput !== undefined) {
372
+ query.set("feeOnOutput", params.feeOnOutput.toString());
373
+ }
374
+ if (params.adapters !== undefined) {
375
+ query.set(
376
+ "adapters",
377
+ Array.isArray(params.adapters)
378
+ ? params.adapters.join(",")
379
+ : String(params.adapters),
380
+ );
381
+ }
382
+
383
+ return `${apiBase.replace(/\/$/, "")}/swap/quote?${query.toString()}`;
384
+ }