@pickhooks/sdk 0.2.0 → 0.5.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/README.md +143 -4
- package/dist/abis/agents.d.ts +2418 -0
- package/dist/abis/agents.js +4 -0
- package/dist/abis/index.d.ts +3 -0
- package/dist/abis/index.js +3 -0
- package/dist/abis/sticky2.d.ts +3750 -0
- package/dist/abis/sticky2.js +8 -0
- package/dist/abis/sticky3.d.ts +2292 -0
- package/dist/abis/sticky3.js +8 -0
- package/dist/addresses.d.ts +25 -1
- package/dist/addresses.js +23 -2
- 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/desks.d.ts +298 -0
- package/dist/desks.js +343 -0
- package/dist/errors.js +97 -2
- package/dist/index.d.ts +59 -4
- package/dist/index.js +86 -9
- package/dist/modes-catalog.d.ts +59 -0
- package/dist/modes-catalog.js +77 -0
- package/dist/modes.d.ts +71 -0
- package/dist/modes.js +86 -0
- package/dist/simulate.d.ts +75 -0
- package/dist/simulate.js +225 -0
- package/dist/sticky-sim.d.ts +125 -0
- package/dist/sticky-sim.js +637 -0
- package/dist/sticky.d.ts +93 -4
- package/dist/sticky.js +281 -43
- package/dist/text-launch.d.ts +52 -0
- package/dist/text-launch.js +178 -0
- package/dist/tlock.d.ts +77 -0
- package/dist/tlock.js +201 -0
- package/dist/tx.d.ts +2 -0
- package/dist/tx.js +9 -0
- package/dist/v2.d.ts +106 -0
- package/dist/v2.js +61 -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 +12 -2
package/dist/sticky.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type Address, type Hex } from "viem";
|
|
2
|
-
import { type
|
|
2
|
+
import { type ModeName } from "./addresses.js";
|
|
3
|
+
import { type ModeSettings } from "./modes-catalog.js";
|
|
4
|
+
import { type EditRow, type Pick as RulePick, compile } from "./sticky-rules.js";
|
|
5
|
+
import { type SimReport } from "./sticky-sim.js";
|
|
3
6
|
import { type Ctx, type SendOptions, type Sent, type TxRequest } from "./tx.js";
|
|
7
|
+
import { type Venue } from "./v2.js";
|
|
4
8
|
/**
|
|
5
9
|
* sticky rules: a coin whose rules stick to the coins, not to the wallet. Every buy is a lot with its time and marks, a
|
|
6
10
|
* send moves lots along, so a fresh wallet takes the rules with it. The creator picks up to eight rows from templates; the
|
|
@@ -52,6 +56,13 @@ export type StickyCoin = {
|
|
|
52
56
|
/** BNB per coin at the V3 pool's price, and the market cap in BNB */
|
|
53
57
|
priceBnb: number;
|
|
54
58
|
mcapBnb: number;
|
|
59
|
+
/** sticky 3: the coin's mode (its name, or "other" for a mode the SDK does not know), its share of buys and the game pot */
|
|
60
|
+
mode?: {
|
|
61
|
+
name: ModeName | "other" | null;
|
|
62
|
+
address: Address;
|
|
63
|
+
gameBps: number;
|
|
64
|
+
gamePot: bigint;
|
|
65
|
+
};
|
|
55
66
|
};
|
|
56
67
|
export type StickyLot = {
|
|
57
68
|
amount: bigint;
|
|
@@ -75,8 +86,10 @@ export type StickyLaunch = {
|
|
|
75
86
|
symbol: string;
|
|
76
87
|
/** a link to the coin's metadata JSON (name, symbol, description, image, website, twitter, telegram) */
|
|
77
88
|
metadata: string;
|
|
78
|
-
/** templates and their numbers; or rows + release for a table built elsewhere */
|
|
79
|
-
picks?:
|
|
89
|
+
/** templates and their numbers (numbers left out take the template's defaults); or rows + release for a table built elsewhere */
|
|
90
|
+
picks?: (Omit<RulePick, "params"> & {
|
|
91
|
+
params?: RulePick["params"];
|
|
92
|
+
})[];
|
|
80
93
|
/**
|
|
81
94
|
* hook script: the creator's own rows, after the templates (the factory's custom switch must be on). Built like the
|
|
82
95
|
* site's editor: emptyEditRow(), VARS for the variables and their units, editToRow() for the row on chain.
|
|
@@ -92,6 +105,15 @@ export type StickyLaunch = {
|
|
|
92
105
|
keepBps?: number;
|
|
93
106
|
/** a fixed salt (bytes32); default random */
|
|
94
107
|
salt?: Hex;
|
|
108
|
+
/**
|
|
109
|
+
* sticky 2 only: rules (0 based) that switch, 2 to 4 drop rows; one of them is on at a time (the first at launch) and
|
|
110
|
+
* the switcher turns another on. Default switcher: the rule auction, where holders burn coins to pick the game.
|
|
111
|
+
*/
|
|
112
|
+
switchRows?: number[];
|
|
113
|
+
switcher?: Address;
|
|
114
|
+
/** sticky 3 only: the coin's mode (fixed for good) and its settings within the mode's bounds (defaults when left out) */
|
|
115
|
+
mode?: ModeName;
|
|
116
|
+
modeSettings?: ModeSettings;
|
|
95
117
|
};
|
|
96
118
|
export type StickyTerms = {
|
|
97
119
|
seed: bigint;
|
|
@@ -104,11 +126,50 @@ export type StickyTerms = {
|
|
|
104
126
|
};
|
|
105
127
|
/** The rows of a coin as sentences, with the template each belongs to. */
|
|
106
128
|
export declare function describeRows(raw: readonly bigint[], release: number, off: number): StickyRow[];
|
|
107
|
-
|
|
129
|
+
/**
|
|
130
|
+
* The table a launch would send: the templates' rows, then the hook script rows (the site's order), one release time per
|
|
131
|
+
* coin (the longer one wins); or rows + release as given. Offline; throws the problems as sentences.
|
|
132
|
+
*/
|
|
133
|
+
export declare function stickyTable(l: Pick<StickyLaunch, "picks" | "script" | "scriptHours" | "rows" | "release">): {
|
|
134
|
+
rows: bigint[];
|
|
135
|
+
release: number;
|
|
136
|
+
};
|
|
137
|
+
export type StickyTestRun = {
|
|
138
|
+
rows: bigint[];
|
|
139
|
+
release: number;
|
|
140
|
+
/** the run's numbers, as the launcher's test run shows them */
|
|
141
|
+
report: SimReport;
|
|
142
|
+
/** the run as sentences, one per line of the launcher's card */
|
|
143
|
+
text: string[];
|
|
144
|
+
};
|
|
145
|
+
/** A test run as sentences: the launcher's card line by line. `names` are the rows' names (rule i + 1). */
|
|
146
|
+
export declare function testRunText(r: SimReport, names: string[]): string[];
|
|
147
|
+
/**
|
|
148
|
+
* The launcher's test run, offline: the table on a fresh coin with random trades (the creator's first buy, three snipers
|
|
149
|
+
* in the first blocks, wallets that buy, hold and sell, bags split to fresh wallets). It runs the coin's own rule engine,
|
|
150
|
+
* checked against the contract trade by trade. The same seed gives the same run; the chain stays the truth.
|
|
151
|
+
*/
|
|
152
|
+
export declare function stickyTestRun(l: Pick<StickyLaunch, "picks" | "script" | "scriptHours" | "rows" | "release"> & {
|
|
153
|
+
devBuyBnb?: number;
|
|
154
|
+
seed?: number;
|
|
155
|
+
trades?: number;
|
|
156
|
+
start?: number;
|
|
157
|
+
}): StickyTestRun;
|
|
158
|
+
/**
|
|
159
|
+
* The sticky module. `v2` points it at the sticky 2 factory (same coins, same rules, same calls; its coins also count
|
|
160
|
+
* the creator's sold coins and can carry switch rows, see ph.sticky2). `3` points it at sticky 3: the same again, plus a
|
|
161
|
+
* mode per coin (launch with `mode` and `modeSettings`; the game itself is ph.modes); a sticky 3 coin leaves its lot
|
|
162
|
+
* views to the lens.
|
|
163
|
+
*/
|
|
164
|
+
export declare function stickyModule(ctx: Ctx, version?: boolean | 3): {
|
|
108
165
|
factory: `0x${string}`;
|
|
109
166
|
terms: () => Promise<StickyTerms>;
|
|
110
167
|
validate: (rows: bigint[], release: number, custom?: boolean) => Promise<string | null>;
|
|
168
|
+
validateSwitch: (rows: bigint[], mask: number, validator?: Address) => Promise<string | null>;
|
|
169
|
+
validateMode: (mode: ModeName, settings: ModeSettings | undefined, rows: bigint[], validator?: Address, creator?: Address) => Promise<string | null>;
|
|
111
170
|
compile: typeof compile;
|
|
171
|
+
table: typeof stickyTable;
|
|
172
|
+
testRun: typeof stickyTestRun;
|
|
112
173
|
describeRows: typeof describeRows;
|
|
113
174
|
prepareMetadata: (m: {
|
|
114
175
|
name: string;
|
|
@@ -136,6 +197,7 @@ export declare function stickyModule(ctx: Ctx): {
|
|
|
136
197
|
balance: bigint;
|
|
137
198
|
sellable: bigint;
|
|
138
199
|
lots: StickyLot[];
|
|
200
|
+
held?: bigint;
|
|
139
201
|
}>;
|
|
140
202
|
check: (c: {
|
|
141
203
|
token: Address;
|
|
@@ -151,8 +213,12 @@ export declare function stickyModule(ctx: Ctx): {
|
|
|
151
213
|
wallet?: Address;
|
|
152
214
|
}) => Promise<{
|
|
153
215
|
amountOut: bigint;
|
|
216
|
+
venue: Venue;
|
|
217
|
+
v3: bigint | null;
|
|
218
|
+
v2: bigint | null;
|
|
154
219
|
}>;
|
|
155
220
|
estimateBuy: (token: Address, bnbIn: bigint) => Promise<bigint>;
|
|
221
|
+
buyTakeBps: (token: Address) => Promise<number>;
|
|
156
222
|
sellApproval: (owner: Address, token: Address, amount: bigint) => Promise<TxRequest | null>;
|
|
157
223
|
build: {
|
|
158
224
|
launch: (l: {
|
|
@@ -164,6 +230,10 @@ export declare function stickyModule(ctx: Ctx): {
|
|
|
164
230
|
salt?: Hex;
|
|
165
231
|
keepBps?: number;
|
|
166
232
|
minCreatorBps: number;
|
|
233
|
+
switcher?: Address;
|
|
234
|
+
switchMask?: number;
|
|
235
|
+
mode?: ModeName;
|
|
236
|
+
modeSettings?: ModeSettings;
|
|
167
237
|
}, value: bigint) => TxRequest;
|
|
168
238
|
buy: (b: {
|
|
169
239
|
token: Address;
|
|
@@ -180,6 +250,7 @@ export declare function stickyModule(ctx: Ctx): {
|
|
|
180
250
|
deadline?: bigint;
|
|
181
251
|
}) => TxRequest;
|
|
182
252
|
lift: (token: Address, row: number) => TxRequest;
|
|
253
|
+
collect: (token: Address) => Promise<TxRequest>;
|
|
183
254
|
};
|
|
184
255
|
launch: (l: StickyLaunch, s?: SendOptions) => Promise<Sent & {
|
|
185
256
|
token: Address;
|
|
@@ -194,10 +265,12 @@ export declare function stickyModule(ctx: Ctx): {
|
|
|
194
265
|
slippageBps?: number;
|
|
195
266
|
minOut?: bigint;
|
|
196
267
|
deadline?: bigint;
|
|
268
|
+
venue?: Venue;
|
|
197
269
|
}, s?: SendOptions) => Promise<Sent & {
|
|
198
270
|
quoted: bigint;
|
|
199
271
|
minOut: bigint;
|
|
200
272
|
amountOut: bigint;
|
|
273
|
+
venue: Venue;
|
|
201
274
|
}>;
|
|
202
275
|
sell: (x: {
|
|
203
276
|
token: Address;
|
|
@@ -205,11 +278,27 @@ export declare function stickyModule(ctx: Ctx): {
|
|
|
205
278
|
slippageBps?: number;
|
|
206
279
|
minOut?: bigint;
|
|
207
280
|
deadline?: bigint;
|
|
281
|
+
venue?: Venue;
|
|
208
282
|
}, s?: SendOptions) => Promise<Sent & {
|
|
209
283
|
quoted: bigint;
|
|
210
284
|
minOut: bigint;
|
|
285
|
+
venue: Venue;
|
|
211
286
|
}>;
|
|
212
287
|
lift: (token: Address, row: number, s?: SendOptions) => Promise<Sent>;
|
|
288
|
+
fees: (token: Address) => Promise<{
|
|
289
|
+
bnb: bigint;
|
|
290
|
+
burned: bigint;
|
|
291
|
+
toCreator: bigint;
|
|
292
|
+
creator: Address;
|
|
293
|
+
creatorBps: number;
|
|
294
|
+
keepBps: number;
|
|
295
|
+
pending: bigint;
|
|
296
|
+
}>;
|
|
297
|
+
collect: (token: Address, s?: SendOptions) => Promise<Sent & {
|
|
298
|
+
bnb: bigint;
|
|
299
|
+
toCreator: bigint;
|
|
300
|
+
burned: bigint;
|
|
301
|
+
}>;
|
|
213
302
|
/** the sender's address, for callers that check before they sign */
|
|
214
303
|
from: (s?: SendOptions) => Promise<`0x${string}` | undefined>;
|
|
215
304
|
};
|
package/dist/sticky.js
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
import { encodeFunctionData, parseEventLogs } from "viem";
|
|
2
2
|
import { stickyFactoryAbi, stickyTokenAbi, stickyValidatorAbi } from "./abis/sticky.js";
|
|
3
|
+
import { sticky2FactoryAbi, sticky2TokenAbi, sticky2ValidatorAbi } from "./abis/sticky2.js";
|
|
4
|
+
import { sticky3FactoryAbi, sticky3LensAbi, sticky3TokenAbi, sticky3ValidatorAbi } from "./abis/sticky3.js";
|
|
5
|
+
import { MODE_NAMES } from "./addresses.js";
|
|
6
|
+
import { encodeModeData } from "./modes-catalog.js";
|
|
3
7
|
import { v3PoolAbi } from "./abis/common.js";
|
|
8
|
+
import { v2LockerAbi } from "./abis/v2.js";
|
|
4
9
|
import { ZERO_ADDRESS } from "./addresses.js";
|
|
5
10
|
import { PickhooksError, errorText, explain, toPickhooksError } from "./errors.js";
|
|
6
|
-
import { ACT, LOCKED, NONE, ON, compile, decodeRow, editToRow, rowProblems, rowText, scannerOf, TEMPLATE_BY_ID, templateOf } from "./sticky-rules.js";
|
|
7
|
-
import {
|
|
11
|
+
import { ACT, LOCKED, NONE, ON, compile, decodeRow, defaults, editToRow, rowProblems, rowText, scannerOf, TEMPLATE_BY_ID, templateOf } from "./sticky-rules.js";
|
|
12
|
+
import { simulate } from "./sticky-sim.js";
|
|
13
|
+
import { chainDeadline, minusBps, sendTx, senderAddress, signer } from "./tx.js";
|
|
14
|
+
import { bestVenue, buildV2Buy, buildV2Sell, v2Quote } from "./v2.js";
|
|
8
15
|
import { approvalFor, buildV3Buy, buildV3Sell, receivedOf, v3EstimateBuy, v3PriceBnb, v3Quote } from "./v3.js";
|
|
16
|
+
const sticky3ModeQuoteAbi = [
|
|
17
|
+
{
|
|
18
|
+
type: "function",
|
|
19
|
+
name: "quote",
|
|
20
|
+
stateMutability: "view",
|
|
21
|
+
inputs: [
|
|
22
|
+
{ name: "creator", type: "address" },
|
|
23
|
+
{ name: "data", type: "bytes" },
|
|
24
|
+
],
|
|
25
|
+
outputs: [{ name: "gameBps", type: "uint16" }],
|
|
26
|
+
},
|
|
27
|
+
];
|
|
9
28
|
const randomSalt = () => {
|
|
10
29
|
const b = new Uint8Array(32);
|
|
11
30
|
globalThis.crypto.getRandomValues(b);
|
|
@@ -30,15 +49,85 @@ export function describeRows(raw, release, off) {
|
|
|
30
49
|
return { i, name, text: rowText(r, release), template: id, sticky: t?.sticky ?? false, lock: r.act === ACT.LOCK, drop: r.act === ACT.DROP, lifted: (off & (1 << i)) !== 0, raw: x };
|
|
31
50
|
});
|
|
32
51
|
}
|
|
33
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The table a launch would send: the templates' rows, then the hook script rows (the site's order), one release time per
|
|
54
|
+
* coin (the longer one wins); or rows + release as given. Offline; throws the problems as sentences.
|
|
55
|
+
*/
|
|
56
|
+
export function stickyTable(l) {
|
|
57
|
+
if (!l.picks && !l.script?.length)
|
|
58
|
+
return { rows: l.rows ?? [], release: l.release ?? 0 };
|
|
59
|
+
// a template's numbers left out take its defaults
|
|
60
|
+
const picks = (l.picks ?? []).map((p) => {
|
|
61
|
+
const t = TEMPLATE_BY_ID.get(p.id);
|
|
62
|
+
if (!t)
|
|
63
|
+
throw new PickhooksError(`There is no template "${p.id}". Templates: ${[...TEMPLATE_BY_ID.keys()].join(", ")}.`, { code: "input" });
|
|
64
|
+
return { id: p.id, params: { ...defaults(t), ...p.params } };
|
|
65
|
+
});
|
|
66
|
+
const own = (l.script ?? []).map(editToRow);
|
|
67
|
+
const c = compile(picks, own);
|
|
68
|
+
const problems = [...c.problems, ...own.flatMap(rowProblems)];
|
|
69
|
+
const hours = l.scriptHours ?? 0;
|
|
70
|
+
const locks = own.some((r) => r.act === ACT.LOCK);
|
|
71
|
+
if (locks && !(hours * 3600 >= 600 && hours <= 720))
|
|
72
|
+
problems.push("Locked shares: unlock over 10 minutes to 30 days.");
|
|
73
|
+
if (problems.length)
|
|
74
|
+
throw new PickhooksError(problems.join(" "), { code: "input" });
|
|
75
|
+
return { rows: c.rows, release: locks ? Math.max(c.release, Math.round(hours * 3600)) : c.release };
|
|
76
|
+
}
|
|
77
|
+
const dur = (s) => s < 90 ? `${Math.round(s)} s` : s < 5400 ? `${Math.round(s / 60)} min` : s < 2 * 86_400 ? `${+(s / 3600).toFixed(1)} h` : `${+(s / 86_400).toFixed(1)} days`;
|
|
78
|
+
/** A test run as sentences: the launcher's card line by line. `names` are the rows' names (rule i + 1). */
|
|
79
|
+
export function testRunText(r, names) {
|
|
80
|
+
const rule = (row) => (row === LOCKED ? "still locked" : `rule ${row + 1}, ${names[row] ?? ""}`);
|
|
81
|
+
const pct = (x) => +x.toFixed(2);
|
|
82
|
+
const out = [
|
|
83
|
+
`Trades: ${r.buys.ok + r.buys.refused} buys, ${r.sells.ok + r.sells.refused} sells, ${r.sends.ok + r.sends.refused} sends in ${+r.hours.toFixed(1)} hours.`,
|
|
84
|
+
`Refused: ${r.refusals.length ? r.refusals.map((x) => `${rule(x.row)} ${x.count}x`).join(", ") : "nothing"}.`,
|
|
85
|
+
`Longest wait to sell: ${r.longestWait ? `${dur(r.longestWait)}${r.stillWaiting ? `, ${r.stillWaiting} wallets still waiting at the end` : ""}` : "nobody had to wait"}.`,
|
|
86
|
+
`Snipers: ${r.snipers.firstSellAfter !== null ? `first sold ${dur(r.snipers.firstSellAfter)} after launch` : "could not sell during the run"}.`,
|
|
87
|
+
`Split bags: ${r.splits.count ? `${r.splits.count} sent to fresh wallets, refused there again ${r.splits.refusedAfter} times` : "none"}.`,
|
|
88
|
+
`Everyone free: ${r.allFreeAfter === 0 ? "every wallet can sell everything right after the run" : r.allFreeAfter !== null ? `every wallet can sell everything ${dur(r.allFreeAfter)} after the run` : "at the latest when every lot is 30 days old"}.`,
|
|
89
|
+
];
|
|
90
|
+
if (r.burnedPct > 0)
|
|
91
|
+
out.push(`Burned: ${pct(r.burnedPct)} % of the supply.`);
|
|
92
|
+
if (r.pot.inPct > 0)
|
|
93
|
+
out.push(`Pot: ${pct(r.pot.inPct)} % of the supply in, ${r.pot.drops} drops paid ${pct(r.pot.paidPct)} %, ${pct(r.pot.leftPct)} % left.`);
|
|
94
|
+
out.push(`A scanner's test: ${r.scanner.buy !== null
|
|
95
|
+
? `its test buy is refused by ${rule(r.scanner.buy)}`
|
|
96
|
+
: r.scanner.sell !== null
|
|
97
|
+
? `its sell right after the buy is refused by ${rule(r.scanner.sell)}: it can show a honeypot`
|
|
98
|
+
: "a small buy and an instant sell pass"}.`);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The launcher's test run, offline: the table on a fresh coin with random trades (the creator's first buy, three snipers
|
|
103
|
+
* in the first blocks, wallets that buy, hold and sell, bags split to fresh wallets). It runs the coin's own rule engine,
|
|
104
|
+
* checked against the contract trade by trade. The same seed gives the same run; the chain stays the truth.
|
|
105
|
+
*/
|
|
106
|
+
export function stickyTestRun(l) {
|
|
107
|
+
const { rows, release } = stickyTable(l);
|
|
108
|
+
const report = simulate(rows, release, { seed: l.seed ?? 7, trades: l.trades ?? 200, devBuyBnb: l.devBuyBnb ?? 0, start: l.start });
|
|
109
|
+
const names = describeRows(rows, release, 0).map((x) => x.name);
|
|
110
|
+
return { rows, release, report, text: testRunText(report, names) };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The sticky module. `v2` points it at the sticky 2 factory (same coins, same rules, same calls; its coins also count
|
|
114
|
+
* the creator's sold coins and can carry switch rows, see ph.sticky2). `3` points it at sticky 3: the same again, plus a
|
|
115
|
+
* mode per coin (launch with `mode` and `modeSettings`; the game itself is ph.modes); a sticky 3 coin leaves its lot
|
|
116
|
+
* views to the lens.
|
|
117
|
+
*/
|
|
118
|
+
export function stickyModule(ctx, version = false) {
|
|
34
119
|
const { pub, a } = ctx;
|
|
35
|
-
const
|
|
36
|
-
const
|
|
120
|
+
const v3 = version === 3;
|
|
121
|
+
const v2 = version === true || v3; // sticky 3 has everything sticky 2 has
|
|
122
|
+
const F = v3 ? a.sticky3Factory : v2 ? a.sticky2Factory : a.stickyFactory;
|
|
123
|
+
const FA = (v3 ? sticky3FactoryAbi : v2 ? sticky2FactoryAbi : stickyFactoryAbi);
|
|
124
|
+
const TA = (v3 ? sticky3TokenAbi : v2 ? sticky2TokenAbi : stickyTokenAbi);
|
|
125
|
+
const modeName = (m) => m.toLowerCase() === ZERO_ADDRESS ? null : (MODE_NAMES.find((n) => a.modes[n].toLowerCase() === m.toLowerCase()) ?? "other");
|
|
37
126
|
const call = (address, abi, functionName, args = []) => pub.readContract({ address, abi, functionName, args });
|
|
38
127
|
/** The factory's terms now: the seed every launch pays into the V2 pair, the creator side, whether own rows are open. */
|
|
39
128
|
async function terms() {
|
|
40
129
|
const fns = ["seedBnb", "creatorBps", "custom", "validator", "startFdv", "platformWallet", "locker"];
|
|
41
|
-
const r = await pub.multicall({ contracts: fns.map((functionName) => ({ address:
|
|
130
|
+
const r = await pub.multicall({ contracts: fns.map((functionName) => ({ address: F, abi: FA, functionName })), allowFailure: false });
|
|
42
131
|
return { seed: r[0], creatorBps: Number(r[1]), custom: r[2], validator: r[3], startFdv: r[4], platformWallet: r[5], locker: r[6] };
|
|
43
132
|
}
|
|
44
133
|
/** Asks the validator on chain whether a table can launch: null when it can, else the reason. */
|
|
@@ -52,6 +141,19 @@ export function stickyModule(ctx) {
|
|
|
52
141
|
return explain(e, "The validator refused this table.").text;
|
|
53
142
|
}
|
|
54
143
|
}
|
|
144
|
+
const maskOf = (rowsOn) => (rowsOn ?? []).reduce((m, i) => m | (1 << i), 0);
|
|
145
|
+
/** sticky 2: asks the validator whether these rows can switch (drop rows only, 2 to 4): null when they can, else why not */
|
|
146
|
+
async function validateSwitch(rows, mask, validator) {
|
|
147
|
+
if (!v2)
|
|
148
|
+
return "Switch rows are a sticky 2 feature.";
|
|
149
|
+
try {
|
|
150
|
+
await call(validator ?? (await terms()).validator, (v3 ? sticky3ValidatorAbi : sticky2ValidatorAbi), "switchRows", [rows, mask]);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
return explain(e, "The validator refused these switch rows.").text;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
55
157
|
/**
|
|
56
158
|
* Stores the image and metadata on pickhooks.com and returns the metadata link (the site's /api/sticky/prepare, rate
|
|
57
159
|
* limited, only while sticky rules are live on the site). A launchpad can host its own JSON and pass that link instead.
|
|
@@ -70,35 +172,65 @@ export function stickyModule(ctx) {
|
|
|
70
172
|
/** The launch call. value = the factory's seed + the creator's first buy. */
|
|
71
173
|
function buildLaunch(l, value) {
|
|
72
174
|
const args = { name: l.name, symbol: l.symbol, metadata: l.metadata, rows: l.rows, release: l.release, salt: l.salt ?? randomSalt(), keepBps: l.keepBps ?? 10_000, minCreatorBps: l.minCreatorBps };
|
|
73
|
-
|
|
175
|
+
if (!v2 && l.switchMask)
|
|
176
|
+
throw new PickhooksError("Switch rows are a sticky 2 feature.", { code: "input" });
|
|
177
|
+
// sticky 2 takes a switcher and its switch rows too: none from a plain launch
|
|
178
|
+
const full2 = v2 ? { ...args, switcher: l.switchMask ? (l.switcher ?? a.ruleAuction) : ZERO_ADDRESS, switchMask: l.switchMask ?? 0 } : args;
|
|
179
|
+
if (!v3 && l.mode)
|
|
180
|
+
throw new PickhooksError("Modes are a sticky 3 feature.", { code: "input" });
|
|
181
|
+
const modeAddr = l.mode ? a.modes[l.mode] : ZERO_ADDRESS;
|
|
182
|
+
if (l.mode && modeAddr.toLowerCase() === ZERO_ADDRESS)
|
|
183
|
+
throw new PickhooksError(`The mode ${l.mode} is not deployed yet.`, { code: "input" });
|
|
184
|
+
const full = v3 ? { ...full2, mode: modeAddr, modeData: l.mode ? encodeModeData(l.mode, l.modeSettings) : "0x" } : full2;
|
|
185
|
+
return { to: F, value, data: encodeFunctionData({ abi: FA, functionName: "launch", args: [full] }), label: "The launch" };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* sticky 3: asks the mode for its share of buys with these settings (it refuses settings out of bounds) and the validator
|
|
189
|
+
* whether that share fits next to the rows' burns and pots (10 % of a buy at most). null when the launch can go.
|
|
190
|
+
*/
|
|
191
|
+
async function validateMode(mode, settings, rows, validator, creator) {
|
|
192
|
+
if (!v3)
|
|
193
|
+
return "Modes are a sticky 3 feature.";
|
|
194
|
+
const m = a.modes[mode];
|
|
195
|
+
if (m.toLowerCase() === ZERO_ADDRESS)
|
|
196
|
+
return `The mode ${mode} is not deployed yet.`;
|
|
197
|
+
try {
|
|
198
|
+
const listed = await call(F, FA, "modes", [m]);
|
|
199
|
+
if (!listed)
|
|
200
|
+
return `The mode ${mode} is not open for launches yet.`;
|
|
201
|
+
const from = creator ?? (await senderAddress(ctx)) ?? ZERO_ADDRESS;
|
|
202
|
+
const bps = await call(m, sticky3ModeQuoteAbi, "quote", [from, encodeModeData(mode, settings)]);
|
|
203
|
+
await call(validator ?? (await terms()).validator, sticky3ValidatorAbi, "game", [rows, bps]);
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
return explain(e, "The mode refused these settings.").text;
|
|
208
|
+
}
|
|
74
209
|
}
|
|
75
210
|
/** Builds the table from templates and hook script rows (or takes rows), checks it locally and on chain, launches, returns the coin. */
|
|
76
211
|
async function launch(l, s = {}) {
|
|
77
|
-
let rows = l.rows ?? [];
|
|
78
|
-
let release = l.release ?? 0;
|
|
79
212
|
const t = await terms();
|
|
80
213
|
if (l.script?.length && !t.custom)
|
|
81
214
|
throw new PickhooksError("The hook script is off on the factory: templates only for now.", { code: "input" });
|
|
82
|
-
|
|
83
|
-
// the site's order: the templates' rows, then the own rows; one release time per coin, the longer one wins
|
|
84
|
-
const own = (l.script ?? []).map(editToRow);
|
|
85
|
-
const c = compile(l.picks ?? [], own);
|
|
86
|
-
const problems = [...c.problems, ...own.flatMap(rowProblems)];
|
|
87
|
-
const hours = l.scriptHours ?? 0;
|
|
88
|
-
const locks = own.some((r) => r.act === ACT.LOCK);
|
|
89
|
-
if (locks && !(hours * 3600 >= 600 && hours <= 720))
|
|
90
|
-
problems.push("Locked shares: unlock over 10 minutes to 30 days.");
|
|
91
|
-
if (problems.length)
|
|
92
|
-
throw new PickhooksError(problems.join(" "), { code: "input" });
|
|
93
|
-
rows = c.rows;
|
|
94
|
-
release = locks ? Math.max(c.release, Math.round(hours * 3600)) : c.release;
|
|
95
|
-
}
|
|
215
|
+
const { rows, release } = stickyTable(l);
|
|
96
216
|
const refused = await validate(rows, release, t.custom);
|
|
97
217
|
if (refused)
|
|
98
218
|
throw new PickhooksError(refused, { code: "Bad" });
|
|
99
|
-
const
|
|
219
|
+
const switchMask = maskOf(l.switchRows);
|
|
220
|
+
if (switchMask) {
|
|
221
|
+
const no = await validateSwitch(rows, switchMask, t.validator);
|
|
222
|
+
if (no)
|
|
223
|
+
throw new PickhooksError(no, { code: "Bad" });
|
|
224
|
+
}
|
|
225
|
+
if (l.mode) {
|
|
226
|
+
const no = await validateMode(l.mode, l.modeSettings, rows, t.validator);
|
|
227
|
+
if (no)
|
|
228
|
+
throw new PickhooksError(no, { code: "Bad" });
|
|
229
|
+
}
|
|
230
|
+
const tx = buildLaunch({ name: l.name, symbol: l.symbol, metadata: l.metadata, rows, release, salt: l.salt, keepBps: l.keepBps, minCreatorBps: t.creatorBps, switcher: l.switcher, switchMask, mode: l.mode, modeSettings: l.modeSettings }, t.seed + (l.devBuy ?? 0n));
|
|
100
231
|
const sent = await sendTx(ctx, tx, s);
|
|
101
|
-
|
|
232
|
+
// both factories emit the same Launched event
|
|
233
|
+
const ev = parseEventLogs({ abi: stickyFactoryAbi, logs: sent.receipt.logs.filter((x) => x.address.toLowerCase() === F.toLowerCase()), eventName: "Launched" })[0];
|
|
102
234
|
if (!ev)
|
|
103
235
|
throw new PickhooksError("Launched, but the coin was not found in the receipt.", { code: "receipt", hash: sent.hash });
|
|
104
236
|
return { ...sent, token: ev.args.token, pool: ev.args.pool, pair: ev.args.pair, rows, release };
|
|
@@ -114,6 +246,16 @@ export function stickyModule(ctx) {
|
|
|
114
246
|
contracts: pools.map((p) => ({ address: (p ?? a.wbnb), abi: v3PoolAbi, functionName: "slot0" })),
|
|
115
247
|
allowFailure: true,
|
|
116
248
|
});
|
|
249
|
+
const modeRes = v3
|
|
250
|
+
? await pub.multicall({
|
|
251
|
+
contracts: tokens.flatMap((t) => [
|
|
252
|
+
{ address: t, abi: TA, functionName: "mode" },
|
|
253
|
+
{ address: t, abi: TA, functionName: "gameBps" },
|
|
254
|
+
{ address: t, abi: TA, functionName: "gamePot" },
|
|
255
|
+
]),
|
|
256
|
+
allowFailure: true,
|
|
257
|
+
})
|
|
258
|
+
: [];
|
|
117
259
|
const out = [];
|
|
118
260
|
tokens.forEach((token, i) => {
|
|
119
261
|
const g = (k) => res[i * per + k];
|
|
@@ -145,12 +287,22 @@ export function stickyModule(ctx) {
|
|
|
145
287
|
totalSupply: supply,
|
|
146
288
|
priceBnb,
|
|
147
289
|
mcapBnb: priceBnb * (Number(supply) / 1e18),
|
|
290
|
+
...(v3 && modeRes[i * 3]?.status === "success"
|
|
291
|
+
? {
|
|
292
|
+
mode: {
|
|
293
|
+
name: modeName(modeRes[i * 3].result),
|
|
294
|
+
address: modeRes[i * 3].result,
|
|
295
|
+
gameBps: Number(modeRes[i * 3 + 1].result ?? 0),
|
|
296
|
+
gamePot: modeRes[i * 3 + 2].result ?? 0n,
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
: {}),
|
|
148
300
|
});
|
|
149
301
|
});
|
|
150
302
|
return out;
|
|
151
303
|
}
|
|
152
304
|
/** How many coins the factory launched. */
|
|
153
|
-
const count = async () => Number(await call(
|
|
305
|
+
const count = async () => Number(await call(F, FA, "coinCount"));
|
|
154
306
|
/** Coins newest first: `limit` of them, skipping the newest `offset`. */
|
|
155
307
|
async function coins(o = {}) {
|
|
156
308
|
const n = await count();
|
|
@@ -160,12 +312,12 @@ export function stickyModule(ctx) {
|
|
|
160
312
|
return [];
|
|
161
313
|
const idx = Array.from({ length: end - from }, (_, i) => BigInt(end - 1 - i));
|
|
162
314
|
const tokens = (await pub.multicall({
|
|
163
|
-
contracts: idx.map((i) => ({ address:
|
|
315
|
+
contracts: idx.map((i) => ({ address: F, abi: FA, functionName: "coins", args: [i] })),
|
|
164
316
|
allowFailure: false,
|
|
165
317
|
}));
|
|
166
318
|
return summaries(tokens);
|
|
167
319
|
}
|
|
168
|
-
const isCoin = async (token) => (await call(
|
|
320
|
+
const isCoin = async (token) => (await call(F, FA, "isCoin", [token]).catch(() => false)) === true;
|
|
169
321
|
/** One sticky coin, or null when the address is not one of the factory's coins. */
|
|
170
322
|
async function coin(token) {
|
|
171
323
|
if (!(await isCoin(token)))
|
|
@@ -174,6 +326,20 @@ export function stickyModule(ctx) {
|
|
|
174
326
|
}
|
|
175
327
|
/** A wallet's lots (amount, locked part, since, marks), its balance and what it can sell now. */
|
|
176
328
|
async function wallet(token, who) {
|
|
329
|
+
if (v3) {
|
|
330
|
+
const [bal, sellable, lots, held] = await Promise.all([
|
|
331
|
+
call(token, TA, "balanceOf", [who]),
|
|
332
|
+
call(a.sticky3Lens, sticky3LensAbi, "sellable", [token, who]),
|
|
333
|
+
call(a.sticky3Lens, sticky3LensAbi, "lotsOf", [token, who]),
|
|
334
|
+
call(token, TA, "heldOf", [who]),
|
|
335
|
+
]);
|
|
336
|
+
return {
|
|
337
|
+
balance: bal,
|
|
338
|
+
sellable,
|
|
339
|
+
lots: lots.amount.map((x, i) => ({ amount: x, locked: lots.locked[i], since: Number(lots.since[i]), flags: Number(lots.flags[i]), open: lots.open[i] })),
|
|
340
|
+
held: held[0],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
177
343
|
const r = await pub.multicall({
|
|
178
344
|
contracts: [
|
|
179
345
|
{ address: token, abi: TA, functionName: "balanceOf", args: [who] },
|
|
@@ -218,11 +384,17 @@ export function stickyModule(ctx) {
|
|
|
218
384
|
* does not pass the reason on: a refused buy is checked again with an estimate of its coins and the refusal names the
|
|
219
385
|
* rule (for `wallet`, by default the quoter the quote ran as).
|
|
220
386
|
*/
|
|
387
|
+
/**
|
|
388
|
+
* The better of the coin's two venues: its V3 pool and its V2 pair (every sticky coin has both at the same start price;
|
|
389
|
+
* after a dark launch the round's BNB sits in the pair, and a sell into the one sided V3 curve would get almost nothing).
|
|
390
|
+
* Terminals pick the pool by themselves; the SDK does the same here. The venue field says which one buy/sell will use.
|
|
391
|
+
*/
|
|
221
392
|
async function quote(q) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
393
|
+
const best = await bestVenue(ctx, { v3: () => v3Quote(ctx, q), v2: () => v2Quote(ctx, q) });
|
|
394
|
+
if (best.v3 !== null || best.v2 !== null)
|
|
395
|
+
return { amountOut: best.amountOut, venue: best.venue, v3: best.v3, v2: best.v2 };
|
|
396
|
+
const e = best.errors.v3 ?? best.errors.v2;
|
|
397
|
+
{
|
|
226
398
|
if (q.side === "buy") {
|
|
227
399
|
const est = await estimateBuy(q.token, q.amount).catch(() => 0n);
|
|
228
400
|
const c = est > 0n ? await check({ token: q.token, side: "buy", wallet: q.wallet ?? a.v3Quoter, amount: est }).catch(() => null) : null;
|
|
@@ -242,34 +414,97 @@ export function stickyModule(ctx) {
|
|
|
242
414
|
if (!c.ok)
|
|
243
415
|
throw new PickhooksError(c.text, { code: "Blocked", args: [c.row === "locked" ? LOCKED : c.row] });
|
|
244
416
|
};
|
|
417
|
+
/**
|
|
418
|
+
* The share of a buy the coin keeps, basis points: its burn and pot rows on buys that are not lifted, and a sticky 3
|
|
419
|
+
* coin's game share (at most 10 %). The V2 router measures what arrives in the wallet, after the coin took its share,
|
|
420
|
+
* so a V2 buy's minimum leaves it out; the V3 router measures what the pool sends, before it.
|
|
421
|
+
*/
|
|
422
|
+
async function buyTakeBps(token) {
|
|
423
|
+
const [raw, , off] = await call(token, TA, "script");
|
|
424
|
+
let take = 0;
|
|
425
|
+
raw.forEach((x, i) => {
|
|
426
|
+
if ((Number(off) >> i) & 1)
|
|
427
|
+
return;
|
|
428
|
+
const r = decodeRow(x);
|
|
429
|
+
if (r.on & ON.BUY && (r.act === ACT.BURN || r.act === ACT.POT))
|
|
430
|
+
take += r.arg;
|
|
431
|
+
});
|
|
432
|
+
if (v3)
|
|
433
|
+
take += Number(await call(token, TA, "gameBps").catch(() => 0));
|
|
434
|
+
return Math.min(take, 1000);
|
|
435
|
+
}
|
|
245
436
|
/** Quotes, checks the coin's rules for this wallet, and buys. A refusing rule stops it before anything is signed. */
|
|
246
437
|
async function buy(b, s = {}) {
|
|
247
438
|
const { address } = await signer(ctx, s.wallet);
|
|
248
439
|
const q = await quote({ token: b.token, side: "buy", amount: b.bnbIn, wallet: address });
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
440
|
+
const venue = b.venue ?? q.venue;
|
|
441
|
+
const quoted = (venue === "v2" ? q.v2 : q.v3) ?? q.amountOut;
|
|
442
|
+
refuse(await check({ token: b.token, side: "buy", wallet: address, amount: quoted }));
|
|
443
|
+
const arrives = venue === "v2" ? minusBps(quoted, await buyTakeBps(b.token)) : quoted;
|
|
444
|
+
const minOut = b.minOut ?? minusBps(arrives, b.slippageBps ?? 100);
|
|
445
|
+
const deadline = b.deadline ?? (await chainDeadline(ctx));
|
|
446
|
+
const tx = venue === "v2" ? buildV2Buy(a, { token: b.token, bnbIn: b.bnbIn, minOut, recipient: address, deadline }) : buildBuy({ token: b.token, bnbIn: b.bnbIn, minOut, recipient: address, deadline });
|
|
447
|
+
const sent = await sendTx(ctx, tx, s);
|
|
448
|
+
return { ...sent, quoted, minOut, amountOut: receivedOf(sent.receipt, b.token, address), venue };
|
|
253
449
|
}
|
|
254
450
|
/** Checks the rules, approves the router if needed, quotes and sells for BNB. */
|
|
255
451
|
async function sell(x, s = {}) {
|
|
256
452
|
const { address } = await signer(ctx, s.wallet);
|
|
257
453
|
refuse(await check({ token: x.token, side: "sell", wallet: address, amount: x.amount }));
|
|
258
|
-
const
|
|
454
|
+
const q = await quote({ token: x.token, side: "sell", amount: x.amount });
|
|
455
|
+
const venue = x.venue ?? q.venue;
|
|
456
|
+
const quoted = (venue === "v2" ? q.v2 : q.v3) ?? q.amountOut;
|
|
457
|
+
const ap = await approvalFor(ctx, address, x.token, x.amount, venue === "v2" ? a.v2Router : a.v3Router, "Allowing PancakeSwap");
|
|
259
458
|
if (ap)
|
|
260
459
|
await sendTx(ctx, ap, s);
|
|
261
|
-
const
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
|
|
460
|
+
const minOut = x.minOut ?? minusBps(quoted, x.slippageBps ?? 100);
|
|
461
|
+
const deadline = x.deadline ?? (await chainDeadline(ctx));
|
|
462
|
+
const tx = venue === "v2" ? buildV2Sell(a, { token: x.token, amount: x.amount, minOut, recipient: address, deadline }) : buildSell({ token: x.token, amount: x.amount, minOut, recipient: address, deadline });
|
|
463
|
+
const sent = await sendTx(ctx, tx, s);
|
|
464
|
+
return { ...sent, quoted, minOut, venue };
|
|
265
465
|
}
|
|
266
466
|
/** The creator lifts a row for good (a drop row cannot be lifted). */
|
|
267
467
|
const buildLift = (token, row) => ({ to: token, value: 0n, data: encodeFunctionData({ abi: TA, functionName: "lift", args: [row] }), label: `Lifting rule ${row + 1}` });
|
|
468
|
+
const lockerOf = (token) => call(token, TA, "locker");
|
|
469
|
+
/** Anyone: collects the coin's trading fees from its locked position; the locker pays the creator's side to the creator. */
|
|
470
|
+
const buildCollect = async (token) => ({
|
|
471
|
+
to: await lockerOf(token),
|
|
472
|
+
value: 0n,
|
|
473
|
+
data: encodeFunctionData({ abi: v2LockerAbi, functionName: "collect", args: [token] }),
|
|
474
|
+
label: "The collect",
|
|
475
|
+
});
|
|
476
|
+
/**
|
|
477
|
+
* What collect() would book now (simulated): the BNB fees of the coin's position and the coins it burns, what of it goes
|
|
478
|
+
* to the creator (creatorBps of the fees is the creator side, keepBps of that the creator keeps, the rest buys back and
|
|
479
|
+
* burns $hooks), and BNB the locker holds for the creator because a payout bounced (withdrawPending() takes it).
|
|
480
|
+
*/
|
|
481
|
+
async function fees(token) {
|
|
482
|
+
const locker = await lockerOf(token);
|
|
483
|
+
const pos = await call(locker, v2LockerAbi, "positions", [token]);
|
|
484
|
+
const [sim, pending] = await Promise.all([
|
|
485
|
+
pub.simulateContract({ address: locker, abi: v2LockerAbi, functionName: "collect", args: [token] }).then((r) => r.result),
|
|
486
|
+
call(locker, v2LockerAbi, "pending", [pos[2]]),
|
|
487
|
+
]);
|
|
488
|
+
// the locker's own split
|
|
489
|
+
const side = (sim[0] * BigInt(pos[3])) / 10000n;
|
|
490
|
+
const toCreator = ((side - (side * BigInt(pos[4])) / 10000n) * BigInt(pos[5])) / 10000n;
|
|
491
|
+
return { bnb: sim[0], burned: sim[1], toCreator, creator: pos[2], creatorBps: Number(pos[3]), keepBps: Number(pos[5]), pending };
|
|
492
|
+
}
|
|
493
|
+
/** Collects the coin's fees; returns what the locker booked (its Collected event). */
|
|
494
|
+
async function collect(token, s = {}) {
|
|
495
|
+
const sent = await sendTx(ctx, await buildCollect(token), s);
|
|
496
|
+
const ev = parseEventLogs({ abi: v2LockerAbi, logs: sent.receipt.logs, eventName: "Collected" })[0];
|
|
497
|
+
return { ...sent, bnb: ev?.args.bnb ?? 0n, toCreator: ev?.args.toCreator ?? 0n, burned: ev?.args.burned ?? 0n };
|
|
498
|
+
}
|
|
268
499
|
return {
|
|
269
|
-
factory:
|
|
500
|
+
factory: F,
|
|
270
501
|
terms,
|
|
271
502
|
validate,
|
|
503
|
+
validateSwitch,
|
|
504
|
+
validateMode,
|
|
272
505
|
compile,
|
|
506
|
+
table: stickyTable,
|
|
507
|
+
testRun: stickyTestRun,
|
|
273
508
|
describeRows,
|
|
274
509
|
prepareMetadata,
|
|
275
510
|
count,
|
|
@@ -280,12 +515,15 @@ export function stickyModule(ctx) {
|
|
|
280
515
|
check,
|
|
281
516
|
quote,
|
|
282
517
|
estimateBuy,
|
|
518
|
+
buyTakeBps,
|
|
283
519
|
sellApproval,
|
|
284
|
-
build: { launch: buildLaunch, buy: buildBuy, sell: buildSell, lift: buildLift },
|
|
520
|
+
build: { launch: buildLaunch, buy: buildBuy, sell: buildSell, lift: buildLift, collect: buildCollect },
|
|
285
521
|
launch,
|
|
286
522
|
buy,
|
|
287
523
|
sell,
|
|
288
524
|
lift: (token, row, s = {}) => sendTx(ctx, buildLift(token, row), s),
|
|
525
|
+
fees,
|
|
526
|
+
collect,
|
|
289
527
|
/** the sender's address, for callers that check before they sign */
|
|
290
528
|
from: (s = {}) => senderAddress(ctx, s.wallet),
|
|
291
529
|
};
|