@gibs/bridge-sdk 1.12.0 → 1.13.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 (117) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +234 -432
  3. package/dist/abis.d.ts +1 -0
  4. package/dist/bridge-limits.d.ts +103 -0
  5. package/dist/bridge-limits.js +84 -0
  6. package/dist/chain-info.d.ts +169 -0
  7. package/dist/chain-info.js +410 -0
  8. package/dist/chain-names.d.ts +160 -0
  9. package/dist/chain-names.js +200 -0
  10. package/dist/chains.d.ts +65 -0
  11. package/dist/chains.js +284 -0
  12. package/dist/config.d.ts +216 -0
  13. package/dist/config.js +493 -0
  14. package/dist/consolidated-quote.d.ts +554 -0
  15. package/dist/consolidated-quote.js +773 -0
  16. package/dist/destination-calldata.d.ts +118 -0
  17. package/dist/destination-calldata.js +152 -0
  18. package/dist/ecosystems.d.ts +217 -0
  19. package/dist/ecosystems.js +288 -0
  20. package/dist/fee-type.d.ts +10 -0
  21. package/dist/fee-type.js +9 -0
  22. package/dist/hyperliquid-execution.d.ts +152 -0
  23. package/dist/hyperliquid-execution.js +168 -0
  24. package/dist/image-links.d.ts +117 -0
  25. package/dist/image-links.js +142 -0
  26. package/dist/index.d.ts +20 -0
  27. package/dist/index.js +20 -0
  28. package/dist/lifi-execution-guard.d.ts +199 -0
  29. package/dist/lifi-execution-guard.js +400 -0
  30. package/dist/near-intents-conformance.d.ts +85 -0
  31. package/dist/near-intents-conformance.js +329 -0
  32. package/dist/near-intents-route.d.ts +47 -0
  33. package/dist/near-intents-route.js +265 -0
  34. package/dist/near-intents.d.ts +524 -0
  35. package/dist/near-intents.js +1055 -0
  36. package/dist/origin-deposit.d.ts +326 -0
  37. package/dist/origin-deposit.js +68 -0
  38. package/dist/pre-quote.d.ts +198 -0
  39. package/dist/pre-quote.js +175 -0
  40. package/dist/prices.d.ts +107 -0
  41. package/dist/prices.js +162 -0
  42. package/dist/providers.d.ts +221 -0
  43. package/dist/providers.js +157 -0
  44. package/dist/quote-fees.d.ts +102 -0
  45. package/dist/quote-fees.js +195 -0
  46. package/dist/referral.d.ts +77 -0
  47. package/dist/referral.js +72 -0
  48. package/dist/relay-currencies.d.ts +47 -0
  49. package/dist/relay-currencies.js +89 -0
  50. package/dist/relay-quote.d.ts +301 -0
  51. package/dist/relay-quote.js +983 -0
  52. package/dist/route-steps.d.ts +138 -0
  53. package/dist/route-steps.js +305 -0
  54. package/dist/routing/amounts.d.ts +80 -0
  55. package/dist/routing/amounts.js +120 -0
  56. package/dist/routing/candidate-route.d.ts +43 -0
  57. package/dist/routing/candidate-route.js +45 -0
  58. package/dist/routing/candidate.d.ts +237 -0
  59. package/dist/routing/candidate.js +96 -0
  60. package/dist/routing/carriers/ask-answer.d.ts +58 -0
  61. package/dist/routing/carriers/ask-answer.js +1 -0
  62. package/dist/routing/carriers/omnibridge.d.ts +137 -0
  63. package/dist/routing/carriers/omnibridge.js +419 -0
  64. package/dist/routing/carriers/pulsex.d.ts +111 -0
  65. package/dist/routing/carriers/pulsex.js +404 -0
  66. package/dist/routing/carriers/wrap.d.ts +25 -0
  67. package/dist/routing/carriers/wrap.js +115 -0
  68. package/dist/routing/composed-route-quality.d.ts +103 -0
  69. package/dist/routing/composed-route-quality.js +108 -0
  70. package/dist/routing/entry-chain-anchor.d.ts +88 -0
  71. package/dist/routing/entry-chain-anchor.js +55 -0
  72. package/dist/routing/format.d.ts +31 -0
  73. package/dist/routing/format.js +58 -0
  74. package/dist/routing/index.d.ts +34 -0
  75. package/dist/routing/index.js +42 -0
  76. package/dist/routing/producers.d.ts +770 -0
  77. package/dist/routing/producers.js +1564 -0
  78. package/dist/routing/providers.d.ts +26 -0
  79. package/dist/routing/providers.js +38 -0
  80. package/dist/routing/route-data.d.ts +355 -0
  81. package/dist/routing/route-data.js +1 -0
  82. package/dist/routing/search/compose.d.ts +168 -0
  83. package/dist/routing/search/compose.js +98 -0
  84. package/dist/routing/search/graph.d.ts +639 -0
  85. package/dist/routing/search/graph.js +445 -0
  86. package/dist/routing/search/plan-route-data.d.ts +244 -0
  87. package/dist/routing/search/plan-route-data.js +767 -0
  88. package/dist/routing/search/score.d.ts +324 -0
  89. package/dist/routing/search/score.js +400 -0
  90. package/dist/routing/usd-value.d.ts +17 -0
  91. package/dist/routing/usd-value.js +25 -0
  92. package/dist/settings.d.ts +692 -0
  93. package/dist/settings.js +909 -0
  94. package/dist/ton-execution.d.ts +117 -0
  95. package/dist/ton-execution.js +247 -0
  96. package/dist/tron-execution.d.ts +146 -0
  97. package/dist/tron-execution.js +239 -0
  98. package/dist/types.d.ts +51 -0
  99. package/dist/types.js +1 -0
  100. package/package.json +203 -23
  101. package/src/chain-info.test.ts +0 -968
  102. package/src/chain-info.ts +0 -498
  103. package/src/chains.test.ts +0 -123
  104. package/src/chains.ts +0 -88
  105. package/src/config.test.ts +0 -679
  106. package/src/config.ts +0 -500
  107. package/src/fee-type.test.ts +0 -45
  108. package/src/fee-type.ts +0 -14
  109. package/src/image-links.test.ts +0 -160
  110. package/src/image-links.ts +0 -65
  111. package/src/index.ts +0 -8
  112. package/src/settings.test.ts +0 -2754
  113. package/src/settings.ts +0 -875
  114. package/src/types.ts +0 -48
  115. package/tsconfig.json +0 -17
  116. package/vitest.config.ts +0 -13
  117. /package/{src/abis.ts → dist/abis.js} +0 -0
@@ -0,0 +1,288 @@
1
+ import { getAddress, isAddress } from 'viem';
2
+ // Solana public keys are base58 (no 0, O, I, l) and 32–44 characters long.
3
+ const base58Solana = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
4
+ // Bitcoin mainnet: bech32 (bc1…) or base58 pay-to-key/script (starts 1 or 3).
5
+ const bech32Bitcoin = /^bc1[0-9ac-hj-np-z]{6,87}$/;
6
+ const base58Bitcoin = /^[13][1-9A-HJ-NP-Za-km-z]{25,39}$/;
7
+ // Tron base58check addresses are always 34 characters and always begin with a
8
+ // capital T (the encoding of the 0x41 mainnet prefix byte).
9
+ const base58Tron = /^T[1-9A-HJ-NP-Za-km-z]{33}$/;
10
+ // The Open Network has two interchangeable address encodings, and Relay uses
11
+ // both: the 48-character "user-friendly" base64url form (EQ/UQ on mainnet,
12
+ // kQ/0Q on testnet), and the "raw" form of a workchain, a colon, and a
13
+ // 64-character hex account identifier.
14
+ const friendlyTon = /^[EUk0]Q[A-Za-z0-9_-]{46}$/;
15
+ const rawTon = /^-?\d+:[0-9a-fA-F]{64}$/;
16
+ // HyperCore token identifiers are hex of a length Ethereum never uses: sixteen
17
+ // bytes for most tokens, nineteen for a few. Twenty is allowed too so a token
18
+ // that is addressed by a full Ethereum address still resolves.
19
+ const hyperliquidTokenId = /^0x[0-9a-fA-F]{32,40}$/;
20
+ // Zcash TRANSPARENT addresses only, and the restriction is the service's, not
21
+ // ours: a shielded (`zs1…`) or unified (`u1…`) address is refused at the door
22
+ // with `refundTo is not valid`, before a solver is ever reached. Both
23
+ // transparent forms are base58check over a two-byte prefix, which always
24
+ // encodes to exactly 35 characters: `t1` pays to a public key hash, `t3` to a
25
+ // script hash. The pattern rejects the shielded forms by construction, because
26
+ // neither begins with `t`.
27
+ const transparentZcash = /^t[13][1-9A-HJ-NP-Za-km-z]{33}$/;
28
+ const evmDescriptor = {
29
+ id: 'evm',
30
+ label: 'Ethereum Virtual Machine',
31
+ caipNamespace: 'eip155',
32
+ // The all-zero address, exactly as the Relay builder uses across every
33
+ // Ethereum Virtual Machine chain for the native gas token.
34
+ nativeAssetSentinel: '0x0000000000000000000000000000000000000000',
35
+ // The only proof the product holds. The bridge's own contracts run on
36
+ // PulseChain, Ethereum and Binance Smart Chain, and every aggregator origin
37
+ // LI.FI carries is an Ethereum Virtual Machine chain, so this is the family
38
+ // real transfers have crossed.
39
+ endToEndProof: 'verified',
40
+ isValidAddress: (value) => isAddress(value),
41
+ // getAddress applies the mixed-case checksum; it throws on a non-address, so
42
+ // fall back to the raw input to keep normalize total and side-effect-free.
43
+ normalizeAddress: (value) => (isAddress(value) ? getAddress(value) : value),
44
+ addressCasing: 'insensitive',
45
+ };
46
+ const svmDescriptor = {
47
+ id: 'svm',
48
+ label: 'Solana',
49
+ caipNamespace: 'solana',
50
+ // Solana native currency (the System Program address), as Relay expects it.
51
+ nativeAssetSentinel: '11111111111111111111111111111111',
52
+ // Connects through the Reown AppKit Solana adapter and signs for real
53
+ // (`useSolanaDepositSigner` builds a versioned transaction and calls the
54
+ // wallet's sign-and-send). Every test around it drives a mocked wallet, and
55
+ // Relay — the only provider that quotes this origin — has never had its
56
+ // on-chain execution confirmed by a funded transfer.
57
+ endToEndProof: 'pending',
58
+ isValidAddress: (value) => base58Solana.test(value),
59
+ // base58 is case-significant; canonical form is the input unchanged.
60
+ normalizeAddress: (value) => value,
61
+ addressCasing: 'significant',
62
+ };
63
+ const bvmDescriptor = {
64
+ id: 'bvm',
65
+ label: 'Bitcoin',
66
+ caipNamespace: 'bip122',
67
+ // Bitcoin native currency string, as Relay expects it.
68
+ nativeAssetSentinel: 'bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8',
69
+ // The best-evidenced of the four, and still short of proof. A partially
70
+ // signed transaction the pipeline builds is accepted by a real node, but
71
+ // that test runs against a local regtest node behind `BITCOIN_REGTEST=1`
72
+ // and never in the normal suite. The browser wallet that signs in
73
+ // production is driven by nothing but a mock, and no funded mainnet
74
+ // transfer has completed.
75
+ endToEndProof: 'pending',
76
+ isValidAddress: (value) => bech32Bitcoin.test(value) || base58Bitcoin.test(value),
77
+ // bech32 and base58 are case-significant; canonical form is the input unchanged.
78
+ normalizeAddress: (value) => value,
79
+ addressCasing: 'significant',
80
+ };
81
+ const tvmDescriptor = {
82
+ id: 'tvm',
83
+ label: 'Tron',
84
+ caipNamespace: 'tron',
85
+ // Tron native currency (TRX), as Relay expects it. Not a real contract: it is
86
+ // the placeholder address Relay publishes for the chain's native asset.
87
+ nativeAssetSentinel: 'T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb',
88
+ // The first wallet the interface reaches outside Reown AppKit: it reads an
89
+ // injected provider directly. That is more of the connection path owned
90
+ // here, and therefore more of it unexercised outside a mock. No funded
91
+ // transfer has completed.
92
+ endToEndProof: 'pending',
93
+ isValidAddress: (value) => base58Tron.test(value),
94
+ // base58check is case-significant; canonical form is the input unchanged.
95
+ normalizeAddress: (value) => value,
96
+ addressCasing: 'significant',
97
+ };
98
+ const tonvmDescriptor = {
99
+ id: 'tonvm',
100
+ label: 'The Open Network',
101
+ caipNamespace: 'ton',
102
+ // Relay publishes Gram — NOT the Toncoin gas asset — as this chain's native
103
+ // currency, so a native-asset route on The Open Network is denominated in
104
+ // Gram. Kept byte-equal to Relay's own value rather than "corrected".
105
+ nativeAssetSentinel: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c',
106
+ // The second wallet outside Reown AppKit, on a separate connection library,
107
+ // and the one origin whose deposit identifier is derived from the returned
108
+ // cell rather than handed back by the wallet. Only a mock has ever returned
109
+ // that cell. No funded transfer has completed.
110
+ endToEndProof: 'pending',
111
+ isValidAddress: (value) => friendlyTon.test(value) || rawTon.test(value),
112
+ // both encodings are case-significant; canonical form is the input unchanged.
113
+ normalizeAddress: (value) => value,
114
+ addressCasing: 'significant',
115
+ };
116
+ const hypevmDescriptor = {
117
+ id: 'hypevm',
118
+ label: 'Hyperliquid',
119
+ caipNamespace: 'hyperliquid',
120
+ // Relay publishes USD Coin — NOT the Hype gas asset — as this chain's native
121
+ // currency, and addresses it by a sixteen-byte HyperCore token identifier
122
+ // rather than the twenty-byte all-zero address every Ethereum Virtual Machine
123
+ // chain uses. The two sentinels are therefore different strings.
124
+ nativeAssetSentinel: '0x00000000000000000000000000000000',
125
+ // THE WORST OF THE SIX, and the one that was actively misleading. The
126
+ // interface's source picker never offers Hyperliquid — nothing can quote it
127
+ // — yet the landing page drew a pill for it that set the source chain to
128
+ // Hyperliquid on click. A reader who pressed it arrived at a chain the form
129
+ // does not list and no provider answers. Removing the pill removes that dead
130
+ // end; the descriptor stays because the route layer still has to be able to
131
+ // NAME the chain.
132
+ endToEndProof: 'pending',
133
+ // This predicate checks TOKEN addresses, and HyperCore identifies a token by a
134
+ // hex string SHORTER than an Ethereum address — sixteen bytes for most,
135
+ // nineteen for a few — so the twenty-byte `isAddress` check would reject the
136
+ // very identifiers it exists to accept. Its only consumer silently falls back
137
+ // to the native sentinel when this returns false, so an over-strict pattern
138
+ // would quietly swallow valid token links instead of surfacing an error.
139
+ // Accounts are a different namespace (ordinary twenty-byte Ethereum
140
+ // addresses) and are validated by the wallet layer, not here.
141
+ isValidAddress: (value) => hyperliquidTokenId.test(value),
142
+ // Hex is case-insensitive and Relay emits lower case, so lower case is the
143
+ // canonical form. It matters: the token-equality check compares non-Ethereum
144
+ // addresses exactly, and only a single canonical case makes that correct.
145
+ normalizeAddress: (value) => hyperliquidTokenId.test(value) ? value.toLowerCase() : value,
146
+ addressCasing: 'significant',
147
+ };
148
+ const zcashDescriptor = {
149
+ id: 'zcash',
150
+ label: 'Zcash',
151
+ // Zcash is not a Relay chain and has no widely-registered chain-agnostic
152
+ // namespace. `zcash` is this repository's own key, and nothing maps a
153
+ // connected wallet through it, because no wallet connects to this family.
154
+ caipNamespace: 'zcash',
155
+ // DERIVED, NOT INVENTED: the transparent pay-to-public-key-hash address of
156
+ // an all-zero key hash, base58check over the mainnet prefix `1cb8` followed
157
+ // by twenty zero bytes. Nobody holds a key for it. It follows the pattern of
158
+ // Relay's Bitcoin sentinel, an all-zero witness program, and for the same
159
+ // reason: the sentinel stands where a token address is expected, so it must
160
+ // pass this family's own address check. Zcash carries no tokens on this
161
+ // route, so the native asset is the only asset.
162
+ nativeAssetSentinel: 't1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs',
163
+ // THE FIRST ECOSYSTEM WITH NO WALLET AT ALL, and therefore the first whose
164
+ // hold cannot be cleared by connecting one. The reader pays an address by
165
+ // hand from wherever they hold the coin, so every part of the journey that
166
+ // this repository owns — the quote, the address, the amount, the deadline,
167
+ // the status poll — is exercised only by a fixture. Nothing funded has
168
+ // crossed. Clearing this is one word, after one transfer.
169
+ endToEndProof: 'pending',
170
+ isValidAddress: (value) => transparentZcash.test(value),
171
+ // base58check is case-significant; canonical form is the input unchanged.
172
+ normalizeAddress: (value) => value,
173
+ addressCasing: 'significant',
174
+ };
175
+ /**
176
+ * Every ecosystem the bridge can originate from. LI.FI-compatible one first.
177
+ *
178
+ * Registration means the library can MODEL and QUOTE the ecosystem, not that an
179
+ * interface can sign it — see the note on {@link Ecosystem}.
180
+ *
181
+ * It means less than that again where a public claim is concerned: read
182
+ * {@link EndToEndProof} before listing this registry anywhere a reader will
183
+ * take it as an offer.
184
+ */
185
+ export const ecosystemRegistry = [
186
+ evmDescriptor,
187
+ svmDescriptor,
188
+ bvmDescriptor,
189
+ tvmDescriptor,
190
+ tonvmDescriptor,
191
+ hypevmDescriptor,
192
+ zcashDescriptor,
193
+ ];
194
+ const byId = new Map(ecosystemRegistry.map((descriptor) => [descriptor.id, descriptor]));
195
+ /**
196
+ * Looks up the descriptor for an ecosystem id. Total over the `Ecosystem` union;
197
+ * throws only if the registry is ever left incomplete (a programming error the
198
+ * round-trip test would catch).
199
+ *
200
+ * @param id - the ecosystem key
201
+ * @returns the descriptor whose `id` equals `id`
202
+ */
203
+ export const ecosystemById = (id) => {
204
+ const descriptor = byId.get(id);
205
+ if (descriptor === undefined) {
206
+ throw new Error(`no ecosystem descriptor registered for id "${id}"`);
207
+ }
208
+ return descriptor;
209
+ };
210
+ /**
211
+ * Whether a transfer has been carried through this ecosystem end to end.
212
+ *
213
+ * The predicate a surface calls before ADVERTISING an origin. It is separate
214
+ * from anything the router asks, and deliberately so: the router must keep
215
+ * quoting and naming every registered ecosystem, because a reader who reaches
216
+ * an unproven chain through a link still needs the page to work. This governs
217
+ * only what the product offers unprompted.
218
+ *
219
+ * @param id - the ecosystem key
220
+ * @returns true when a real transfer has completed through it
221
+ */
222
+ export const isEcosystemProvenEndToEnd = (id) => ecosystemById(id).endToEndProof === 'verified';
223
+ const byNamespace = new Map(ecosystemRegistry.map((descriptor) => [descriptor.caipNamespace, descriptor]));
224
+ /**
225
+ * Resolves an ecosystem from a chain-agnostic identifier namespace.
226
+ *
227
+ * @param namespace - the leading segment of a "namespace:reference:address" id
228
+ * @returns the matching descriptor, or undefined when the namespace is unknown
229
+ */
230
+ export const ecosystemByCaipNamespace = (namespace) => byNamespace.get(namespace);
231
+ /**
232
+ * Whether two addresses in the same ecosystem name the same thing.
233
+ *
234
+ * THE ONE PLACE THIS QUESTION IS ANSWERED. It was answered in two: this
235
+ * registry, which knows each family's address rules, and a ternary in the
236
+ * interface's routing module that hard-coded "Ethereum is case-insensitive,
237
+ * everything else is not". Two homes means a family added here is still
238
+ * compared wrongly there, silently, because a loose comparison does not fail —
239
+ * it merges two different accounts into one.
240
+ *
241
+ * @param a - an address
242
+ * @param b - an address, in the same ecosystem
243
+ * @param ecosystem - the ecosystem both belong to
244
+ * @returns whether they are the same address
245
+ */
246
+ export const sameAddress = (a, b, ecosystem) => ecosystemById(ecosystem).addressCasing === 'insensitive'
247
+ ? a.toLowerCase() === b.toLowerCase()
248
+ : a === b;
249
+ /**
250
+ * Whether an address is the ecosystem's own native asset rather than a token.
251
+ *
252
+ * Every ecosystem already declares its `nativeAssetSentinel`, and this is the
253
+ * predicate that reads it. The comparison goes through {@link sameAddress}
254
+ * rather than `toLowerCase()`, which matters for exactly the families where the
255
+ * hand-written checks were wrong to begin with: Solana's, Bitcoin's and The
256
+ * Open Network's sentinels are case-significant, so lower-casing them before
257
+ * comparing can match an address that is not the sentinel.
258
+ *
259
+ * @param value - the address to test
260
+ * @param ecosystem - the ecosystem it belongs to
261
+ * @returns whether it names the native asset
262
+ */
263
+ export const isNativeAsset = (value, ecosystem) => sameAddress(value, ecosystemById(ecosystem).nativeAssetSentinel, ecosystem);
264
+ /**
265
+ * Whether a string is a well-formed address in this ecosystem.
266
+ *
267
+ * A free function over the registry, so a caller needs the ecosystem key and
268
+ * nothing else. Reaching for viem's `isAddress` directly is the thing this
269
+ * replaces: it answers only for Ethereum and answers FALSE — confidently — for
270
+ * every valid Solana, Bitcoin, Tron and Open Network address.
271
+ *
272
+ * @param value - the string to test
273
+ * @param ecosystem - the ecosystem to test it against
274
+ * @returns whether the string is a valid address there
275
+ */
276
+ export const isValidAddressIn = (value, ecosystem) => ecosystemById(ecosystem).isValidAddress(value);
277
+ /**
278
+ * The ecosystem's canonical spelling of an address.
279
+ *
280
+ * Total and side-effect-free: an input this ecosystem cannot parse comes back
281
+ * unchanged rather than throwing, because normalizing is something callers do
282
+ * on the way to a comparison and a throw there would be a crash on a typo.
283
+ *
284
+ * @param value - the address to normalize
285
+ * @param ecosystem - the ecosystem it belongs to
286
+ * @returns the canonical form, or the input when it cannot be parsed
287
+ */
288
+ export const normalizeAddressIn = (value, ecosystem) => ecosystemById(ecosystem).normalizeAddress(value);
@@ -0,0 +1,10 @@
1
+ /** The type of fee to use for the bridge */
2
+ export declare enum FeeType {
3
+ PERCENT = "%",
4
+ GAS_TIP = "gas+%",
5
+ FIXED = "fixed"
6
+ }
7
+ /** The keys of the fee type */
8
+ export type FeeTypeKeys = keyof typeof FeeType;
9
+ /** A map of fee type values to keys */
10
+ export declare const feeTypeValToKeyMap: Map<FeeType, "PERCENT" | "GAS_TIP" | "FIXED">;
@@ -0,0 +1,9 @@
1
+ /** The type of fee to use for the bridge */
2
+ export var FeeType;
3
+ (function (FeeType) {
4
+ FeeType["PERCENT"] = "%";
5
+ FeeType["GAS_TIP"] = "gas+%";
6
+ FeeType["FIXED"] = "fixed";
7
+ })(FeeType || (FeeType = {}));
8
+ /** A map of fee type values to keys */
9
+ export const feeTypeValToKeyMap = new Map(Object.keys(FeeType).map((key) => [FeeType[key], key]));
@@ -0,0 +1,152 @@
1
+ import { type Hex } from 'viem';
2
+ import type { HyperliquidAuthorization, HyperliquidDeposit, TypedDataField } from './origin-deposit.js';
3
+ /**
4
+ * Executing a Hyperliquid deposit, as pure data.
5
+ *
6
+ * Hyperliquid is the one origin whose deposit is never broadcast to a chain.
7
+ * The user signs a typed-data ACTION and that signed action is posted to
8
+ * Hyperliquid's own exchange API; Relay then observes the transfer and fills the
9
+ * route. Two details make this impossible to derive from the quote alone, and
10
+ * both are encoded here rather than rediscovered at a call site:
11
+ *
12
+ * 1. Relay's quote supplies `eip712Types`, `eip712PrimaryType` and the action,
13
+ * but NOT the signing domain. The domain is fixed by Hyperliquid and is
14
+ * reconstructed by {@link buildHyperliquidTypedData}.
15
+ * 2. Hyperliquid wants the signature split into `r`/`s`/`v` components, not the
16
+ * concatenated hex string a wallet returns.
17
+ *
18
+ * Kept free of transport so it is fully testable: nothing here signs, fetches,
19
+ * or reads a clock. The caller supplies the signature and performs the request.
20
+ *
21
+ * The shape mirrors Relay's own client, which is the only reference
22
+ * implementation of this exchange — their public documentation covers the
23
+ * generic `signature` step but not this action-bearing `transaction` step.
24
+ */
25
+ /** Where a signed Hyperliquid action is submitted. Hyperliquid's API, not Relay's. */
26
+ export declare const hyperliquidExchangeUrl = "https://api.hyperliquid.xyz/exchange";
27
+ /** The signing domain Hyperliquid requires. Fixed; never supplied by the quote. */
28
+ export declare const hyperliquidDomainName = "HyperliquidSignTransaction";
29
+ /** The signing domain's version. Fixed alongside {@link hyperliquidDomainName}. */
30
+ export declare const hyperliquidDomainVersion = "1";
31
+ /**
32
+ * The domain's `verifyingContract`. Hyperliquid actions are verified off-chain,
33
+ * so no contract is involved and the field is the all-zero address.
34
+ */
35
+ export declare const hyperliquidVerifyingContract: "0x0000000000000000000000000000000000000000";
36
+ /** A complete typed-data payload, ready to hand to a wallet's typed-data signing call. */
37
+ export type HyperliquidTypedData = {
38
+ domain: {
39
+ name: string;
40
+ version: string;
41
+ chainId: number;
42
+ verifyingContract: Hex;
43
+ };
44
+ types: Record<string, TypedDataField[]>;
45
+ primaryType: string;
46
+ /**
47
+ * The signed message. This exact object is ALSO the `action` submitted to the
48
+ * exchange, so the bytes signed and the bytes submitted cannot drift apart.
49
+ */
50
+ message: Record<string, unknown>;
51
+ };
52
+ /**
53
+ * Builds the typed-data payload for a Hyperliquid deposit.
54
+ *
55
+ * The message is the action's parameters plus the action `type` and a
56
+ * `signatureChainId`; the latter is the signing chain in hex and MUST agree
57
+ * with `domain.chainId`, because Hyperliquid recovers the signer using both.
58
+ * They are derived from the single `signatureChainId` argument so they cannot
59
+ * disagree.
60
+ *
61
+ * @param deposit - the parsed Hyperliquid origin deposit
62
+ * @param options.signatureChainId - the chain id the connected wallet signs
63
+ * under. Not the Relay origin chain id: it identifies the signing domain, and
64
+ * Relay's own client uses the wallet's active chain here.
65
+ * @returns the payload to sign, whose `message` doubles as the submitted action
66
+ */
67
+ export declare const buildHyperliquidTypedData: (deposit: HyperliquidDeposit, { signatureChainId }: {
68
+ signatureChainId: number;
69
+ }) => HyperliquidTypedData;
70
+ /** The JSON body Hyperliquid's exchange endpoint expects for a signed action. */
71
+ export type HyperliquidExchangeRequest = {
72
+ /** The signature split into components; the concatenated hex form is rejected. */
73
+ signature: {
74
+ r: Hex;
75
+ s: Hex;
76
+ v: number;
77
+ };
78
+ /** Replay-protection nonce, which must match the nonce inside the signed action. */
79
+ nonce: number;
80
+ /** The signed message, submitted verbatim. */
81
+ action: Record<string, unknown>;
82
+ };
83
+ /**
84
+ * Builds the exchange request for an action the user has signed.
85
+ *
86
+ * The nonce is read back out of the signed message rather than passed in: it is
87
+ * covered by the signature, so any independently supplied value could disagree
88
+ * with what was signed and be rejected.
89
+ *
90
+ * @param options.typedData - the payload that was signed
91
+ * @param options.signature - the wallet's signature over it
92
+ * @returns the request body, or null when the signed action carries no usable nonce
93
+ */
94
+ export declare const buildHyperliquidExchangeRequest: ({ typedData, signature, }: {
95
+ typedData: HyperliquidTypedData;
96
+ signature: Hex;
97
+ }) => HyperliquidExchangeRequest | null;
98
+ /**
99
+ * Whether an exchange response reports success. Hyperliquid answers with HTTP
100
+ * 200 even for a rejected action, so the body's own status is the only reliable
101
+ * signal — treating the status code as the outcome would report a failed
102
+ * transfer as a completed one.
103
+ *
104
+ * @param payload - the parsed response body
105
+ * @returns true only when the body explicitly reports success
106
+ */
107
+ export declare const isHyperliquidExchangeSuccess: (payload: unknown) => boolean;
108
+ /**
109
+ * The typed-data payload for the authorization that must be completed BEFORE
110
+ * the action is submitted.
111
+ *
112
+ * Relay builds the authorization against a chain it picks when quoting, but the
113
+ * signature is produced by whichever chain the wallet is actually on. Both the
114
+ * domain here and `signatureChainId` in {@link buildHyperliquidAuthorizationRequest}
115
+ * are therefore overridden with the active wallet chain; leaving Relay's value in
116
+ * place makes the recovered signer disagree with the submitted one and the
117
+ * authorization is rejected.
118
+ *
119
+ * @param authorization - the authorization carried by the deposit
120
+ * @param options.signatureChainId - the chain id the connected wallet signs under
121
+ * @returns the payload to sign
122
+ */
123
+ export declare const buildHyperliquidAuthorizationTypedData: (authorization: HyperliquidAuthorization, { signatureChainId }: {
124
+ signatureChainId: number;
125
+ }) => HyperliquidTypedData;
126
+ /** A prepared authorization submission: where to send it, and what to send. */
127
+ export type HyperliquidAuthorizationRequest = {
128
+ /**
129
+ * The absolute URL, INCLUDING the signature as a query parameter. Relay reads
130
+ * the signature from the query string, not the body — a request carrying it in
131
+ * the body is accepted and then fails to authorize.
132
+ */
133
+ url: string;
134
+ method: string;
135
+ body: Record<string, unknown>;
136
+ };
137
+ /**
138
+ * Builds the authorization submission for a signed authorization.
139
+ *
140
+ * @param options.authorization - the authorization that was signed
141
+ * @param options.signature - the wallet's signature over its typed data
142
+ * @param options.signatureChainId - the chain id the wallet signed under, which
143
+ * replaces the value Relay quoted so the body agrees with the signature
144
+ * @param options.baseUrl - Relay's API root; the step's endpoint is relative
145
+ * @returns the request to perform, or null when the endpoint is not a usable path
146
+ */
147
+ export declare const buildHyperliquidAuthorizationRequest: ({ authorization, signature, signatureChainId, baseUrl, }: {
148
+ authorization: HyperliquidAuthorization;
149
+ signature: Hex;
150
+ signatureChainId: number;
151
+ baseUrl?: string;
152
+ }) => HyperliquidAuthorizationRequest | null;
@@ -0,0 +1,168 @@
1
+ import { parseSignature } from 'viem';
2
+ import { relayApiBaseUrl } from './relay-quote.js';
3
+ /**
4
+ * Executing a Hyperliquid deposit, as pure data.
5
+ *
6
+ * Hyperliquid is the one origin whose deposit is never broadcast to a chain.
7
+ * The user signs a typed-data ACTION and that signed action is posted to
8
+ * Hyperliquid's own exchange API; Relay then observes the transfer and fills the
9
+ * route. Two details make this impossible to derive from the quote alone, and
10
+ * both are encoded here rather than rediscovered at a call site:
11
+ *
12
+ * 1. Relay's quote supplies `eip712Types`, `eip712PrimaryType` and the action,
13
+ * but NOT the signing domain. The domain is fixed by Hyperliquid and is
14
+ * reconstructed by {@link buildHyperliquidTypedData}.
15
+ * 2. Hyperliquid wants the signature split into `r`/`s`/`v` components, not the
16
+ * concatenated hex string a wallet returns.
17
+ *
18
+ * Kept free of transport so it is fully testable: nothing here signs, fetches,
19
+ * or reads a clock. The caller supplies the signature and performs the request.
20
+ *
21
+ * The shape mirrors Relay's own client, which is the only reference
22
+ * implementation of this exchange — their public documentation covers the
23
+ * generic `signature` step but not this action-bearing `transaction` step.
24
+ */
25
+ /** Where a signed Hyperliquid action is submitted. Hyperliquid's API, not Relay's. */
26
+ export const hyperliquidExchangeUrl = 'https://api.hyperliquid.xyz/exchange';
27
+ /** The signing domain Hyperliquid requires. Fixed; never supplied by the quote. */
28
+ export const hyperliquidDomainName = 'HyperliquidSignTransaction';
29
+ /** The signing domain's version. Fixed alongside {@link hyperliquidDomainName}. */
30
+ export const hyperliquidDomainVersion = '1';
31
+ /**
32
+ * The domain's `verifyingContract`. Hyperliquid actions are verified off-chain,
33
+ * so no contract is involved and the field is the all-zero address.
34
+ */
35
+ export const hyperliquidVerifyingContract = '0x0000000000000000000000000000000000000000';
36
+ /** The standard typed-data domain type, which the action's type table must include. */
37
+ const eip712DomainType = [
38
+ { name: 'name', type: 'string' },
39
+ { name: 'version', type: 'string' },
40
+ { name: 'chainId', type: 'uint256' },
41
+ { name: 'verifyingContract', type: 'address' },
42
+ ];
43
+ /**
44
+ * Builds the typed-data payload for a Hyperliquid deposit.
45
+ *
46
+ * The message is the action's parameters plus the action `type` and a
47
+ * `signatureChainId`; the latter is the signing chain in hex and MUST agree
48
+ * with `domain.chainId`, because Hyperliquid recovers the signer using both.
49
+ * They are derived from the single `signatureChainId` argument so they cannot
50
+ * disagree.
51
+ *
52
+ * @param deposit - the parsed Hyperliquid origin deposit
53
+ * @param options.signatureChainId - the chain id the connected wallet signs
54
+ * under. Not the Relay origin chain id: it identifies the signing domain, and
55
+ * Relay's own client uses the wallet's active chain here.
56
+ * @returns the payload to sign, whose `message` doubles as the submitted action
57
+ */
58
+ export const buildHyperliquidTypedData = (deposit, { signatureChainId }) => ({
59
+ domain: {
60
+ name: hyperliquidDomainName,
61
+ version: hyperliquidDomainVersion,
62
+ chainId: signatureChainId,
63
+ verifyingContract: hyperliquidVerifyingContract,
64
+ },
65
+ types: {
66
+ ...deposit.eip712Types,
67
+ EIP712Domain: eip712DomainType,
68
+ },
69
+ primaryType: deposit.eip712PrimaryType,
70
+ message: {
71
+ ...deposit.action.parameters,
72
+ type: deposit.action.type,
73
+ signatureChainId: `0x${signatureChainId.toString(16)}`,
74
+ },
75
+ });
76
+ /**
77
+ * Builds the exchange request for an action the user has signed.
78
+ *
79
+ * The nonce is read back out of the signed message rather than passed in: it is
80
+ * covered by the signature, so any independently supplied value could disagree
81
+ * with what was signed and be rejected.
82
+ *
83
+ * @param options.typedData - the payload that was signed
84
+ * @param options.signature - the wallet's signature over it
85
+ * @returns the request body, or null when the signed action carries no usable nonce
86
+ */
87
+ export const buildHyperliquidExchangeRequest = ({ typedData, signature, }) => {
88
+ // Hyperliquid names it `nonce`; the older action shapes name it `time`.
89
+ const rawNonce = typedData.message.nonce ?? typedData.message.time;
90
+ if (typeof rawNonce !== 'number' && typeof rawNonce !== 'bigint') {
91
+ return null;
92
+ }
93
+ const { r, s, v, yParity } = parseSignature(signature);
94
+ return {
95
+ // The exchange wants the 27/28 form. A signature whose final byte is the
96
+ // PARITY form (0 or 1) — which some hardware wallets and smart-account flows
97
+ // produce — parses with `v` absent and the parity carried separately, so
98
+ // reading `v` alone and defaulting would send recovery value 0: not a value
99
+ // the exchange accepts, and the deposit dies for those wallets only. The
100
+ // parity is the same fact in the other encoding, so shift it rather than
101
+ // guess.
102
+ signature: { r, s, v: v === undefined ? yParity + 27 : Number(v) },
103
+ nonce: Number(rawNonce),
104
+ action: typedData.message,
105
+ };
106
+ };
107
+ /**
108
+ * Whether an exchange response reports success. Hyperliquid answers with HTTP
109
+ * 200 even for a rejected action, so the body's own status is the only reliable
110
+ * signal — treating the status code as the outcome would report a failed
111
+ * transfer as a completed one.
112
+ *
113
+ * @param payload - the parsed response body
114
+ * @returns true only when the body explicitly reports success
115
+ */
116
+ export const isHyperliquidExchangeSuccess = (payload) => {
117
+ if (typeof payload !== 'object' || payload === null) {
118
+ return false;
119
+ }
120
+ return payload.status === 'ok';
121
+ };
122
+ /**
123
+ * The typed-data payload for the authorization that must be completed BEFORE
124
+ * the action is submitted.
125
+ *
126
+ * Relay builds the authorization against a chain it picks when quoting, but the
127
+ * signature is produced by whichever chain the wallet is actually on. Both the
128
+ * domain here and `signatureChainId` in {@link buildHyperliquidAuthorizationRequest}
129
+ * are therefore overridden with the active wallet chain; leaving Relay's value in
130
+ * place makes the recovered signer disagree with the submitted one and the
131
+ * authorization is rejected.
132
+ *
133
+ * @param authorization - the authorization carried by the deposit
134
+ * @param options.signatureChainId - the chain id the connected wallet signs under
135
+ * @returns the payload to sign
136
+ */
137
+ export const buildHyperliquidAuthorizationTypedData = (authorization, { signatureChainId }) => ({
138
+ domain: {
139
+ ...authorization.sign.domain,
140
+ chainId: signatureChainId,
141
+ },
142
+ types: authorization.sign.types,
143
+ primaryType: authorization.sign.primaryType,
144
+ message: authorization.sign.value,
145
+ });
146
+ /**
147
+ * Builds the authorization submission for a signed authorization.
148
+ *
149
+ * @param options.authorization - the authorization that was signed
150
+ * @param options.signature - the wallet's signature over its typed data
151
+ * @param options.signatureChainId - the chain id the wallet signed under, which
152
+ * replaces the value Relay quoted so the body agrees with the signature
153
+ * @param options.baseUrl - Relay's API root; the step's endpoint is relative
154
+ * @returns the request to perform, or null when the endpoint is not a usable path
155
+ */
156
+ export const buildHyperliquidAuthorizationRequest = ({ authorization, signature, signatureChainId, baseUrl = relayApiBaseUrl, }) => {
157
+ const { endpoint, method, body } = authorization.post;
158
+ if (!endpoint.startsWith('/')) {
159
+ return null;
160
+ }
161
+ const url = new URL(`${baseUrl}${endpoint}`);
162
+ url.searchParams.set('signature', signature);
163
+ return {
164
+ url: url.href,
165
+ method,
166
+ body: { ...body, signatureChainId },
167
+ };
168
+ };