@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
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
// Copied from web/src/lib/sticky-rules.ts (the site's encoder, templates and checks; same encoding as src/sticky/StickyRules.sol).
|
|
2
|
+
// Keep the two in step: node scripts/check-rules-copy.mjs compares them.
|
|
3
|
+
/**
|
|
4
|
+
* sticky rules: the rule table of a sticky coin, the same encoding as src/sticky/StickyRules.sol.
|
|
5
|
+
* A row is one uint256: on (bits 0..7), act (8..15), arg (16..31), then four conditions of 56 bits (var, op, value).
|
|
6
|
+
* The launcher offers templates; each template is one or two rows. The chain is the truth: the validator checks every
|
|
7
|
+
* table at launch and the coin checks every transfer. POT rows fill the coin's pot from buys, DROP rows pay it out to
|
|
8
|
+
* buyers (at most the coins of the buy, never to the creator).
|
|
9
|
+
*/
|
|
10
|
+
export const ON = { BUY: 1, SELL: 2, SEND: 4 };
|
|
11
|
+
export const ACT = { DENY: 1, FLAG: 2, LOCK: 3, BURN: 4, POT: 5, DROP: 6 };
|
|
12
|
+
export const VAR = { AGE: 1, LAUNCH: 2, BLOCKS: 3, AMOUNT: 4, BALANCE: 5, PRICE: 6, DIP: 7, SOLD: 8, HOUR: 9, WEEKDAY: 10, FLAGS: 11, BUYS: 12, HOLDERS: 13, IDLE: 14, POTSIZE: 15 };
|
|
13
|
+
export const OP = { LT: 1, GE: 2, HAS: 3, NOT: 4, MODLT: 5, MODGE: 6 };
|
|
14
|
+
export const FLAG = { SNIPER: 1, MARK: 2, DEV: 4 };
|
|
15
|
+
/** answers of token.check(): allowed, and "no row stops it but the coins are still locked" */
|
|
16
|
+
export const NONE = 255;
|
|
17
|
+
export const LOCKED = 254;
|
|
18
|
+
export const STICKY_LIMITS = {
|
|
19
|
+
maxRows: 8,
|
|
20
|
+
maxLots: 8,
|
|
21
|
+
exitDays: 30,
|
|
22
|
+
maxBurnPct: 10,
|
|
23
|
+
/** burn and pot together, per buy */
|
|
24
|
+
maxTakePct: 10,
|
|
25
|
+
maxHoldDays: 7,
|
|
26
|
+
minReleaseMinutes: 10,
|
|
27
|
+
maxReleaseDays: 30,
|
|
28
|
+
};
|
|
29
|
+
export function encodeRow(r) {
|
|
30
|
+
let x = BigInt(r.on) | (BigInt(r.act) << BigInt(8)) | (BigInt(r.arg) << BigInt(16));
|
|
31
|
+
r.conds.slice(0, 4).forEach((c, i) => {
|
|
32
|
+
const packed = BigInt(c.v) | (BigInt(c.op) << BigInt(8)) | (c.val << BigInt(16));
|
|
33
|
+
x |= packed << BigInt(32 + 56 * i);
|
|
34
|
+
});
|
|
35
|
+
return x;
|
|
36
|
+
}
|
|
37
|
+
export function decodeRow(x) {
|
|
38
|
+
const b = (v, n) => Number(v & ((BigInt(1) << BigInt(n)) - BigInt(1)));
|
|
39
|
+
const conds = [];
|
|
40
|
+
for (let i = 0; i < 4; i++) {
|
|
41
|
+
const c = x >> BigInt(32 + 56 * i);
|
|
42
|
+
const v = b(c, 8);
|
|
43
|
+
if (v === 0)
|
|
44
|
+
break;
|
|
45
|
+
conds.push({ v, op: b(c >> BigInt(8), 8), val: (c >> BigInt(16)) & ((BigInt(1) << BigInt(40)) - BigInt(1)) });
|
|
46
|
+
}
|
|
47
|
+
return { on: b(x, 8), act: b(x >> BigInt(8), 8), arg: b(x >> BigInt(16), 16), conds };
|
|
48
|
+
}
|
|
49
|
+
const c = (v, op, val) => ({ v, op, val: BigInt(val) });
|
|
50
|
+
const MIN = 60;
|
|
51
|
+
const HOUR = 3600;
|
|
52
|
+
const DAY = 86_400;
|
|
53
|
+
const ppm = (pct) => Math.round(pct * 10_000);
|
|
54
|
+
const potRow = (pct) => ({ on: ON.BUY, act: ACT.POT, arg: Math.round(pct * 100), conds: [] });
|
|
55
|
+
/** every n-th buy: the buys before it are n-1, 2n-1, ... (buys % n >= n - 1) */
|
|
56
|
+
export const every = (n) => (BigInt(Math.round(n)) << BigInt(20)) | BigInt(Math.max(0, Math.round(n) - 1));
|
|
57
|
+
export const TEMPLATES = [
|
|
58
|
+
{
|
|
59
|
+
id: "hold",
|
|
60
|
+
name: "Hold time",
|
|
61
|
+
line: "Coins must be {m} minutes old before they can be sold, in any wallet they move to.",
|
|
62
|
+
sticky: true,
|
|
63
|
+
scanner: "red",
|
|
64
|
+
scannerNote: "A scanner's test sell right after its test buy fails and can show a honeypot.",
|
|
65
|
+
params: [{ key: "m", label: "Hold time", unit: "minutes", min: 1, max: 7 * 24 * 60, step: 1, def: 30, hint: "1 minute to 7 days" }],
|
|
66
|
+
rows: (p) => [{ on: ON.SELL, act: ACT.DENY, arg: 0, conds: [c(VAR.AGE, OP.LT, Math.round(p.m * MIN))] }],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "sniper",
|
|
70
|
+
name: "Sniper jail",
|
|
71
|
+
line: "Buys in the first {b} blocks are marked. Marked coins cannot be sold for {h} hours, in any wallet.",
|
|
72
|
+
sticky: true,
|
|
73
|
+
scanner: "green",
|
|
74
|
+
scannerNote: "Only coins bought in the first blocks. A scanner that tests later sees nothing.",
|
|
75
|
+
params: [
|
|
76
|
+
{ key: "b", label: "First blocks", unit: "blocks", min: 1, max: 400, step: 1, def: 20, hint: "about 0.45 seconds each" },
|
|
77
|
+
{ key: "h", label: "Jail time", unit: "hours", min: 1, max: 168, step: 1, def: 24, hint: "1 hour to 7 days" },
|
|
78
|
+
],
|
|
79
|
+
rows: (p) => [
|
|
80
|
+
{ on: ON.BUY, act: ACT.FLAG, arg: FLAG.SNIPER, conds: [c(VAR.BLOCKS, OP.LT, p.b), c(VAR.FLAGS, OP.NOT, FLAG.DEV)] },
|
|
81
|
+
{ on: ON.SELL, act: ACT.DENY, arg: 0, conds: [c(VAR.FLAGS, OP.HAS, FLAG.SNIPER), c(VAR.AGE, OP.LT, Math.round(p.h * HOUR))] },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "devlock",
|
|
86
|
+
name: "Dev lock",
|
|
87
|
+
line: "Your own first buy cannot be sold for {d} days after launch, wherever you send it.",
|
|
88
|
+
sticky: true,
|
|
89
|
+
scanner: "green",
|
|
90
|
+
scannerNote: "Only the creator's coins. Scanners see nothing.",
|
|
91
|
+
params: [{ key: "d", label: "Locked for", unit: "days", min: 1, max: 30, step: 1, def: 7, hint: "1 to 30 days, needs a first buy" }],
|
|
92
|
+
rows: (p) => [{ on: ON.SELL, act: ACT.DENY, arg: 0, conds: [c(VAR.FLAGS, OP.HAS, FLAG.DEV), c(VAR.LAUNCH, OP.LT, Math.round(p.d * DAY))] }],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "drip",
|
|
96
|
+
name: "Drip",
|
|
97
|
+
line: "{pct} % of every buy{big} unlocks over {h} hours. Ten wallets sell no faster than one.",
|
|
98
|
+
sticky: true,
|
|
99
|
+
scanner: "yellow",
|
|
100
|
+
scannerNote: "Only buys from the set size lock, so a scanner's small test buy and sell pass. With 0 every buy locks and a scanner's test sell fails.",
|
|
101
|
+
params: [
|
|
102
|
+
{ key: "pct", label: "Share locked", unit: "%", min: 1, max: 100, step: 1, def: 100, hint: "of each buy" },
|
|
103
|
+
{ key: "h", label: "Unlocks over", unit: "hours", min: 1, max: 720, step: 1, def: 6, hint: "1 hour to 30 days, on a straight line" },
|
|
104
|
+
{ key: "big", label: "Only buys from", unit: "%", min: 0, max: 5, step: 0.01, def: 0.5, hint: "of the supply, 0 for every buy" },
|
|
105
|
+
],
|
|
106
|
+
rows: (p) => [
|
|
107
|
+
p.big > 0
|
|
108
|
+
? { on: ON.BUY, act: ACT.LOCK, arg: Math.round(p.pct * 100), conds: [c(VAR.AMOUNT, OP.GE, ppm(p.big))] }
|
|
109
|
+
: { on: ON.BUY, act: ACT.LOCK, arg: Math.round(p.pct * 100), conds: [] },
|
|
110
|
+
],
|
|
111
|
+
release: (p) => Math.round(p.h * HOUR),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "maxbuy",
|
|
115
|
+
name: "Max per buy",
|
|
116
|
+
line: "No buy above {pct} % of the supply. Your own first buy is free of it.",
|
|
117
|
+
sticky: false,
|
|
118
|
+
scanner: "yellow",
|
|
119
|
+
scannerNote: 'Scanners show "anti whale".',
|
|
120
|
+
params: [{ key: "pct", label: "Biggest buy", unit: "%", min: 0.05, max: 10, step: 0.05, def: 1, hint: "of the supply" }],
|
|
121
|
+
rows: (p) => [{ on: ON.BUY, act: ACT.DENY, arg: 0, conds: [c(VAR.AMOUNT, OP.GE, ppm(p.pct) + 1), c(VAR.FLAGS, OP.NOT, FLAG.DEV)] }],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "maxwallet",
|
|
125
|
+
name: "Max wallet",
|
|
126
|
+
line: "No buy that takes a wallet above {pct} % of the supply. Your own first buy is free of it.",
|
|
127
|
+
sticky: false,
|
|
128
|
+
scanner: "yellow",
|
|
129
|
+
scannerNote: 'Scanners show "anti whale". Sends to other wallets are not capped.',
|
|
130
|
+
params: [{ key: "pct", label: "Biggest wallet", unit: "%", min: 0.1, max: 20, step: 0.1, def: 2, hint: "of the supply" }],
|
|
131
|
+
rows: (p) => [{ on: ON.BUY, act: ACT.DENY, arg: 0, conds: [c(VAR.BALANCE, OP.GE, ppm(p.pct) + 1), c(VAR.FLAGS, OP.NOT, FLAG.DEV)] }],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "window",
|
|
135
|
+
name: "Sell window",
|
|
136
|
+
line: "Sells open for {m} minutes every {h} hours, counted from the launch.",
|
|
137
|
+
sticky: false,
|
|
138
|
+
scanner: "red",
|
|
139
|
+
scannerNote: "Outside the window a scanner's test sell fails and can show a honeypot.",
|
|
140
|
+
params: [
|
|
141
|
+
{ key: "h", label: "Every", unit: "hours", min: 0.5, max: 24, step: 0.5, def: 2, hint: "half an hour to a day" },
|
|
142
|
+
{ key: "m", label: "Open for", unit: "minutes", min: 1, max: 1439, step: 1, def: 15, hint: "shorter than the period" },
|
|
143
|
+
],
|
|
144
|
+
rows: (p) => {
|
|
145
|
+
const period = Math.round(p.h * HOUR);
|
|
146
|
+
const open = Math.round(p.m * MIN);
|
|
147
|
+
return [{ on: ON.SELL, act: ACT.DENY, arg: 0, conds: [c(VAR.LAUNCH, OP.MODGE, (BigInt(period) << BigInt(20)) | BigInt(open))] }];
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "hours",
|
|
152
|
+
name: "Market hours",
|
|
153
|
+
line: "Buys and sells only from {open}:00 to {close}:00 UTC.",
|
|
154
|
+
sticky: false,
|
|
155
|
+
scanner: "red",
|
|
156
|
+
scannerNote: "Outside the hours a scanner's test trade fails.",
|
|
157
|
+
params: [
|
|
158
|
+
{ key: "open", label: "Opens", unit: "UTC", min: 1, max: 22, step: 1, def: 6, hint: "hour of the day" },
|
|
159
|
+
{ key: "close", label: "Closes", unit: "UTC", min: 2, max: 23, step: 1, def: 22, hint: "after the opening hour" },
|
|
160
|
+
],
|
|
161
|
+
rows: (p) => [
|
|
162
|
+
{ on: ON.BUY | ON.SELL, act: ACT.DENY, arg: 0, conds: [c(VAR.HOUR, OP.LT, p.open)] },
|
|
163
|
+
{ on: ON.BUY | ON.SELL, act: ACT.DENY, arg: 0, conds: [c(VAR.HOUR, OP.GE, p.close)] },
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "burn",
|
|
168
|
+
name: "Auto burn",
|
|
169
|
+
line: "{pct} % of every buy is burned. Sells are never taxed.",
|
|
170
|
+
sticky: false,
|
|
171
|
+
scanner: "yellow",
|
|
172
|
+
scannerNote: "Scanners show it as a buy tax.",
|
|
173
|
+
params: [{ key: "pct", label: "Burned per buy", unit: "%", min: 0.1, max: 10, step: 0.1, def: 1, hint: "0.1 to 10 %" }],
|
|
174
|
+
rows: (p) => [{ on: ON.BUY, act: ACT.BURN, arg: Math.round(p.pct * 100), conds: [] }],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: "dipdrop",
|
|
178
|
+
name: "Dip drop",
|
|
179
|
+
line: "{pot} % of every buy fills the pot. A buy while the price is more than {dip} % under the hour's high gets {share} % of the pot, up to as many coins as it buys.",
|
|
180
|
+
sticky: false,
|
|
181
|
+
gives: true,
|
|
182
|
+
scanner: "yellow",
|
|
183
|
+
scannerNote: "Scanners show the pot share as a buy tax. A drop is extra coins for the buyer, never for you.",
|
|
184
|
+
params: [
|
|
185
|
+
{ key: "pot", label: "Into the pot", unit: "%", min: 0.1, max: 10, step: 0.1, def: 2, hint: "of every buy" },
|
|
186
|
+
{ key: "dip", label: "Dip", unit: "%", min: 1, max: 90, step: 1, def: 20, hint: "under the high of the last hour" },
|
|
187
|
+
{ key: "share", label: "Drop", unit: "%", min: 1, max: 100, step: 1, def: 50, hint: "of the pot, up to the buy" },
|
|
188
|
+
],
|
|
189
|
+
rows: (p) => [potRow(p.pot), { on: ON.BUY, act: ACT.DROP, arg: Math.round(p.share * 100), conds: [c(VAR.DIP, OP.LT, Math.round((100 - p.dip) * 100))] }],
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: "lucky",
|
|
193
|
+
name: "Lucky buy",
|
|
194
|
+
line: "{pot} % of every buy fills the pot. Every {n}th buy gets {share} % of the pot, up to as many coins as it buys.",
|
|
195
|
+
sticky: false,
|
|
196
|
+
gives: true,
|
|
197
|
+
scanner: "yellow",
|
|
198
|
+
scannerNote: "Scanners show the pot share as a buy tax. A drop is extra coins for the buyer, never for you.",
|
|
199
|
+
params: [
|
|
200
|
+
{ key: "pot", label: "Into the pot", unit: "%", min: 0.1, max: 10, step: 0.1, def: 2, hint: "of every buy" },
|
|
201
|
+
{ key: "n", label: "Every", unit: "th buy", min: 2, max: 10_000, step: 1, def: 25, hint: "2 to 10,000" },
|
|
202
|
+
{ key: "share", label: "Drop", unit: "%", min: 1, max: 100, step: 1, def: 100, hint: "of the pot, up to the buy" },
|
|
203
|
+
],
|
|
204
|
+
rows: (p) => [potRow(p.pot), { on: ON.BUY, act: ACT.DROP, arg: Math.round(p.share * 100), conds: [c(VAR.BUYS, OP.MODGE, every(p.n))] }],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: "wakeup",
|
|
208
|
+
name: "Wake up",
|
|
209
|
+
line: "{pot} % of every buy fills the pot. The first buy after {m} minutes without a buy gets {share} % of the pot, up to as many coins as it buys.",
|
|
210
|
+
sticky: false,
|
|
211
|
+
gives: true,
|
|
212
|
+
scanner: "yellow",
|
|
213
|
+
scannerNote: "Scanners show the pot share as a buy tax. A drop is extra coins for the buyer, never for you.",
|
|
214
|
+
params: [
|
|
215
|
+
{ key: "pot", label: "Into the pot", unit: "%", min: 0.1, max: 10, step: 0.1, def: 2, hint: "of every buy" },
|
|
216
|
+
{ key: "m", label: "Quiet for", unit: "minutes", min: 1, max: 1440, step: 1, def: 30, hint: "1 minute to a day" },
|
|
217
|
+
{ key: "share", label: "Drop", unit: "%", min: 1, max: 100, step: 1, def: 100, hint: "of the pot, up to the buy" },
|
|
218
|
+
],
|
|
219
|
+
rows: (p) => [potRow(p.pot), { on: ON.BUY, act: ACT.DROP, arg: Math.round(p.share * 100), conds: [c(VAR.IDLE, OP.GE, Math.round(p.m * MIN))] }],
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
export const TEMPLATE_BY_ID = new Map(TEMPLATES.map((t) => [t.id, t]));
|
|
223
|
+
export function defaults(t) {
|
|
224
|
+
return Object.fromEntries(t.params.map((p) => [p.key, p.def]));
|
|
225
|
+
}
|
|
226
|
+
/** Problems of a template's numbers, as sentences (the validator has the same bounds). */
|
|
227
|
+
export function paramProblems(t, p) {
|
|
228
|
+
const out = [];
|
|
229
|
+
for (const q of t.params) {
|
|
230
|
+
const v = p[q.key];
|
|
231
|
+
if (!(Number.isFinite(v) && v >= q.min && v <= q.max))
|
|
232
|
+
out.push(`${q.label}: ${q.min} to ${q.max} ${q.unit}.`);
|
|
233
|
+
}
|
|
234
|
+
if (t.id === "hours" && !(p.open < p.close))
|
|
235
|
+
out.push("Market hours: close after the opening hour.");
|
|
236
|
+
if (t.id === "window" && !(p.m * MIN < p.h * HOUR - 59))
|
|
237
|
+
out.push("Sell window: open for less than the period.");
|
|
238
|
+
if (t.id === "lucky" && !Number.isInteger(p.n))
|
|
239
|
+
out.push("Every: a whole number of buys.");
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
/** The rows and release time of the picked templates (and the creator's own rows after them), and the problems that
|
|
243
|
+
* keep the table from launching. The own rows' single checks are rowProblems; the table checks run over everything. */
|
|
244
|
+
export function compile(picks, own = []) {
|
|
245
|
+
const rows = [];
|
|
246
|
+
const problems = [];
|
|
247
|
+
let release = 0;
|
|
248
|
+
for (const pk of picks) {
|
|
249
|
+
const t = TEMPLATE_BY_ID.get(pk.id);
|
|
250
|
+
if (!t)
|
|
251
|
+
continue;
|
|
252
|
+
problems.push(...paramProblems(t, pk.params));
|
|
253
|
+
rows.push(...t.rows(pk.params));
|
|
254
|
+
if (t.release)
|
|
255
|
+
release = Math.max(release, t.release(pk.params));
|
|
256
|
+
}
|
|
257
|
+
rows.push(...own);
|
|
258
|
+
if (rows.length > STICKY_LIMITS.maxRows)
|
|
259
|
+
problems.push(`At most ${STICKY_LIMITS.maxRows} rows: these rules make ${rows.length}.`);
|
|
260
|
+
problems.push(...tableProblems(rows));
|
|
261
|
+
return { rows: rows.map(encodeRow), release, problems };
|
|
262
|
+
}
|
|
263
|
+
export const VARS = [
|
|
264
|
+
{ v: VAR.AGE, label: "age of the coins", unit: "minutes", toChain: (x) => Math.round(x * MIN), fromChain: (x) => x / MIN, lotOnly: true },
|
|
265
|
+
{ v: VAR.LAUNCH, label: "time since launch", unit: "minutes", toChain: (x) => Math.round(x * MIN), fromChain: (x) => x / MIN },
|
|
266
|
+
{ v: VAR.BLOCKS, label: "blocks since launch", unit: "blocks", toChain: (x) => Math.round(x), fromChain: (x) => x },
|
|
267
|
+
{ v: VAR.AMOUNT, label: "size of the trade", unit: "% of supply", toChain: (x) => ppm(x), fromChain: (x) => x / 10_000 },
|
|
268
|
+
{ v: VAR.BALANCE, label: "wallet balance", unit: "% of supply", toChain: (x) => ppm(x), fromChain: (x) => x / 10_000 },
|
|
269
|
+
{ v: VAR.PRICE, label: "price against the start", unit: "x", toChain: (x) => Math.round(x * 10_000), fromChain: (x) => x / 10_000 },
|
|
270
|
+
{ v: VAR.DIP, label: "price against the hour's high", unit: "%", toChain: (x) => Math.round(x * 100), fromChain: (x) => x / 100 },
|
|
271
|
+
{ v: VAR.SOLD, label: "curve sold", unit: "%", toChain: (x) => Math.round(x * 100), fromChain: (x) => x / 100 },
|
|
272
|
+
{ v: VAR.HOUR, label: "hour of the day", unit: "UTC", toChain: (x) => Math.round(x), fromChain: (x) => x },
|
|
273
|
+
{ v: VAR.WEEKDAY, label: "weekday", unit: "0 = Sunday", toChain: (x) => Math.round(x), fromChain: (x) => x },
|
|
274
|
+
{ v: VAR.BUYS, label: "buys since launch", unit: "buys", toChain: (x) => Math.round(x), fromChain: (x) => x },
|
|
275
|
+
{ v: VAR.HOLDERS, label: "holders", unit: "wallets", toChain: (x) => Math.round(x), fromChain: (x) => x },
|
|
276
|
+
{ v: VAR.IDLE, label: "time since the last buy", unit: "minutes", toChain: (x) => Math.round(x * MIN), fromChain: (x) => x / MIN },
|
|
277
|
+
{ v: VAR.POTSIZE, label: "the pot", unit: "% of supply", toChain: (x) => ppm(x), fromChain: (x) => x / 10_000 },
|
|
278
|
+
{ v: VAR.FLAGS, label: "marks of the coins", unit: "", toChain: (x) => x, fromChain: (x) => x, lotOnly: true },
|
|
279
|
+
];
|
|
280
|
+
export const VAR_BY_ID = new Map(VARS.map((d) => [d.v, d]));
|
|
281
|
+
export function emptyEditRow() {
|
|
282
|
+
return { on: ON.SELL, act: ACT.DENY, arg: 0, conds: [{ v: VAR.AGE, op: OP.LT, x: 30 }] };
|
|
283
|
+
}
|
|
284
|
+
/** The editor's row as an on chain row. A window (MODLT / MODGE on the launch time) packs period << 20 | minutes. */
|
|
285
|
+
export function editToRow(e) {
|
|
286
|
+
return {
|
|
287
|
+
on: e.on,
|
|
288
|
+
act: e.act,
|
|
289
|
+
arg: e.act === ACT.LOCK || e.act === ACT.BURN || e.act === ACT.POT || e.act === ACT.DROP ? Math.round(e.arg * 100) : e.act === ACT.FLAG ? e.arg : 0,
|
|
290
|
+
conds: e.conds.slice(0, 4).map((k) => {
|
|
291
|
+
// every n-th buy
|
|
292
|
+
if (k.v === VAR.BUYS && k.op === OP.MODGE)
|
|
293
|
+
return { v: VAR.BUYS, op: OP.MODGE, val: every(k.x) };
|
|
294
|
+
if (k.op === OP.MODLT || k.op === OP.MODGE) {
|
|
295
|
+
const period = Math.round((k.period ?? 60) * MIN);
|
|
296
|
+
return { v: VAR.LAUNCH, op: k.op, val: (BigInt(period) << BigInt(20)) | BigInt(Math.round(k.x * MIN)) };
|
|
297
|
+
}
|
|
298
|
+
const d = VAR_BY_ID.get(k.v);
|
|
299
|
+
return { v: k.v, op: k.op, val: BigInt(Math.max(0, d ? d.toChain(k.x) : Math.round(k.x))) };
|
|
300
|
+
}),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
const ALL_HOURS = (1 << 24) - 1;
|
|
304
|
+
const ALL_DAYS = (1 << 7) - 1;
|
|
305
|
+
export const HOLD_LIMITS = { maxAgeDays: 7, maxHoldBlocks: 1_000_000, minSellCapPpm: 500 };
|
|
306
|
+
/**
|
|
307
|
+
* A drop row that pays on every buy: no condition, or only ones every buy meets (the coins of a buy are 0 seconds old,
|
|
308
|
+
* a stranger's coins are not the creator's). Bots then buy small again and again and empty the pot.
|
|
309
|
+
*/
|
|
310
|
+
export function dropOnEveryBuy(r) {
|
|
311
|
+
return r.act === ACT.DROP && r.conds.every((k) => (k.v === VAR.AGE && k.op === OP.LT) || (k.v === VAR.FLAGS && k.op === OP.NOT));
|
|
312
|
+
}
|
|
313
|
+
export function holdOf(r) {
|
|
314
|
+
const h = { ends: false, window: false, hasHour: false, hours: ALL_HOURS, hasDay: false, days: ALL_DAYS };
|
|
315
|
+
for (const k of r.conds) {
|
|
316
|
+
const n = Number(k.val);
|
|
317
|
+
if (k.v === VAR.FLAGS) {
|
|
318
|
+
if (k.op === OP.HAS && n === FLAG.DEV)
|
|
319
|
+
h.ends = true; // the creator's coins only
|
|
320
|
+
}
|
|
321
|
+
else if (k.op === OP.MODLT || k.op === OP.MODGE) {
|
|
322
|
+
if (k.v === VAR.LAUNCH)
|
|
323
|
+
h.window = true;
|
|
324
|
+
}
|
|
325
|
+
else if (k.v === VAR.AGE) {
|
|
326
|
+
if (k.op === OP.LT)
|
|
327
|
+
h.ends = true;
|
|
328
|
+
}
|
|
329
|
+
else if (k.v === VAR.LAUNCH) {
|
|
330
|
+
if (k.op === OP.LT && n <= HOLD_LIMITS.maxAgeDays * DAY)
|
|
331
|
+
h.ends = true;
|
|
332
|
+
}
|
|
333
|
+
else if (k.v === VAR.BLOCKS) {
|
|
334
|
+
if (k.op === OP.LT && n <= HOLD_LIMITS.maxHoldBlocks)
|
|
335
|
+
h.ends = true;
|
|
336
|
+
}
|
|
337
|
+
else if (k.v === VAR.AMOUNT) {
|
|
338
|
+
if (k.op === OP.GE && n >= HOLD_LIMITS.minSellCapPpm)
|
|
339
|
+
h.ends = true;
|
|
340
|
+
}
|
|
341
|
+
else if (k.v === VAR.HOUR && n >= 1 && n <= 23) {
|
|
342
|
+
const below = (1 << n) - 1;
|
|
343
|
+
h.hours &= k.op === OP.LT ? below : ALL_HOURS & ~below;
|
|
344
|
+
h.hasHour = true;
|
|
345
|
+
}
|
|
346
|
+
else if (k.v === VAR.WEEKDAY && n >= 1 && n <= 6) {
|
|
347
|
+
const below = (1 << n) - 1;
|
|
348
|
+
h.days &= k.op === OP.LT ? below : ALL_DAYS & ~below;
|
|
349
|
+
h.hasDay = true;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return h;
|
|
353
|
+
}
|
|
354
|
+
const stopsSells = (r) => r.act === ACT.DENY && (r.on & (ON.SELL | ON.SEND)) !== 0;
|
|
355
|
+
/** The validator's checks for one row, as sentences (the chain checks again at launch). */
|
|
356
|
+
export function rowProblems(r) {
|
|
357
|
+
const out = [];
|
|
358
|
+
if (r.on === 0 || r.on > 7)
|
|
359
|
+
out.push("Pick buy, sell or send.");
|
|
360
|
+
if (r.act !== ACT.DENY && r.on !== ON.BUY)
|
|
361
|
+
out.push("Mark, lock, burn, pot and drop work on buys only.");
|
|
362
|
+
if (r.act === ACT.FLAG && !(r.arg >= 1 && r.arg <= 3))
|
|
363
|
+
out.push("Pick a mark.");
|
|
364
|
+
if (r.act === ACT.LOCK && !(r.arg >= 1 && r.arg <= 10_000))
|
|
365
|
+
out.push("Lock: 0.01 to 100 % of the buy.");
|
|
366
|
+
if (r.act === ACT.BURN && !(r.arg >= 1 && r.arg <= 1000))
|
|
367
|
+
out.push("Burn: 0.01 to 10 % of the buy.");
|
|
368
|
+
if (r.act === ACT.POT && !(r.arg >= 1 && r.arg <= 1000))
|
|
369
|
+
out.push("Pot: 0.01 to 10 % of the buy.");
|
|
370
|
+
if (r.act === ACT.DROP && !(r.arg >= 1 && r.arg <= 10_000))
|
|
371
|
+
out.push("Drop: 0.01 to 100 % of the pot.");
|
|
372
|
+
for (const k of r.conds) {
|
|
373
|
+
const n = Number(k.val);
|
|
374
|
+
if (k.v === VAR.FLAGS) {
|
|
375
|
+
if (k.op === OP.NOT && n !== FLAG.DEV)
|
|
376
|
+
out.push('"Not marked" only works with the creator\'s coins.');
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (k.v === VAR.BUYS && (k.op === OP.MODLT || k.op === OP.MODGE)) {
|
|
380
|
+
const n = Number(k.val >> BigInt(20));
|
|
381
|
+
const at = Number(k.val & BigInt(0xfffff));
|
|
382
|
+
if (!(n >= 2 && n <= 1_000_000 && at > 0 && at < n))
|
|
383
|
+
out.push("Every n-th buy: n from 2 to 1,000,000.");
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (k.op === OP.MODLT || k.op === OP.MODGE) {
|
|
387
|
+
const period = Number(k.val >> BigInt(20));
|
|
388
|
+
const th = Number(k.val & BigInt(0xfffff));
|
|
389
|
+
const open = k.op === OP.MODLT ? period - th : th;
|
|
390
|
+
if (!(period > 0 && period <= DAY && th > 0 && th < period && open >= 60))
|
|
391
|
+
out.push("Window: a period of at most a day, open at least a minute.");
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (k.v === VAR.AGE) {
|
|
395
|
+
if (n > 30 * DAY)
|
|
396
|
+
out.push("Age: at most 30 days.");
|
|
397
|
+
else if (k.op === OP.LT && n > 7 * DAY)
|
|
398
|
+
out.push("Age: at most 7 days.");
|
|
399
|
+
}
|
|
400
|
+
else if (k.v === VAR.PRICE) {
|
|
401
|
+
if (n > 1e9)
|
|
402
|
+
out.push("Price: at most 100,000x the start.");
|
|
403
|
+
}
|
|
404
|
+
else if (k.v === VAR.BUYS || k.v === VAR.HOLDERS) {
|
|
405
|
+
if (n > 1e9)
|
|
406
|
+
out.push("Count: at most 1,000,000,000.");
|
|
407
|
+
}
|
|
408
|
+
else if (k.v === VAR.LAUNCH) {
|
|
409
|
+
if (n > 30 * DAY)
|
|
410
|
+
out.push("Time since launch: at most 30 days.");
|
|
411
|
+
}
|
|
412
|
+
else if (k.v === VAR.BLOCKS) {
|
|
413
|
+
if (n > 6_000_000)
|
|
414
|
+
out.push("Blocks: at most 6,000,000.");
|
|
415
|
+
}
|
|
416
|
+
else if (k.v === VAR.AMOUNT || k.v === VAR.BALANCE) {
|
|
417
|
+
if (!(n >= 1 && n <= 1e6))
|
|
418
|
+
out.push("Size: more than 0 and at most 100 %.");
|
|
419
|
+
}
|
|
420
|
+
else if (k.v === VAR.HOUR) {
|
|
421
|
+
if (!(n >= 1 && n <= 23))
|
|
422
|
+
out.push("Hour: 1 to 23.");
|
|
423
|
+
}
|
|
424
|
+
else if (k.v === VAR.WEEKDAY) {
|
|
425
|
+
if (!(n >= 1 && n <= 6))
|
|
426
|
+
out.push("Weekday: 1 to 6.");
|
|
427
|
+
}
|
|
428
|
+
else if ((k.v === VAR.DIP || k.v === VAR.SOLD) && n > 10_000) {
|
|
429
|
+
out.push("Share: at most 100 %.");
|
|
430
|
+
}
|
|
431
|
+
else if (k.v === VAR.IDLE && n > 30 * DAY) {
|
|
432
|
+
out.push("Time since the last buy: at most 30 days.");
|
|
433
|
+
}
|
|
434
|
+
else if (k.v === VAR.POTSIZE && n > 1e6) {
|
|
435
|
+
out.push("The pot: at most 100 % of the supply.");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const h = holdOf(r);
|
|
439
|
+
if (stopsSells(r) && !h.ends && !h.window && !h.hasHour && !h.hasDay) {
|
|
440
|
+
out.push("A row that stops sells or sends must let go by itself: the coins' age or the time since launch up to 7 days, blocks since launch up to 1,000,000, a size from 0.05 % of the supply on, the creator's coins only, or a clock (one window, hours, weekdays).");
|
|
441
|
+
}
|
|
442
|
+
return out;
|
|
443
|
+
}
|
|
444
|
+
/** Checks of the whole table that no single row shows: burn and pot together, a pot without a drop. */
|
|
445
|
+
export function tableProblems(rows) {
|
|
446
|
+
const out = [];
|
|
447
|
+
const sum = (a) => rows.filter((r) => r.act === a).reduce((x, r) => x + r.arg, 0);
|
|
448
|
+
const burn = sum(ACT.BURN);
|
|
449
|
+
const pot = sum(ACT.POT);
|
|
450
|
+
if (burn + pot > STICKY_LIMITS.maxTakePct * 100)
|
|
451
|
+
out.push(pot ? `Burn and pot: at most ${STICKY_LIMITS.maxTakePct} % of a buy together.` : `Auto burn: at most ${STICKY_LIMITS.maxBurnPct} % in total.`);
|
|
452
|
+
if (pot && !rows.some((r) => r.act === ACT.DROP))
|
|
453
|
+
out.push("A pot needs a drop row that pays it out.");
|
|
454
|
+
// the rows that close sells or sends only by the clock must leave a way out every week
|
|
455
|
+
let windows = 0;
|
|
456
|
+
let hours = 0;
|
|
457
|
+
let days = 0;
|
|
458
|
+
for (const r of rows) {
|
|
459
|
+
if (!stopsSells(r))
|
|
460
|
+
continue;
|
|
461
|
+
const h = holdOf(r);
|
|
462
|
+
if (h.ends)
|
|
463
|
+
continue;
|
|
464
|
+
if (h.window)
|
|
465
|
+
windows++;
|
|
466
|
+
else if (h.hasHour)
|
|
467
|
+
hours |= h.hours;
|
|
468
|
+
else if (h.hasDay)
|
|
469
|
+
days |= h.days;
|
|
470
|
+
}
|
|
471
|
+
if (windows > 1 || (windows === 1 && (hours || days)))
|
|
472
|
+
out.push("One sell window at most, and not next to rows on hours or weekdays.");
|
|
473
|
+
if (hours === ALL_HOURS)
|
|
474
|
+
out.push("The rows on hours close every hour of the day: leave one open.");
|
|
475
|
+
if (days === ALL_DAYS)
|
|
476
|
+
out.push("The rows on weekdays close every day: leave one open.");
|
|
477
|
+
return out;
|
|
478
|
+
}
|
|
479
|
+
// ---------------------------------------------------------------- reading a coin's table back
|
|
480
|
+
const dur = (s) => s % DAY === 0 ? `${s / DAY} ${s === DAY ? "day" : "days"}` : s % HOUR === 0 ? `${s / HOUR} ${s === HOUR ? "hour" : "hours"}` : s % MIN === 0 ? `${s / MIN} min` : `${s} s`;
|
|
481
|
+
const pctOf = (ppmVal) => `${+(ppmVal / 10_000).toFixed(3)} %`;
|
|
482
|
+
const flagName = (f) => [f & FLAG.SNIPER ? "marked as sniper" : "", f & FLAG.MARK ? "marked" : "", f & FLAG.DEV ? "the creator's" : ""].filter(Boolean).join(" or ");
|
|
483
|
+
const ordinal = (n) => `${n}${n % 100 >= 11 && n % 100 <= 13 ? "th" : (["th", "st", "nd", "rd"][n % 10] ?? "th")}`;
|
|
484
|
+
const markAs = (f) => [f & FLAG.SNIPER ? "sniper coins" : "", f & FLAG.MARK ? "marked coins" : ""].filter(Boolean).join(" and ");
|
|
485
|
+
function condText(k) {
|
|
486
|
+
const n = Number(k.val);
|
|
487
|
+
switch (k.v) {
|
|
488
|
+
case VAR.AGE:
|
|
489
|
+
return k.op === OP.LT ? `the coins are younger than ${dur(n)}` : `the coins are at least ${dur(n)} old`;
|
|
490
|
+
case VAR.LAUNCH:
|
|
491
|
+
if (k.op === OP.MODLT || k.op === OP.MODGE) {
|
|
492
|
+
const period = Number(k.val >> BigInt(20));
|
|
493
|
+
const th = Number(k.val & BigInt(0xfffff));
|
|
494
|
+
return k.op === OP.MODGE ? `outside the first ${dur(th)} of every ${dur(period)}` : `inside the first ${dur(th)} of every ${dur(period)}`;
|
|
495
|
+
}
|
|
496
|
+
return k.op === OP.LT ? `the coin is younger than ${dur(n)}` : `the coin is at least ${dur(n)} old`;
|
|
497
|
+
case VAR.BLOCKS:
|
|
498
|
+
return k.op === OP.LT ? `in the first ${n} blocks` : `after the first ${n} blocks`;
|
|
499
|
+
case VAR.AMOUNT:
|
|
500
|
+
return k.op === OP.GE ? `the trade is ${pctOf(n)} of the supply or more` : `the trade is under ${pctOf(n)} of the supply`;
|
|
501
|
+
case VAR.BALANCE:
|
|
502
|
+
return k.op === OP.GE ? `the wallet holds ${pctOf(n)} of the supply or more` : `the wallet holds under ${pctOf(n)} of the supply`;
|
|
503
|
+
case VAR.PRICE:
|
|
504
|
+
return k.op === OP.LT ? `the price is under ${(n / 10_000).toFixed(2)}x the start` : `the price is at least ${(n / 10_000).toFixed(2)}x the start`;
|
|
505
|
+
case VAR.DIP:
|
|
506
|
+
return k.op === OP.LT ? `the price is more than ${100 - n / 100} % under the high of the hour` : `the price is within ${100 - n / 100} % of the high of the hour`;
|
|
507
|
+
case VAR.SOLD:
|
|
508
|
+
return k.op === OP.LT ? `under ${n / 100} % of the curve is sold` : `${n / 100} % of the curve or more is sold`;
|
|
509
|
+
case VAR.HOUR:
|
|
510
|
+
return k.op === OP.LT ? `before ${n}:00 UTC` : `from ${n}:00 UTC`;
|
|
511
|
+
case VAR.WEEKDAY:
|
|
512
|
+
return k.op === OP.LT ? `before ${["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][n]}` : `from ${["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][n]} on`;
|
|
513
|
+
case VAR.FLAGS:
|
|
514
|
+
return k.op === OP.HAS ? `the coins are ${flagName(n)}` : `the coins are not ${flagName(n)}`;
|
|
515
|
+
case VAR.BUYS: {
|
|
516
|
+
if (k.op === OP.MODGE || k.op === OP.MODLT) {
|
|
517
|
+
const m = Number(k.val >> BigInt(20));
|
|
518
|
+
const at = Number(k.val & BigInt(0xfffff));
|
|
519
|
+
return k.op === OP.MODGE && at === m - 1 ? `it is the ${ordinal(m)} buy, the ${ordinal(2 * m)}, and so on` : `the count of buys modulo ${m} is ${k.op === OP.MODGE ? "at least" : "under"} ${at}`;
|
|
520
|
+
}
|
|
521
|
+
return k.op === OP.LT ? `the coin has had fewer than ${n} buys` : `the coin has had ${n} buys or more`;
|
|
522
|
+
}
|
|
523
|
+
case VAR.HOLDERS:
|
|
524
|
+
return k.op === OP.LT ? `fewer than ${n} wallets hold the coin` : `${n} wallets or more hold the coin`;
|
|
525
|
+
case VAR.IDLE:
|
|
526
|
+
return k.op === OP.LT ? `the last buy was less than ${dur(n)} ago` : `nobody bought for ${dur(n)}`;
|
|
527
|
+
case VAR.POTSIZE:
|
|
528
|
+
return k.op === OP.LT ? `the pot holds under ${pctOf(n)} of the supply` : `the pot holds ${pctOf(n)} of the supply or more`;
|
|
529
|
+
}
|
|
530
|
+
return "a condition the site does not know";
|
|
531
|
+
}
|
|
532
|
+
function onText(on) {
|
|
533
|
+
const parts = [on & ON.BUY ? "buy" : "", on & ON.SELL ? "sell" : "", on & ON.SEND ? "send" : ""].filter(Boolean);
|
|
534
|
+
return parts.length > 1 ? `${parts.slice(0, -1).join(", ")} or ${parts[parts.length - 1]}` : parts[0] ?? "trade";
|
|
535
|
+
}
|
|
536
|
+
/** One row as a sentence, for the coin page and the API. */
|
|
537
|
+
export function rowText(r, release) {
|
|
538
|
+
const when = r.conds.length ? ` when ${r.conds.map(condText).join(" and ")}` : "";
|
|
539
|
+
switch (r.act) {
|
|
540
|
+
case ACT.DENY:
|
|
541
|
+
return `A ${onText(r.on)} is refused${when}.`;
|
|
542
|
+
case ACT.FLAG:
|
|
543
|
+
return `A buy marks the new coins as ${markAs(r.arg)}${when}.`;
|
|
544
|
+
case ACT.LOCK:
|
|
545
|
+
return `${r.arg / 100} % of a buy unlocks over ${dur(release)}${when}.`;
|
|
546
|
+
case ACT.BURN:
|
|
547
|
+
return `${r.arg / 100} % of a buy is burned${when}.`;
|
|
548
|
+
case ACT.POT:
|
|
549
|
+
return `${r.arg / 100} % of a buy goes into the pot${when}.`;
|
|
550
|
+
case ACT.DROP:
|
|
551
|
+
return `A buy also gets ${r.arg / 100} % of the pot, up to as many coins as it buys${when}. Never the creator's buys.`;
|
|
552
|
+
}
|
|
553
|
+
return "A row the site does not know.";
|
|
554
|
+
}
|
|
555
|
+
/** Which template a row belongs to, by its shape (null for a custom row). */
|
|
556
|
+
export function templateOf(r) {
|
|
557
|
+
const shape = (x) => `${x.on}:${x.act}:${x.conds.map((k) => `${k.v}.${k.op}`).join(",")}`;
|
|
558
|
+
const s = shape(r);
|
|
559
|
+
for (const t of TEMPLATES) {
|
|
560
|
+
// defaults and every number at its maximum: covers the branches of a template (drip for every buy or big ones)
|
|
561
|
+
const variants = [defaults(t), Object.fromEntries(t.params.map((q) => [q.key, q.max]))];
|
|
562
|
+
if (variants.some((p) => t.rows(p).some((x) => shape(x) === s)))
|
|
563
|
+
return t.id;
|
|
564
|
+
}
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
/** Scanner color of a whole table: the worst of its templates. */
|
|
568
|
+
export function scannerOf(rows) {
|
|
569
|
+
let worst = "green";
|
|
570
|
+
for (const r of rows) {
|
|
571
|
+
const id = templateOf(r);
|
|
572
|
+
// drip on every buy: a scanner's test sell fails; drip on big buys only: its small test passes
|
|
573
|
+
const s = id === "drip" && r.conds.length === 0 ? "red" : id ? TEMPLATE_BY_ID.get(id).scanner : "yellow";
|
|
574
|
+
if (s === "red")
|
|
575
|
+
return "red";
|
|
576
|
+
if (s === "yellow")
|
|
577
|
+
worst = "yellow";
|
|
578
|
+
}
|
|
579
|
+
return worst;
|
|
580
|
+
}
|