@getcofund/protocol 0.0.0 → 0.1.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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +413 -1
  3. package/dist/btc/index.d.ts +12 -0
  4. package/dist/btc/index.js +13 -0
  5. package/dist/btc/psbt.d.ts +121 -0
  6. package/dist/btc/psbt.js +377 -0
  7. package/dist/btc/reader.d.ts +42 -0
  8. package/dist/btc/reader.js +85 -0
  9. package/dist/btc/select.d.ts +48 -0
  10. package/dist/btc/select.js +59 -0
  11. package/dist/btc/spend-rules.d.ts +24 -0
  12. package/dist/btc/spend-rules.js +43 -0
  13. package/dist/btc/spend.d.ts +136 -0
  14. package/dist/btc/spend.js +180 -0
  15. package/dist/btc/types.d.ts +65 -0
  16. package/dist/btc/types.js +62 -0
  17. package/dist/btc/vault/bip341-nums.d.ts +35 -0
  18. package/dist/btc/vault/bip341-nums.js +121 -0
  19. package/dist/btc/vault/build.d.ts +5 -0
  20. package/dist/btc/vault/build.js +136 -0
  21. package/dist/btc/vault/canonical-order.d.ts +10 -0
  22. package/dist/btc/vault/canonical-order.js +18 -0
  23. package/dist/btc/vault/from-contract-id.d.ts +9 -0
  24. package/dist/btc/vault/from-contract-id.js +74 -0
  25. package/dist/btc/vault/helpers.d.ts +73 -0
  26. package/dist/btc/vault/helpers.js +129 -0
  27. package/dist/btc/vault/index.d.ts +21 -0
  28. package/dist/btc/vault/index.js +21 -0
  29. package/dist/btc/vault/limits.d.ts +9 -0
  30. package/dist/btc/vault/limits.js +9 -0
  31. package/dist/btc/vault/participant-order.d.ts +69 -0
  32. package/dist/btc/vault/participant-order.js +135 -0
  33. package/dist/btc/vault/script.d.ts +13 -0
  34. package/dist/btc/vault/script.js +39 -0
  35. package/dist/btc/vault/spend.d.ts +63 -0
  36. package/dist/btc/vault/spend.js +93 -0
  37. package/dist/btc/vault/tx-size.d.ts +66 -0
  38. package/dist/btc/vault/tx-size.js +71 -0
  39. package/dist/btc/vault/types.d.ts +49 -0
  40. package/dist/btc/vault/types.js +1 -0
  41. package/dist/btc/vault/vault-policy.d.ts +109 -0
  42. package/dist/btc/vault/vault-policy.js +213 -0
  43. package/dist/btc/vault/vaultContractUtil.d.ts +80 -0
  44. package/dist/btc/vault/vaultContractUtil.js +87 -0
  45. package/dist/btc/vault/xpub.d.ts +28 -0
  46. package/dist/btc/vault/xpub.js +41 -0
  47. package/dist/btc/witness.d.ts +101 -0
  48. package/dist/btc/witness.js +439 -0
  49. package/dist/btc-policies/activation.d.ts +76 -0
  50. package/dist/btc-policies/activation.js +153 -0
  51. package/dist/btc-policies/index.d.ts +7 -0
  52. package/dist/btc-policies/index.js +8 -0
  53. package/dist/btc-policies/policy.d.ts +55 -0
  54. package/dist/btc-policies/policy.js +210 -0
  55. package/dist/evm/abis/index.d.ts +10 -0
  56. package/dist/evm/abis/index.js +11 -0
  57. package/dist/evm/abis/policies.d.ts +279 -0
  58. package/dist/evm/abis/policies.js +363 -0
  59. package/dist/evm/abis/sponsor-guard.d.ts +201 -0
  60. package/dist/evm/abis/sponsor-guard.js +263 -0
  61. package/dist/evm/abis/state.d.ts +989 -0
  62. package/dist/evm/abis/state.js +1290 -0
  63. package/dist/evm/abis/users.d.ts +408 -0
  64. package/dist/evm/abis/users.js +532 -0
  65. package/dist/evm/abis/vault-factory.d.ts +92 -0
  66. package/dist/evm/abis/vault-factory.js +126 -0
  67. package/dist/evm/abis/vault.d.ts +343 -0
  68. package/dist/evm/abis/vault.js +444 -0
  69. package/dist/evm/address.d.ts +2 -0
  70. package/dist/evm/address.js +4 -0
  71. package/dist/evm/calls/common.d.ts +30 -0
  72. package/dist/evm/calls/common.js +51 -0
  73. package/dist/evm/calls/index.d.ts +11 -0
  74. package/dist/evm/calls/index.js +4 -0
  75. package/dist/evm/calls/policies.d.ts +25 -0
  76. package/dist/evm/calls/policies.js +173 -0
  77. package/dist/evm/calls/users.d.ts +61 -0
  78. package/dist/evm/calls/users.js +353 -0
  79. package/dist/evm/calls/vault.d.ts +25 -0
  80. package/dist/evm/calls/vault.js +184 -0
  81. package/dist/evm/chain.d.ts +74 -0
  82. package/dist/evm/chain.js +48 -0
  83. package/dist/evm/company-name.d.ts +46 -0
  84. package/dist/evm/company-name.js +64 -0
  85. package/dist/evm/deployment.d.ts +47 -0
  86. package/dist/evm/deployment.js +110 -0
  87. package/dist/evm/eip712.d.ts +271 -0
  88. package/dist/evm/eip712.js +224 -0
  89. package/dist/evm/index.d.ts +20 -0
  90. package/dist/evm/index.js +21 -0
  91. package/dist/evm/reads.d.ts +64 -0
  92. package/dist/evm/reads.js +131 -0
  93. package/dist/evm/signatures.d.ts +57 -0
  94. package/dist/evm/signatures.js +105 -0
  95. package/dist/evm/simulate/balances.d.ts +76 -0
  96. package/dist/evm/simulate/balances.js +201 -0
  97. package/dist/evm/simulate/describe.d.ts +79 -0
  98. package/dist/evm/simulate/describe.js +188 -0
  99. package/dist/evm/simulate/index.d.ts +12 -0
  100. package/dist/evm/simulate/index.js +13 -0
  101. package/dist/evm/simulate/own-node.d.ts +62 -0
  102. package/dist/evm/simulate/own-node.js +122 -0
  103. package/dist/evm/simulate/request.d.ts +86 -0
  104. package/dist/evm/simulate/request.js +235 -0
  105. package/dist/evm/simulate/revert.d.ts +29 -0
  106. package/dist/evm/simulate/revert.js +192 -0
  107. package/dist/evm/simulate/run.d.ts +51 -0
  108. package/dist/evm/simulate/run.js +214 -0
  109. package/dist/evm/stocks/decode-stock-swap.d.ts +32 -0
  110. package/dist/evm/stocks/decode-stock-swap.js +79 -0
  111. package/dist/evm/stocks/encode-swap.d.ts +45 -0
  112. package/dist/evm/stocks/encode-swap.js +45 -0
  113. package/dist/evm/stocks/index.d.ts +9 -0
  114. package/dist/evm/stocks/index.js +10 -0
  115. package/dist/evm/stocks/quote.d.ts +19 -0
  116. package/dist/evm/stocks/quote.js +46 -0
  117. package/dist/evm/stocks/stock-pools.d.ts +61 -0
  118. package/dist/evm/stocks/stock-pools.js +179 -0
  119. package/dist/evm/submitters.d.ts +32 -0
  120. package/dist/evm/submitters.js +105 -0
  121. package/dist/evm/types.d.ts +146 -0
  122. package/dist/evm/types.js +128 -0
  123. package/dist/keys/index.d.ts +6 -0
  124. package/dist/keys/index.js +7 -0
  125. package/dist/keys/keys.d.ts +63 -0
  126. package/dist/keys/keys.js +80 -0
  127. package/dist/keys/xpub-ownership.d.ts +10 -0
  128. package/dist/keys/xpub-ownership.js +13 -0
  129. package/package.json +61 -3
@@ -0,0 +1,214 @@
1
+ import { z } from "zod";
2
+ import { Bytes32, CallEffectSchema, CallSimulationSchema, Hex, } from "../types.js";
3
+ import { balanceOf, capBalances, claimedDecimals, claimedSymbol, decodeBalanceLogs, NATIVE_ASSET, RpcLogSchema, RpcQuantity, toChanges, tokenReads, } from "./balances.js";
4
+ import { guardFailure, guardsOf, simulateParams, txCall, } from "./request.js";
5
+ import { cleanReason, nodeFailure, revertReason } from "./revert.js";
6
+ /** `node` as a `send`. */
7
+ export function nodeSend(node) {
8
+ return typeof node === "function"
9
+ ? node
10
+ : (method, params) => node.request({ method, params });
11
+ }
12
+ const SimulatedCallResult = z.object({
13
+ status: RpcQuantity,
14
+ returnData: Hex,
15
+ gasUsed: RpcQuantity,
16
+ logs: z.array(RpcLogSchema).default([]),
17
+ error: z
18
+ .object({
19
+ message: z.string().max(10_000).optional(),
20
+ data: Hex.optional(),
21
+ })
22
+ .loose()
23
+ .optional(),
24
+ });
25
+ const SimulateResult = z
26
+ .array(z.object({ calls: z.array(SimulatedCallResult) }).loose())
27
+ .length(1);
28
+ const BlockHead = z
29
+ .object({
30
+ number: RpcQuantity,
31
+ hash: Bytes32,
32
+ baseFeePerGas: RpcQuantity.optional(),
33
+ })
34
+ .loose();
35
+ const unavailable = (reason) => ({
36
+ outcome: "unavailable",
37
+ reason,
38
+ });
39
+ /**
40
+ * Simulate `target` on `node`: the call's balance changes, or why it
41
+ * reverts, or `unavailable` (the node failed, a guard did not hold, the call
42
+ * ran differently twice, or the result is not one the API can carry). Never
43
+ * throws.
44
+ */
45
+ export async function simulateCall(node, target, options = {}) {
46
+ const at = (options.now?.() ?? new Date()).toISOString();
47
+ let outcome;
48
+ try {
49
+ outcome = await run(nodeSend(node), target, options);
50
+ }
51
+ catch (error) {
52
+ outcome = unavailable(nodeFailure(error));
53
+ }
54
+ const parsed = CallSimulationSchema.safeParse({ ...outcome, at });
55
+ return parsed.success
56
+ ? parsed.data
57
+ : {
58
+ outcome: "unavailable",
59
+ at,
60
+ reason: "the simulation's result could not be reported",
61
+ };
62
+ }
63
+ async function run(send, target, options) {
64
+ const head = BlockHead.parse(await send("eth_getBlockByNumber", ["latest", false]));
65
+ const block = { number: BigInt(head.number).toString(), hash: head.hash };
66
+ const { overrides, guards } = guardsOf(target);
67
+ const pass = { head, overrides, guards };
68
+ const first = await simulateBlock(send, pass, [
69
+ ...pass.guards,
70
+ txCall(target),
71
+ ]);
72
+ const failure = guardFailure(target, first);
73
+ if (failure) {
74
+ return unavailable(failure);
75
+ }
76
+ const result = first[pass.guards.length];
77
+ if (!result) {
78
+ return unavailable("the node returned no result for the call");
79
+ }
80
+ if (BigInt(result.status) !== 1n) {
81
+ return { outcome: "revert", block, reason: reasonOf(result, options) };
82
+ }
83
+ const roles = { vault: target.vault, wrapper: target.wrapper };
84
+ const decoded = decodeBalanceLogs(result.logs);
85
+ const { kept, dropped } = capBalances(decoded.balances, roles);
86
+ const exact = await exactPass({ send, target, pass, options }, kept, result.logs);
87
+ if (typeof exact === "string") {
88
+ return unavailable(exact);
89
+ }
90
+ return {
91
+ outcome: "success",
92
+ block,
93
+ gasUsed: BigInt(result.gasUsed).toString(),
94
+ changes: toChanges(exact.balances, roles, exact.token),
95
+ incomplete: decoded.incomplete || dropped || exact.incomplete,
96
+ };
97
+ }
98
+ /** Why a simulated call reverted: its revert data decoded, else the node's message (out of gas). */
99
+ function reasonOf(result, { unlisted }) {
100
+ const data = result.returnData !== "0x"
101
+ ? result.returnData
102
+ : (result.error?.data ?? "0x");
103
+ if (data === "0x" && result.error?.message) {
104
+ return cleanReason(result.error.message);
105
+ }
106
+ return revertReason(data, { unlisted });
107
+ }
108
+ async function simulateBlock(send, pass, calls) {
109
+ const [blockResult] = SimulateResult.parse(await send("eth_simulateV1", simulateParams(pass.head, pass.overrides, calls)));
110
+ return blockResult.calls;
111
+ }
112
+ const sameLogs = (a, b) => a.length === b.length &&
113
+ a.every((log, i) => log.address.toLowerCase() === b[i].address.toLowerCase() &&
114
+ log.data.toLowerCase() === b[i].data.toLowerCase() &&
115
+ log.topics.join().toLowerCase() === b[i].topics.join().toLowerCase());
116
+ /**
117
+ * The same block again with every kept ERC-20 balance read before and after
118
+ * the call, and the unlisted tokens' own symbol and decimals: the deltas are
119
+ * these reads. A native balance is its transfers' net, which is exact. A
120
+ * balance that cannot be read keeps its logs' net, and marks it incomplete.
121
+ * Why not, when the guards fail or the call does not run as it did.
122
+ */
123
+ async function exactPass({ send, target, pass, options }, kept, logs) {
124
+ const from = target.tx.from;
125
+ const erc20 = kept.filter(({ asset }) => asset !== NATIVE_ASSET);
126
+ const unlisted = [
127
+ ...new Set(erc20.map(({ asset }) => asset).filter((a) => !options.listedToken?.(a))),
128
+ ];
129
+ const meta = unlisted.flatMap((token) => [
130
+ { from, to: token, data: tokenReads.symbol },
131
+ { from, to: token, data: tokenReads.decimals },
132
+ ]);
133
+ const reads = erc20.map(({ asset, holder }) => ({
134
+ from,
135
+ to: asset,
136
+ data: tokenReads.balanceOf(holder),
137
+ }));
138
+ const results = await simulateBlock(send, pass, [
139
+ ...pass.guards,
140
+ ...meta,
141
+ ...reads,
142
+ txCall(target),
143
+ ...reads,
144
+ ]);
145
+ const callAt = pass.guards.length + meta.length + reads.length;
146
+ const call = results[callAt];
147
+ if (guardFailure(target, results) ||
148
+ !call ||
149
+ BigInt(call.status) !== 1n ||
150
+ !sameLogs(call.logs, logs)) {
151
+ return "the call did not run the same way twice in one block";
152
+ }
153
+ const claims = new Map(unlisted.map((token, i) => {
154
+ const [symbol, decimals] = results.slice(pass.guards.length + 2 * i, pass.guards.length + 2 * i + 2);
155
+ return [
156
+ token,
157
+ {
158
+ symbol: BigInt(symbol.status) === 1n
159
+ ? claimedSymbol(symbol.returnData)
160
+ : null,
161
+ decimals: BigInt(decimals.status) === 1n
162
+ ? claimedDecimals(decimals.returnData)
163
+ : null,
164
+ listed: false,
165
+ },
166
+ ];
167
+ }));
168
+ let incomplete = false;
169
+ const exact = new Map();
170
+ erc20.forEach(({ asset, holder }, i) => {
171
+ const before = balanceOf(results[callAt - reads.length + i]);
172
+ const after = balanceOf(results[callAt + 1 + i]);
173
+ if (before === null || after === null) {
174
+ incomplete = true;
175
+ }
176
+ else {
177
+ exact.set(`${asset}|${holder}`, after - before);
178
+ }
179
+ });
180
+ return {
181
+ balances: kept.map((b) => ({
182
+ ...b,
183
+ delta: exact.get(`${b.asset}|${b.holder}`) ?? b.delta,
184
+ })),
185
+ token: (address) => {
186
+ const listed = options.listedToken?.(address);
187
+ return listed
188
+ ? { ...listed, listed: true }
189
+ : (claims.get(address) ?? {
190
+ symbol: null,
191
+ decimals: null,
192
+ listed: false,
193
+ });
194
+ },
195
+ incomplete,
196
+ };
197
+ }
198
+ /**
199
+ * What an executed call did, from its own logs (a sponsored operation's, not
200
+ * its bundle's): summed per holder, the tokens named by `token`. The native
201
+ * coin moves without a log, so its moves are not in it.
202
+ */
203
+ export function effectFromLogs(input, token, now = new Date()) {
204
+ const decoded = decodeBalanceLogs(input.logs);
205
+ const { kept, dropped } = capBalances(decoded.balances, input.roles);
206
+ const parsed = CallEffectSchema.safeParse({
207
+ at: now.toISOString(),
208
+ block: { number: input.block.number.toString(), hash: input.block.hash },
209
+ hash: input.hash,
210
+ changes: toChanges(kept, input.roles, token),
211
+ incomplete: decoded.incomplete || dropped,
212
+ });
213
+ return parsed.success ? parsed.data : null;
214
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * HOOD: read a StockSwap protocol call back from its signed instructions, so
3
+ * the Requests table, the review drawer and the emails can say "Buy TSLA with
4
+ * 2.50 USDG" instead of "StockSwap · N/A". Pure: no server imports, so the
5
+ * client-side table can use it too.
6
+ */
7
+ export type DecodedStockSwap = {
8
+ side: "buy" | "sell";
9
+ symbol: string;
10
+ /** Issuer name from the pool table, or null for an unknown token. */
11
+ name: string | null;
12
+ /** Base units: USDG (6 dec) on a buy, shares (18 dec) on a sell. */
13
+ amountIn: string;
14
+ /** Base units: shares on a buy, USDG on a sell. */
15
+ minOut: string;
16
+ };
17
+ /** Null when the payload is not a wrapper swap (foreign protocol, bad hex). */
18
+ export declare function decodeStockSwap(instructions: unknown): DecodedStockSwap | null;
19
+ export type StockSwapCopy = {
20
+ /** "Buy TSLA with 2.50 USDG" / "Sell 0.007 TSLA for USDG". */
21
+ title: string;
22
+ /** "Buy" / "Sell". */
23
+ action: string;
24
+ /** What leaves the vault: the USDG on a buy, the shares on a sell. */
25
+ amountLabel: string;
26
+ rows: {
27
+ label: string;
28
+ value: string;
29
+ }[];
30
+ };
31
+ /** Plain-English copy for a decoded swap, shared by table, drawer and mail. */
32
+ export declare function describeStockSwap(swap: DecodedStockSwap): StockSwapCopy;
@@ -0,0 +1,79 @@
1
+ import { decodeAbiParameters, formatUnits } from "viem";
2
+ import { SWAP_PARAMS } from "./encode-swap.js";
3
+ import { STOCK_POOLS, STOCK_SWAP_SLIPPAGE_BPS, STOCK_TOKEN_DECIMALS, USDG, } from "./stock-pools.js";
4
+ function poolForToken(address) {
5
+ const lower = address.toLowerCase();
6
+ return Object.values(STOCK_POOLS).find((p) => p.address.toLowerCase() === lower);
7
+ }
8
+ /** Null when the payload is not a wrapper swap (foreign protocol, bad hex). */
9
+ export function decodeStockSwap(instructions) {
10
+ if (typeof instructions !== "string") {
11
+ return null;
12
+ }
13
+ try {
14
+ const [tokenIn, tokenOut, amountIn, minOut] = decodeAbiParameters(SWAP_PARAMS, instructions);
15
+ const isBuy = String(tokenIn).toLowerCase() === USDG.address.toLowerCase();
16
+ const pool = poolForToken(String(isBuy ? tokenOut : tokenIn));
17
+ return {
18
+ side: isBuy ? "buy" : "sell",
19
+ symbol: pool?.symbol ?? "?",
20
+ name: pool?.name ?? null,
21
+ amountIn: String(amountIn),
22
+ minOut: String(minOut),
23
+ };
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ function usdgLabel(units) {
30
+ const value = Number(formatUnits(BigInt(units), USDG.decimals));
31
+ return `${value.toLocaleString("en-US", {
32
+ minimumFractionDigits: 2,
33
+ maximumFractionDigits: 2,
34
+ })} ${USDG.symbol}`;
35
+ }
36
+ function sharesLabel(units, symbol) {
37
+ const value = Number(formatUnits(BigInt(units), STOCK_TOKEN_DECIMALS));
38
+ return `${value.toLocaleString("en-US", { maximumFractionDigits: 4 })} ${symbol}`;
39
+ }
40
+ /** Plain-English copy for a decoded swap, shared by table, drawer and mail. */
41
+ export function describeStockSwap(swap) {
42
+ const slippage = `${STOCK_SWAP_SLIPPAGE_BPS / 100}% slippage`;
43
+ const stock = swap.name ? `${swap.symbol} · ${swap.name}` : swap.symbol;
44
+ const venue = "Uniswap v4 · Robinhood Chain";
45
+ if (swap.side === "buy") {
46
+ const pay = usdgLabel(swap.amountIn);
47
+ return {
48
+ title: `Buy ${swap.symbol} with ${pay}`,
49
+ action: "Buy",
50
+ amountLabel: pay,
51
+ rows: [
52
+ { label: "Action", value: "Buy" },
53
+ { label: "Stock", value: stock },
54
+ { label: "You pay", value: pay },
55
+ {
56
+ label: "Minimum received",
57
+ value: `${sharesLabel(swap.minOut, swap.symbol)} (${slippage})`,
58
+ },
59
+ { label: "Venue", value: venue },
60
+ ],
61
+ };
62
+ }
63
+ const sell = sharesLabel(swap.amountIn, swap.symbol);
64
+ return {
65
+ title: `Sell ${sell} for ${USDG.symbol}`,
66
+ action: "Sell",
67
+ amountLabel: sell,
68
+ rows: [
69
+ { label: "Action", value: "Sell" },
70
+ { label: "Stock", value: stock },
71
+ { label: "You sell", value: sell },
72
+ {
73
+ label: "Minimum received",
74
+ value: `${usdgLabel(swap.minOut)} (${slippage})`,
75
+ },
76
+ { label: "Venue", value: venue },
77
+ ],
78
+ };
79
+ }
@@ -0,0 +1,45 @@
1
+ import { type Hex } from "viem";
2
+ import { type StockPool } from "./stock-pools.ts";
3
+ /**
4
+ * HOOD: instruction encoding for `GroupCashSwapWrapperV4.routerWrapper`.
5
+ * The wrapper decodes exactly
6
+ * (address tokenIn, address tokenOut, uint256 amountIn, uint256 minOut,
7
+ * uint24 fee, int24 tickSpacing)
8
+ * pulls `amountIn` of tokenIn from the calling vault, swaps on the v4 pool
9
+ * identified by (fee, tickSpacing) and reverts unless the vault receives at
10
+ * least `minOut`. The approvers therefore sign the economic bounds, not a
11
+ * price.
12
+ */
13
+ export type SwapSide = "buy" | "sell";
14
+ /** The wrapper's six-field instructions. */
15
+ export declare const SWAP_PARAMS: readonly [{
16
+ readonly type: "address";
17
+ }, {
18
+ readonly type: "address";
19
+ }, {
20
+ readonly type: "uint256";
21
+ }, {
22
+ readonly type: "uint256";
23
+ }, {
24
+ readonly type: "uint24";
25
+ }, {
26
+ readonly type: "int24";
27
+ }];
28
+ export type SwapLegs = {
29
+ tokenIn: string;
30
+ tokenOut: string;
31
+ decimalsIn: number;
32
+ decimalsOut: number;
33
+ };
34
+ /** Which token goes in and out for a buy (USDG -> stock) or sell. */
35
+ export declare function swapLegs(pool: StockPool, side: SwapSide): SwapLegs;
36
+ export declare function encodeV4SwapInstructions(params: {
37
+ tokenIn: string;
38
+ tokenOut: string;
39
+ amountIn: bigint;
40
+ minOut: bigint;
41
+ fee: number;
42
+ tickSpacing: number;
43
+ }): Hex;
44
+ /** Minimum acceptable output for a quoted amount, floored. */
45
+ export declare function applySlippage(amountOut: bigint, bps?: number): bigint;
@@ -0,0 +1,45 @@
1
+ import { encodeAbiParameters } from "viem";
2
+ import { STOCK_SWAP_SLIPPAGE_BPS, STOCK_TOKEN_DECIMALS, USDG, } from "./stock-pools.js";
3
+ /** The wrapper's six-field instructions. */
4
+ export const SWAP_PARAMS = [
5
+ { type: "address" },
6
+ { type: "address" },
7
+ { type: "uint256" },
8
+ { type: "uint256" },
9
+ { type: "uint24" },
10
+ { type: "int24" },
11
+ ];
12
+ /** Which token goes in and out for a buy (USDG -> stock) or sell. */
13
+ export function swapLegs(pool, side) {
14
+ if (side === "buy") {
15
+ return {
16
+ tokenIn: USDG.address,
17
+ tokenOut: pool.address,
18
+ decimalsIn: USDG.decimals,
19
+ decimalsOut: STOCK_TOKEN_DECIMALS,
20
+ };
21
+ }
22
+ return {
23
+ tokenIn: pool.address,
24
+ tokenOut: USDG.address,
25
+ decimalsIn: STOCK_TOKEN_DECIMALS,
26
+ decimalsOut: USDG.decimals,
27
+ };
28
+ }
29
+ export function encodeV4SwapInstructions(params) {
30
+ if (params.amountIn <= 0n || params.minOut <= 0n) {
31
+ throw new Error("Swap amounts must be positive");
32
+ }
33
+ return encodeAbiParameters(SWAP_PARAMS, [
34
+ params.tokenIn,
35
+ params.tokenOut,
36
+ params.amountIn,
37
+ params.minOut,
38
+ params.fee,
39
+ params.tickSpacing,
40
+ ]);
41
+ }
42
+ /** Minimum acceptable output for a quoted amount, floored. */
43
+ export function applySlippage(amountOut, bps = STOCK_SWAP_SLIPPAGE_BPS) {
44
+ return amountOut - (amountOut * BigInt(bps)) / 10000n;
45
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The StockSwap verb on Robinhood Chain: the pool table, the wrapper's
3
+ * instruction encoder and decoder, and the quote. Shared by the webapp and the
4
+ * SDK, so a request the app shows and one the CLI signs decode the same way.
5
+ */
6
+ export * from "./decode-stock-swap.ts";
7
+ export * from "./encode-swap.ts";
8
+ export * from "./quote.ts";
9
+ export * from "./stock-pools.ts";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The StockSwap verb on Robinhood Chain: the pool table, the wrapper's
3
+ * instruction encoder and decoder, and the quote. Shared by the webapp and the
4
+ * SDK, so a request the app shows and one the CLI signs decode the same way.
5
+ */
6
+ // biome-ignore lint/performance/noBarrelFile: the stock swap part of the `./evm` entry
7
+ export * from "./decode-stock-swap.js";
8
+ export * from "./encode-swap.js";
9
+ export * from "./quote.js";
10
+ export * from "./stock-pools.js";
@@ -0,0 +1,19 @@
1
+ import { type SwapSide } from "./encode-swap.ts";
2
+ import { type StockPool } from "./stock-pools.ts";
3
+ /** An `eth_call` on Robinhood Chain: any provider's `call`. */
4
+ export type EthCall = (tx: {
5
+ to: string;
6
+ data: string;
7
+ }) => Promise<string>;
8
+ /**
9
+ * A live quote from the Uniswap V4 Quoter on Robinhood Chain, and the minimum
10
+ * output signed into the swap. `quoteExactInputSingle` is not a view function
11
+ * (it unlocks the PoolManager and reverts internally to surface the amount),
12
+ * so it is read through eth_call, never sent.
13
+ */
14
+ export declare function quoteStockSwap(call: EthCall, pool: StockPool, side: SwapSide,
15
+ /** Exact input in base units (USDG 6-dec for buys, stock 18-dec for sells). */
16
+ amountIn: bigint): Promise<{
17
+ amountOut: bigint;
18
+ minOut: bigint;
19
+ }>;
@@ -0,0 +1,46 @@
1
+ import { decodeFunctionResult, encodeFunctionData, parseAbi, zeroAddress, } from "viem";
2
+ import { applySlippage, swapLegs } from "./encode-swap.js";
3
+ import { V4_QUOTER_ADDRESS } from "./stock-pools.js";
4
+ const QUOTER = parseAbi([
5
+ "function quoteExactInputSingle(((address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) poolKey, bool zeroForOne, uint128 exactAmount, bytes hookData) params) returns (uint256 amountOut, uint256 gasEstimate)",
6
+ ]);
7
+ /**
8
+ * A live quote from the Uniswap V4 Quoter on Robinhood Chain, and the minimum
9
+ * output signed into the swap. `quoteExactInputSingle` is not a view function
10
+ * (it unlocks the PoolManager and reverts internally to surface the amount),
11
+ * so it is read through eth_call, never sent.
12
+ */
13
+ export async function quoteStockSwap(call, pool, side,
14
+ /** Exact input in base units (USDG 6-dec for buys, stock 18-dec for sells). */
15
+ amountIn) {
16
+ const { tokenIn, tokenOut } = swapLegs(pool, side);
17
+ // v4 orders the pair by address; zeroForOne says which way we cross it.
18
+ const zeroForOne = tokenIn.toLowerCase() < tokenOut.toLowerCase();
19
+ const [currency0, currency1] = zeroForOne
20
+ ? [tokenIn, tokenOut]
21
+ : [tokenOut, tokenIn];
22
+ const data = encodeFunctionData({
23
+ abi: QUOTER,
24
+ functionName: "quoteExactInputSingle",
25
+ args: [
26
+ {
27
+ poolKey: {
28
+ currency0: currency0,
29
+ currency1: currency1,
30
+ fee: pool.fee,
31
+ tickSpacing: pool.tickSpacing,
32
+ hooks: zeroAddress,
33
+ },
34
+ zeroForOne,
35
+ exactAmount: amountIn,
36
+ hookData: "0x",
37
+ },
38
+ ],
39
+ });
40
+ const [amountOut] = decodeFunctionResult({
41
+ abi: QUOTER,
42
+ functionName: "quoteExactInputSingle",
43
+ data: (await call({ to: V4_QUOTER_ADDRESS, data })),
44
+ });
45
+ return { amountOut, minOut: applySlippage(amountOut) };
46
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * HOOD: Robinhood Chain mainnet (4663) swap venue — USDG <> tokenized stocks
3
+ * through Uniswap v4, executed by the `GroupCashSwapWrapperV4` wrapper that
4
+ * has been live since July 2026. Everything Robinhood-specific hangs off this
5
+ * module so the feature can be lifted out in one piece.
6
+ *
7
+ * Every pool below was live-quoted through the V4 Quoter on Sept 6, 2026
8
+ * (100 USDG in). A pool that stops quoting means its fee / tickSpacing
9
+ * changed or liquidity left — re-probe before changing anything here. The
10
+ * token addresses match the seeded Token rows in
11
+ * `packages/database/prisma/src/eth-token-metadata.ts`; update both together.
12
+ */
13
+ /** Quote and settlement token on Robinhood Chain (there is no USDC). */
14
+ export declare const USDG: {
15
+ readonly symbol: "USDG";
16
+ readonly name: "Global Dollar";
17
+ readonly address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168";
18
+ readonly decimals: 6;
19
+ };
20
+ /** Robinhood Stock Tokens are 18 decimals. */
21
+ export declare const STOCK_TOKEN_DECIMALS = 18;
22
+ /** Uniswap V4 Quoter on 4663 (`quoteExactInputSingle`). */
23
+ export declare const V4_QUOTER_ADDRESS = "0x8Dc178eFB8111BB0973Dd9d722ebeFF267c98F94";
24
+ /** Slippage bound signed into every swap: 1% for equities. */
25
+ export declare const STOCK_SWAP_SLIPPAGE_BPS = 100;
26
+ export type StockPool = {
27
+ symbol: string;
28
+ /** Issuer name, e.g. "Apple Inc." */
29
+ name: string;
30
+ /** Short label, e.g. "Apple" */
31
+ label: string;
32
+ /** Brand-ish accent for avatars and charts. */
33
+ color: string;
34
+ /** Robinhood Stock Token contract (matches the seeded Token row). */
35
+ address: string;
36
+ /** Uniswap v4 pool fee in hundredths of a bip (3000 = 0.3%). */
37
+ fee: number;
38
+ tickSpacing: number;
39
+ /** True for ETFs (SPY); equities otherwise. */
40
+ etf?: boolean;
41
+ };
42
+ /**
43
+ * Stocks with a real USDG pool, in display order. NFLX is seeded as a token
44
+ * but has no pool, so it is deliberately absent: offering it would sign
45
+ * proposals that can only revert.
46
+ */
47
+ export declare const STOCK_POOLS: Record<string, StockPool>;
48
+ export declare const TRADABLE_STOCK_SYMBOLS: string[];
49
+ export declare function isTradableStock(symbol: string): boolean;
50
+ /** Pool fee as a percentage string, e.g. "0.3%". */
51
+ export declare function poolFeeLabel(pool: StockPool): string;
52
+ /**
53
+ * `GroupCashSwapWrapperV4` on Robinhood Chain mainnet — the contract every
54
+ * swap policy is activated against and every swap calls. Live since July
55
+ * 2026 (GroupCash). Deliberately NOT read from NEXT_PUBLIC_STOCK_SWAP_WRAPPER:
56
+ * the Vercel rows for that variable predate mainnet and point at a retired
57
+ * testnet mock with no code on 4663, so honoring them would pin new swap
58
+ * policies to a dead address.
59
+ */
60
+ export declare const STOCK_SWAP_WRAPPER_MAINNET = "0xf21C3C4Cb339bA038c0B374b55F47Fa22ce4d419";
61
+ export declare function stockSwapWrapperAddress(): string;