@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.
- package/dist/api/fees/api.d.ts +2 -0
- package/dist/api/fees/api.js +2 -0
- package/dist/api/sweeper/api.d.ts +109 -1
- package/dist/api/sweeper/api.js +225 -1
- package/dist/api/sweeper/hypercore.d.ts +103 -0
- package/dist/api/sweeper/hypercore.js +357 -0
- package/dist/api/sweeper/hyperevm.d.ts +50 -0
- package/dist/api/sweeper/hyperevm.js +282 -0
- package/dist/api/vault/api.d.ts +28 -0
- package/dist/api/vault/api.js +136 -42
- package/dist/errors/errors.d.ts +44 -0
- package/dist/errors/errors.js +61 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/sdk.js +4 -0
- package/package.json +3 -3
package/dist/api/fees/api.d.ts
CHANGED
|
@@ -90,6 +90,8 @@ export declare class FeesAPIImpl extends BaseAPI implements FeesAPI {
|
|
|
90
90
|
* tradingPairId: "123e4567-e89b-12d3-a456-426614174000",
|
|
91
91
|
* });
|
|
92
92
|
* console.log(`Current tier: ${tier.currentTierLevel}`);
|
|
93
|
+
* console.log(`Rate source: ${tier.rateSource}`);
|
|
94
|
+
* console.log(`Maker ${tier.makerFeeBps} bps, taker ${tier.takerFeeBps} bps`);
|
|
93
95
|
* console.log(`14d volume: ${tier.weightedVolume14d}`);
|
|
94
96
|
* for (const row of tier.feeSchedule) {
|
|
95
97
|
* console.log(`Tier ${row.tierLevel}: taker ${row.takerFeeBps} bps`);
|
package/dist/api/fees/api.js
CHANGED
|
@@ -108,6 +108,8 @@ export class FeesAPIImpl extends BaseAPI {
|
|
|
108
108
|
* tradingPairId: "123e4567-e89b-12d3-a456-426614174000",
|
|
109
109
|
* });
|
|
110
110
|
* console.log(`Current tier: ${tier.currentTierLevel}`);
|
|
111
|
+
* console.log(`Rate source: ${tier.rateSource}`);
|
|
112
|
+
* console.log(`Maker ${tier.makerFeeBps} bps, taker ${tier.takerFeeBps} bps`);
|
|
111
113
|
* console.log(`14d volume: ${tier.weightedVolume14d}`);
|
|
112
114
|
* for (const row of tier.feeSchedule) {
|
|
113
115
|
* console.log(`Tier ${row.tierLevel}: taker ${row.takerFeeBps} bps`);
|
|
@@ -22,9 +22,16 @@
|
|
|
22
22
|
* const margin = await sdk.sweeper.register({ depositTarget: "margin" });
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
|
-
import type { GetSweeperChainsResponse, GetSweeperDepositStatusResponse, ListSweeperDepositsParams, ListSweeperDepositsResponse, RegisterSweeperRequest, RegisterSweeperResponse, SweeperAPI } from "@0xmonaco/types";
|
|
25
|
+
import type { DepositFromHypercoreRequest, DepositFromHypercoreResponse, DepositFromHyperEvmRequest, DepositFromHyperEvmResponse, GetSweeperChainsResponse, GetSweeperDepositStatusResponse, HypercoreDepositInfo, HyperEvmDepositInfo, ListSweeperDepositsParams, ListSweeperDepositsResponse, RegisterSweeperRequest, RegisterSweeperResponse, SweeperAPI } from "@0xmonaco/types";
|
|
26
|
+
import { type WalletClient } from "viem";
|
|
26
27
|
import { BaseAPI } from "../base";
|
|
27
28
|
export declare class SweeperAPIImpl extends BaseAPI implements SweeperAPI {
|
|
29
|
+
private walletClient;
|
|
30
|
+
/**
|
|
31
|
+
* Set the wallet client that signs {@link depositFromHypercore}. Its
|
|
32
|
+
* account must be the one holding the Hyperliquid balance.
|
|
33
|
+
*/
|
|
34
|
+
setWalletClient(walletClient: WalletClient): void;
|
|
28
35
|
/**
|
|
29
36
|
* Registers an (application, user, deposit target) deposit address for
|
|
30
37
|
* monitoring. Requires authentication; the user is the session's own
|
|
@@ -90,4 +97,105 @@ export declare class SweeperAPIImpl extends BaseAPI implements SweeperAPI {
|
|
|
90
97
|
* on the last page)
|
|
91
98
|
*/
|
|
92
99
|
listDeposits(params?: ListSweeperDepositsParams): Promise<ListSweeperDepositsResponse>;
|
|
100
|
+
/**
|
|
101
|
+
* Deposits USDC the wallet holds on Hyperliquid (HyperCore). The wallet
|
|
102
|
+
* signs Circle's CCTP withdrawal (`sendToEvmWithData`) to the caller's
|
|
103
|
+
* deposit address on Sei, and the SDK posts it to Hyperliquid, which
|
|
104
|
+
* executes it — no gas and no HyperEVM transaction from the user. Circle
|
|
105
|
+
* forwards the mint to Sei, keeping up to `maxFee` of it, and the sweeper
|
|
106
|
+
* credits the rest like any deposit made straight to the address; it lists
|
|
107
|
+
* in {@link listDeposits} by its Sei mint.
|
|
108
|
+
*
|
|
109
|
+
* The Hyperliquid balance is chosen from the account itself (see
|
|
110
|
+
* {@link getHypercoreDepositInfo}): spot for a unified or portfolio-margin
|
|
111
|
+
* account, otherwise whichever of perp (first) or spot covers `amount`.
|
|
112
|
+
* Before registering or signing, this refuses an amount below the minimum
|
|
113
|
+
* — a credit under the hub's floor would sit on the address unswept, and a
|
|
114
|
+
* withdrawal that does not exceed Circle's fee reverts on HyperEVM — and an
|
|
115
|
+
* amount no single balance covers. It then registers the deposit address
|
|
116
|
+
* (so it is monitored) and signs.
|
|
117
|
+
*
|
|
118
|
+
* @param body - The raw USDC amount (6 decimals), and optionally the
|
|
119
|
+
* deposit target
|
|
120
|
+
* @returns Promise resolving once Hyperliquid accepts the withdrawal
|
|
121
|
+
* @throws {InvalidConfigError} On an invalid amount, a missing wallet, a
|
|
122
|
+
* wallet off Sei, or an amount below the minimum or above what one
|
|
123
|
+
* balance holds
|
|
124
|
+
* @throws {InvalidStateError} When the hub does not sweep USDC
|
|
125
|
+
* @throws {APIError} When a Hyperliquid read fails (nothing signed), or
|
|
126
|
+
* when Hyperliquid refuses the action (nothing moved). A lost or
|
|
127
|
+
* unreadable reply to the withdrawal itself is an unknown outcome and
|
|
128
|
+
* carries `retryable: false`: calling again signs a second withdrawal, so
|
|
129
|
+
* check {@link listDeposits} or the Hyperliquid balance first.
|
|
130
|
+
*/
|
|
131
|
+
depositFromHypercore(body: DepositFromHypercoreRequest): Promise<DepositFromHypercoreResponse>;
|
|
132
|
+
/**
|
|
133
|
+
* What the wallet can deposit from Hyperliquid: its account mode and USDC
|
|
134
|
+
* balances, the most one deposit can draw, Circle's fee, and the smallest
|
|
135
|
+
* amount {@link depositFromHypercore} accepts. Reads only public data
|
|
136
|
+
* (Hyperliquid's info endpoint, Circle's CoreDepositWallet on HyperEVM and
|
|
137
|
+
* {@link getChains}), so it needs a wallet client on Sei but no session.
|
|
138
|
+
*
|
|
139
|
+
* @returns Promise resolving to the account's deposit info, amounts in raw
|
|
140
|
+
* USDC base units (6 decimals)
|
|
141
|
+
* @throws {InvalidConfigError} On a missing wallet or a wallet off Sei
|
|
142
|
+
* @throws {InvalidStateError} When the hub does not sweep USDC
|
|
143
|
+
* @throws {APIError} When a Hyperliquid read fails
|
|
144
|
+
*/
|
|
145
|
+
getHypercoreDepositInfo(): Promise<HypercoreDepositInfo>;
|
|
146
|
+
/**
|
|
147
|
+
* Deposits Circle USDC the wallet holds on HyperEVM. The wallet approves
|
|
148
|
+
* (when its allowance is short) and burns it through Circle's
|
|
149
|
+
* TokenMessengerV2 on HyperEVM, naming the caller's Sei deposit address as
|
|
150
|
+
* the recipient and asking Circle to forward the mint. Circle mints on Sei,
|
|
151
|
+
* keeping up to `maxFee`, and the sweeper credits the rest like any deposit
|
|
152
|
+
* made straight to the address; it lists in {@link listDeposits} by its Sei
|
|
153
|
+
* mint. The wallet pays HYPE for gas; no Monaco contract is involved on
|
|
154
|
+
* HyperEVM.
|
|
155
|
+
*
|
|
156
|
+
* A browser or embedded wallet is switched to HyperEVM (added first when it
|
|
157
|
+
* does not know the chain) and back to Sei afterwards. Before registering
|
|
158
|
+
* or sending, this refuses an amount below the minimum or above the
|
|
159
|
+
* wallet's HyperEVM USDC, and a wallet with no HYPE.
|
|
160
|
+
*
|
|
161
|
+
* @param body - The raw USDC amount (6 decimals), and optionally the
|
|
162
|
+
* deposit target
|
|
163
|
+
* @returns Promise resolving once the burn lands on HyperEVM
|
|
164
|
+
* @throws {InvalidConfigError} On an invalid amount, a missing wallet, a
|
|
165
|
+
* wallet off Sei, or an amount below the minimum or above the balance
|
|
166
|
+
* @throws {InvalidStateError} When the hub does not sweep USDC or the
|
|
167
|
+
* wallet has no HYPE
|
|
168
|
+
* @throws {APIError} When Circle's fee quote fails (nothing sent), or when
|
|
169
|
+
* the burn was sent but its receipt cannot be read: an unknown outcome
|
|
170
|
+
* carrying `retryable: false` and the burn's hash, since calling again
|
|
171
|
+
* burns a second time
|
|
172
|
+
* @throws {ContractError} When the approval or the burn reverts (nothing
|
|
173
|
+
* burned)
|
|
174
|
+
*/
|
|
175
|
+
depositFromHyperEvm(body: DepositFromHyperEvmRequest): Promise<DepositFromHyperEvmResponse>;
|
|
176
|
+
/**
|
|
177
|
+
* What the wallet can deposit from HyperEVM: its Circle USDC and HYPE
|
|
178
|
+
* there, Circle's forwarding fee, and the smallest amount
|
|
179
|
+
* {@link depositFromHyperEvm} accepts. Reads only public data (the
|
|
180
|
+
* HyperEVM RPC, Circle's fee API and {@link getChains}), so it needs a
|
|
181
|
+
* wallet client on Sei but no session.
|
|
182
|
+
*
|
|
183
|
+
* @returns Promise resolving to the wallet's HyperEVM deposit info
|
|
184
|
+
* @throws {InvalidConfigError} On a missing wallet or a wallet off Sei
|
|
185
|
+
* @throws {InvalidStateError} When the hub does not sweep USDC
|
|
186
|
+
* @throws {APIError} When Circle's fee quote fails
|
|
187
|
+
*/
|
|
188
|
+
getHyperEvmDepositInfo(): Promise<HyperEvmDepositInfo>;
|
|
189
|
+
/** The reads both Hyperliquid (HyperCore) methods start from, made in parallel. */
|
|
190
|
+
private readHypercoreDepositInfo;
|
|
191
|
+
/** The reads both HyperEVM methods start from, made in parallel. */
|
|
192
|
+
private readHyperEvmDepositInfo;
|
|
193
|
+
/** The wallet client, which must be on Sei, and the Hyperliquid network that pairs with its chain. */
|
|
194
|
+
private walletOnSei;
|
|
195
|
+
/**
|
|
196
|
+
* The Sei hub's minimum for Circle's USDC, raw. By address, never by symbol:
|
|
197
|
+
* Sei also carries Noble's bridged USDC.n, and both routes mint Circle's
|
|
198
|
+
* native USDC.
|
|
199
|
+
*/
|
|
200
|
+
private hubUsdcFloor;
|
|
93
201
|
}
|
package/dist/api/sweeper/api.js
CHANGED
|
@@ -22,9 +22,13 @@
|
|
|
22
22
|
* const margin = await sdk.sweeper.register({ depositTarget: "margin" });
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
|
-
import {
|
|
25
|
+
import { formatUnits } from "viem";
|
|
26
|
+
import { sei, seiTestnet } from "viem/chains";
|
|
27
|
+
import { InvalidConfigError, InvalidStateError } from "../../errors";
|
|
26
28
|
import { BaseAPI } from "../base";
|
|
27
29
|
import { perpRoutes } from "../perp/routes";
|
|
30
|
+
import { availableToDraw, chooseSource, hypercoreNetworkFor, postToExchange, readHypercoreAccount, readMaxFee, signWithdrawalToSei, USDC_DECIMALS, } from "./hypercore";
|
|
31
|
+
import { burnToSei, quoteForwardFee, readHyperEvmAccount } from "./hyperevm";
|
|
28
32
|
/**
|
|
29
33
|
* Ergonomic target to its wire form.
|
|
30
34
|
*
|
|
@@ -52,6 +56,14 @@ function toWireDepositTarget(target) {
|
|
|
52
56
|
throw new InvalidConfigError(`Invalid deposit target: ${JSON.stringify(target)}. Expected "spot" or "margin".`);
|
|
53
57
|
}
|
|
54
58
|
export class SweeperAPIImpl extends BaseAPI {
|
|
59
|
+
walletClient;
|
|
60
|
+
/**
|
|
61
|
+
* Set the wallet client that signs {@link depositFromHypercore}. Its
|
|
62
|
+
* account must be the one holding the Hyperliquid balance.
|
|
63
|
+
*/
|
|
64
|
+
setWalletClient(walletClient) {
|
|
65
|
+
this.walletClient = walletClient;
|
|
66
|
+
}
|
|
55
67
|
/**
|
|
56
68
|
* Registers an (application, user, deposit target) deposit address for
|
|
57
69
|
* monitoring. Requires authentication; the user is the session's own
|
|
@@ -143,6 +155,218 @@ export class SweeperAPIImpl extends BaseAPI {
|
|
|
143
155
|
method: "GET",
|
|
144
156
|
});
|
|
145
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Deposits USDC the wallet holds on Hyperliquid (HyperCore). The wallet
|
|
160
|
+
* signs Circle's CCTP withdrawal (`sendToEvmWithData`) to the caller's
|
|
161
|
+
* deposit address on Sei, and the SDK posts it to Hyperliquid, which
|
|
162
|
+
* executes it — no gas and no HyperEVM transaction from the user. Circle
|
|
163
|
+
* forwards the mint to Sei, keeping up to `maxFee` of it, and the sweeper
|
|
164
|
+
* credits the rest like any deposit made straight to the address; it lists
|
|
165
|
+
* in {@link listDeposits} by its Sei mint.
|
|
166
|
+
*
|
|
167
|
+
* The Hyperliquid balance is chosen from the account itself (see
|
|
168
|
+
* {@link getHypercoreDepositInfo}): spot for a unified or portfolio-margin
|
|
169
|
+
* account, otherwise whichever of perp (first) or spot covers `amount`.
|
|
170
|
+
* Before registering or signing, this refuses an amount below the minimum
|
|
171
|
+
* — a credit under the hub's floor would sit on the address unswept, and a
|
|
172
|
+
* withdrawal that does not exceed Circle's fee reverts on HyperEVM — and an
|
|
173
|
+
* amount no single balance covers. It then registers the deposit address
|
|
174
|
+
* (so it is monitored) and signs.
|
|
175
|
+
*
|
|
176
|
+
* @param body - The raw USDC amount (6 decimals), and optionally the
|
|
177
|
+
* deposit target
|
|
178
|
+
* @returns Promise resolving once Hyperliquid accepts the withdrawal
|
|
179
|
+
* @throws {InvalidConfigError} On an invalid amount, a missing wallet, a
|
|
180
|
+
* wallet off Sei, or an amount below the minimum or above what one
|
|
181
|
+
* balance holds
|
|
182
|
+
* @throws {InvalidStateError} When the hub does not sweep USDC
|
|
183
|
+
* @throws {APIError} When a Hyperliquid read fails (nothing signed), or
|
|
184
|
+
* when Hyperliquid refuses the action (nothing moved). A lost or
|
|
185
|
+
* unreadable reply to the withdrawal itself is an unknown outcome and
|
|
186
|
+
* carries `retryable: false`: calling again signs a second withdrawal, so
|
|
187
|
+
* check {@link listDeposits} or the Hyperliquid balance first.
|
|
188
|
+
*/
|
|
189
|
+
async depositFromHypercore(body) {
|
|
190
|
+
const { amount, depositTarget } = body;
|
|
191
|
+
if (typeof amount !== "bigint" || amount <= 0n) {
|
|
192
|
+
throw new InvalidConfigError(`Invalid amount: ${String(amount)}. Expected a positive bigint of raw USDC base units (6 decimals).`, "amount", amount);
|
|
193
|
+
}
|
|
194
|
+
const { walletClient, network, account, info } = await this.readHypercoreDepositInfo("depositFromHypercore");
|
|
195
|
+
if (amount < info.minimumAmount) {
|
|
196
|
+
throw new InvalidConfigError(`Deposit at least ${formatUnits(info.minimumAmount, USDC_DECIMALS)} USDC from Hyperliquid: Circle keeps up to ${formatUnits(info.maxFee, USDC_DECIMALS)} USDC to forward it to Sei, and the hub only sweeps a credit above its minimum.`, "amount", amount);
|
|
197
|
+
}
|
|
198
|
+
const source = chooseSource(account, amount);
|
|
199
|
+
if (!source) {
|
|
200
|
+
throw new InvalidConfigError(`Hyperliquid holds too little USDC: one deposit can draw at most ${formatUnits(info.available, USDC_DECIMALS)} USDC (perp ${formatUnits(info.perpBalance, USDC_DECIMALS)}, spot ${formatUnits(info.spotBalance, USDC_DECIMALS)}).`, "amount", amount);
|
|
201
|
+
}
|
|
202
|
+
const { sweeperAddress } = await this.register(depositTarget === undefined ? {} : { depositTarget });
|
|
203
|
+
const nonce = Date.now();
|
|
204
|
+
const withdrawal = await signWithdrawalToSei({
|
|
205
|
+
walletClient,
|
|
206
|
+
network,
|
|
207
|
+
amount: formatUnits(amount, USDC_DECIMALS),
|
|
208
|
+
sourceDex: source === "perp" ? "" : "spot",
|
|
209
|
+
recipient: sweeperAddress,
|
|
210
|
+
nonce,
|
|
211
|
+
});
|
|
212
|
+
await postToExchange(network, withdrawal);
|
|
213
|
+
return { sweeperAddress, amount, source, maxFee: info.maxFee, minimumCredit: amount - info.maxFee, nonce };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* What the wallet can deposit from Hyperliquid: its account mode and USDC
|
|
217
|
+
* balances, the most one deposit can draw, Circle's fee, and the smallest
|
|
218
|
+
* amount {@link depositFromHypercore} accepts. Reads only public data
|
|
219
|
+
* (Hyperliquid's info endpoint, Circle's CoreDepositWallet on HyperEVM and
|
|
220
|
+
* {@link getChains}), so it needs a wallet client on Sei but no session.
|
|
221
|
+
*
|
|
222
|
+
* @returns Promise resolving to the account's deposit info, amounts in raw
|
|
223
|
+
* USDC base units (6 decimals)
|
|
224
|
+
* @throws {InvalidConfigError} On a missing wallet or a wallet off Sei
|
|
225
|
+
* @throws {InvalidStateError} When the hub does not sweep USDC
|
|
226
|
+
* @throws {APIError} When a Hyperliquid read fails
|
|
227
|
+
*/
|
|
228
|
+
async getHypercoreDepositInfo() {
|
|
229
|
+
return (await this.readHypercoreDepositInfo("getHypercoreDepositInfo")).info;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Deposits Circle USDC the wallet holds on HyperEVM. The wallet approves
|
|
233
|
+
* (when its allowance is short) and burns it through Circle's
|
|
234
|
+
* TokenMessengerV2 on HyperEVM, naming the caller's Sei deposit address as
|
|
235
|
+
* the recipient and asking Circle to forward the mint. Circle mints on Sei,
|
|
236
|
+
* keeping up to `maxFee`, and the sweeper credits the rest like any deposit
|
|
237
|
+
* made straight to the address; it lists in {@link listDeposits} by its Sei
|
|
238
|
+
* mint. The wallet pays HYPE for gas; no Monaco contract is involved on
|
|
239
|
+
* HyperEVM.
|
|
240
|
+
*
|
|
241
|
+
* A browser or embedded wallet is switched to HyperEVM (added first when it
|
|
242
|
+
* does not know the chain) and back to Sei afterwards. Before registering
|
|
243
|
+
* or sending, this refuses an amount below the minimum or above the
|
|
244
|
+
* wallet's HyperEVM USDC, and a wallet with no HYPE.
|
|
245
|
+
*
|
|
246
|
+
* @param body - The raw USDC amount (6 decimals), and optionally the
|
|
247
|
+
* deposit target
|
|
248
|
+
* @returns Promise resolving once the burn lands on HyperEVM
|
|
249
|
+
* @throws {InvalidConfigError} On an invalid amount, a missing wallet, a
|
|
250
|
+
* wallet off Sei, or an amount below the minimum or above the balance
|
|
251
|
+
* @throws {InvalidStateError} When the hub does not sweep USDC or the
|
|
252
|
+
* wallet has no HYPE
|
|
253
|
+
* @throws {APIError} When Circle's fee quote fails (nothing sent), or when
|
|
254
|
+
* the burn was sent but its receipt cannot be read: an unknown outcome
|
|
255
|
+
* carrying `retryable: false` and the burn's hash, since calling again
|
|
256
|
+
* burns a second time
|
|
257
|
+
* @throws {ContractError} When the approval or the burn reverts (nothing
|
|
258
|
+
* burned)
|
|
259
|
+
*/
|
|
260
|
+
async depositFromHyperEvm(body) {
|
|
261
|
+
const { amount, depositTarget } = body;
|
|
262
|
+
if (typeof amount !== "bigint" || amount <= 0n) {
|
|
263
|
+
throw new InvalidConfigError(`Invalid amount: ${String(amount)}. Expected a positive bigint of raw USDC base units (6 decimals).`, "amount", amount);
|
|
264
|
+
}
|
|
265
|
+
const { walletClient, network, account, info } = await this.readHyperEvmDepositInfo("depositFromHyperEvm");
|
|
266
|
+
if (amount < info.minimumAmount) {
|
|
267
|
+
throw new InvalidConfigError(`Deposit at least ${formatUnits(info.minimumAmount, USDC_DECIMALS)} USDC from HyperEVM: Circle keeps up to ${formatUnits(info.maxFee, USDC_DECIMALS)} USDC to forward it to Sei, and the hub only sweeps a credit above its minimum.`, "amount", amount);
|
|
268
|
+
}
|
|
269
|
+
if (amount > info.usdcBalance) {
|
|
270
|
+
throw new InvalidConfigError(`The wallet holds ${formatUnits(info.usdcBalance, USDC_DECIMALS)} USDC on HyperEVM.`, "amount", amount);
|
|
271
|
+
}
|
|
272
|
+
if (info.gasBalance === 0n) {
|
|
273
|
+
throw new InvalidStateError("The wallet holds no HYPE on HyperEVM to pay gas for the approval and burn.");
|
|
274
|
+
}
|
|
275
|
+
const { sweeperAddress } = await this.register(depositTarget === undefined ? {} : { depositTarget });
|
|
276
|
+
const { approveTxHash, burnTxHash } = await burnToSei({
|
|
277
|
+
walletClient,
|
|
278
|
+
network,
|
|
279
|
+
amount,
|
|
280
|
+
recipient: sweeperAddress,
|
|
281
|
+
maxFee: info.maxFee,
|
|
282
|
+
allowance: account.allowance,
|
|
283
|
+
});
|
|
284
|
+
return {
|
|
285
|
+
sweeperAddress,
|
|
286
|
+
amount,
|
|
287
|
+
maxFee: info.maxFee,
|
|
288
|
+
minimumCredit: amount - info.maxFee,
|
|
289
|
+
...(approveTxHash === undefined ? {} : { approveTxHash }),
|
|
290
|
+
burnTxHash,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* What the wallet can deposit from HyperEVM: its Circle USDC and HYPE
|
|
295
|
+
* there, Circle's forwarding fee, and the smallest amount
|
|
296
|
+
* {@link depositFromHyperEvm} accepts. Reads only public data (the
|
|
297
|
+
* HyperEVM RPC, Circle's fee API and {@link getChains}), so it needs a
|
|
298
|
+
* wallet client on Sei but no session.
|
|
299
|
+
*
|
|
300
|
+
* @returns Promise resolving to the wallet's HyperEVM deposit info
|
|
301
|
+
* @throws {InvalidConfigError} On a missing wallet or a wallet off Sei
|
|
302
|
+
* @throws {InvalidStateError} When the hub does not sweep USDC
|
|
303
|
+
* @throws {APIError} When Circle's fee quote fails
|
|
304
|
+
*/
|
|
305
|
+
async getHyperEvmDepositInfo() {
|
|
306
|
+
return (await this.readHyperEvmDepositInfo("getHyperEvmDepositInfo")).info;
|
|
307
|
+
}
|
|
308
|
+
/** The reads both Hyperliquid (HyperCore) methods start from, made in parallel. */
|
|
309
|
+
async readHypercoreDepositInfo(caller) {
|
|
310
|
+
const { walletClient, network, address } = this.walletOnSei(caller);
|
|
311
|
+
const [floor, maxFee, account] = await Promise.all([this.hubUsdcFloor(network), readMaxFee(network), readHypercoreAccount(network, address)]);
|
|
312
|
+
const info = {
|
|
313
|
+
account: address,
|
|
314
|
+
accountMode: account.mode,
|
|
315
|
+
perpBalance: account.perp,
|
|
316
|
+
spotBalance: account.spot,
|
|
317
|
+
available: availableToDraw(account),
|
|
318
|
+
maxFee,
|
|
319
|
+
minimumAmount: minimumAmount(maxFee, floor),
|
|
320
|
+
};
|
|
321
|
+
return { walletClient, network, account, info };
|
|
322
|
+
}
|
|
323
|
+
/** The reads both HyperEVM methods start from, made in parallel. */
|
|
324
|
+
async readHyperEvmDepositInfo(caller) {
|
|
325
|
+
const { walletClient, network, address } = this.walletOnSei(caller);
|
|
326
|
+
const [floor, maxFee, account] = await Promise.all([this.hubUsdcFloor(network), quoteForwardFee(network), readHyperEvmAccount(network, address)]);
|
|
327
|
+
const info = {
|
|
328
|
+
account: address,
|
|
329
|
+
usdcBalance: account.usdc,
|
|
330
|
+
gasBalance: account.gas,
|
|
331
|
+
maxFee,
|
|
332
|
+
minimumAmount: minimumAmount(maxFee, floor),
|
|
333
|
+
};
|
|
334
|
+
return { walletClient, network, account, info };
|
|
335
|
+
}
|
|
336
|
+
/** The wallet client, which must be on Sei, and the Hyperliquid network that pairs with its chain. */
|
|
337
|
+
walletOnSei(caller) {
|
|
338
|
+
const walletClient = this.walletClient;
|
|
339
|
+
if (!walletClient?.account) {
|
|
340
|
+
throw new InvalidConfigError("Wallet client not set. Connect the wallet that holds the USDC first.", "walletClient");
|
|
341
|
+
}
|
|
342
|
+
const network = hypercoreNetworkFor(walletClient.chain?.id);
|
|
343
|
+
if (!network) {
|
|
344
|
+
throw new InvalidConfigError(`${caller} needs the wallet on Sei (chain ${sei.id} or ${seiTestnet.id}); got chain ${walletClient.chain?.id}.`, "walletClient");
|
|
345
|
+
}
|
|
346
|
+
return { walletClient, network, address: walletClient.account.address };
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* The Sei hub's minimum for Circle's USDC, raw. By address, never by symbol:
|
|
350
|
+
* Sei also carries Noble's bridged USDC.n, and both routes mint Circle's
|
|
351
|
+
* native USDC.
|
|
352
|
+
*/
|
|
353
|
+
async hubUsdcFloor(network) {
|
|
354
|
+
const { chains } = await this.getChains();
|
|
355
|
+
const seiUsdc = network.seiUsdc.toLowerCase();
|
|
356
|
+
const usdc = chains.find((chain) => chain.hub)?.assets.find((asset) => asset.tokenAddress.toLowerCase() === seiUsdc);
|
|
357
|
+
if (!usdc) {
|
|
358
|
+
throw new InvalidStateError("The Sei hub does not sweep USDC, so a deposit from Hyperliquid would not be credited.");
|
|
359
|
+
}
|
|
360
|
+
return BigInt(usdc.minSweepRaw);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* The smallest amount a forwarded deposit accepts: Circle's fee plus the
|
|
365
|
+
* hub's floor. Circle reverts a transfer that does not exceed its fee, and the
|
|
366
|
+
* hub leaves a credit below its floor on the address until a later deposit.
|
|
367
|
+
*/
|
|
368
|
+
function minimumAmount(maxFee, floor) {
|
|
369
|
+
return maxFee + (floor > 0n ? floor : 1n);
|
|
146
370
|
}
|
|
147
371
|
/** The deposit listing's page size bounds, matching the endpoint's schema. */
|
|
148
372
|
const MAX_DEPOSIT_PAGE_SIZE = 100;
|
|
@@ -0,0 +1,103 @@
|
|
|
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 type { HypercoreAccountMode, HypercoreBalanceSource } from "@0xmonaco/types";
|
|
15
|
+
import { type Address, type Chain, type Hex, type WalletClient } from "viem";
|
|
16
|
+
/** Sei's CCTP domain: where the withdrawal mints. Not its EVM chain id. */
|
|
17
|
+
export declare const SEI_CCTP_DOMAIN = 16;
|
|
18
|
+
/** USDC's decimals on HyperEVM and Sei. */
|
|
19
|
+
export declare const USDC_DECIMALS = 6;
|
|
20
|
+
/** Where one Hyperliquid network's withdrawals are signed for and sent. */
|
|
21
|
+
export interface HypercoreNetwork {
|
|
22
|
+
hyperliquidChain: "Mainnet" | "Testnet";
|
|
23
|
+
exchangeUrl: string;
|
|
24
|
+
infoUrl: string;
|
|
25
|
+
hyperEvm: Chain;
|
|
26
|
+
/** Circle's CoreDepositWallet (developers.circle.com/cctp/references/hypercore-contract-addresses) */
|
|
27
|
+
coreDepositWallet: Address;
|
|
28
|
+
/** Circle's native USDC on the paired Sei chain: what the withdrawal mints */
|
|
29
|
+
seiUsdc: Address;
|
|
30
|
+
/** Circle's native USDC on HyperEVM */
|
|
31
|
+
hyperEvmUsdc: Address;
|
|
32
|
+
/** Circle's TokenMessengerV2 on HyperEVM */
|
|
33
|
+
tokenMessenger: Address;
|
|
34
|
+
/** Circle's attestation API (Iris), which quotes CCTP fees */
|
|
35
|
+
irisUrl: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The Hyperliquid network that pairs with the SDK's Sei chain: mainnet with
|
|
39
|
+
* Sei mainnet, testnet with Sei testnet (where every non-mainnet Monaco
|
|
40
|
+
* network settles). `undefined` for any other chain.
|
|
41
|
+
*/
|
|
42
|
+
export declare function hypercoreNetworkFor(chainId: number | undefined): HypercoreNetwork | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* The most Circle deducts from a forwarded withdrawal to Sei: the CCTP
|
|
45
|
+
* `maxFee` its CoreDepositWallet burns with (its forwarding fee for the
|
|
46
|
+
* domain plus CCTP's own cap). Owner-configurable per domain, so it is read
|
|
47
|
+
* rather than assumed. The wallet also reverts a withdrawal that does not
|
|
48
|
+
* exceed it.
|
|
49
|
+
*/
|
|
50
|
+
export declare function readMaxFee(network: HypercoreNetwork): Promise<bigint>;
|
|
51
|
+
/** A Hyperliquid account's USDC, in raw 6-decimal base units. */
|
|
52
|
+
export interface HypercoreAccount {
|
|
53
|
+
mode: HypercoreAccountMode;
|
|
54
|
+
perp: bigint;
|
|
55
|
+
spot: bigint;
|
|
56
|
+
}
|
|
57
|
+
/** Read the account's mode and USDC balances from Hyperliquid's public info endpoint. */
|
|
58
|
+
export declare function readHypercoreAccount(network: HypercoreNetwork, user: Address): Promise<HypercoreAccount>;
|
|
59
|
+
/**
|
|
60
|
+
* The balance one withdrawal of `amount` draws on, or `undefined` when none
|
|
61
|
+
* covers it with room for Hyperliquid's fee. A unified or portfolio-margin
|
|
62
|
+
* account draws on spot: Hyperliquid refuses any other source for it
|
|
63
|
+
* ("Unified account only supports sending assets through spot"). Otherwise
|
|
64
|
+
* perp comes first, where a deposit to Hyperliquid lands by default.
|
|
65
|
+
*/
|
|
66
|
+
export declare function chooseSource(account: HypercoreAccount, amount: bigint): HypercoreBalanceSource | undefined;
|
|
67
|
+
/** The most one withdrawal can draw: it draws on a single balance and leaves room for Hyperliquid's fee. */
|
|
68
|
+
export declare function availableToDraw(account: HypercoreAccount): bigint;
|
|
69
|
+
/** One signed `sendToEvmWithData` action, as Hyperliquid's exchange endpoint takes it. */
|
|
70
|
+
export interface SignedWithdrawal {
|
|
71
|
+
action: Record<string, unknown>;
|
|
72
|
+
nonce: number;
|
|
73
|
+
signature: {
|
|
74
|
+
r: Hex;
|
|
75
|
+
s: Hex;
|
|
76
|
+
v: number;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Sign a USDC withdrawal from Hyperliquid to `recipient` on Sei.
|
|
81
|
+
*
|
|
82
|
+
* The signature's domain uses the wallet's own chain (Sei), which Hyperliquid
|
|
83
|
+
* accepts from any chain and wallets that hold the domain to the active
|
|
84
|
+
* network sign without switching.
|
|
85
|
+
*/
|
|
86
|
+
export declare function signWithdrawalToSei(params: {
|
|
87
|
+
walletClient: WalletClient;
|
|
88
|
+
network: HypercoreNetwork;
|
|
89
|
+
amount: string;
|
|
90
|
+
sourceDex: "" | "spot";
|
|
91
|
+
recipient: string;
|
|
92
|
+
nonce: number;
|
|
93
|
+
}): Promise<SignedWithdrawal>;
|
|
94
|
+
/**
|
|
95
|
+
* Post a signed withdrawal to Hyperliquid's exchange endpoint.
|
|
96
|
+
*
|
|
97
|
+
* Hyperliquid answers a decided rejection with HTTP 200 and
|
|
98
|
+
* `{"status":"err","response":"<reason>"}`, so the status alone does not tell
|
|
99
|
+
* success from refusal. A refusal is a decided outcome (nothing moved); a
|
|
100
|
+
* dropped connection, a 5xx, or a reply that is neither `ok` nor `err` leaves
|
|
101
|
+
* the outcome unknown. The signed body is never attached to an error.
|
|
102
|
+
*/
|
|
103
|
+
export declare function postToExchange(network: HypercoreNetwork, withdrawal: SignedWithdrawal): Promise<void>;
|