@pickhooks/sdk 0.0.0-stage → 0.4.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/LICENSE +21 -0
- package/README.md +418 -3
- package/dist/abis/agents.d.ts +2418 -0
- package/dist/abis/agents.js +4 -0
- package/dist/abis/any3.d.ts +2123 -0
- package/dist/abis/any3.js +3 -0
- package/dist/abis/anytax.d.ts +1786 -0
- package/dist/abis/anytax.js +3 -0
- package/dist/abis/calls.d.ts +2515 -0
- package/dist/abis/calls.js +4 -0
- package/dist/abis/common.d.ts +449 -0
- package/dist/abis/common.js +91 -0
- package/dist/abis/dips.d.ts +897 -0
- package/dist/abis/dips.js +2 -0
- package/dist/abis/index.d.ts +14 -0
- package/dist/abis/index.js +14 -0
- package/dist/abis/infra.d.ts +701 -0
- package/dist/abis/infra.js +4 -0
- package/dist/abis/lever.d.ts +1710 -0
- package/dist/abis/lever.js +5 -0
- package/dist/abis/lever2.d.ts +2710 -0
- package/dist/abis/lever2.js +5 -0
- package/dist/abis/pools.d.ts +1898 -0
- package/dist/abis/pools.js +4 -0
- package/dist/abis/sticky.d.ts +953 -0
- package/dist/abis/sticky.js +4 -0
- package/dist/abis/sticky2.d.ts +3750 -0
- package/dist/abis/sticky2.js +8 -0
- package/dist/abis/v1.d.ts +1627 -0
- package/dist/abis/v1.js +5 -0
- package/dist/abis/v2.d.ts +3264 -0
- package/dist/abis/v2.js +8 -0
- package/dist/addresses.d.ts +100 -0
- package/dist/addresses.js +71 -0
- package/dist/agents-catalog.d.ts +318 -0
- package/dist/agents-catalog.js +353 -0
- package/dist/agents.d.ts +543 -0
- package/dist/agents.js +667 -0
- package/dist/any.d.ts +447 -0
- package/dist/any.js +729 -0
- package/dist/api.d.ts +62 -0
- package/dist/api.js +30 -0
- package/dist/blocks.d.ts +107 -0
- package/dist/blocks.js +126 -0
- package/dist/coin-rules-config.d.ts +8 -0
- package/dist/coin-rules-config.js +13 -0
- package/dist/coin-rules.d.ts +383 -0
- package/dist/coin-rules.js +878 -0
- package/dist/coins.d.ts +217 -0
- package/dist/coins.js +520 -0
- package/dist/desks.d.ts +298 -0
- package/dist/desks.js +343 -0
- package/dist/dips.d.ts +133 -0
- package/dist/dips.js +148 -0
- package/dist/errors.d.ts +35 -0
- package/dist/errors.js +414 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +91 -0
- package/dist/infinity.d.ts +63 -0
- package/dist/infinity.js +96 -0
- package/dist/infra.d.ts +112 -0
- package/dist/infra.js +114 -0
- package/dist/lever.d.ts +391 -0
- package/dist/lever.js +419 -0
- package/dist/pools.d.ts +274 -0
- package/dist/pools.js +405 -0
- package/dist/sticky-rules.d.ts +179 -0
- package/dist/sticky-rules.js +580 -0
- package/dist/sticky-sim.d.ts +125 -0
- package/dist/sticky-sim.js +637 -0
- package/dist/sticky.d.ts +286 -0
- package/dist/sticky.js +420 -0
- package/dist/tlock.d.ts +67 -0
- package/dist/tlock.js +185 -0
- package/dist/tx.d.ts +72 -0
- package/dist/tx.js +78 -0
- package/dist/v2.d.ts +106 -0
- package/dist/v2.js +61 -0
- package/dist/v3.d.ts +42 -0
- package/dist/v3.js +98 -0
- package/dist/wallet.d.ts +80 -0
- package/dist/wallet.js +115 -0
- package/dist/watch.d.ts +82 -0
- package/dist/watch.js +194 -0
- package/package.json +67 -4
package/dist/lever.js
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { encodeFunctionData, isAddressEqual, parseEventLogs } from "viem";
|
|
2
|
+
import { plainTokenAbi } from "./abis/pools.js";
|
|
3
|
+
import { leverEngineAbi, leverFactoryAbi, leverHookAbi, leverRouterAbi } from "./abis/lever.js";
|
|
4
|
+
import { lever2EngineAbi, lever2FactoryAbi, lever2HookAbi, lever2LensAbi } from "./abis/lever2.js";
|
|
5
|
+
import { PickhooksError, toPickhooksError } from "./errors.js";
|
|
6
|
+
import { buildInfiSwap, infiQuote, permit2Approvals, poolIdOf, toKey } from "./infinity.js";
|
|
7
|
+
import { deadlineIn, minusBps, sendAll, sendTx, signer } from "./tx.js";
|
|
8
|
+
import { approvalFor } from "./v3.js";
|
|
9
|
+
/**
|
|
10
|
+
* Lever: coins whose curve lends BNB to leveraged positions, liquidated inside the trade that reaches them.
|
|
11
|
+
* - lever (v1, factory 0xeaB9…9615): the curve lives in the lever engine behind an Infinity hook; spot trades and longs
|
|
12
|
+
* go through the lever router and hook.
|
|
13
|
+
* - Lever (v2, factory 0x03Eb…A87A): the curve is real liquidity in the coin's Infinity pool (LP fee 0, hook fee 1 %),
|
|
14
|
+
* so terminals see its price; longs borrow BNB from the curve, shorts borrow coins, 2x to 5x, priced by the Lens.
|
|
15
|
+
* Both: up to 3x in a coin's first 10 minutes, then up to 5x; at least 0.01 BNB margin; a position is closed after 24
|
|
16
|
+
* hours at the latest.
|
|
17
|
+
*/
|
|
18
|
+
const TEN_K = 10000n;
|
|
19
|
+
/** room on a Lens or engine quote for a position's bound (minTokens, maxCoins, minBnb), basis points */
|
|
20
|
+
const QUOTE_ROOM = 100n;
|
|
21
|
+
const down = (x, bps = QUOTE_ROOM) => (x * (TEN_K - bps)) / TEN_K;
|
|
22
|
+
const up = (x, bps = QUOTE_ROOM) => (x * (TEN_K + bps) + TEN_K - 1n) / TEN_K;
|
|
23
|
+
/** lever v1 */
|
|
24
|
+
export function leverModule(ctx) {
|
|
25
|
+
const { pub, a } = ctx;
|
|
26
|
+
const F = leverFactoryAbi;
|
|
27
|
+
const H = leverHookAbi;
|
|
28
|
+
const E = leverEngineAbi;
|
|
29
|
+
const RT = leverRouterAbi;
|
|
30
|
+
let parts = null;
|
|
31
|
+
const call = (address, abi, functionName, args = []) => pub.readContract({ address, abi, functionName, args });
|
|
32
|
+
/** The hook, router and engine of the lever factory. */
|
|
33
|
+
function contracts() {
|
|
34
|
+
parts ??= (async () => {
|
|
35
|
+
const [hook, router] = await Promise.all([call(a.leverFactory, F, "hook"), call(a.leverFactory, F, "router")]);
|
|
36
|
+
return { hook, router, engine: await call(hook, H, "engine") };
|
|
37
|
+
})();
|
|
38
|
+
return parts;
|
|
39
|
+
}
|
|
40
|
+
const count = async () => Number(await call(a.leverFactory, F, "coinCount"));
|
|
41
|
+
const isCoin = async (token) => (await call(a.leverFactory, F, "isCoin", [token]).catch(() => false)) === true;
|
|
42
|
+
async function summaries(tokens) {
|
|
43
|
+
if (!tokens.length)
|
|
44
|
+
return [];
|
|
45
|
+
const { engine } = await contracts();
|
|
46
|
+
const res = await pub.multicall({
|
|
47
|
+
contracts: tokens.flatMap((t) => [
|
|
48
|
+
{ address: t, abi: plainTokenAbi, functionName: "name" },
|
|
49
|
+
{ address: t, abi: plainTokenAbi, functionName: "symbol" },
|
|
50
|
+
{ address: t, abi: plainTokenAbi, functionName: "metadata" },
|
|
51
|
+
{ address: engine, abi: E, functionName: "marketOf", args: [t] },
|
|
52
|
+
{ address: engine, abi: E, functionName: "activePositions", args: [t] },
|
|
53
|
+
]),
|
|
54
|
+
allowFailure: true,
|
|
55
|
+
});
|
|
56
|
+
const out = [];
|
|
57
|
+
tokens.forEach((token, i) => {
|
|
58
|
+
const g = (k) => res[i * 5 + k];
|
|
59
|
+
if (g(3).status !== "success")
|
|
60
|
+
return;
|
|
61
|
+
const m = g(3).result;
|
|
62
|
+
const priceBnb = m.q > 0n ? Number(m.r) / Number(m.q) : 0;
|
|
63
|
+
out.push({
|
|
64
|
+
token,
|
|
65
|
+
name: String(g(0).result ?? ""),
|
|
66
|
+
symbol: String(g(1).result ?? ""),
|
|
67
|
+
metadata: String(g(2).result ?? ""),
|
|
68
|
+
market: { ...m, start: Number(m.start), creatorBps: Number(m.creatorBps) },
|
|
69
|
+
priceBnb,
|
|
70
|
+
mcapBnb: priceBnb * (Number(m.supply) / 1e18),
|
|
71
|
+
positions: (g(4).result ?? []).length,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
async function coins(o = {}) {
|
|
77
|
+
const n = await count();
|
|
78
|
+
const end = Math.max(0, n - (o.offset ?? 0));
|
|
79
|
+
const from = Math.max(0, end - (o.limit ?? 50));
|
|
80
|
+
if (end <= from)
|
|
81
|
+
return [];
|
|
82
|
+
const tokens = (await pub.multicall({
|
|
83
|
+
contracts: Array.from({ length: end - from }, (_, i) => ({ address: a.leverFactory, abi: F, functionName: "coins", args: [BigInt(end - 1 - i)] })),
|
|
84
|
+
allowFailure: false,
|
|
85
|
+
}));
|
|
86
|
+
return summaries(tokens);
|
|
87
|
+
}
|
|
88
|
+
/** Every open long of a coin, with its liquidation trigger and what closing it pays now. */
|
|
89
|
+
async function positions(token) {
|
|
90
|
+
const { engine } = await contracts();
|
|
91
|
+
const [ids, ps, triggers] = await call(engine, E, "positionsOf", [token]);
|
|
92
|
+
const quotes = ids.length ? await pub.multicall({ contracts: ids.map((id) => ({ address: engine, abi: E, functionName: "quoteClose", args: [token, id] })), allowFailure: true }) : [];
|
|
93
|
+
return ids.map((id, i) => ({
|
|
94
|
+
id,
|
|
95
|
+
owner: ps[i].owner,
|
|
96
|
+
tokens: ps[i].tokens,
|
|
97
|
+
debt: ps[i].debt,
|
|
98
|
+
margin: ps[i].margin,
|
|
99
|
+
opened: Number(ps[i].opened),
|
|
100
|
+
trigger: triggers[i],
|
|
101
|
+
closeQuote: quotes[i]?.status === "success" ? quotes[i].result : null,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
const engineRead = async (fn, args) => {
|
|
105
|
+
try {
|
|
106
|
+
return await call((await contracts()).engine, E, fn, args);
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
throw toPickhooksError(e, "The lever engine refused this.");
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
/** Coins a buy gets, BNB a sell gets (and how many longs it liquidates), a long's coins, debt, fee and trigger. */
|
|
113
|
+
const quoteBuy = (token, bnbIn) => engineRead("quoteBuy", [token, bnbIn]);
|
|
114
|
+
const quoteSell = async (token, amount) => {
|
|
115
|
+
const [out, liquidations] = await engineRead("quoteSell", [token, amount]);
|
|
116
|
+
return { out, liquidations: Number(liquidations) };
|
|
117
|
+
};
|
|
118
|
+
const quoteOpen = async (token, gross, leverage) => {
|
|
119
|
+
const [tokens, debt, fee, trigger] = await engineRead("quoteOpen", [token, gross, leverage]);
|
|
120
|
+
return { tokens, debt, fee, trigger };
|
|
121
|
+
};
|
|
122
|
+
const tx = async (to, abi, fn, args, value = 0n, label) => ({
|
|
123
|
+
to: (await contracts())[to],
|
|
124
|
+
value,
|
|
125
|
+
data: encodeFunctionData({ abi, functionName: fn, args }),
|
|
126
|
+
gasPct: 150,
|
|
127
|
+
label,
|
|
128
|
+
});
|
|
129
|
+
const build = {
|
|
130
|
+
buy: (token, bnbIn, minOut) => tx("router", RT, "buy", [token, minOut, deadlineIn()], bnbIn, "The buy"),
|
|
131
|
+
/** needs the coin approved to the lever router */
|
|
132
|
+
sell: (token, amount, minOut) => tx("router", RT, "sell", [token, amount, minOut, deadlineIn()], 0n, "The sell"),
|
|
133
|
+
openLong: (token, gross, leverage, minTokens) => tx("hook", H, "openLong", [token, leverage, minTokens, deadlineIn()], gross, "Opening the long"),
|
|
134
|
+
closeLong: (token, id, minBnb) => tx("hook", H, "closeLong", [token, id, minBnb, deadlineIn()], 0n, "Closing the long"),
|
|
135
|
+
/** anyone, after 24 hours: the payout waits for the owner as a refund */
|
|
136
|
+
closeExpired: (token, id) => tx("hook", H, "closeExpired", [token, id], 0n, "Closing an expired long"),
|
|
137
|
+
claimRefund: (token) => tx("hook", H, "claimRefund", [token], 0n, "The refund"),
|
|
138
|
+
claimCreator: (token) => tx("hook", H, "claimCreator", [token], 0n, "The creator claim"),
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
factory: a.leverFactory,
|
|
142
|
+
contracts,
|
|
143
|
+
count,
|
|
144
|
+
coins,
|
|
145
|
+
coin: async (token) => ((await isCoin(token)) ? ((await summaries([token]))[0] ?? null) : null),
|
|
146
|
+
isCoin,
|
|
147
|
+
positions,
|
|
148
|
+
quoteBuy,
|
|
149
|
+
quoteSell,
|
|
150
|
+
quoteOpen,
|
|
151
|
+
refundOwed: async (token, who) => engineRead("refundOwed", [token, who]),
|
|
152
|
+
poolKey: async (token) => toKey(await call(a.leverFactory, F, "poolKeyOf", [token])),
|
|
153
|
+
build,
|
|
154
|
+
buy: async (b, s = {}) => {
|
|
155
|
+
const out = await quoteBuy(b.token, b.bnbIn);
|
|
156
|
+
return { ...(await sendTx(ctx, await build.buy(b.token, b.bnbIn, minusBps(out, b.slippageBps ?? 100)), s)), quoted: out };
|
|
157
|
+
},
|
|
158
|
+
sell: async (x, s = {}) => {
|
|
159
|
+
const { address } = await signer(ctx, s.wallet);
|
|
160
|
+
const ap = await approvalFor(ctx, address, x.token, x.amount, (await contracts()).router, "Allowing the lever router");
|
|
161
|
+
if (ap)
|
|
162
|
+
await sendTx(ctx, ap, s);
|
|
163
|
+
const q = await quoteSell(x.token, x.amount);
|
|
164
|
+
return { ...(await sendTx(ctx, await build.sell(x.token, x.amount, minusBps(q.out, x.slippageBps ?? 100)), s)), quoted: q.out, liquidations: q.liquidations };
|
|
165
|
+
},
|
|
166
|
+
/** gross = margin plus the open fee; the long gets at least the quote less roomBps (default 100) coins */
|
|
167
|
+
openLong: async (o, s = {}) => {
|
|
168
|
+
const q = await quoteOpen(o.token, o.gross, o.leverage);
|
|
169
|
+
const sent = await sendTx(ctx, await build.openLong(o.token, o.gross, o.leverage, down(q.tokens, BigInt(o.roomBps ?? 100))), s);
|
|
170
|
+
const ev = parseEventLogs({ abi: E, logs: sent.receipt.logs }).find((e) => e.eventName === "LongOpened");
|
|
171
|
+
return { ...sent, id: ev?.args.id ?? 0n, tokens: ev?.args.tokens ?? q.tokens };
|
|
172
|
+
},
|
|
173
|
+
closeLong: async (o, s = {}) => {
|
|
174
|
+
const net = await engineRead("quoteClose", [o.token, o.id]);
|
|
175
|
+
return { ...(await sendTx(ctx, await build.closeLong(o.token, o.id, down(net, BigInt(o.roomBps ?? 100))), s)), quoted: net };
|
|
176
|
+
},
|
|
177
|
+
closeExpired: async (token, id, s = {}) => sendTx(ctx, await build.closeExpired(token, id), s),
|
|
178
|
+
claimRefund: async (token, s = {}) => sendTx(ctx, await build.claimRefund(token), s),
|
|
179
|
+
claimCreator: async (token, s = {}) => sendTx(ctx, await build.claimCreator(token), s),
|
|
180
|
+
/** one transaction: the coin and the creator's first buy; returns the coin */
|
|
181
|
+
launch: async (l, s = {}) => {
|
|
182
|
+
const t = {
|
|
183
|
+
to: a.leverFactory,
|
|
184
|
+
value: l.firstBuy ?? 0n,
|
|
185
|
+
data: encodeFunctionData({ abi: F, functionName: "launch", args: [l.name, l.symbol, l.metadata, l.minSeedTokens ?? 0n] }),
|
|
186
|
+
gasPct: 125,
|
|
187
|
+
label: "The launch",
|
|
188
|
+
};
|
|
189
|
+
const sent = await sendTx(ctx, t, s);
|
|
190
|
+
const ev = parseEventLogs({ abi: F, logs: sent.receipt.logs.filter((x) => isAddressEqual(x.address, a.leverFactory)) }).find((e) => e.eventName === "Launched");
|
|
191
|
+
if (!ev)
|
|
192
|
+
throw new PickhooksError("Launched, but the coin was not found in the receipt.", { code: "receipt", hash: sent.hash });
|
|
193
|
+
return { ...sent, token: ev.args.token };
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/** Lens reasons as sentences */
|
|
198
|
+
export const LEVER2_REASON = {
|
|
199
|
+
0: "OK",
|
|
200
|
+
1: "Up to 3x in a coin's first 10 minutes, then up to 5x.",
|
|
201
|
+
2: "The smallest position is 0.01 BNB of margin plus the open fee.",
|
|
202
|
+
3: "All 8 slots of this side are taken; a bigger margin takes the smallest one's slot.",
|
|
203
|
+
4: "This side is full for now: each side may borrow at most a quarter of the real BNB in the curve. Try a smaller position or wait for more buys.",
|
|
204
|
+
5: "This position would sit at its liquidation point. Try a smaller margin or a lower leverage.",
|
|
205
|
+
6: "This trade would leave another position at its liquidation point. Try a smaller margin or a lower leverage.",
|
|
206
|
+
7: "This is not a lever coin.",
|
|
207
|
+
8: "The price was pushed in this second. Try again in a moment.",
|
|
208
|
+
};
|
|
209
|
+
const KIND = { creator: 3, refund: 4, platform: 5 };
|
|
210
|
+
export function lever2Module(ctx) {
|
|
211
|
+
const { pub, a } = ctx;
|
|
212
|
+
const F = lever2FactoryAbi;
|
|
213
|
+
const H = lever2HookAbi;
|
|
214
|
+
const E = lever2EngineAbi;
|
|
215
|
+
const L = lever2LensAbi;
|
|
216
|
+
let parts = null;
|
|
217
|
+
const keys = new Map();
|
|
218
|
+
const call = (address, abi, functionName, args = []) => pub.readContract({ address, abi, functionName, args });
|
|
219
|
+
function contracts() {
|
|
220
|
+
parts ??= (async () => {
|
|
221
|
+
const hook = await call(a.lever2Factory, F, "hook");
|
|
222
|
+
return { hook, engine: await call(hook, H, "engine"), lens: a.lever2Lens };
|
|
223
|
+
})();
|
|
224
|
+
return parts;
|
|
225
|
+
}
|
|
226
|
+
const count = async () => Number(await call(a.lever2Factory, F, "coinCount"));
|
|
227
|
+
const isCoin = async (token) => (await call(a.lever2Factory, F, "isCoin", [token]).catch(() => false)) === true;
|
|
228
|
+
async function poolKey(token) {
|
|
229
|
+
const hit = keys.get(token.toLowerCase());
|
|
230
|
+
if (hit)
|
|
231
|
+
return hit;
|
|
232
|
+
const k = toKey(await call(a.lever2Factory, F, "poolKeyOf", [token]));
|
|
233
|
+
keys.set(token.toLowerCase(), k);
|
|
234
|
+
return k;
|
|
235
|
+
}
|
|
236
|
+
/** The curve now (Lens stateOf), the market (engine), and the coin. */
|
|
237
|
+
async function coin(token) {
|
|
238
|
+
if (!(await isCoin(token)))
|
|
239
|
+
return null;
|
|
240
|
+
const { engine, lens } = await contracts();
|
|
241
|
+
const r = await pub.multicall({
|
|
242
|
+
contracts: [
|
|
243
|
+
{ address: token, abi: plainTokenAbi, functionName: "name" },
|
|
244
|
+
{ address: token, abi: plainTokenAbi, functionName: "symbol" },
|
|
245
|
+
{ address: token, abi: plainTokenAbi, functionName: "metadata" },
|
|
246
|
+
{ address: token, abi: plainTokenAbi, functionName: "totalSupply" },
|
|
247
|
+
{ address: lens, abi: L, functionName: "stateOf", args: [token] },
|
|
248
|
+
{ address: engine, abi: E, functionName: "marketOf", args: [token] },
|
|
249
|
+
],
|
|
250
|
+
allowFailure: false,
|
|
251
|
+
});
|
|
252
|
+
const state = r[4];
|
|
253
|
+
const market = r[5];
|
|
254
|
+
const priceBnb = Number(state.price) / 1e18;
|
|
255
|
+
return {
|
|
256
|
+
token,
|
|
257
|
+
name: String(r[0]),
|
|
258
|
+
symbol: String(r[1]),
|
|
259
|
+
metadata: String(r[2]),
|
|
260
|
+
supply: r[3],
|
|
261
|
+
state: { ...state, tS: Number(state.tS), tB: Number(state.tB) },
|
|
262
|
+
market: { ...market, start: Number(market.start), creatorBps: Number(market.creatorBps) },
|
|
263
|
+
key: await poolKey(token),
|
|
264
|
+
id: poolIdOf(await poolKey(token)),
|
|
265
|
+
priceBnb,
|
|
266
|
+
mcapBnb: priceBnb * (Number(r[3]) / 1e18),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
async function coins(o = {}) {
|
|
270
|
+
const n = await count();
|
|
271
|
+
const end = Math.max(0, n - (o.offset ?? 0));
|
|
272
|
+
const from = Math.max(0, end - (o.limit ?? 50));
|
|
273
|
+
if (end <= from)
|
|
274
|
+
return [];
|
|
275
|
+
const tokens = (await pub.multicall({
|
|
276
|
+
contracts: Array.from({ length: end - from }, (_, i) => ({ address: a.lever2Factory, abi: F, functionName: "coins", args: [BigInt(end - 1 - i)] })),
|
|
277
|
+
allowFailure: false,
|
|
278
|
+
}));
|
|
279
|
+
return (await Promise.all(tokens.map((t) => coin(t)))).filter((c) => c !== null);
|
|
280
|
+
}
|
|
281
|
+
/** Open longs and shorts with their trigger price and what closing pays now (Lens rows). */
|
|
282
|
+
async function positions(token) {
|
|
283
|
+
const { lens } = await contracts();
|
|
284
|
+
const [longs, shorts] = await Promise.all([call(lens, L, "longsOf", [token]), call(lens, L, "shortsOf", [token])]);
|
|
285
|
+
const fix = (x) => ({ ...x, opened: Number(x.opened) });
|
|
286
|
+
return { longs: longs.map(fix), shorts: shorts.map(fix) };
|
|
287
|
+
}
|
|
288
|
+
/** The Lens quote of a position: margin, borrowed, fee, coins, trigger, and reason (0 = it would open). */
|
|
289
|
+
async function quoteOpen(token, side, gross, leverage) {
|
|
290
|
+
const q = await call((await contracts()).lens, L, side === "long" ? "quoteOpenLong" : "quoteOpenShort", [token, gross, leverage]);
|
|
291
|
+
return { ...q, reason: Number(q.reason), text: LEVER2_REASON[Number(q.reason)] ?? "The Lens refused this position." };
|
|
292
|
+
}
|
|
293
|
+
const quoteClose = async (token, side, id) => call((await contracts()).lens, L, side === "long" ? "quoteCloseLong" : "quoteCloseShort", [token, id]);
|
|
294
|
+
/** Spot: what a trade gets in the coin's pool (the hook's fee and every liquidation it reaches are in it). */
|
|
295
|
+
async function quote(q) {
|
|
296
|
+
try {
|
|
297
|
+
return { amountOut: (await infiQuote(ctx, { key: await poolKey(q.token), buy: q.side === "buy", amount: q.amount, from: q.from })).amountOut };
|
|
298
|
+
}
|
|
299
|
+
catch (e) {
|
|
300
|
+
throw toPickhooksError(e, "The pool refused this trade.");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* How a sale of `amount` goes through the pool. While shorts are open, a sale bigger than the curve's room (Lens
|
|
305
|
+
* sellRoom) goes in two legs of one transaction: the room first (it liquidates every long on its way and ends at the
|
|
306
|
+
* start), then the rest, which closes shorts at the start (most coins first) until it fits. fits false: even closing
|
|
307
|
+
* every short leaves no room; nothing would be sold.
|
|
308
|
+
*/
|
|
309
|
+
async function sellPlan(token, amount) {
|
|
310
|
+
const { lens, engine } = await contracts();
|
|
311
|
+
const s = await call(lens, L, "stateOf", [token]);
|
|
312
|
+
if (s.lent === 0n || amount <= s.sellRoom || s.sellRoom === 0n)
|
|
313
|
+
return { legs: [amount], closes: 0, fits: true, sellRoom: s.sellRoom };
|
|
314
|
+
const shorts = await call(engine, E, "shortsOf", [token]);
|
|
315
|
+
const rest = amount - s.sellRoom;
|
|
316
|
+
const sizes = shorts[1].map((x) => x.coins).sort((x, y) => (x > y ? -1 : x < y ? 1 : 0));
|
|
317
|
+
let bought = 0n;
|
|
318
|
+
let closes = 0;
|
|
319
|
+
for (const c of sizes) {
|
|
320
|
+
if (rest <= bought)
|
|
321
|
+
break;
|
|
322
|
+
bought += c;
|
|
323
|
+
closes++;
|
|
324
|
+
}
|
|
325
|
+
return { legs: [s.sellRoom, rest], closes, fits: rest < bought + 1000000000n, sellRoom: s.sellRoom };
|
|
326
|
+
}
|
|
327
|
+
const hookTx = async (fn, args, value = 0n, label) => ({
|
|
328
|
+
to: (await contracts()).hook,
|
|
329
|
+
value,
|
|
330
|
+
data: encodeFunctionData({ abi: H, functionName: fn, args }),
|
|
331
|
+
gasPct: 150,
|
|
332
|
+
label,
|
|
333
|
+
});
|
|
334
|
+
const build = {
|
|
335
|
+
buy: (key, bnbIn, minOut) => buildInfiSwap(a, { key, buy: true, legs: [bnbIn], minOut, gasPct: 150 }),
|
|
336
|
+
/** legs from sellPlan(); the router pulls the coins with Permit2 */
|
|
337
|
+
sell: (key, legs, minOut) => buildInfiSwap(a, { key, buy: false, legs, minOut, gasPct: 150 }),
|
|
338
|
+
openLong: (token, gross, leverage, minTokens) => hookTx("openLong", [token, leverage, minTokens, deadlineIn()], gross, "Opening the long"),
|
|
339
|
+
openShort: (token, gross, leverage, maxCoins) => hookTx("openShort", [token, leverage, maxCoins, deadlineIn()], gross, "Opening the short"),
|
|
340
|
+
close: (token, side, id, minBnb) => hookTx(side === "long" ? "closeLong" : "closeShort", [token, id, minBnb, deadlineIn()], 0n, `Closing the ${side}`),
|
|
341
|
+
/** the pad's keeper after 24 hours, anyone 24 hours later; the payout waits for the owner as a refund */
|
|
342
|
+
closeExpired: (token, side, id, minBnb) => hookTx("closeExpired", [token, id, side === "short", minBnb], 0n, `Closing an expired ${side}`),
|
|
343
|
+
/** refund: the caller's refunds; creator: the creator's fees; platform: the pad's share (anyone may send it) */
|
|
344
|
+
claim: (token, kind) => hookTx("claim", [token, KIND[kind]], 0n, `The ${kind} claim`),
|
|
345
|
+
};
|
|
346
|
+
return {
|
|
347
|
+
factory: a.lever2Factory,
|
|
348
|
+
contracts,
|
|
349
|
+
count,
|
|
350
|
+
coins,
|
|
351
|
+
coin,
|
|
352
|
+
isCoin,
|
|
353
|
+
poolKey,
|
|
354
|
+
positions,
|
|
355
|
+
quoteOpen,
|
|
356
|
+
quoteClose,
|
|
357
|
+
quote,
|
|
358
|
+
sellPlan,
|
|
359
|
+
refundOwed: async (token, who) => call((await contracts()).engine, E, "refundOwed", [token, who]),
|
|
360
|
+
/** the gross (margin plus the open fee) for at least `margin` at `leverage`, rounded up to a millionth of a BNB */
|
|
361
|
+
grossForMargin: (margin, leverage) => {
|
|
362
|
+
const bps = TEN_K + BigInt(leverage * 30);
|
|
363
|
+
const g = (margin * bps + TEN_K - 1n) / TEN_K;
|
|
364
|
+
const step = 10n ** 12n;
|
|
365
|
+
return ((g + step - 1n) / step) * step;
|
|
366
|
+
},
|
|
367
|
+
build,
|
|
368
|
+
buy: async (b, s = {}) => {
|
|
369
|
+
const { address } = await signer(ctx, s.wallet);
|
|
370
|
+
const q = await quote({ token: b.token, side: "buy", amount: b.bnbIn, from: address });
|
|
371
|
+
return { ...(await sendTx(ctx, build.buy(await poolKey(b.token), b.bnbIn, minusBps(q.amountOut, b.slippageBps ?? 100)), s)), quoted: q.amountOut };
|
|
372
|
+
},
|
|
373
|
+
/** sells in one or two legs (sellPlan); the minimum is the pool's quote of the first leg plus the rest, less slippage */
|
|
374
|
+
sell: async (x, s = {}) => {
|
|
375
|
+
const { address } = await signer(ctx, s.wallet);
|
|
376
|
+
const plan = await sellPlan(x.token, x.amount);
|
|
377
|
+
if (!plan.fits)
|
|
378
|
+
throw new PickhooksError("This sale is bigger than what closing every short frees before the curve's start. Sell less.", { code: "input" });
|
|
379
|
+
await sendAll(ctx, await permit2Approvals(ctx, address, x.token, x.amount), s);
|
|
380
|
+
const q = plan.legs.length === 1 ? await quote({ token: x.token, side: "sell", amount: x.amount, from: address }) : await quote({ token: x.token, side: "sell", amount: plan.legs[0], from: address });
|
|
381
|
+
const minOut = x.minOut ?? minusBps(q.amountOut, x.slippageBps ?? 100);
|
|
382
|
+
return { ...(await sendTx(ctx, build.sell(await poolKey(x.token), plan.legs, minOut), s)), quoted: q.amountOut, plan };
|
|
383
|
+
},
|
|
384
|
+
/** opens a long or short at the Lens quote with roomBps (default 100) on its bound; a refusing reason throws its sentence */
|
|
385
|
+
open: async (o, s = {}) => {
|
|
386
|
+
const q = await quoteOpen(o.token, o.side, o.gross, o.leverage);
|
|
387
|
+
if (q.reason !== 0 && q.reason !== 3)
|
|
388
|
+
throw new PickhooksError(q.text, { code: "lens", args: [q.reason] });
|
|
389
|
+
const room = BigInt(o.roomBps ?? 100);
|
|
390
|
+
const t = o.side === "long" ? await build.openLong(o.token, o.gross, o.leverage, down(q.amount, room)) : await build.openShort(o.token, o.gross, o.leverage, up(q.amount, room));
|
|
391
|
+
const sent = await sendTx(ctx, t, s);
|
|
392
|
+
const ev = parseEventLogs({ abi: E, logs: sent.receipt.logs }).find((e) => e.eventName === (o.side === "long" ? "LongOpened" : "ShortOpened"));
|
|
393
|
+
return { ...sent, id: ev?.args.id ?? 0n, quote: q };
|
|
394
|
+
},
|
|
395
|
+
close: async (o, s = {}) => {
|
|
396
|
+
const q = await quoteClose(o.token, o.side, o.id);
|
|
397
|
+
if (q.pushed)
|
|
398
|
+
throw new PickhooksError(LEVER2_REASON[8], { code: "lens", args: [8] });
|
|
399
|
+
return { ...(await sendTx(ctx, await build.close(o.token, o.side, o.id, down(q.net, BigInt(o.roomBps ?? 100))), s)), quoted: q.net };
|
|
400
|
+
},
|
|
401
|
+
claim: async (token, kind, s = {}) => sendTx(ctx, await build.claim(token, kind), s),
|
|
402
|
+
/** one transaction: the hook's launch dust plus the creator's first buy; returns the coin */
|
|
403
|
+
launch: async (l, s = {}) => {
|
|
404
|
+
const dust = await call((await contracts()).hook, H, "LAUNCH_DUST");
|
|
405
|
+
const t = {
|
|
406
|
+
to: a.lever2Factory,
|
|
407
|
+
value: dust + (l.firstBuy ?? 0n),
|
|
408
|
+
data: encodeFunctionData({ abi: F, functionName: "launch", args: [l.name, l.symbol, l.metadata, l.minSeedTokens ?? 0n] }),
|
|
409
|
+
gasPct: 125,
|
|
410
|
+
label: "The launch",
|
|
411
|
+
};
|
|
412
|
+
const sent = await sendTx(ctx, t, s);
|
|
413
|
+
const ev = parseEventLogs({ abi: F, logs: sent.receipt.logs.filter((x) => isAddressEqual(x.address, a.lever2Factory)) }).find((e) => e.eventName === "Launched");
|
|
414
|
+
if (!ev)
|
|
415
|
+
throw new PickhooksError("Launched, but the coin was not found in the receipt.", { code: "receipt", hash: sent.hash });
|
|
416
|
+
return { ...sent, token: ev.args.token };
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|