@pickhooks/sdk 0.0.0-stage → 0.2.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 +309 -3
- 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 +12 -0
- package/dist/abis/index.js +12 -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/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 +87 -0
- package/dist/addresses.js +62 -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/dips.d.ts +133 -0
- package/dist/dips.js +148 -0
- package/dist/errors.d.ts +35 -0
- package/dist/errors.js +347 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +58 -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.d.ts +216 -0
- package/dist/sticky.js +292 -0
- package/dist/tx.d.ts +70 -0
- package/dist/tx.js +69 -0
- package/dist/v3.d.ts +42 -0
- package/dist/v3.js +98 -0
- package/package.json +58 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { anyModule } from "./any.js";
|
|
2
|
+
import { apiModule } from "./api.js";
|
|
3
|
+
import { blocksCatalog } from "./blocks.js";
|
|
4
|
+
import { coinsModule } from "./coins.js";
|
|
5
|
+
import { dipsModule } from "./dips.js";
|
|
6
|
+
import { infraModule } from "./infra.js";
|
|
7
|
+
import { lever2Module, leverModule } from "./lever.js";
|
|
8
|
+
import { callsModule, poolsModule } from "./pools.js";
|
|
9
|
+
import { stickyModule } from "./sticky.js";
|
|
10
|
+
import { makeCtx, sendAll, sendTx } from "./tx.js";
|
|
11
|
+
/**
|
|
12
|
+
* The pickhooks SDK. One client, a part per product:
|
|
13
|
+
* any hook any coin: quotes, trades with the recapture room, opening a pool with a partner, the payouts
|
|
14
|
+
* sticky sticky rules: launching from templates, checking a trade against the coin's table, trading, lifting
|
|
15
|
+
* coins coins with rules in the coin (v1, v2): rules as sentences, the rule that would refuse a trade, launches
|
|
16
|
+
* pools rules in the pool; calls: curve calls; lever: Lever longs and shorts (leverV1 the first lever); dips: dip bids
|
|
17
|
+
* api pickhooks.com's public JSON API (launch feed, board, activity)
|
|
18
|
+
*
|
|
19
|
+
* const ph = createPickhooks({ walletClient });
|
|
20
|
+
* const [pool] = await ph.any.pools({ limit: 1 });
|
|
21
|
+
* const r = await ph.any.buy({ pool, bnbIn: parseEther("0.05") });
|
|
22
|
+
*/
|
|
23
|
+
export function createPickhooks(options = {}) {
|
|
24
|
+
const ctx = makeCtx(options);
|
|
25
|
+
return {
|
|
26
|
+
addresses: ctx.a,
|
|
27
|
+
publicClient: ctx.pub,
|
|
28
|
+
any: anyModule(ctx),
|
|
29
|
+
sticky: stickyModule(ctx),
|
|
30
|
+
coins: coinsModule(ctx),
|
|
31
|
+
pools: poolsModule(ctx),
|
|
32
|
+
calls: callsModule(ctx),
|
|
33
|
+
lever: lever2Module(ctx),
|
|
34
|
+
leverV1: leverModule(ctx),
|
|
35
|
+
dips: dipsModule(ctx),
|
|
36
|
+
infra: infraModule(ctx),
|
|
37
|
+
blocks: { catalog: blocksCatalog, block: (id) => blocksCatalog().sticky.blocks.find((b) => b.id === id) ?? null },
|
|
38
|
+
api: apiModule(ctx),
|
|
39
|
+
send: (tx, o) => sendTx(ctx, tx, o),
|
|
40
|
+
sendAll: (txs, o) => sendAll(ctx, txs, o),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export { BSC_MAINNET, LIMITS, NATIVE, ZERO_ADDRESS } from "./addresses.js";
|
|
44
|
+
export { CHECK_TEXT, PickhooksError, decodeRevert, errorText, explain, findRevertData } from "./errors.js";
|
|
45
|
+
export { DEFAULT_RPC, deadlineIn, minusBps } from "./tx.js";
|
|
46
|
+
export { blockedKind, buildInfiSwap, infiPriceBnb, infiQuote, infiSwapInput, permit2Approvals, poolIdOf, toKey } from "./infinity.js";
|
|
47
|
+
export { approvalFor, buildV3Buy, buildV3Sell, receivedOf, v3PriceBnb, v3Quote } from "./v3.js";
|
|
48
|
+
export { MAX_TICK, MIN_TICK, TAX_QUOTE_MARK, afterTax, depthNeeds, fitLadder, partnerLink, rangeFor, tickOf, } from "./any.js";
|
|
49
|
+
export { describeRows, } from "./sticky.js";
|
|
50
|
+
export { ruleText } from "./coins.js";
|
|
51
|
+
export {} from "./pools.js";
|
|
52
|
+
export { LEVER2_REASON, } from "./lever.js";
|
|
53
|
+
export { coinsFor, coverOf, strikeFromPrice } from "./dips.js";
|
|
54
|
+
export {} from "./api.js";
|
|
55
|
+
export {} from "./infra.js";
|
|
56
|
+
export { BLOCKS_VERSION, BLOCK_ACTIONS, BLOCK_CHECKS, BLOCK_MARKS, BLOCK_OPERATORS, BLOCK_TRIGGERS, blockLineVars, blocksCatalog, } from "./blocks.js";
|
|
57
|
+
export { ACT, FLAG, LOCKED, NONE, ON, OP, STICKY_LIMITS, TEMPLATES, TEMPLATE_BY_ID, VAR, VARS, VAR_BY_ID, compile, decodeRow, defaults, editToRow, emptyEditRow, encodeRow, paramProblems, rowProblems, rowText, scannerOf, tableProblems, templateOf, } from "./sticky-rules.js";
|
|
58
|
+
export { GROUPS, RULES, RULE_BY_ID, RULE_BY_KIND, buildRules, emptyExtras, emptyRules, fieldProblem, kindsOf, refusal, selectionProblem, v2Costs, worstScanner, defaults as ruleDefaults, } from "./coin-rules.js";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type Address, type Hex } from "viem";
|
|
2
|
+
import { type Addresses } from "./addresses.js";
|
|
3
|
+
import { type Ctx, type TxRequest } from "./tx.js";
|
|
4
|
+
/**
|
|
5
|
+
* PancakeSwap Infinity, shared by every pickhooks product whose coin trades in an Infinity CL pool (hook any coin, pools,
|
|
6
|
+
* Lever, curve calls): pool keys and ids, the UniversalRouter's swap encoding, the quoter, Permit2 approvals for sells,
|
|
7
|
+
* and the rule a hook names when it refuses a swap.
|
|
8
|
+
*/
|
|
9
|
+
/** An Infinity CL pool key. currency0 is always BNB (the zero address), currency1 the coin. */
|
|
10
|
+
export type PoolKey = {
|
|
11
|
+
currency0: Address;
|
|
12
|
+
currency1: Address;
|
|
13
|
+
hooks: Address;
|
|
14
|
+
poolManager: Address;
|
|
15
|
+
fee: number;
|
|
16
|
+
parameters: Hex;
|
|
17
|
+
};
|
|
18
|
+
/** The pool id of a key: keccak256 of the encoded key, as Infinity's PoolIdLibrary computes it. */
|
|
19
|
+
export declare function poolIdOf(key: PoolKey): Hex;
|
|
20
|
+
/** a key as a contract returns it (fee as a number) */
|
|
21
|
+
export declare const toKey: (raw: PoolKey) => PoolKey;
|
|
22
|
+
/**
|
|
23
|
+
* The UniversalRouter's INFI_SWAP input: every leg an exact-in swap on the same pool (CL_SWAP_EXACT_IN_SINGLE), then the
|
|
24
|
+
* whole input settled once (SETTLE_ALL) and the whole output taken once (TAKE_ALL, at least minOut). A single leg also
|
|
25
|
+
* carries minOut as its own limit; with more legs only the total is bounded.
|
|
26
|
+
*/
|
|
27
|
+
export declare function infiSwapInput(key: PoolKey, buy: boolean, legs: readonly bigint[], minOut: bigint, hookData?: Hex): Hex;
|
|
28
|
+
/** A swap through the UniversalRouter. A buy sends its BNB along; a sell needs the Permit2 approvals first. */
|
|
29
|
+
export declare function buildInfiSwap(a: Addresses, s: {
|
|
30
|
+
key: PoolKey;
|
|
31
|
+
buy: boolean;
|
|
32
|
+
legs: readonly bigint[];
|
|
33
|
+
minOut: bigint;
|
|
34
|
+
deadline?: bigint;
|
|
35
|
+
extraGas?: bigint;
|
|
36
|
+
gasPct?: number;
|
|
37
|
+
hookData?: Hex;
|
|
38
|
+
label?: string;
|
|
39
|
+
}): TxRequest;
|
|
40
|
+
/**
|
|
41
|
+
* What a swap gets, from PancakeSwap's CL quoter running the real swap in an eth_call (the hook's fee, rules and
|
|
42
|
+
* liquidations are in it). `from` is the trader: hooks read tx.origin. A refusal throws the raw error; blockedKind() and
|
|
43
|
+
* explain() read it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function infiQuote(ctx: Ctx, q: {
|
|
46
|
+
key: PoolKey;
|
|
47
|
+
buy: boolean;
|
|
48
|
+
amount: bigint;
|
|
49
|
+
from?: Address;
|
|
50
|
+
hookData?: Hex;
|
|
51
|
+
}): Promise<{
|
|
52
|
+
amountOut: bigint;
|
|
53
|
+
gasEstimate: bigint;
|
|
54
|
+
}>;
|
|
55
|
+
/** The approvals a sell of `amount` through the UniversalRouter needs first: the coin to Permit2 (once), Permit2 to the router (30 days). */
|
|
56
|
+
export declare function permit2Approvals(ctx: Ctx, owner: Address, token: Address, amount: bigint): Promise<TxRequest[]>;
|
|
57
|
+
/**
|
|
58
|
+
* The rule a hook names in Blocked(uint8) (pools, curve calls): PancakeSwap wraps the hook's error (and the quoter once
|
|
59
|
+
* more), so the selector is searched anywhere in the revert data and the next word is the rule kind.
|
|
60
|
+
*/
|
|
61
|
+
export declare function blockedKind(err: unknown): number | null;
|
|
62
|
+
/** BNB per coin at an Infinity pool's sqrt price (the price is coins per BNB). */
|
|
63
|
+
export declare const infiPriceBnb: (sqrtPriceX96: bigint, decimals?: number) => number;
|
package/dist/infinity.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { concatHex, encodeAbiParameters, encodeFunctionData, keccak256, maxUint256, toFunctionSelector } from "viem";
|
|
2
|
+
import { POOL_KEY, SWAP_EXACT_IN_SINGLE, clQuoterAbi, erc20Abi, permit2Abi, universalRouterAbi } from "./abis/common.js";
|
|
3
|
+
import { NATIVE } from "./addresses.js";
|
|
4
|
+
import { findRevertData } from "./errors.js";
|
|
5
|
+
import { deadlineIn } from "./tx.js";
|
|
6
|
+
/** The pool id of a key: keccak256 of the encoded key, as Infinity's PoolIdLibrary computes it. */
|
|
7
|
+
export function poolIdOf(key) {
|
|
8
|
+
return keccak256(encodeAbiParameters([POOL_KEY], [key]));
|
|
9
|
+
}
|
|
10
|
+
/** a key as a contract returns it (fee as a number) */
|
|
11
|
+
export const toKey = (raw) => ({ ...raw, fee: Number(raw.fee) });
|
|
12
|
+
/**
|
|
13
|
+
* The UniversalRouter's INFI_SWAP input: every leg an exact-in swap on the same pool (CL_SWAP_EXACT_IN_SINGLE), then the
|
|
14
|
+
* whole input settled once (SETTLE_ALL) and the whole output taken once (TAKE_ALL, at least minOut). A single leg also
|
|
15
|
+
* carries minOut as its own limit; with more legs only the total is bounded.
|
|
16
|
+
*/
|
|
17
|
+
export function infiSwapInput(key, buy, legs, minOut, hookData = "0x") {
|
|
18
|
+
const coin = key.currency1;
|
|
19
|
+
const total = legs.reduce((x, y) => x + y, 0n);
|
|
20
|
+
const actions = concatHex([...legs.map(() => "0x06"), "0x0c", "0x0f"]);
|
|
21
|
+
const params = [
|
|
22
|
+
...legs.map((amountIn) => encodeAbiParameters(SWAP_EXACT_IN_SINGLE, [{ poolKey: key, zeroForOne: buy, amountIn, amountOutMinimum: legs.length === 1 ? minOut : 0n, hookData }])),
|
|
23
|
+
encodeAbiParameters([{ type: "address" }, { type: "uint256" }], [buy ? NATIVE : coin, total]),
|
|
24
|
+
encodeAbiParameters([{ type: "address" }, { type: "uint256" }], [buy ? coin : NATIVE, minOut]),
|
|
25
|
+
];
|
|
26
|
+
return encodeAbiParameters([{ type: "bytes" }, { type: "bytes[]" }], [actions, params]);
|
|
27
|
+
}
|
|
28
|
+
/** A swap through the UniversalRouter. A buy sends its BNB along; a sell needs the Permit2 approvals first. */
|
|
29
|
+
export function buildInfiSwap(a, s) {
|
|
30
|
+
const total = s.legs.reduce((x, y) => x + y, 0n);
|
|
31
|
+
return {
|
|
32
|
+
to: a.universalRouter,
|
|
33
|
+
value: s.buy ? total : 0n,
|
|
34
|
+
data: encodeFunctionData({ abi: universalRouterAbi, functionName: "execute", args: ["0x10", [infiSwapInput(s.key, s.buy, s.legs, s.minOut, s.hookData)], s.deadline ?? deadlineIn()] }),
|
|
35
|
+
extraGas: s.extraGas,
|
|
36
|
+
gasPct: s.gasPct,
|
|
37
|
+
label: s.label ?? (s.buy ? "The buy" : "The sell"),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* What a swap gets, from PancakeSwap's CL quoter running the real swap in an eth_call (the hook's fee, rules and
|
|
42
|
+
* liquidations are in it). `from` is the trader: hooks read tx.origin. A refusal throws the raw error; blockedKind() and
|
|
43
|
+
* explain() read it.
|
|
44
|
+
*/
|
|
45
|
+
export async function infiQuote(ctx, q) {
|
|
46
|
+
const r = await ctx.pub.simulateContract({
|
|
47
|
+
address: ctx.a.clQuoter,
|
|
48
|
+
abi: clQuoterAbi,
|
|
49
|
+
functionName: "quoteExactInputSingle",
|
|
50
|
+
args: [{ poolKey: q.key, zeroForOne: q.buy, exactAmount: q.amount, hookData: q.hookData ?? "0x" }],
|
|
51
|
+
account: q.from,
|
|
52
|
+
});
|
|
53
|
+
return { amountOut: r.result[0], gasEstimate: r.result[1] };
|
|
54
|
+
}
|
|
55
|
+
/** The approvals a sell of `amount` through the UniversalRouter needs first: the coin to Permit2 (once), Permit2 to the router (30 days). */
|
|
56
|
+
export async function permit2Approvals(ctx, owner, token, amount) {
|
|
57
|
+
const { pub, a } = ctx;
|
|
58
|
+
const out = [];
|
|
59
|
+
const [toPermit2, p2] = await Promise.all([
|
|
60
|
+
pub.readContract({ address: token, abi: erc20Abi, functionName: "allowance", args: [owner, a.permit2] }),
|
|
61
|
+
pub.readContract({ address: a.permit2, abi: permit2Abi, functionName: "allowance", args: [owner, token, a.universalRouter] }),
|
|
62
|
+
]);
|
|
63
|
+
if (toPermit2 < amount) {
|
|
64
|
+
out.push({ to: token, value: 0n, data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [a.permit2, maxUint256] }), label: "Allowing Permit2" });
|
|
65
|
+
}
|
|
66
|
+
const now = Math.floor(Date.now() / 1000);
|
|
67
|
+
if (p2[0] < amount || Number(p2[1]) < now + 60) {
|
|
68
|
+
out.push({
|
|
69
|
+
to: a.permit2,
|
|
70
|
+
value: 0n,
|
|
71
|
+
data: encodeFunctionData({ abi: permit2Abi, functionName: "approve", args: [token, a.universalRouter, (1n << 160n) - 1n, now + 30 * 86_400] }),
|
|
72
|
+
label: "Allowing the router through Permit2",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
const BLOCKED = toFunctionSelector("Blocked(uint8)").slice(2).toLowerCase();
|
|
78
|
+
/**
|
|
79
|
+
* The rule a hook names in Blocked(uint8) (pools, curve calls): PancakeSwap wraps the hook's error (and the quoter once
|
|
80
|
+
* more), so the selector is searched anywhere in the revert data and the next word is the rule kind.
|
|
81
|
+
*/
|
|
82
|
+
export function blockedKind(err) {
|
|
83
|
+
const data = findRevertData(err)?.toLowerCase();
|
|
84
|
+
if (!data)
|
|
85
|
+
return null;
|
|
86
|
+
const i = data.indexOf(BLOCKED);
|
|
87
|
+
if (i < 0)
|
|
88
|
+
return null;
|
|
89
|
+
const word = data.slice(i + BLOCKED.length, i + BLOCKED.length + 64);
|
|
90
|
+
return word.length === 64 ? Number.parseInt(word, 16) : null;
|
|
91
|
+
}
|
|
92
|
+
/** BNB per coin at an Infinity pool's sqrt price (the price is coins per BNB). */
|
|
93
|
+
export const infiPriceBnb = (sqrtPriceX96, decimals = 18) => {
|
|
94
|
+
const p = (Number(sqrtPriceX96) / 2 ** 96) ** 2 * 10 ** (18 - decimals);
|
|
95
|
+
return p > 0 ? 1 / p : 0;
|
|
96
|
+
};
|
package/dist/infra.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { type Address, type Hex } from "viem";
|
|
2
|
+
import { type Ctx, type SendOptions, type Sent, type TxRequest } from "./tx.js";
|
|
3
|
+
/**
|
|
4
|
+
* HOOKS infra.
|
|
5
|
+
* - Floors: a BNB reserve under a coin (HooksFloor, one per coin from HooksFloorFactory). Anyone feeds it; a holder hands
|
|
6
|
+
* coins in for reserve / circulating, less 2 % (1 % stays and lifts the floor, 1 % to the protocol). The coins go to
|
|
7
|
+
* 0xdead. No owner, no withdraw: the floor only rises.
|
|
8
|
+
* - Protected buy: HooksExecutor buys through PancakeSwap's own routers and a floor for what arrived from a live dip bid,
|
|
9
|
+
* in one transaction. pickhooks.com picks the route and the bid (/api/infra/protect); protect() sends what it built.
|
|
10
|
+
* Off while addresses.hooksFloorFactory / hooksExecutor are zero (until they are on mainnet).
|
|
11
|
+
*/
|
|
12
|
+
export type FloorState = {
|
|
13
|
+
token: Address;
|
|
14
|
+
/** the coin's floor, or null while nobody opened it */
|
|
15
|
+
floor: Address | null;
|
|
16
|
+
/** where the floor is (or will be once opened) */
|
|
17
|
+
predicted: Address;
|
|
18
|
+
reserve: bigint;
|
|
19
|
+
/** coins that could be handed in: supply minus 0xdead */
|
|
20
|
+
circulating: bigint;
|
|
21
|
+
/** BNB wei per whole coin a hand in is worth before the 2 % */
|
|
22
|
+
perCoin: bigint;
|
|
23
|
+
totalFed: bigint;
|
|
24
|
+
totalPaid: bigint;
|
|
25
|
+
totalRedeemed: bigint;
|
|
26
|
+
};
|
|
27
|
+
/** pickhooks.com's answer for a protected buy (loosely typed: the site may add fields) */
|
|
28
|
+
export type ProtectOffer = {
|
|
29
|
+
token: Address;
|
|
30
|
+
route: string | null;
|
|
31
|
+
/** why there is no protected buy, when there is none */
|
|
32
|
+
reason: string | null;
|
|
33
|
+
bnbIn: string;
|
|
34
|
+
coinsOut: string;
|
|
35
|
+
options: unknown[];
|
|
36
|
+
best: {
|
|
37
|
+
bidId?: string | number;
|
|
38
|
+
floorPct?: number;
|
|
39
|
+
premium?: string;
|
|
40
|
+
[k: string]: unknown;
|
|
41
|
+
} | null;
|
|
42
|
+
executor: Address | null;
|
|
43
|
+
tx: {
|
|
44
|
+
to: Address;
|
|
45
|
+
data: Hex;
|
|
46
|
+
value: string;
|
|
47
|
+
} | null;
|
|
48
|
+
simulated: {
|
|
49
|
+
coins: string;
|
|
50
|
+
floorId: string;
|
|
51
|
+
premium: string;
|
|
52
|
+
} | {
|
|
53
|
+
error: string;
|
|
54
|
+
} | null;
|
|
55
|
+
[k: string]: unknown;
|
|
56
|
+
};
|
|
57
|
+
export declare function infraModule(ctx: Ctx): {
|
|
58
|
+
on: {
|
|
59
|
+
floors: boolean;
|
|
60
|
+
executor: boolean;
|
|
61
|
+
};
|
|
62
|
+
floorAddress: (token: Address) => Promise<Address | null>;
|
|
63
|
+
floorOf: (token: Address) => Promise<FloorState>;
|
|
64
|
+
quoteRedeem: (token: Address, coins: bigint) => Promise<{
|
|
65
|
+
payout: bigint;
|
|
66
|
+
kept: bigint;
|
|
67
|
+
protocolFee: bigint;
|
|
68
|
+
}>;
|
|
69
|
+
build: {
|
|
70
|
+
/** opens a coin's floor (anyone can) and feeds it `seed` */
|
|
71
|
+
open: (token: Address, seed?: bigint) => TxRequest;
|
|
72
|
+
/** adds BNB to a floor's reserve (plain BNB to the floor does the same) */
|
|
73
|
+
feed: (floor: Address, wei: bigint) => TxRequest;
|
|
74
|
+
/** hands coins in (the floor needs the approval first) */
|
|
75
|
+
redeem: (floor: Address, coins: bigint, minBnb: bigint, to: Address) => TxRequest;
|
|
76
|
+
};
|
|
77
|
+
open: (o: {
|
|
78
|
+
token: Address;
|
|
79
|
+
seed?: bigint;
|
|
80
|
+
}, s?: SendOptions) => Promise<Sent & {
|
|
81
|
+
floor: Address;
|
|
82
|
+
}>;
|
|
83
|
+
feed: (token: Address, wei: bigint, s?: SendOptions) => Promise<Sent>;
|
|
84
|
+
redeem: (o: {
|
|
85
|
+
token: Address;
|
|
86
|
+
coins: bigint;
|
|
87
|
+
slippageBps?: number;
|
|
88
|
+
minBnb?: bigint;
|
|
89
|
+
}, s?: SendOptions) => Promise<Sent & {
|
|
90
|
+
quoted: bigint;
|
|
91
|
+
}>;
|
|
92
|
+
offer: (q: {
|
|
93
|
+
token: Address;
|
|
94
|
+
bnbIn: bigint;
|
|
95
|
+
wallet?: Address;
|
|
96
|
+
minFloorPct?: number;
|
|
97
|
+
bid?: number;
|
|
98
|
+
slippagePct?: number;
|
|
99
|
+
}) => Promise<ProtectOffer>;
|
|
100
|
+
protect: (q: {
|
|
101
|
+
token: Address;
|
|
102
|
+
bnbIn: bigint;
|
|
103
|
+
minFloorPct?: number;
|
|
104
|
+
bid?: number;
|
|
105
|
+
slippagePct?: number;
|
|
106
|
+
}, s?: SendOptions) => Promise<Sent & {
|
|
107
|
+
coins: bigint;
|
|
108
|
+
floorId: bigint;
|
|
109
|
+
premium: bigint;
|
|
110
|
+
}>;
|
|
111
|
+
};
|
|
112
|
+
export type InfraModule = ReturnType<typeof infraModule>;
|
package/dist/infra.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { encodeFunctionData, formatEther, parseEventLogs } from "viem";
|
|
2
|
+
import { hooksExecutorAbi, hooksFloorAbi, hooksFloorFactoryAbi } from "./abis/infra.js";
|
|
3
|
+
import { ZERO_ADDRESS } from "./addresses.js";
|
|
4
|
+
import { PickhooksError, toPickhooksError } from "./errors.js";
|
|
5
|
+
import { minusBps, sendTx, signer } from "./tx.js";
|
|
6
|
+
import { approvalFor } from "./v3.js";
|
|
7
|
+
export function infraModule(ctx) {
|
|
8
|
+
const { a, pub } = ctx;
|
|
9
|
+
const on = { floors: a.hooksFloorFactory !== ZERO_ADDRESS, executor: a.hooksExecutor !== ZERO_ADDRESS };
|
|
10
|
+
const need = (part) => {
|
|
11
|
+
if (!on[part])
|
|
12
|
+
throw new PickhooksError(part === "floors" ? "HOOKS floors are not live yet." : "The protected buy is not live yet.", { code: "input" });
|
|
13
|
+
};
|
|
14
|
+
const tx = (to, abi, functionName, args, value = 0n, label) => ({
|
|
15
|
+
to,
|
|
16
|
+
value,
|
|
17
|
+
data: encodeFunctionData({ abi, functionName, args }),
|
|
18
|
+
label,
|
|
19
|
+
});
|
|
20
|
+
async function floorAddress(token) {
|
|
21
|
+
need("floors");
|
|
22
|
+
const f = (await pub.readContract({ address: a.hooksFloorFactory, abi: hooksFloorFactoryAbi, functionName: "floorOf", args: [token] }));
|
|
23
|
+
return f === ZERO_ADDRESS ? null : f;
|
|
24
|
+
}
|
|
25
|
+
/** A coin's floor: reserve, circulating, BNB per whole coin, and what it paid so far (zeros while it is not open). */
|
|
26
|
+
async function floorOf(token) {
|
|
27
|
+
const [floor, predicted] = await Promise.all([
|
|
28
|
+
floorAddress(token),
|
|
29
|
+
pub.readContract({ address: a.hooksFloorFactory, abi: hooksFloorFactoryAbi, functionName: "predict", args: [token] }),
|
|
30
|
+
]);
|
|
31
|
+
if (!floor)
|
|
32
|
+
return { token, floor: null, predicted, reserve: 0n, circulating: 0n, perCoin: 0n, totalFed: 0n, totalPaid: 0n, totalRedeemed: 0n };
|
|
33
|
+
const fns = ["reserve", "circulating", "floor", "totalFed", "totalPaid", "totalRedeemed"];
|
|
34
|
+
const r = (await pub.multicall({ contracts: fns.map((functionName) => ({ address: floor, abi: hooksFloorAbi, functionName })), allowFailure: false }));
|
|
35
|
+
return { token, floor, predicted, reserve: r[0], circulating: r[1], perCoin: r[2], totalFed: r[3], totalPaid: r[4], totalRedeemed: r[5] };
|
|
36
|
+
}
|
|
37
|
+
async function mustFloor(token) {
|
|
38
|
+
const f = await floorAddress(token);
|
|
39
|
+
if (!f)
|
|
40
|
+
throw new PickhooksError("This coin has no floor yet. Anyone can open it with open().", { code: "input" });
|
|
41
|
+
return f;
|
|
42
|
+
}
|
|
43
|
+
/** What handing in `coins` pays now: the payout, what stays in the reserve, the protocol's part. */
|
|
44
|
+
async function quoteRedeem(token, coins) {
|
|
45
|
+
const [payout, kept, protocolFee] = (await pub.readContract({ address: await mustFloor(token), abi: hooksFloorAbi, functionName: "quote", args: [coins] }));
|
|
46
|
+
return { payout, kept, protocolFee };
|
|
47
|
+
}
|
|
48
|
+
const build = {
|
|
49
|
+
/** opens a coin's floor (anyone can) and feeds it `seed` */
|
|
50
|
+
open: (token, seed = 0n) => tx(a.hooksFloorFactory, hooksFloorFactoryAbi, "open", [token], seed, "Opening the floor"),
|
|
51
|
+
/** adds BNB to a floor's reserve (plain BNB to the floor does the same) */
|
|
52
|
+
feed: (floor, wei) => tx(floor, hooksFloorAbi, "feed", [], wei, "Feeding the floor"),
|
|
53
|
+
/** hands coins in (the floor needs the approval first) */
|
|
54
|
+
redeem: (floor, coins, minBnb, to) => tx(floor, hooksFloorAbi, "redeem", [coins, minBnb, to], 0n, "Handing in"),
|
|
55
|
+
};
|
|
56
|
+
/** Opens the coin's floor, fed with `seed` BNB. */
|
|
57
|
+
async function open(o, s = {}) {
|
|
58
|
+
need("floors");
|
|
59
|
+
const sent = await sendTx(ctx, build.open(o.token, o.seed ?? 0n), s);
|
|
60
|
+
const ev = parseEventLogs({ abi: hooksFloorFactoryAbi, logs: sent.receipt.logs, eventName: "FloorOpened" })[0];
|
|
61
|
+
return { ...sent, floor: ev?.args.floor ?? (await mustFloor(o.token)) };
|
|
62
|
+
}
|
|
63
|
+
/** Feeds the coin's floor with `wei` BNB. */
|
|
64
|
+
async function feed(token, wei, s = {}) {
|
|
65
|
+
return sendTx(ctx, build.feed(await mustFloor(token), wei), s);
|
|
66
|
+
}
|
|
67
|
+
/** Hands `coins` in for BNB: approval to the floor if needed, then the hand in with the quote less `slippageBps` (default 50) as the minimum. */
|
|
68
|
+
async function redeem(o, s = {}) {
|
|
69
|
+
const floor = await mustFloor(o.token);
|
|
70
|
+
const { address } = await signer(ctx, s.wallet);
|
|
71
|
+
const q = await quoteRedeem(o.token, o.coins);
|
|
72
|
+
if (q.payout === 0n)
|
|
73
|
+
throw new PickhooksError("The floor pays nothing for these coins yet.", { code: "input" });
|
|
74
|
+
const ap = await approvalFor(ctx, address, o.token, o.coins, floor, "Allowing the floor");
|
|
75
|
+
if (ap)
|
|
76
|
+
await sendTx(ctx, ap, s);
|
|
77
|
+
const sent = await sendTx(ctx, build.redeem(floor, o.coins, o.minBnb ?? minusBps(q.payout, o.slippageBps ?? 50), address), s);
|
|
78
|
+
return { ...sent, quoted: q.payout };
|
|
79
|
+
}
|
|
80
|
+
/** pickhooks.com's protected buy for `bnbIn`: the route, the coins, the dip bids that can cover them, and with `wallet` the transaction. */
|
|
81
|
+
async function offer(q) {
|
|
82
|
+
const query = { token: q.token, bnb: formatEther(q.bnbIn), wallet: q.wallet, minFloor: q.minFloorPct, bid: q.bid, slippage: q.slippagePct };
|
|
83
|
+
const qs = Object.entries(query)
|
|
84
|
+
.filter(([, v]) => v !== undefined)
|
|
85
|
+
.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`)
|
|
86
|
+
.join("&");
|
|
87
|
+
const res = await fetch(`${ctx.site}/api/infra/protect?${qs}`, { headers: { accept: "application/json" } });
|
|
88
|
+
const j = (await res.json().catch(() => null));
|
|
89
|
+
if (!res.ok || !j)
|
|
90
|
+
throw new PickhooksError(j?.error ?? `The protected buy answered ${res.status}.`, { code: res.status === 429 ? "rate" : "site" });
|
|
91
|
+
return j;
|
|
92
|
+
}
|
|
93
|
+
/** Buys `bnbIn` of the coin with a floor in one transaction, as pickhooks.com builds it; refuses when the site found no cover. */
|
|
94
|
+
async function protect(q, s = {}) {
|
|
95
|
+
need("executor");
|
|
96
|
+
const { address } = await signer(ctx, s.wallet);
|
|
97
|
+
const o = await offer({ ...q, wallet: address });
|
|
98
|
+
if (!o.tx)
|
|
99
|
+
throw new PickhooksError(o.reason ?? "No dip bid can cover this buy right now.", { code: "input" });
|
|
100
|
+
if (o.simulated && "error" in o.simulated)
|
|
101
|
+
throw new PickhooksError(o.simulated.error, { code: "reverted" });
|
|
102
|
+
if (o.tx.to.toLowerCase() !== a.hooksExecutor.toLowerCase())
|
|
103
|
+
throw new PickhooksError("The site built a transaction to another contract than the executor.", { code: "input" });
|
|
104
|
+
try {
|
|
105
|
+
const sent = await sendTx(ctx, { to: o.tx.to, data: o.tx.data, value: BigInt(o.tx.value), label: "The protected buy" }, s);
|
|
106
|
+
const ev = parseEventLogs({ abi: hooksExecutorAbi, logs: sent.receipt.logs, eventName: "ProtectedBuy" })[0];
|
|
107
|
+
return { ...sent, coins: ev?.args.coins ?? 0n, floorId: ev?.args.floorId ?? 0n, premium: ev?.args.premium ?? 0n };
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
throw toPickhooksError(e, "The protected buy would fail.");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { on, floorAddress, floorOf, quoteRedeem, build, open, feed, redeem, offer, protect };
|
|
114
|
+
}
|