hookr-sdk 0.2.1 → 1.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.
- package/CHANGELOG.md +70 -0
- package/LICENSE +21 -0
- package/MIGRATING.md +89 -0
- package/README.md +308 -105
- package/dist/core/abi.d.ts +10930 -0
- package/dist/core/abi.js +455 -0
- package/dist/core/advisories.d.ts +184 -0
- package/dist/core/advisories.js +291 -0
- package/dist/core/client.d.ts +55 -0
- package/dist/core/client.js +107 -0
- package/dist/core/generated.d.ts +14295 -0
- package/dist/core/generated.js +1058 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.js +17 -0
- package/dist/core/launch.d.ts +200 -0
- package/dist/core/launch.js +427 -0
- package/dist/core/pair.d.ts +64 -0
- package/dist/core/pair.js +136 -0
- package/dist/core/params.d.ts +415 -0
- package/dist/core/params.js +1235 -0
- package/dist/core/policies.d.ts +136 -0
- package/dist/core/policies.js +125 -0
- package/dist/core/proofs.d.ts +25 -0
- package/dist/core/proofs.js +118 -0
- package/dist/core/quotes.d.ts +185 -0
- package/dist/core/quotes.js +550 -0
- package/dist/core/referrals.d.ts +32 -0
- package/dist/core/referrals.js +36 -0
- package/dist/core/registry.d.ts +357 -0
- package/dist/core/registry.js +378 -0
- package/dist/core/rules.d.ts +675 -0
- package/dist/core/rules.js +1049 -0
- package/dist/core/schema.d.ts +358 -0
- package/dist/core/schema.js +105 -0
- package/dist/core/stocks.generated.d.ts +11 -0
- package/dist/core/stocks.generated.js +1228 -0
- package/dist/core/vault.d.ts +59 -0
- package/dist/core/vault.js +112 -0
- package/dist/core/wallet.d.ts +142 -0
- package/dist/core/wallet.js +304 -0
- package/dist/deployments.d.ts +8 -0
- package/dist/deployments.js +26 -0
- package/dist/index.d.ts +42 -15
- package/dist/index.js +40 -15
- package/dist/releases.d.ts +219 -0
- package/dist/releases.js +365 -0
- package/dist/wallet-cap.d.ts +159 -0
- package/dist/wallet-cap.js +234 -0
- package/package.json +46 -10
- package/provenance.json +31 -0
- package/dist/abi.d.ts +0 -6404
- package/dist/abi.js +0 -8300
- package/dist/chain.d.ts +0 -87
- package/dist/chain.js +0 -86
- package/dist/claims.d.ts +0 -32
- package/dist/claims.js +0 -31
- package/dist/fees.d.ts +0 -59
- package/dist/fees.js +0 -142
- package/dist/links.d.ts +0 -48
- package/dist/links.js +0 -93
- package/dist/market.d.ts +0 -264
- package/dist/market.js +0 -310
- package/dist/reads.d.ts +0 -136
- package/dist/reads.js +0 -219
- package/dist/rules.d.ts +0 -84
- package/dist/rules.js +0 -296
- package/dist/swap.d.ts +0 -108
- package/dist/swap.js +0 -144
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import { decodeFunctionResult, encodeFunctionData, formatUnits, parseAbi, parseUnits } from "viem";
|
|
2
|
+
import { registryAbi } from "./abi.js";
|
|
3
|
+
import { decodeCuratedQuote, encodeCuratedQuoteCall } from "./registry.js";
|
|
4
|
+
import { address, assert, uint, ZERO } from "./schema.js";
|
|
5
|
+
import { ROBINHOOD_STOCK_TOKENS } from "./stocks.generated.js";
|
|
6
|
+
import { contract } from "./wallet.js";
|
|
7
|
+
/**
|
|
8
|
+
* Quote assets: every asset a Hookr 1 market may quote in, the classes the app and the release tooling name them by,
|
|
9
|
+
* their decimals, and the ERC-20 funding a launch or trade needs.
|
|
10
|
+
*
|
|
11
|
+
* On chain the registry decides. HookrRoot.initializePool refuses a quote `isQuote` does not admit (UnqualifiedQuote);
|
|
12
|
+
* native ETH is always admitted, an ERC-20 through the reviewed catalog (SET_QUOTE), a whole class by runtime code hash
|
|
13
|
+
* (SET_QUOTE_CLASS, every Robinhood stock token) or, while open quotes are on (OPEN_QUOTES), any contract that is not an
|
|
14
|
+
* EIP-7702 delegated account, as the live launchpads accept. `curatedQuote` tells the first three from an open quote.
|
|
15
|
+
* Nothing here can admit a quote: `quoteAdmission` reads the registry at a block, and a class below is a label for
|
|
16
|
+
* guidance, never an admission.
|
|
17
|
+
*
|
|
18
|
+
* The classification follows the live app's v2-asset-class.ts: the curated catalog first (the only source that can
|
|
19
|
+
* name a stablecoin), then the ERC-1967 beacon slot (every Robinhood stock token is a BeaconProxy of one beacon), then
|
|
20
|
+
* fail closed to an unconfident "erc20".
|
|
21
|
+
*/
|
|
22
|
+
/** ERC-1967 beacon slot: bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1). */
|
|
23
|
+
export const ERC1967_BEACON_SLOT = "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50";
|
|
24
|
+
/** ERC-1967 implementation slot: bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1). */
|
|
25
|
+
export const ERC1967_IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
|
|
26
|
+
/** The one beacon every Robinhood tokenized stock and ETF on chain 4663 proxies to (also its pause registry). */
|
|
27
|
+
export const ROBINHOOD_STOCK_BEACON = "0xe10b6f6b275de231345c20d14ab812db62151b00";
|
|
28
|
+
/** The runtime keccak256 every one of those proxies shares: OpenZeppelin BeaconProxy with that beacon immutable. */
|
|
29
|
+
export const ROBINHOOD_STOCK_PROXY_RUNTIME_KECCAK256 = "0x6c1fdd40002dcb440c7fff6a84171404d279ccb057803b65826f7546acd65630";
|
|
30
|
+
/** Canonical Permit2: HookrForwarder pulls a gasless swap's ERC-20 input through it. */
|
|
31
|
+
export const PERMIT2 = "0x000000000022d473030f116ddee9f6b43ac78ba3";
|
|
32
|
+
/** The block the catalog below was read at (chain 4663, through Alchemy, 2026-10-06 00:54 UTC). */
|
|
33
|
+
export const ROBINHOOD_QUOTE_CATALOG_BLOCK = 81_225_166;
|
|
34
|
+
/**
|
|
35
|
+
* The genesis catalog the owner set on 2026-10-05: the 20 largest tokens on Robinhood Chain by market cap at
|
|
36
|
+
* ROBINHOOD_QUOTE_CATALOG_BLOCK that pass review (WETH and USDG among them) plus HOOKR and What The Hook's WTH, in the
|
|
37
|
+
* registry's queue order (the release template's `quoteAssets`).
|
|
38
|
+
* Every field was read from chain 4663 at ROBINHOOD_QUOTE_CATALOG_BLOCK, not copied from a document (USDG and
|
|
39
|
+
* syrupUSDG are 6 decimals). WETH, USDG and U are ERC-1967 proxies: a runtime hash alone does not review their upgrade
|
|
40
|
+
* authority.
|
|
41
|
+
*/
|
|
42
|
+
export const ROBINHOOD_QUOTE_CATALOG = Object.freeze([
|
|
43
|
+
{
|
|
44
|
+
address: "0x0bd7d308f8e1639fab988df18a8011f41eacad73",
|
|
45
|
+
symbol: "WETH",
|
|
46
|
+
name: "WETH",
|
|
47
|
+
decimals: 18,
|
|
48
|
+
class: "weth",
|
|
49
|
+
runtimeKeccak256: "0x5706be52f64875fee65a2cec0d80e47a23d8793cbe85d214b48445e2d05f5353",
|
|
50
|
+
implementation: "0xc6b81b429797e0f555440b70cd99e032d7ae947e",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
address: "0x5fc5360d0400a0fd4f2af552add042d716f1d168",
|
|
54
|
+
symbol: "USDG",
|
|
55
|
+
name: "Global Dollar",
|
|
56
|
+
decimals: 6,
|
|
57
|
+
class: "stable",
|
|
58
|
+
runtimeKeccak256: "0x864cc9ad53b338b82da1f7cab85ab0b3d5c8861acb422b6fec63cf36234f36a6",
|
|
59
|
+
implementation: "0x68184c449e1a8f34fa18d289737129fd27b66f8f",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
address: "0x18e674231a58c239dc7daedcffe15ec3a24cff5c",
|
|
63
|
+
symbol: "HOOKR",
|
|
64
|
+
name: "Hookr.fun",
|
|
65
|
+
decimals: 18,
|
|
66
|
+
class: "hookr",
|
|
67
|
+
runtimeKeccak256: "0xd9346eaf1a9878650549765e1d4ce8b3d0516d93d3203e1c8b99e382428ebc8d",
|
|
68
|
+
implementation: null,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
address: "0xb8fa8010833463aac5595b55b9045479239eff79",
|
|
72
|
+
symbol: "WTH",
|
|
73
|
+
name: "what the hook?",
|
|
74
|
+
decimals: 18,
|
|
75
|
+
class: "listed",
|
|
76
|
+
runtimeKeccak256: "0x21d9e3d68d1f15f3cb3080c8f9321e5a2dc3fe4d05f135ab203e9d0e966a8275",
|
|
77
|
+
implementation: null,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
address: "0x51250b135174ca09450ec01c4aff73cf69dbb590",
|
|
81
|
+
symbol: "NOVAAI",
|
|
82
|
+
name: "NovaAI",
|
|
83
|
+
decimals: 18,
|
|
84
|
+
class: "listed",
|
|
85
|
+
runtimeKeccak256: "0x53fb452d8017996465eebb6bcd7c2dcd7ee1ad4997289cabd7141099fc716a49",
|
|
86
|
+
implementation: null,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
address: "0x39dbed3a2bd333467115de45665cc57f813c4571",
|
|
90
|
+
symbol: "PONS",
|
|
91
|
+
name: "Pons",
|
|
92
|
+
decimals: 18,
|
|
93
|
+
class: "listed",
|
|
94
|
+
runtimeKeccak256: "0x16c3d3ede897688ddff79262606f13bead398332e65001f192460fbac4e1fb85",
|
|
95
|
+
implementation: null,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
address: "0x5d3a1ff2b6bab83b63cd9ad0787074081a52ef34",
|
|
99
|
+
symbol: "USDe",
|
|
100
|
+
name: "USDe",
|
|
101
|
+
decimals: 18,
|
|
102
|
+
class: "listed",
|
|
103
|
+
runtimeKeccak256: "0x43d4a9c80fe87d7d878b8c8bdcb419261a1cb037e6833918c67c9b74e8519ca4",
|
|
104
|
+
implementation: null,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
address: "0x020bfc650a365f8bb26819deaabf3e21291018b4",
|
|
108
|
+
symbol: "CASHCAT",
|
|
109
|
+
name: "Cash Cat",
|
|
110
|
+
decimals: 18,
|
|
111
|
+
class: "listed",
|
|
112
|
+
runtimeKeccak256: "0x725781ccca82a0bf9b1f8920b2fe58ea1e4facf46d0db9f50226d6409de890fe",
|
|
113
|
+
implementation: null,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
address: "0x57c0e45cb534413d1c20a4240955d6bb250bb4f1",
|
|
117
|
+
symbol: "UP",
|
|
118
|
+
name: "up",
|
|
119
|
+
decimals: 18,
|
|
120
|
+
class: "listed",
|
|
121
|
+
runtimeKeccak256: "0xb4d0e29f0aa13a275c7615fbe5946d2b05df8a01811b068203aa73f55b069d8c",
|
|
122
|
+
implementation: null,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
address: "0x40858070814a57fdf33a613ae84fe0a8b4a874f7",
|
|
126
|
+
symbol: "syrupUSDG",
|
|
127
|
+
name: "syrupUSDG",
|
|
128
|
+
decimals: 6,
|
|
129
|
+
class: "listed",
|
|
130
|
+
runtimeKeccak256: "0xc91c79f8c922a0c3a31f130aa8199c2f9ae03027ac49f2ad759dde9cb8b1dd61",
|
|
131
|
+
implementation: null,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
address: "0x2e8c31162b855a2ffa90f6f8634643ad6f111e18",
|
|
135
|
+
symbol: "AI",
|
|
136
|
+
name: "Artificial Inu",
|
|
137
|
+
decimals: 18,
|
|
138
|
+
class: "listed",
|
|
139
|
+
runtimeKeccak256: "0xf10f86b05965a827a332e6c73086f18026fbe3917f4bffbec3f938b3b5397b56",
|
|
140
|
+
implementation: null,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
address: "0x07ebb29a38fbcb41563817e5e19f2cec619c90d2",
|
|
144
|
+
symbol: "BUN",
|
|
145
|
+
name: "Bundle Cat",
|
|
146
|
+
decimals: 18,
|
|
147
|
+
class: "listed",
|
|
148
|
+
runtimeKeccak256: "0x7c16047fae9f8fee5effb88675360c56f8fbfa4f4d430181205f299eac934ff9",
|
|
149
|
+
implementation: null,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
address: "0xaa07a0e9209e16ac99708c3ec70159c6ef3128a3",
|
|
153
|
+
symbol: "ORBIO",
|
|
154
|
+
name: "Orbio.so",
|
|
155
|
+
decimals: 18,
|
|
156
|
+
class: "listed",
|
|
157
|
+
runtimeKeccak256: "0x67d7a9d1677c238d9dae8679f500586f7d189e7bcda59964f3ce634dcf7363cf",
|
|
158
|
+
implementation: null,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
address: "0x98096d17e191b3da1d5f99a6d7b3584351b11e18",
|
|
162
|
+
symbol: "BONER",
|
|
163
|
+
name: "Boner Coin",
|
|
164
|
+
decimals: 18,
|
|
165
|
+
class: "listed",
|
|
166
|
+
runtimeKeccak256: "0xf10f86b05965a827a332e6c73086f18026fbe3917f4bffbec3f938b3b5397b56",
|
|
167
|
+
implementation: null,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
address: "0x0e0d2c89a5a019fe1cf762e5e33187631dacc21b",
|
|
171
|
+
symbol: "CHUMP",
|
|
172
|
+
name: "Chump Coin",
|
|
173
|
+
decimals: 18,
|
|
174
|
+
class: "listed",
|
|
175
|
+
runtimeKeccak256: "0x45d441a5e2161cd6e1c85796510b7629bf4729788efa88db59c07164aa9a524a",
|
|
176
|
+
implementation: null,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
address: "0x8e41fd25e85e624560fa50ff9f117a02cd30b8b3",
|
|
180
|
+
symbol: "SIF",
|
|
181
|
+
name: "Super Inu Force",
|
|
182
|
+
decimals: 18,
|
|
183
|
+
class: "listed",
|
|
184
|
+
runtimeKeccak256: "0xd6b9feb551dea0e2dd7fe152cf14408773760f153134d29a33a0686af976e5db",
|
|
185
|
+
implementation: null,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
address: "0xce24439f2d9c6a2289f741120fe202248b666666",
|
|
189
|
+
symbol: "U",
|
|
190
|
+
name: "United Stables",
|
|
191
|
+
decimals: 18,
|
|
192
|
+
class: "listed",
|
|
193
|
+
runtimeKeccak256: "0xae12b1d61f7ed4febf649ccf319cb49a0e921cca3739dcb01b47d316049d46ff",
|
|
194
|
+
implementation: "0x261c4ee7e337852f919b90372e2518d6bff7ea4c",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
address: "0xc38c332012a9116dcade0c7f43b5d3c71799a9f8",
|
|
198
|
+
symbol: "HOODCHAN",
|
|
199
|
+
name: "HOODCHAN",
|
|
200
|
+
decimals: 18,
|
|
201
|
+
class: "listed",
|
|
202
|
+
runtimeKeccak256: "0xc59b76e554d38563071118131d593fa1ff92dcac9df1d6ce19b6349d96f05c7f",
|
|
203
|
+
implementation: null,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
address: "0x385f4f8ae47651ce5f58f5265395a669f8281e18",
|
|
207
|
+
symbol: "MEME",
|
|
208
|
+
name: "A Meme Coin",
|
|
209
|
+
decimals: 18,
|
|
210
|
+
class: "listed",
|
|
211
|
+
runtimeKeccak256: "0xf10f86b05965a827a332e6c73086f18026fbe3917f4bffbec3f938b3b5397b56",
|
|
212
|
+
implementation: null,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
address: "0x56910d4409f3a0c78c64dd8d0545ff0705389870",
|
|
216
|
+
symbol: "Index",
|
|
217
|
+
name: "The Index",
|
|
218
|
+
decimals: 18,
|
|
219
|
+
class: "listed",
|
|
220
|
+
runtimeKeccak256: "0x4ee19c3a5cda0991509f65faa494c490942df5f19fc79659306a125c15b41d84",
|
|
221
|
+
implementation: null,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
address: "0x5cb6f181081301b44905f3ae15419112ecabd8a6",
|
|
225
|
+
symbol: "PIPEDOG",
|
|
226
|
+
name: "pipedog",
|
|
227
|
+
decimals: 18,
|
|
228
|
+
class: "listed",
|
|
229
|
+
runtimeKeccak256: "0xc0d0d825734a0b6d070991217c8c0fb8530b574e839d2ff7685341fd9d707912",
|
|
230
|
+
implementation: null,
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
address: "0x2d8d6f4a93acd7a916a5a654ec8b690ba3b3eadd",
|
|
234
|
+
symbol: "STATICS",
|
|
235
|
+
name: "Statics",
|
|
236
|
+
decimals: 18,
|
|
237
|
+
class: "listed",
|
|
238
|
+
runtimeKeccak256: "0xf10f86b05965a827a332e6c73086f18026fbe3917f4bffbec3f938b3b5397b56",
|
|
239
|
+
implementation: null,
|
|
240
|
+
},
|
|
241
|
+
]);
|
|
242
|
+
/** Native ETH: the zero address in every pool key, 18 decimals, always admitted by the registry. */
|
|
243
|
+
export const NATIVE_QUOTE = Object.freeze({
|
|
244
|
+
address: ZERO,
|
|
245
|
+
symbol: "ETH",
|
|
246
|
+
decimals: 18,
|
|
247
|
+
class: "native",
|
|
248
|
+
source: "native",
|
|
249
|
+
confident: true,
|
|
250
|
+
});
|
|
251
|
+
/** Display labels for each class. */
|
|
252
|
+
export const QUOTE_CLASS_LABELS = Object.freeze({
|
|
253
|
+
native: "ETH",
|
|
254
|
+
weth: "WETH",
|
|
255
|
+
stable: "USDG / stablecoin",
|
|
256
|
+
hookr: "HOOKR",
|
|
257
|
+
listed: "Listed token",
|
|
258
|
+
rwa: "Robinhood stock token",
|
|
259
|
+
erc20: "Other ERC-20",
|
|
260
|
+
});
|
|
261
|
+
/** What a stock quote means for the pool, in the live classifier's words. */
|
|
262
|
+
export const STOCK_QUOTE_NOTE = "This token is a Robinhood tokenized stock: its proxy points at the shared beacon, so a pause there halts every pool holding it.";
|
|
263
|
+
/** Why an unclassified quote still warns. */
|
|
264
|
+
export const UNCLASSIFIED_QUOTE_NOTE = "This address is not on the verified list and does not point at the shared Robinhood stock beacon. It is treated as an ERC-20 you do not control: verify what it does on transfer before you use it.";
|
|
265
|
+
/** An upgradeable catalog quote. */
|
|
266
|
+
export const PROXY_QUOTE_NOTE = "This token is an upgradeable proxy: its issuer can change what it does, and every pool quoting it follows.";
|
|
267
|
+
/** A quote whose transfers move less or more than asked is refused by every Hookr transfer (exact settlement). */
|
|
268
|
+
export const EXACT_TRANSFER_NOTE = "Hookr settles exact amounts: a token that takes a fee on transfer, rebases or blocks addresses can make launches, trades and fee claims revert.";
|
|
269
|
+
const lower = (a) => a.toLowerCase();
|
|
270
|
+
/** The reviewed catalog entry for an address, or null. */
|
|
271
|
+
export function catalogQuote(target) {
|
|
272
|
+
return ROBINHOOD_QUOTE_CATALOG.find((q) => q.address === lower(target)) ?? null;
|
|
273
|
+
}
|
|
274
|
+
const STOCKS = new Map(ROBINHOOD_STOCK_TOKENS.map((t) => [t.address, t]));
|
|
275
|
+
/** The scanned stock token at an address (its label), or null. A token absent here can still be a stock token. */
|
|
276
|
+
export function stockToken(target) {
|
|
277
|
+
return STOCKS.get(lower(target)) ?? null;
|
|
278
|
+
}
|
|
279
|
+
/** A token's decimals as the contracts can use them: an integer from 0 to 36. Anything else throws. */
|
|
280
|
+
export function quoteDecimals(value) {
|
|
281
|
+
const n = typeof value === "bigint" ? Number(value) : value;
|
|
282
|
+
assert(typeof n === "number" && Number.isInteger(n) && n >= 0 && n <= 36, "Quote decimals must be an integer from 0 to 36");
|
|
283
|
+
return n;
|
|
284
|
+
}
|
|
285
|
+
/** Parses a decimal amount typed in quote units ("1.5") into raw units, refusing more places than the token has. */
|
|
286
|
+
export function parseQuoteAmount(text, decimals) {
|
|
287
|
+
const places = quoteDecimals(decimals);
|
|
288
|
+
const t = typeof text === "string" ? text.trim() : "";
|
|
289
|
+
assert(/^(0|[1-9]\d*)(\.\d+)?$|^0?\.\d+$/.test(t), "Enter a plain decimal amount");
|
|
290
|
+
const fraction = t.includes(".") ? t.split(".")[1] : "";
|
|
291
|
+
assert(fraction.length <= places, `This quote has ${places} decimal places`);
|
|
292
|
+
return parseUnits(t.startsWith(".") ? "0" + t : t, places);
|
|
293
|
+
}
|
|
294
|
+
/** Formats a raw quote amount in its token's units. */
|
|
295
|
+
export function formatQuoteAmount(raw, decimals) {
|
|
296
|
+
return formatUnits(uint(raw), quoteDecimals(decimals));
|
|
297
|
+
}
|
|
298
|
+
/** The address a 32-byte storage word holds, or null for an empty or malformed word. */
|
|
299
|
+
export function addressFromWord(word) {
|
|
300
|
+
if (typeof word !== "string" || !/^0x[\da-fA-F]{1,64}$/.test(word))
|
|
301
|
+
return null;
|
|
302
|
+
const tail = word.slice(2).padStart(64, "0").slice(-40);
|
|
303
|
+
return /^0+$/.test(tail) ? null : `0x${tail.toLowerCase()}`;
|
|
304
|
+
}
|
|
305
|
+
/** The classification that needs no network: native ETH, the catalog and the scanned stock list. Null otherwise. */
|
|
306
|
+
export function classifyQuoteOffline(target) {
|
|
307
|
+
const a = address(target, true);
|
|
308
|
+
if (a === ZERO)
|
|
309
|
+
return NATIVE_QUOTE;
|
|
310
|
+
const listed = catalogQuote(a);
|
|
311
|
+
if (listed)
|
|
312
|
+
return {
|
|
313
|
+
address: a,
|
|
314
|
+
symbol: listed.symbol,
|
|
315
|
+
decimals: listed.decimals,
|
|
316
|
+
class: listed.class,
|
|
317
|
+
source: "catalog",
|
|
318
|
+
confident: true,
|
|
319
|
+
};
|
|
320
|
+
const stock = stockToken(a);
|
|
321
|
+
// The scan read each listed proxy's beacon slot; the list is still only a label source, so the answer says beacon.
|
|
322
|
+
if (stock)
|
|
323
|
+
return {
|
|
324
|
+
address: a,
|
|
325
|
+
symbol: stock.symbol,
|
|
326
|
+
decimals: 18,
|
|
327
|
+
class: "rwa",
|
|
328
|
+
source: "beacon",
|
|
329
|
+
confident: true,
|
|
330
|
+
name: stock.name,
|
|
331
|
+
};
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
const METADATA = parseAbi([
|
|
335
|
+
"function decimals() view returns (uint8)",
|
|
336
|
+
"function symbol() view returns (string)",
|
|
337
|
+
"function name() view returns (string)",
|
|
338
|
+
]);
|
|
339
|
+
async function readMetadata(reader, target, fn) {
|
|
340
|
+
try {
|
|
341
|
+
const data = await reader.call(target, encodeFunctionData({ abi: METADATA, functionName: fn }));
|
|
342
|
+
return decodeFunctionResult({ abi: METADATA, functionName: fn, data });
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* The full classification: native, the catalog, then the ERC-1967 beacon slot, then an unconfident "erc20". Decimals
|
|
350
|
+
* and symbol are read from the token and are null when it does not answer (a quote without decimals cannot be priced
|
|
351
|
+
* or funded, so callers refuse it). A read that throws degrades to "erc20", never to a safer class.
|
|
352
|
+
*/
|
|
353
|
+
export async function classifyQuote(reader, target) {
|
|
354
|
+
const a = address(target, true);
|
|
355
|
+
const offline = classifyQuoteOffline(a);
|
|
356
|
+
if (offline && offline.source !== "beacon")
|
|
357
|
+
return offline;
|
|
358
|
+
let beacon = null;
|
|
359
|
+
try {
|
|
360
|
+
beacon = addressFromWord(await reader.getStorageAt(a, ERC1967_BEACON_SLOT));
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
beacon = null;
|
|
364
|
+
}
|
|
365
|
+
const [decimals, symbol, name] = await Promise.all([
|
|
366
|
+
readMetadata(reader, a, "decimals"),
|
|
367
|
+
readMetadata(reader, a, "symbol"),
|
|
368
|
+
readMetadata(reader, a, "name"),
|
|
369
|
+
]);
|
|
370
|
+
let d = null;
|
|
371
|
+
try {
|
|
372
|
+
d = quoteDecimals(decimals);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
d = null;
|
|
376
|
+
}
|
|
377
|
+
const s = typeof symbol === "string" && symbol.trim() !== "" ? symbol : null;
|
|
378
|
+
if (beacon === ROBINHOOD_STOCK_BEACON) {
|
|
379
|
+
const n = typeof name === "string" && name.trim() !== "" ? name : offline?.name;
|
|
380
|
+
return {
|
|
381
|
+
address: a,
|
|
382
|
+
symbol: s,
|
|
383
|
+
decimals: d,
|
|
384
|
+
class: "rwa",
|
|
385
|
+
source: "beacon",
|
|
386
|
+
confident: true,
|
|
387
|
+
...(n ? { name: n } : {}),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return { address: a, symbol: s, decimals: d, class: "erc20", source: "unclassified", confident: false };
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* The warnings a quote carries before anyone launches or trades against it, in display order. `curated` is the
|
|
394
|
+
* registry's `curatedQuote` answer (`quoteAdmission`): false adds the open-quote note.
|
|
395
|
+
*/
|
|
396
|
+
export function quoteWarnings(q, curated) {
|
|
397
|
+
const out = [];
|
|
398
|
+
if (q.class === "rwa")
|
|
399
|
+
out.push(STOCK_QUOTE_NOTE);
|
|
400
|
+
if (curated === false && q.class !== "native")
|
|
401
|
+
out.push(OPEN_QUOTE_NOTE);
|
|
402
|
+
if (!q.confident)
|
|
403
|
+
out.push(UNCLASSIFIED_QUOTE_NOTE);
|
|
404
|
+
if (q.source === "catalog" && catalogQuote(q.address)?.implementation)
|
|
405
|
+
out.push(PROXY_QUOTE_NOTE);
|
|
406
|
+
if (q.class === "rwa" || q.class === "erc20")
|
|
407
|
+
out.push(EXACT_TRANSFER_NOTE);
|
|
408
|
+
if (q.decimals === null && q.class !== "native")
|
|
409
|
+
out.push("This token did not report its decimals, so no amount in it can be read.");
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
/** A read-only registry isQuote call for `asset` at the manifest's registry. */
|
|
413
|
+
export function buildIsQuote(m, asset) {
|
|
414
|
+
return {
|
|
415
|
+
to: contract(m, "registry"),
|
|
416
|
+
data: encodeFunctionData({ abi: registryAbi, functionName: "isQuote", args: [address(asset, true)] }),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
/** Whether the registry admits `asset` as a new market's quote, by an eth_call the caller pins to a block. */
|
|
420
|
+
export async function quoteAdmitted(ethCall, m, asset) {
|
|
421
|
+
const { to, data } = buildIsQuote(m, asset);
|
|
422
|
+
return decodeFunctionResult({ abi: registryAbi, functionName: "isQuote", data: await ethCall(to, data) });
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* What the registry says of a quote at the caller's block: `admitted`, whether a new pool may bind it (`isQuote`), and
|
|
426
|
+
* `curated`, whether it is native ETH, a catalog asset or a class member rather than admitted only by open quotes.
|
|
427
|
+
* `curated` is null when the registry does not answer `curatedQuote` (a release without quote classes, where every
|
|
428
|
+
* admitted quote is a catalog asset) or the read fails; a caller then labels the quote by its classification alone.
|
|
429
|
+
*/
|
|
430
|
+
export async function quoteAdmission(ethCall, m, asset) {
|
|
431
|
+
const admitted = await quoteAdmitted(ethCall, m, asset);
|
|
432
|
+
let curated = null;
|
|
433
|
+
try {
|
|
434
|
+
curated = decodeCuratedQuote(await ethCall(contract(m, "registry"), encodeCuratedQuoteCall(asset)));
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
curated = null;
|
|
438
|
+
}
|
|
439
|
+
return { admitted, curated: curated === null ? null : admitted && curated };
|
|
440
|
+
}
|
|
441
|
+
/** The note an open quote carries: the registry admits it without any review. */
|
|
442
|
+
export const OPEN_QUOTE_NOTE = "The registry admits this token only because open quotes are on: nobody reviewed it. Check its address before you use it.";
|
|
443
|
+
/**
|
|
444
|
+
* Refuses a member quote the Launcher or the root would refuse before any pool opens, and one no amount could be read
|
|
445
|
+
* in: the subject itself, a quote repeated in one family, or an ERC-20 without known decimals. Registry admission is
|
|
446
|
+
* checked on chain (`quoteAdmitted`).
|
|
447
|
+
*/
|
|
448
|
+
export function checkLaunchQuotes(subject, quotes) {
|
|
449
|
+
assert(quotes.length > 0 && quotes.length <= 8, "A family has one to eight quote markets");
|
|
450
|
+
const seen = new Set();
|
|
451
|
+
for (const q of quotes) {
|
|
452
|
+
const a = address(q.address, true);
|
|
453
|
+
assert(subject === null || a !== address(subject), "A market cannot quote its own token");
|
|
454
|
+
assert(!seen.has(a), "Each family member needs a different quote");
|
|
455
|
+
seen.add(a);
|
|
456
|
+
assert(a === ZERO || q.decimals !== null, "This quote did not report its decimals");
|
|
457
|
+
if (q.decimals !== null)
|
|
458
|
+
quoteDecimals(q.decimals);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* The funding HookrLauncher.launch / launchAdvised / launchWithBuy / launchFamily collects from the caller: for every
|
|
463
|
+
* member, its quote side's maximum (amount1Max when the subject sorts first, else amount0Max) plus the dev buy's quoteIn
|
|
464
|
+
* on each buy's member (launchFamily takes up to one per member); for an existing subject, the subject side's maxima too
|
|
465
|
+
* (a new token is minted to the Launcher); and the launch fee in native value (r31, `launchFee`, read at the launch
|
|
466
|
+
* block). Native ETH is the call's value; each ERC-20 needs an allowance to the Launcher of the listed amount.
|
|
467
|
+
*/
|
|
468
|
+
export function launchFunding(m, subject, existingSubject, members, launchFee, buys = []) {
|
|
469
|
+
const s = address(subject);
|
|
470
|
+
const launcher = contract(m, "launcher");
|
|
471
|
+
assert(typeof launchFee === "bigint" && launchFee >= 0n, "Invalid launch fee");
|
|
472
|
+
let native = launchFee;
|
|
473
|
+
const erc20 = new Map();
|
|
474
|
+
const add = (token, amount) => {
|
|
475
|
+
if (amount === 0n)
|
|
476
|
+
return;
|
|
477
|
+
if (token === ZERO)
|
|
478
|
+
native += amount;
|
|
479
|
+
else
|
|
480
|
+
erc20.set(token, (erc20.get(token) ?? 0n) + amount);
|
|
481
|
+
};
|
|
482
|
+
const seen = new Set();
|
|
483
|
+
for (const buy of buys) {
|
|
484
|
+
assert(Number.isSafeInteger(buy.member) && buy.member >= 0 && buy.member < members.length, "The dev buy names no family member");
|
|
485
|
+
assert(!seen.has(buy.member), "At most one dev buy per member");
|
|
486
|
+
seen.add(buy.member);
|
|
487
|
+
}
|
|
488
|
+
members.forEach((member, i) => {
|
|
489
|
+
const q = address(member.quote, true);
|
|
490
|
+
assert(q !== s, "A market cannot quote its own token");
|
|
491
|
+
const subjectFirst = BigInt(s) < BigInt(q);
|
|
492
|
+
const quoteMax = uint(subjectFirst ? member.amount1Max : member.amount0Max, 128);
|
|
493
|
+
const subjectMax = uint(subjectFirst ? member.amount0Max : member.amount1Max, 128);
|
|
494
|
+
const buy = buys.find((b) => b.member === i);
|
|
495
|
+
add(q, quoteMax + (buy ? uint(buy.quoteIn, 128) : 0n));
|
|
496
|
+
if (existingSubject)
|
|
497
|
+
add(s, subjectMax);
|
|
498
|
+
});
|
|
499
|
+
return {
|
|
500
|
+
native,
|
|
501
|
+
allowances: [...erc20].map(([token, amount]) => ({ token: token, spender: launcher, amount })),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* The ERC-20 allowance HookrRouter.swap needs: the input currency's exact amount for exact input, the bound for exact
|
|
506
|
+
* output, to the router. Null when the input is native ETH (sent as value). Through HookrForwarder (gasless) the
|
|
507
|
+
* allowance goes to Permit2 instead: `permit2Allowance`.
|
|
508
|
+
*/
|
|
509
|
+
export function swapAllowance(m, s) {
|
|
510
|
+
const input = address(s.zeroForOne ? s.key.currency0 : s.key.currency1, true);
|
|
511
|
+
if (input === ZERO)
|
|
512
|
+
return null;
|
|
513
|
+
const amount = s.amountSpecified < 0n ? -s.amountSpecified : uint(s.amountBound, 128);
|
|
514
|
+
return { token: input, spender: contract(m, "router"), amount };
|
|
515
|
+
}
|
|
516
|
+
/** A gasless swap's ERC-20 input needs a Permit2 allowance; the intent's signature then authorizes each pull. */
|
|
517
|
+
export function permit2Allowance(token, amount) {
|
|
518
|
+
const t = address(token);
|
|
519
|
+
return { token: t, spender: PERMIT2, amount: uint(amount) };
|
|
520
|
+
}
|
|
521
|
+
const ERC20_APPROVE = parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]);
|
|
522
|
+
/**
|
|
523
|
+
* The ERC-20 approve for an allowance. `current` is the signer's allowance now: a token that refuses to move a
|
|
524
|
+
* nonzero allowance to another nonzero value (USDT-style) is reset to zero first, so pass it and send the calls in
|
|
525
|
+
* order. Returns no call when the allowance already covers the amount.
|
|
526
|
+
*/
|
|
527
|
+
export function buildApprovals(m, a, current = 0n) {
|
|
528
|
+
const chainId = Number(uint(m.chainId));
|
|
529
|
+
const token = address(a.token), spender = address(a.spender);
|
|
530
|
+
uint(a.amount);
|
|
531
|
+
if (uint(current) >= a.amount)
|
|
532
|
+
return [];
|
|
533
|
+
const call = (amount) => ({
|
|
534
|
+
chainId,
|
|
535
|
+
to: token,
|
|
536
|
+
data: encodeFunctionData({ abi: ERC20_APPROVE, functionName: "approve", args: [spender, amount] }),
|
|
537
|
+
value: 0n,
|
|
538
|
+
});
|
|
539
|
+
return current > 0n ? [call(0n), call(a.amount)] : [call(a.amount)];
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* The Robinhood stock tokens as one reviewed quote class (SET_QUOTE_CLASS): the runtime keccak256 every proxy of the
|
|
543
|
+
* stock beacon shares, the beacon it embeds, and AAPL as the witness the registry checks the hash against.
|
|
544
|
+
*/
|
|
545
|
+
export const ROBINHOOD_STOCK_QUOTE_CLASS = Object.freeze({
|
|
546
|
+
label: "Robinhood stock tokens",
|
|
547
|
+
runtimeKeccak256: ROBINHOOD_STOCK_PROXY_RUNTIME_KECCAK256,
|
|
548
|
+
beacon: ROBINHOOD_STOCK_BEACON,
|
|
549
|
+
witness: "0xaf3d76f1834a1d425780943c99ea8a608f8a93f9",
|
|
550
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Address, Hash, DeploymentManifest, Call } from "./schema.js";
|
|
2
|
+
export declare function referralAuthorization(m: DeploymentManifest, message: {
|
|
3
|
+
payer: Address;
|
|
4
|
+
referrer: Address;
|
|
5
|
+
programKey: Hash;
|
|
6
|
+
codeHash: Hash;
|
|
7
|
+
nonce: bigint;
|
|
8
|
+
deadline: bigint;
|
|
9
|
+
}): {
|
|
10
|
+
domain: {
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
chainId: number;
|
|
14
|
+
verifyingContract: `0x${string}`;
|
|
15
|
+
};
|
|
16
|
+
primaryType: "ReferralAuthorization";
|
|
17
|
+
types: {
|
|
18
|
+
ReferralAuthorization: {
|
|
19
|
+
name: string;
|
|
20
|
+
type: string;
|
|
21
|
+
}[];
|
|
22
|
+
};
|
|
23
|
+
message: {
|
|
24
|
+
payer: Address;
|
|
25
|
+
referrer: Address;
|
|
26
|
+
programKey: Hash;
|
|
27
|
+
codeHash: Hash;
|
|
28
|
+
nonce: bigint;
|
|
29
|
+
deadline: bigint;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export declare function buildReferralBinding(m: DeploymentManifest, key: Hash, referrer: Address, code: Hash): Call;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { uint, address, deployment, hash, assert } from "./schema.js";
|
|
2
|
+
import { referralsAbi } from "./abi.js";
|
|
3
|
+
import { contract, buildCall } from "./wallet.js";
|
|
4
|
+
export function referralAuthorization(m, message) {
|
|
5
|
+
deployment(m);
|
|
6
|
+
hash(message.programKey);
|
|
7
|
+
hash(message.codeHash);
|
|
8
|
+
assert(message.payer.toLowerCase() !== message.referrer.toLowerCase(), "Self referral");
|
|
9
|
+
address(message.payer);
|
|
10
|
+
address(message.referrer);
|
|
11
|
+
uint(message.nonce);
|
|
12
|
+
uint(message.deadline);
|
|
13
|
+
return {
|
|
14
|
+
domain: {
|
|
15
|
+
name: "HookrReferralRegistry",
|
|
16
|
+
version: "1",
|
|
17
|
+
chainId: Number(m.chainId),
|
|
18
|
+
verifyingContract: contract(m, "referrals"),
|
|
19
|
+
},
|
|
20
|
+
primaryType: "ReferralAuthorization",
|
|
21
|
+
types: {
|
|
22
|
+
ReferralAuthorization: [
|
|
23
|
+
{ name: "payer", type: "address" },
|
|
24
|
+
{ name: "referrer", type: "address" },
|
|
25
|
+
{ name: "programKey", type: "bytes32" },
|
|
26
|
+
{ name: "codeHash", type: "bytes32" },
|
|
27
|
+
{ name: "nonce", type: "uint256" },
|
|
28
|
+
{ name: "deadline", type: "uint256" },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
message,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function buildReferralBinding(m, key, referrer, code) {
|
|
35
|
+
return buildCall(m, "referrals", referralsAbi, "register", [key, address(referrer), code]);
|
|
36
|
+
}
|