@0xmonaco/core 1.1.7 → 1.1.8

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.
@@ -0,0 +1,357 @@
1
+ /**
2
+ * USDC withdrawals from Hyperliquid (HyperCore) to a Sei deposit address.
3
+ *
4
+ * Circle's CCTP integration on HyperCore withdraws USDC to any CCTP domain
5
+ * through one user-signed HyperCore action, `sendToEvmWithData`
6
+ * (developers.circle.com/cctp/howtos/withdraw-usdc-from-hypercore-to-evm).
7
+ * Hyperliquid executes the signed action itself, so the user pays no gas: its
8
+ * system call into Circle's CoreDepositWallet on HyperEVM burns the USDC with
9
+ * Sei as the destination domain and the deposit address as the mint
10
+ * recipient. Empty `data` selects Circle's default forwarding hook, so Circle
11
+ * mints on Sei and keeps its fee out of the mint. The hub then sweeps the
12
+ * deposit address like any deposit made straight to it.
13
+ */
14
+ import { concat, createPublicClient, hashDomain, hashStruct, http, keccak256, parseAbi, parseSignature, serializeTypedData, } from "viem";
15
+ import { hyperEvm, hyperliquidEvmTestnet, sei, seiTestnet } from "viem/chains";
16
+ import { APIError } from "../../errors";
17
+ /** Sei's CCTP domain: where the withdrawal mints. Not its EVM chain id. */
18
+ export const SEI_CCTP_DOMAIN = 16;
19
+ /** Gas for Hyperliquid's call into the CoreDepositWallet on HyperEVM, as in Circle's guide. */
20
+ const HYPEREVM_GAS_LIMIT = 200_000;
21
+ /** USDC's decimals on HyperEVM and Sei. */
22
+ export const USDC_DECIMALS = 6;
23
+ /**
24
+ * The Hyperliquid network that pairs with the SDK's Sei chain: mainnet with
25
+ * Sei mainnet, testnet with Sei testnet (where every non-mainnet Monaco
26
+ * network settles). `undefined` for any other chain.
27
+ */
28
+ export function hypercoreNetworkFor(chainId) {
29
+ if (chainId === sei.id) {
30
+ return {
31
+ hyperliquidChain: "Mainnet",
32
+ exchangeUrl: "https://api.hyperliquid.xyz/exchange",
33
+ infoUrl: "https://api.hyperliquid.xyz/info",
34
+ hyperEvm,
35
+ coreDepositWallet: "0x6B9E773128f453f5c2C60935Ee2DE2CBc5390A24",
36
+ seiUsdc: "0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392",
37
+ hyperEvmUsdc: "0xb88339CB7199b77E23DB6E890353E22632Ba630f",
38
+ tokenMessenger: "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d",
39
+ irisUrl: "https://iris-api.circle.com",
40
+ };
41
+ }
42
+ if (chainId === seiTestnet.id) {
43
+ return {
44
+ hyperliquidChain: "Testnet",
45
+ exchangeUrl: "https://api.hyperliquid-testnet.xyz/exchange",
46
+ infoUrl: "https://api.hyperliquid-testnet.xyz/info",
47
+ hyperEvm: hyperliquidEvmTestnet,
48
+ coreDepositWallet: "0x0B80659a4076E9E93C7DbE0f10675A16a3e5C206",
49
+ seiUsdc: "0x4fCF1784B31630811181f670Aea7A7bEF803eaED",
50
+ hyperEvmUsdc: "0x2B3370eE501B4a559b57D449569354196457D8Ab",
51
+ tokenMessenger: "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA",
52
+ irisUrl: "https://iris-api-sandbox.circle.com",
53
+ };
54
+ }
55
+ return undefined;
56
+ }
57
+ const CORE_DEPOSIT_WALLET_ABI = parseAbi(["function calculateCrossChainWithdrawalFee(bool shouldForward, uint32 destinationChainId) view returns (uint256)"]);
58
+ /**
59
+ * The most Circle deducts from a forwarded withdrawal to Sei: the CCTP
60
+ * `maxFee` its CoreDepositWallet burns with (its forwarding fee for the
61
+ * domain plus CCTP's own cap). Owner-configurable per domain, so it is read
62
+ * rather than assumed. The wallet also reverts a withdrawal that does not
63
+ * exceed it.
64
+ */
65
+ export async function readMaxFee(network) {
66
+ const client = createPublicClient({ chain: network.hyperEvm, transport: http() });
67
+ return client.readContract({
68
+ address: network.coreDepositWallet,
69
+ abi: CORE_DEPOSIT_WALLET_ABI,
70
+ functionName: "calculateCrossChainWithdrawalFee",
71
+ args: [true, SEI_CCTP_DOMAIN],
72
+ });
73
+ }
74
+ /** USDC's token index on HyperCore, on mainnet and testnet alike (`spotMeta`). */
75
+ const HYPERCORE_USDC_TOKEN = 0;
76
+ /**
77
+ * Account modes that keep one balance per asset. Hyperliquid reports such an
78
+ * account's whole balance in its spot state, and its perp state "is not
79
+ * meaningful" (hyperliquid-docs: trading/account-abstraction-modes).
80
+ */
81
+ function isUnified(mode) {
82
+ return mode === "unifiedAccount" || mode === "portfolioMargin";
83
+ }
84
+ /** Read the account's mode and USDC balances from Hyperliquid's public info endpoint. */
85
+ export async function readHypercoreAccount(network, user) {
86
+ const query = (type) => postToInfo(network, { type, user: user.toLowerCase() });
87
+ const [mode, perpState, spotState] = await Promise.all([
88
+ query("userAbstraction"),
89
+ query("clearinghouseState"),
90
+ query("spotClearinghouseState"),
91
+ ]);
92
+ const usdc = spotState.balances?.find((balance) => balance.token === HYPERCORE_USDC_TOKEN);
93
+ // Subtract at Hyperliquid's own precision and truncate only the result:
94
+ // truncating total and hold separately can overstate the unheld balance.
95
+ const spot = usdc ? toExact(usdc.total, network) - toExact(usdc.hold, network) : 0n;
96
+ return {
97
+ mode,
98
+ perp: isUnified(mode) ? 0n : toRawUsdc(toExact(perpState.withdrawable, network)),
99
+ spot: spot > 0n ? toRawUsdc(spot) : 0n,
100
+ };
101
+ }
102
+ /**
103
+ * Raw USDC kept back for Hyperliquid's own fee on a withdrawal to HyperEVM,
104
+ * which it charges on top of the amount. Hyperliquid documents the fee as
105
+ * 200k gas at the next HyperEVM block's base fee but does not publish the
106
+ * USDC rate it charges. Derived, not quoted: on 2026-11-05 a mainnet
107
+ * withdrawal was charged 0.012948 USDC and testnet withdrawals 0.07122 to
108
+ * 0.072664, and 0.1 covers both with room.
109
+ */
110
+ const HYPERLIQUID_FEE_RESERVE = 100000n;
111
+ /**
112
+ * The most a perp draw can take. Hyperliquid charges its fee to spot when
113
+ * spot covers it, and to the balance withdrawn otherwise (both measured on
114
+ * testnet, the second with 0.01 USDC in spot), so perp keeps the reserve back
115
+ * only when spot holds less than it.
116
+ */
117
+ function perpDrawable(account) {
118
+ if (account.spot >= HYPERLIQUID_FEE_RESERVE) {
119
+ return account.perp;
120
+ }
121
+ return account.perp > HYPERLIQUID_FEE_RESERVE ? account.perp - HYPERLIQUID_FEE_RESERVE : 0n;
122
+ }
123
+ /** The most a spot draw can take: spot pays Hyperliquid's fee on top of it. */
124
+ function spotDrawable(account) {
125
+ return account.spot > HYPERLIQUID_FEE_RESERVE ? account.spot - HYPERLIQUID_FEE_RESERVE : 0n;
126
+ }
127
+ /**
128
+ * The balance one withdrawal of `amount` draws on, or `undefined` when none
129
+ * covers it with room for Hyperliquid's fee. A unified or portfolio-margin
130
+ * account draws on spot: Hyperliquid refuses any other source for it
131
+ * ("Unified account only supports sending assets through spot"). Otherwise
132
+ * perp comes first, where a deposit to Hyperliquid lands by default.
133
+ */
134
+ export function chooseSource(account, amount) {
135
+ if (!isUnified(account.mode) && amount <= perpDrawable(account)) {
136
+ return "perp";
137
+ }
138
+ return amount <= spotDrawable(account) ? "spot" : undefined;
139
+ }
140
+ /** The most one withdrawal can draw: it draws on a single balance and leaves room for Hyperliquid's fee. */
141
+ export function availableToDraw(account) {
142
+ const spot = spotDrawable(account);
143
+ if (isUnified(account.mode)) {
144
+ return spot;
145
+ }
146
+ // Not Math.max: it does not take bigints.
147
+ const perp = perpDrawable(account);
148
+ if (perp > spot) {
149
+ return perp;
150
+ }
151
+ return spot;
152
+ }
153
+ /** POST one query to Hyperliquid's info endpoint; any failure is an `APIError`. */
154
+ async function postToInfo(network, body) {
155
+ const endpoint = network.infoUrl;
156
+ let response;
157
+ try {
158
+ response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
159
+ }
160
+ catch (error) {
161
+ throw new APIError("Network request to Hyperliquid failed", { endpoint, cause: error instanceof Error ? error : new Error(String(error)) });
162
+ }
163
+ const text = await response.text().catch(() => "");
164
+ if (!response.ok) {
165
+ throw new APIError(`Hyperliquid's ${body.type} query answered ${response.status}`, {
166
+ endpoint,
167
+ statusCode: response.status,
168
+ responseBody: text.slice(0, 500),
169
+ });
170
+ }
171
+ try {
172
+ return JSON.parse(text);
173
+ }
174
+ catch {
175
+ throw new APIError(`Hyperliquid's ${body.type} reply was not readable`, { endpoint, statusCode: response.status, responseBody: text.slice(0, 500) });
176
+ }
177
+ }
178
+ /**
179
+ * Fixed-point scale for Hyperliquid's decimal strings: above any precision it
180
+ * reports (USDC carries 8 `weiDecimals`), so arithmetic on it is exact.
181
+ */
182
+ const EXACT_DECIMALS = 18;
183
+ /** Hyperliquid's decimal-string amount at {@link EXACT_DECIMALS}. */
184
+ function toExact(value, network) {
185
+ const match = typeof value === "string" ? /^(\d+)(?:\.(\d+))?$/.exec(value) : null;
186
+ if (!match) {
187
+ throw new APIError(`Hyperliquid reported an unreadable USDC amount: ${JSON.stringify(value)}`, { endpoint: network.infoUrl });
188
+ }
189
+ const [, whole = "0", fraction = ""] = match;
190
+ return BigInt(whole) * 10n ** BigInt(EXACT_DECIMALS) + BigInt(fraction.slice(0, EXACT_DECIMALS).padEnd(EXACT_DECIMALS, "0"));
191
+ }
192
+ /** A non-negative exact amount in raw 6-decimal USDC, truncated so the SDK never offers more than the account holds. */
193
+ function toRawUsdc(exact) {
194
+ return exact / 10n ** BigInt(EXACT_DECIMALS - USDC_DECIMALS);
195
+ }
196
+ const PRIMARY_TYPE = "HyperliquidTransaction:SendToEvmWithData";
197
+ /** The action's EIP-712 type, as Circle documents it; field order is part of the hash. */
198
+ const SEND_TO_EVM_WITH_DATA_TYPES = {
199
+ [PRIMARY_TYPE]: [
200
+ { name: "hyperliquidChain", type: "string" },
201
+ { name: "token", type: "string" },
202
+ { name: "amount", type: "string" },
203
+ { name: "sourceDex", type: "string" },
204
+ { name: "destinationRecipient", type: "string" },
205
+ { name: "addressEncoding", type: "string" },
206
+ { name: "destinationChainId", type: "uint32" },
207
+ { name: "gasLimit", type: "uint64" },
208
+ { name: "data", type: "bytes" },
209
+ { name: "nonce", type: "uint64" },
210
+ ],
211
+ };
212
+ /** The signature domain's type: every field `HyperliquidSignTransaction` sets. */
213
+ const EIP712_DOMAIN_TYPE = [
214
+ { name: "name", type: "string" },
215
+ { name: "version", type: "string" },
216
+ { name: "chainId", type: "uint256" },
217
+ { name: "verifyingContract", type: "address" },
218
+ ];
219
+ /**
220
+ * Sign a USDC withdrawal from Hyperliquid to `recipient` on Sei.
221
+ *
222
+ * The signature's domain uses the wallet's own chain (Sei), which Hyperliquid
223
+ * accepts from any chain and wallets that hold the domain to the active
224
+ * network sign without switching.
225
+ */
226
+ export async function signWithdrawalToSei(params) {
227
+ const { walletClient, network, amount, sourceDex, recipient, nonce } = params;
228
+ const chainId = walletClient.chain?.id;
229
+ const account = walletClient.account;
230
+ if (chainId === undefined || account === undefined) {
231
+ throw new Error("signWithdrawalToSei needs a wallet client with a chain and an account");
232
+ }
233
+ const message = {
234
+ hyperliquidChain: network.hyperliquidChain,
235
+ token: "USDC",
236
+ amount,
237
+ sourceDex,
238
+ destinationRecipient: recipient,
239
+ addressEncoding: "hex",
240
+ destinationChainId: SEI_CCTP_DOMAIN,
241
+ gasLimit: BigInt(HYPEREVM_GAS_LIMIT),
242
+ data: "0x",
243
+ nonce: BigInt(nonce),
244
+ };
245
+ const domain = {
246
+ name: "HyperliquidSignTransaction",
247
+ version: "1",
248
+ chainId,
249
+ verifyingContract: "0x0000000000000000000000000000000000000000",
250
+ };
251
+ const signature = await signHyperliquidTypedData(walletClient, account, domain, message);
252
+ const { r, s, yParity } = parseSignature(signature);
253
+ return {
254
+ action: {
255
+ type: "sendToEvmWithData",
256
+ signatureChainId: `0x${chainId.toString(16)}`,
257
+ ...message,
258
+ gasLimit: HYPEREVM_GAS_LIMIT,
259
+ nonce,
260
+ },
261
+ nonce,
262
+ signature: { r, s, v: 27 + yParity },
263
+ };
264
+ }
265
+ /**
266
+ * Sign Hyperliquid typed data with the wallet's account.
267
+ *
268
+ * viem's `signTypedData` refuses this type from 2.57.12 (wevm/viem#5308 lets
269
+ * a struct name hold only letters, digits, `_` and `$`; wevm/viem#5403 allows
270
+ * `:` again in a later release), and Hyperliquid names every user-signed
271
+ * action `HyperliquidTransaction:<Action>` — the colon is part of the hashed
272
+ * type, so it cannot be dropped. The SDK runs on whatever viem the app
273
+ * resolves, so it does not rely on the fix being installed. A local account therefore
274
+ * signs the EIP-712 digest built from viem's unvalidated `hashDomain` and
275
+ * `hashStruct`; any other account (a browser or embedded wallet) is sent the
276
+ * typed data as `eth_signTypedData_v4`, exactly as viem would send it.
277
+ */
278
+ async function signHyperliquidTypedData(walletClient, account, domain, message) {
279
+ const types = { EIP712Domain: EIP712_DOMAIN_TYPE, ...SEND_TO_EVM_WITH_DATA_TYPES };
280
+ if (account.type === "local") {
281
+ const sign = account.sign;
282
+ if (!sign) {
283
+ throw new Error("The wallet's local account cannot sign a raw digest");
284
+ }
285
+ const digest = keccak256(concat([
286
+ "0x1901",
287
+ hashDomain({ domain: { ...domain, chainId: BigInt(domain.chainId) }, types: { EIP712Domain: EIP712_DOMAIN_TYPE } }),
288
+ hashStruct({ data: message, primaryType: PRIMARY_TYPE, types }),
289
+ ]));
290
+ return sign({ hash: digest });
291
+ }
292
+ return walletClient.request({
293
+ method: "eth_signTypedData_v4",
294
+ params: [account.address, serializeTypedData({ domain, message, primaryType: PRIMARY_TYPE, types })],
295
+ });
296
+ }
297
+ /**
298
+ * The suggestion every unknown outcome carries. Calling the method again signs
299
+ * a fresh nonce, so a retry after an executed withdrawal moves the funds twice.
300
+ */
301
+ const UNKNOWN_OUTCOME_SUGGESTION = "Hyperliquid may have executed the withdrawal. Do not call depositFromHypercore again, which signs a second one: check sdk.sweeper.listDeposits (the deposit lists once it is minted on Sei) or the Hyperliquid balance first.";
302
+ /** Clear `retryable` on an error whose outcome is unknown (see {@link UNKNOWN_OUTCOME_SUGGESTION}). */
303
+ function unknownOutcome(error) {
304
+ error.retryable = false;
305
+ error.suggestion = UNKNOWN_OUTCOME_SUGGESTION;
306
+ return error;
307
+ }
308
+ /**
309
+ * Post a signed withdrawal to Hyperliquid's exchange endpoint.
310
+ *
311
+ * Hyperliquid answers a decided rejection with HTTP 200 and
312
+ * `{"status":"err","response":"<reason>"}`, so the status alone does not tell
313
+ * success from refusal. A refusal is a decided outcome (nothing moved); a
314
+ * dropped connection, a 5xx, or a reply that is neither `ok` nor `err` leaves
315
+ * the outcome unknown. The signed body is never attached to an error.
316
+ */
317
+ export async function postToExchange(network, withdrawal) {
318
+ const endpoint = network.exchangeUrl;
319
+ let response;
320
+ try {
321
+ response = await fetch(endpoint, {
322
+ method: "POST",
323
+ headers: { "Content-Type": "application/json" },
324
+ body: JSON.stringify(withdrawal),
325
+ });
326
+ }
327
+ catch (error) {
328
+ throw unknownOutcome(new APIError(`Network request to Hyperliquid failed`, { endpoint, cause: error instanceof Error ? error : new Error(String(error)) }));
329
+ }
330
+ const text = await response.text().catch(() => "");
331
+ if (response.status >= 500) {
332
+ throw unknownOutcome(new APIError(`Hyperliquid answered ${response.status}`, { endpoint, statusCode: response.status, responseBody: text.slice(0, 500) }));
333
+ }
334
+ if (!response.ok) {
335
+ throw new APIError(`Hyperliquid refused the withdrawal: ${response.status}`, { endpoint, statusCode: response.status, responseBody: text.slice(0, 500) });
336
+ }
337
+ let body;
338
+ try {
339
+ body = JSON.parse(text);
340
+ }
341
+ catch {
342
+ body = undefined;
343
+ }
344
+ const status = body !== null && typeof body === "object" ? body.status : undefined;
345
+ if (status === "ok") {
346
+ return;
347
+ }
348
+ if (status === "err") {
349
+ const reason = body.response;
350
+ throw new APIError(`Hyperliquid rejected the withdrawal: ${typeof reason === "string" ? reason : JSON.stringify(reason)}`, {
351
+ endpoint,
352
+ statusCode: response.status,
353
+ responseBody: body,
354
+ });
355
+ }
356
+ throw unknownOutcome(new APIError("Hyperliquid's reply was not readable", { endpoint, statusCode: response.status, responseBody: text.slice(0, 500) }));
357
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * USDC deposits from HyperEVM to a Sei deposit address.
3
+ *
4
+ * The wallet burns its Circle USDC on HyperEVM through Circle's
5
+ * TokenMessengerV2, naming the Sei deposit address as the mint recipient and
6
+ * attaching Circle's forwarding hook, so Circle mints on Sei
7
+ * (developers.circle.com/cctp/concepts/forwarding-service). The hub sweeps the
8
+ * mint like any deposit made straight to the address, so nothing of Monaco's
9
+ * runs on HyperEVM. The wallet pays HYPE for the approval and burn.
10
+ */
11
+ import { type Address, type Hash, type WalletClient } from "viem";
12
+ import { type HypercoreNetwork } from "./hypercore";
13
+ /**
14
+ * Circle's forwarding fee for a HyperEVM-to-Sei transfer on the standard lane,
15
+ * at Circle's "high" level. Every forwarded transfer we sent was charged its
16
+ * whole `maxFee`, so the quote is used as is rather than padded; and a
17
+ * `maxFee` below the forwarding fee would leave the mint unforwarded, so the
18
+ * top of Circle's range is the safe choice.
19
+ */
20
+ export declare function quoteForwardFee(network: HypercoreNetwork): Promise<bigint>;
21
+ /** The wallet's Circle USDC, its allowance to TokenMessengerV2, and its HYPE, on HyperEVM. */
22
+ export interface HyperEvmAccount {
23
+ usdc: bigint;
24
+ allowance: bigint;
25
+ gas: bigint;
26
+ }
27
+ export declare function readHyperEvmAccount(network: HypercoreNetwork, address: Address): Promise<HyperEvmAccount>;
28
+ /**
29
+ * Approve (when the allowance is short) and burn `amount` of the wallet's
30
+ * HyperEVM USDC to `recipient` on Sei, with Circle forwarding the mint.
31
+ *
32
+ * Decided failures — nothing was burned — throw as they are: a wallet's
33
+ * refusal, a failure before the burn was broadcast, and a `ContractError` for
34
+ * a reverted, cancelled or replaced approval or burn. A burn that may have
35
+ * been broadcast but whose outcome cannot be read is an unknown outcome: the
36
+ * `APIError` carries `retryable: false` and, when known, the burn's hash,
37
+ * because calling again burns a second time. An approval in that state stays
38
+ * retryable, since the next call re-reads the allowance.
39
+ */
40
+ export declare function burnToSei(params: {
41
+ walletClient: WalletClient;
42
+ network: HypercoreNetwork;
43
+ amount: bigint;
44
+ recipient: string;
45
+ maxFee: bigint;
46
+ allowance: bigint;
47
+ }): Promise<{
48
+ approveTxHash?: Hash;
49
+ burnTxHash: Hash;
50
+ }>;