@pickhooks/sdk 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +418 -3
- package/dist/abis/agents.d.ts +2418 -0
- package/dist/abis/agents.js +4 -0
- package/dist/abis/any3.d.ts +2123 -0
- package/dist/abis/any3.js +3 -0
- package/dist/abis/anytax.d.ts +1786 -0
- package/dist/abis/anytax.js +3 -0
- package/dist/abis/calls.d.ts +2515 -0
- package/dist/abis/calls.js +4 -0
- package/dist/abis/common.d.ts +449 -0
- package/dist/abis/common.js +91 -0
- package/dist/abis/dips.d.ts +897 -0
- package/dist/abis/dips.js +2 -0
- package/dist/abis/index.d.ts +14 -0
- package/dist/abis/index.js +14 -0
- package/dist/abis/infra.d.ts +701 -0
- package/dist/abis/infra.js +4 -0
- package/dist/abis/lever.d.ts +1710 -0
- package/dist/abis/lever.js +5 -0
- package/dist/abis/lever2.d.ts +2710 -0
- package/dist/abis/lever2.js +5 -0
- package/dist/abis/pools.d.ts +1898 -0
- package/dist/abis/pools.js +4 -0
- package/dist/abis/sticky.d.ts +953 -0
- package/dist/abis/sticky.js +4 -0
- package/dist/abis/sticky2.d.ts +3750 -0
- package/dist/abis/sticky2.js +8 -0
- package/dist/abis/v1.d.ts +1627 -0
- package/dist/abis/v1.js +5 -0
- package/dist/abis/v2.d.ts +3264 -0
- package/dist/abis/v2.js +8 -0
- package/dist/addresses.d.ts +100 -0
- package/dist/addresses.js +71 -0
- package/dist/agents-catalog.d.ts +318 -0
- package/dist/agents-catalog.js +353 -0
- package/dist/agents.d.ts +543 -0
- package/dist/agents.js +667 -0
- package/dist/any.d.ts +447 -0
- package/dist/any.js +729 -0
- package/dist/api.d.ts +62 -0
- package/dist/api.js +30 -0
- package/dist/blocks.d.ts +107 -0
- package/dist/blocks.js +126 -0
- package/dist/coin-rules-config.d.ts +8 -0
- package/dist/coin-rules-config.js +13 -0
- package/dist/coin-rules.d.ts +383 -0
- package/dist/coin-rules.js +878 -0
- package/dist/coins.d.ts +217 -0
- package/dist/coins.js +520 -0
- package/dist/desks.d.ts +298 -0
- package/dist/desks.js +343 -0
- package/dist/dips.d.ts +133 -0
- package/dist/dips.js +148 -0
- package/dist/errors.d.ts +35 -0
- package/dist/errors.js +414 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +91 -0
- package/dist/infinity.d.ts +63 -0
- package/dist/infinity.js +96 -0
- package/dist/infra.d.ts +112 -0
- package/dist/infra.js +114 -0
- package/dist/lever.d.ts +391 -0
- package/dist/lever.js +419 -0
- package/dist/pools.d.ts +274 -0
- package/dist/pools.js +405 -0
- package/dist/sticky-rules.d.ts +179 -0
- package/dist/sticky-rules.js +580 -0
- package/dist/sticky-sim.d.ts +125 -0
- package/dist/sticky-sim.js +637 -0
- package/dist/sticky.d.ts +286 -0
- package/dist/sticky.js +420 -0
- package/dist/tlock.d.ts +67 -0
- package/dist/tlock.js +185 -0
- package/dist/tx.d.ts +72 -0
- package/dist/tx.js +78 -0
- package/dist/v2.d.ts +106 -0
- package/dist/v2.js +61 -0
- package/dist/v3.d.ts +42 -0
- package/dist/v3.js +98 -0
- package/dist/wallet.d.ts +80 -0
- package/dist/wallet.js +115 -0
- package/dist/watch.d.ts +82 -0
- package/dist/watch.js +194 -0
- package/package.json +67 -4
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
// Copied from web/src/lib/rules.ts (the site's rule catalog for coins with rules in the coin and in the pool), with one line changed:
|
|
2
|
+
// the import of the site's config. Keep the two in step: node scripts/check-rules-copy.mjs compares them.
|
|
3
|
+
/**
|
|
4
|
+
* The rule catalog for hooks on BNB: one entry per rule of HooksToken (src/HooksToken.sol).
|
|
5
|
+
* The launcher builds its form from `fields` and turns the values into the token's `Rules` struct
|
|
6
|
+
* with `apply`; the coin page explains a live rule with `describe` and `live`.
|
|
7
|
+
* Every rule says what a scanner (GoPlus, honeypot.is) will make of it: `scanner`.
|
|
8
|
+
* Shared by client and server.
|
|
9
|
+
*/
|
|
10
|
+
import { V2 } from "./coin-rules-config.js";
|
|
11
|
+
export const SUPPLY = 1_000_000_000;
|
|
12
|
+
export const MAX_RULES = 8;
|
|
13
|
+
export const DAY = 86_400;
|
|
14
|
+
const E18 = BigInt(10) ** BigInt(18);
|
|
15
|
+
/** Binance Account Bound Token: the soulbound token of identity-verified Binance users, one per account, balanceOf is 0 or 1. */
|
|
16
|
+
export const BABT = "0x2B09d47D550061f995A3b5C6F0Fd58005215D7c8";
|
|
17
|
+
/** The pickhooks token on BNB Chain (flap, migrated to PancakeSwap). Gate for the set "$hooks holders only". */
|
|
18
|
+
export const HOOKS_TOKEN = "0x07bcB8E8226f671300e135b4A61Dc157359b7777";
|
|
19
|
+
export const GROUPS = [
|
|
20
|
+
{ id: "hold", title: "Who can hold", blurb: "Caps, clubs and gates." },
|
|
21
|
+
{ id: "buy", title: "How you buy", blurb: "Size, pace and order of buys." },
|
|
22
|
+
{ id: "sell", title: "How you sell", blurb: "Sell caps, holds and locks." },
|
|
23
|
+
{ id: "time", title: "When it trades", blurb: "Clocks, turns and windows." },
|
|
24
|
+
{ id: "games", title: "Games", blurb: "Rules that turn trading into play." },
|
|
25
|
+
{ id: "pay", title: "Who gets paid", blurb: "BNB that moves with buys." },
|
|
26
|
+
{ id: "linked", title: "Linked coins", blurb: "One coin reacts to another." },
|
|
27
|
+
{ id: "move", title: "Where it moves", blurb: "Pool only or wallet to wallet." },
|
|
28
|
+
{ id: "hooks", title: "Hooks rules", blurb: "Shows that cost $hooks." },
|
|
29
|
+
{ id: "trust", title: "Trust", blurb: "The dev earns it, the coin defends itself." },
|
|
30
|
+
];
|
|
31
|
+
const G = (note = "Scanners see nothing special.") => ({ level: "green", note });
|
|
32
|
+
const Y = (note) => ({ level: "yellow", note });
|
|
33
|
+
const RED = (note) => ({ level: "red", note });
|
|
34
|
+
const WHALE = Y('Scanners show "anti whale". A hint, not a red flag.');
|
|
35
|
+
const SELLCAP = RED('A sell cap per trade: scanners show "cannot sell all" and many buyers read that as a red flag.');
|
|
36
|
+
const HONEYPOT = RED("A fresh wallet cannot sell right after buying. Scanners test exactly that and will mark the coin as a honeypot.");
|
|
37
|
+
const ZERO = "0x0000000000000000000000000000000000000000";
|
|
38
|
+
export function emptyRules() {
|
|
39
|
+
return {
|
|
40
|
+
mask: BigInt(0),
|
|
41
|
+
maxWalletBps: 0, riseStartBps: 0, riseStepBps: 0, riseSeconds: 0,
|
|
42
|
+
chapterStartBps: 0, chapterVolume: BigInt(0), clubMax: 0, gateToken: ZERO, gateMin: BigInt(0),
|
|
43
|
+
maxTradeBps: 0, buysPerBlock: 0, maxGasPrice: BigInt(0), raiseBps: 0, raiseReset: 0,
|
|
44
|
+
minBuy: BigInt(0), slowSeconds: 0, buyLimit: 0,
|
|
45
|
+
dumpBuyBps: 0, dumpSellBps: 0, curveSmallBps: 0, curveFloorBps: 0, curveBigBagBps: 0,
|
|
46
|
+
taperStartBps: 0, taperEndBps: 0, taperSoldBps: 0, unlockBuys: 0,
|
|
47
|
+
devLockSeconds: 0, devCapBps: 0, devGapSeconds: 0, diamondSeconds: 0, cooldownSeconds: 0,
|
|
48
|
+
jailWindow: 0, jailSeconds: 0,
|
|
49
|
+
openHour: 0, closeHour: 0, weekdays: 0, sellsOpen: false,
|
|
50
|
+
sellWindow: 0, sellPeriod: 0, tickSeconds: 0, bellSeconds: 0, bellSoldBps: 0,
|
|
51
|
+
rewardPerBuy: BigInt(0), rewardMinBuy: BigInt(0), rewardEveryN: 0,
|
|
52
|
+
jackpotSilence: 0, jackpotShareBps: 0, jackpotMinBuy: BigInt(0), jackpotFeeBps: 0,
|
|
53
|
+
dipBps: 0, beacon: ZERO, beaconTargetBps: 0, partner: ZERO, reactBaseBps: 0, reactSensitivity: 0,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function emptyExtras() {
|
|
57
|
+
return {
|
|
58
|
+
buyoutMask: BigInt(0),
|
|
59
|
+
buyoutPrice: [BigInt(0), BigInt(0), BigInt(0), BigInt(0), BigInt(0), BigInt(0), BigInt(0), BigInt(0)],
|
|
60
|
+
votable: BigInt(0), quorumBps: 0, voteSeconds: 0,
|
|
61
|
+
duelOpponent: ZERO, duelStake: BigInt(0),
|
|
62
|
+
feed: ZERO, target: BigInt(0), sellsAtSeconds: 0,
|
|
63
|
+
deadManSeconds: 0,
|
|
64
|
+
floorDeposit: BigInt(0), floorBps: 0,
|
|
65
|
+
shiftSeconds: 0, payrollMin: BigInt(0), payrollDeposit: BigInt(0),
|
|
66
|
+
creatorKeepBps: 10_000,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// ---------------------------------------------------------------- formatting
|
|
70
|
+
export function pct(bps) {
|
|
71
|
+
const v = bps / 100;
|
|
72
|
+
return `${v >= 10 ? Math.round(v * 10) / 10 : Number(v.toPrecision(3))}%`;
|
|
73
|
+
}
|
|
74
|
+
export function dur(s) {
|
|
75
|
+
if (s < 90)
|
|
76
|
+
return `${Math.round(s)}s`;
|
|
77
|
+
if (s < 5400)
|
|
78
|
+
return `${Math.round(s / 60)} min`;
|
|
79
|
+
if (s < 2 * DAY)
|
|
80
|
+
return `${Math.round((s / 3600) * 10) / 10} h`;
|
|
81
|
+
return `${Math.round((s / DAY) * 10) / 10} days`;
|
|
82
|
+
}
|
|
83
|
+
export const bnb = (v) => `${Number(v.toPrecision(4))} BNB`;
|
|
84
|
+
export const short = (a) => (a && a !== ZERO ? `${a.slice(0, 6)}..${a.slice(-4)}` : "none");
|
|
85
|
+
const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
86
|
+
export const tokens = (t) => `${t.toLocaleString("en-US", { maximumFractionDigits: 0 })} tokens`;
|
|
87
|
+
const bps = (p) => Math.round(Number(p) * 100);
|
|
88
|
+
const n = (v) => Math.round(Number(v));
|
|
89
|
+
/** share of supply in % -> raw token units */
|
|
90
|
+
const supplyShare = (p) => (BigInt(Math.round(Number(p) * 1e6)) * BigInt(SUPPLY) * E18) / BigInt(1e8);
|
|
91
|
+
const wei = (b) => BigInt(Math.round(Number(b) * 1e9)) * BigInt(1e9);
|
|
92
|
+
export const fromRaw = (x) => Number(x / BigInt(1e12)) / 1e6;
|
|
93
|
+
const shareOf = (raw) => Math.round((fromRaw(raw) / SUPPLY) * 10_000);
|
|
94
|
+
const age = (s, now) => Math.max(0, now - s.start);
|
|
95
|
+
const bit = (id) => BigInt(1) << BigInt(id);
|
|
96
|
+
/** UI days: bit 0 = Monday. Token: bit 0 = Sunday. */
|
|
97
|
+
const toChainDays = (mask) => DAYS.reduce((m, _, i) => ((mask >> i) & 1 ? m | (1 << ((i + 1) % 7)) : m), 0);
|
|
98
|
+
const fromChainDays = (mask) => DAYS.reduce((m, _, i) => ((mask >> ((i + 1) % 7)) & 1 ? m | (1 << i) : m), 0);
|
|
99
|
+
const hh = (h) => `${String(h).padStart(2, "0")}:00`;
|
|
100
|
+
/** rule kinds of the v1 catalog (the v2 factory takes them all as well) */
|
|
101
|
+
export const V1_KINDS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 28, 31, 34, 35, 36, 37, 38];
|
|
102
|
+
/** rules that only restrict trading: the only ones a buyout or a vote can lift (never trust rules, games, pots or linked coins) */
|
|
103
|
+
export const LIFTABLE = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 22, 23, 24, 25, 28, 38, 42];
|
|
104
|
+
/** rules that cannot have an end time on the v2 factory: trust rules, pots, games and the v2 machinery */
|
|
105
|
+
export const NO_UNTIL = [21, 31, 36, 39, 40, 41, 42, 43, 44, 45, 46];
|
|
106
|
+
export const FEED_NAMES = ["BNB", "BTC", "ETH", "DOGE"];
|
|
107
|
+
const feedAddress = (i) => V2.feeds[FEED_NAMES[Number(i)] ?? "BNB"];
|
|
108
|
+
const feedName = (addr) => FEED_NAMES.find((k) => V2.feeds[k].toLowerCase() === addr.toLowerCase()) ?? "the feed";
|
|
109
|
+
const usd = (v) => `${v.toLocaleString("en-US", { maximumFractionDigits: v < 10 ? 4 : 0 })} USD`;
|
|
110
|
+
/** the kinds a comma list names, kept to the rules that are picked and can be lifted */
|
|
111
|
+
const listedKinds = (v, picked) => {
|
|
112
|
+
const chosen = String(v ?? "").split(",").map((s) => Number(s.trim())).filter((k) => Number.isInteger(k) && k > 0);
|
|
113
|
+
const present = new Set(picked.map((p) => RULE_BY_ID.get(p.id)?.kind ?? 0));
|
|
114
|
+
return chosen.filter((k) => LIFTABLE.includes(k) && present.has(k)).sort((a, b) => a - b);
|
|
115
|
+
};
|
|
116
|
+
const maskOf = (kinds) => kinds.reduce((m, k) => m | bit(k), BigInt(0));
|
|
117
|
+
const namesOf = (mask) => kindsOf(mask).map((k) => RULE_BY_KIND.get(k)?.name ?? `rule ${k}`).join(", ");
|
|
118
|
+
const SHIFT_LABEL = { 14_400: "4 hour", 28_800: "8 hour", 86_400: "24 hour" };
|
|
119
|
+
// ---------------------------------------------------------------- the rules
|
|
120
|
+
export const RULES = [
|
|
121
|
+
// who can hold
|
|
122
|
+
{
|
|
123
|
+
kind: 1, id: "max_wallet", name: "Max wallet", group: "hold", scanner: WHALE,
|
|
124
|
+
line: "No wallet holds more than a share of supply.",
|
|
125
|
+
fields: [{ id: "cap", label: "Max per wallet", kind: "pct", def: 1, min: 0.1, max: 100 }],
|
|
126
|
+
apply: (v) => ({ maxWalletBps: bps(v.cap) }),
|
|
127
|
+
describe: (r) => `No wallet holds more than ${pct(r.maxWalletBps)} of supply.`,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
kind: 10, id: "rising_wallet", name: "Rising max wallet", group: "hold", scanner: WHALE,
|
|
131
|
+
line: "The cap starts tiny and rises on a timer, by a step or doubling.",
|
|
132
|
+
fields: [
|
|
133
|
+
{ id: "start", label: "Cap at launch", kind: "pct", def: 0.2, min: 0.1, max: 100 },
|
|
134
|
+
{ id: "double", label: "Double every step instead of adding", kind: "bool", def: false },
|
|
135
|
+
{ id: "step", label: "Added every step", kind: "pct", def: 0.2, min: 0.01, max: 100, hint: "ignored when doubling" },
|
|
136
|
+
{ id: "every", label: "Step every", kind: "dur", def: 600, min: 60, max: 7 * DAY },
|
|
137
|
+
],
|
|
138
|
+
apply: (v) => ({ riseStartBps: bps(v.start), riseStepBps: v.double ? 0 : bps(v.step), riseSeconds: n(v.every) }),
|
|
139
|
+
describe: (r) => `Max wallet starts at ${pct(r.riseStartBps)} and ${r.riseStepBps === 0 ? "doubles" : `rises by ${pct(r.riseStepBps)}`} every ${dur(r.riseSeconds)}.`,
|
|
140
|
+
live: (r, s, now) => {
|
|
141
|
+
const steps = Math.floor(age(s, now) / r.riseSeconds);
|
|
142
|
+
const cap = Math.min(10_000, r.riseStepBps === 0 ? r.riseStartBps * 2 ** Math.min(steps, 14) : r.riseStartBps + r.riseStepBps * steps);
|
|
143
|
+
return cap >= 10_000 ? "No cap anymore." : `Cap now ${pct(cap)}, next step in ${dur(r.riseSeconds - (age(s, now) % r.riseSeconds))}.`;
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
kind: 6, id: "chapters", name: "Chapters", group: "hold", scanner: WHALE,
|
|
148
|
+
line: "The wallet cap doubles every time a set volume has traded.",
|
|
149
|
+
fields: [
|
|
150
|
+
{ id: "start", label: "Cap in chapter 1", kind: "pct", def: 0.5, min: 0.1, max: 100 },
|
|
151
|
+
{ id: "volume", label: "Volume per chapter (share of supply traded)", kind: "pct", def: 10, min: 0.1, max: 1000 },
|
|
152
|
+
],
|
|
153
|
+
apply: (v) => ({ chapterStartBps: bps(v.start), chapterVolume: supplyShare(v.volume) }),
|
|
154
|
+
describe: (r) => `Max wallet ${pct(r.chapterStartBps)}, doubling every time ${pct(shareOf(r.chapterVolume))} of supply has traded.`,
|
|
155
|
+
live: (r, s) => {
|
|
156
|
+
const ch = Math.min(14, Math.floor(s.volumeTokens / fromRaw(r.chapterVolume)));
|
|
157
|
+
return `Chapter ${ch + 1}: cap ${pct(Math.min(10_000, r.chapterStartBps * 2 ** ch))}.`;
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
kind: 17, id: "holder_cap", name: "The club", group: "hold", scanner: Y("While the club is full, a scanner's test buy fails and can show \"cannot buy\"."),
|
|
162
|
+
line: "Only so many holders at a time. A seat opens when someone sells out.",
|
|
163
|
+
fields: [{ id: "max", label: "Seats", kind: "int", def: 100, min: 10, max: 1_000_000 }],
|
|
164
|
+
apply: (v) => ({ clubMax: n(v.max) }),
|
|
165
|
+
describe: (r) => `At most ${r.clubMax} holders at a time.`,
|
|
166
|
+
live: (r, s) => (s.holders >= r.clubMax ? `Full: ${s.holders} of ${r.clubMax} seats.` : `${r.clubMax - s.holders} of ${r.clubMax} seats free.`),
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
kind: 8, id: "gated", name: "Holder gated", group: "hold",
|
|
170
|
+
scanner: RED("A scanner's test wallet holds no gate token, so its buy fails: likely shown as \"cannot buy\", plus \"external call\"."),
|
|
171
|
+
line: "Only wallets that hold another token can buy or receive. Works with NFTs and soulbound tokens too, for example the Binance Account Bound Token.",
|
|
172
|
+
fields: [
|
|
173
|
+
{ id: "gate", label: "Gate token (BNB Chain address)", kind: "address" },
|
|
174
|
+
{ id: "min", label: "Smallest amount to hold", kind: "int", def: 1, min: 1, max: 1_000_000_000_000, hint: "whole tokens, or pieces for an NFT or soulbound token" },
|
|
175
|
+
{ id: "nft", label: "Gate token counts pieces (NFT or soulbound token, no decimals)", kind: "bool", def: false, hint: "tick this for BABT and other NFTs" },
|
|
176
|
+
],
|
|
177
|
+
apply: (v) => ({ gateToken: String(v.gate).trim(), gateMin: v.nft ? BigInt(n(v.min)) : BigInt(n(v.min)) * E18 }),
|
|
178
|
+
describe: (r) => r.gateToken.toLowerCase() === BABT.toLowerCase()
|
|
179
|
+
? "Only verified Binance users can buy or receive: wallets with a Binance Account Bound Token."
|
|
180
|
+
: r.gateToken.toLowerCase() === HOOKS_TOKEN.toLowerCase()
|
|
181
|
+
? `Only wallets with at least ${Number(r.gateMin / E18).toLocaleString("en-US")} $hooks can buy or receive.`
|
|
182
|
+
: `Only holders of ${short(r.gateToken)} can receive.`,
|
|
183
|
+
},
|
|
184
|
+
// how you buy
|
|
185
|
+
{
|
|
186
|
+
kind: 2, id: "max_tx", name: "Max per buy", alias: "Trade guard", group: "buy", scanner: WHALE,
|
|
187
|
+
line: "No single buy over a share of supply.",
|
|
188
|
+
fields: [{ id: "cap", label: "Max per buy", kind: "pct", def: 0.5, min: 0.1, max: 100 }],
|
|
189
|
+
apply: (v) => ({ maxTradeBps: bps(v.cap) }),
|
|
190
|
+
describe: (r) => `No buy over ${pct(r.maxTradeBps)} of supply.`,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
kind: 5, id: "anti_bundle", name: "Anti bundle", group: "buy", scanner: G(),
|
|
194
|
+
line: "A limit on buys per block, so nobody packs the first block.",
|
|
195
|
+
fields: [{ id: "max", label: "Buys per block", kind: "int", def: 2, min: 1, max: 64 }],
|
|
196
|
+
apply: (v) => ({ buysPerBlock: n(v.max) }),
|
|
197
|
+
describe: (r) => `At most ${r.buysPerBlock} ${r.buysPerBlock === 1 ? "buy" : "buys"} per block.`,
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
kind: 28, id: "gas_cap", name: "Gas cap", alias: "Sniper-fee cap", group: "buy", scanner: G(),
|
|
201
|
+
line: "A buy that outbids everyone with a high gas price is refused.",
|
|
202
|
+
fields: [{ id: "gwei", label: "Max gas price", kind: "gwei", def: 1, max: 1000, hint: "BNB Chain runs at about 0.05 gwei" }],
|
|
203
|
+
apply: (v) => ({ maxGasPrice: BigInt(Math.round(Number(v.gwei) * 1e9)) }),
|
|
204
|
+
describe: (r) => `Buys pay at most ${Number(r.maxGasPrice) / 1e9} gwei gas.`,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
kind: 12, id: "raise", name: "Raise", group: "buy", scanner: Y("A small test buy right after a big buy fails."),
|
|
208
|
+
line: "Every buy must beat the last one, until nobody buys for a while.",
|
|
209
|
+
fields: [
|
|
210
|
+
{ id: "by", label: "Beat the last buy by", kind: "pct", def: 1, min: 0.01, max: 100 },
|
|
211
|
+
{ id: "reset", label: "Resets after a quiet", kind: "dur", def: 120, min: 10, max: DAY },
|
|
212
|
+
],
|
|
213
|
+
apply: (v) => ({ raiseBps: bps(v.by), raiseReset: n(v.reset) }),
|
|
214
|
+
describe: (r) => `Each buy must beat the last by ${pct(r.raiseBps)}. Resets after ${dur(r.raiseReset)} without a buy.`,
|
|
215
|
+
live: (r, s, now) => {
|
|
216
|
+
if (!s.lastBuyTime || now - s.lastBuyTime >= r.raiseReset)
|
|
217
|
+
return "Open: any size goes.";
|
|
218
|
+
return `Beat ${tokens(s.lastBuyTokens * (1 + r.raiseBps / 10_000))}, or wait ${dur(r.raiseReset - (now - s.lastBuyTime))}.`;
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
kind: 13, id: "no_repeat", name: "No repeat", group: "buy", scanner: G(),
|
|
223
|
+
line: "The same wallet cannot buy twice in a row.",
|
|
224
|
+
fields: [],
|
|
225
|
+
apply: () => ({}),
|
|
226
|
+
describe: () => "No wallet buys twice in a row.",
|
|
227
|
+
live: (_r, s) => (s.lastBuyer ? `Last buyer ${short(s.lastBuyer)} waits for someone else.` : null),
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
kind: 18, id: "min_buy", name: "Min buy", group: "buy", scanner: Y("A scanner that tests with a tiny buy sees it fail."),
|
|
231
|
+
line: "No dust: every buy has a smallest size.",
|
|
232
|
+
fields: [{ id: "min", label: "Smallest buy (share of supply)", kind: "pct", def: 0.01, min: 0.0001, max: 1 }],
|
|
233
|
+
apply: (v) => ({ minBuy: supplyShare(v.min) }),
|
|
234
|
+
describe: (r) => `Every buy is at least ${tokens(fromRaw(r.minBuy))}.`,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
kind: 19, id: "slow_mode", name: "Slow mode", group: "buy", scanner: Y("A test buy right after another buy fails."),
|
|
238
|
+
line: "One buy every so often, for everyone.",
|
|
239
|
+
fields: [{ id: "gap", label: "Time between buys", kind: "dur", def: 10, min: 1, max: 3600 }],
|
|
240
|
+
apply: (v) => ({ slowSeconds: n(v.gap) }),
|
|
241
|
+
describe: (r) => `One buy every ${dur(r.slowSeconds)}.`,
|
|
242
|
+
live: (r, s, now) => (s.lastBuyTime && now - s.lastBuyTime < r.slowSeconds ? `Next buy opens in ${dur(r.slowSeconds - (now - s.lastBuyTime))}.` : "Open now."),
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
kind: 23, id: "buy_limit", name: "Buy limit", group: "buy", scanner: G(),
|
|
246
|
+
line: "Each wallet buys a set number of times. Counted on the wallet that signs.",
|
|
247
|
+
fields: [{ id: "max", label: "Buys per wallet", kind: "int", def: 1, min: 1, max: 100 }],
|
|
248
|
+
apply: (v) => ({ buyLimit: n(v.max) }),
|
|
249
|
+
describe: (r) => `Each wallet buys ${r.buyLimit === 1 ? "once" : `${r.buyLimit} times`}.`,
|
|
250
|
+
},
|
|
251
|
+
// how you sell
|
|
252
|
+
{
|
|
253
|
+
kind: 3, id: "anti_dump", name: "Anti dump", group: "sell", scanner: SELLCAP,
|
|
254
|
+
line: "Separate caps for a buy and for a sell.",
|
|
255
|
+
fields: [
|
|
256
|
+
{ id: "buy", label: "Max buy", kind: "pct", def: 1, min: 0.1, max: 100 },
|
|
257
|
+
{ id: "sell", label: "Max sell", kind: "pct", def: 0.25, min: 0.1, max: 100 },
|
|
258
|
+
],
|
|
259
|
+
apply: (v) => ({ dumpBuyBps: bps(v.buy), dumpSellBps: bps(v.sell) }),
|
|
260
|
+
describe: (r) => `Max buy ${pct(r.dumpBuyBps)}, max sell ${pct(r.dumpSellBps)} of supply at once.`,
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
kind: 4, id: "sell_curve", name: "Sell curve", alias: "Graduated sell caps", group: "sell", scanner: SELLCAP,
|
|
264
|
+
line: "The bigger the bag, the smaller the share of supply it can sell at once.",
|
|
265
|
+
fields: [
|
|
266
|
+
{ id: "small", label: "Sell cap for a small bag", kind: "pct", def: 1, min: 0.05, max: 100 },
|
|
267
|
+
{ id: "floor", label: "Sell cap for a big bag", kind: "pct", def: 0.1, min: 0.05, max: 100 },
|
|
268
|
+
{ id: "big", label: "A big bag is", kind: "pct", def: 3, min: 0.01, max: 100, hint: "share of supply" },
|
|
269
|
+
],
|
|
270
|
+
apply: (v) => ({ curveSmallBps: bps(v.small), curveFloorBps: bps(v.floor), curveBigBagBps: bps(v.big) }),
|
|
271
|
+
describe: (r) => `Small bags sell up to ${pct(r.curveSmallBps)} at once, bags of ${pct(r.curveBigBagBps)} or more only ${pct(r.curveFloorBps)}.`,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
kind: 11, id: "taper", name: "Taper", alias: "Sliding caps", group: "sell", scanner: SELLCAP,
|
|
275
|
+
line: "The sell cap shrinks as the curve sells out.",
|
|
276
|
+
fields: [
|
|
277
|
+
{ id: "start", label: "Sell cap at launch", kind: "pct", def: 1, min: 0.05, max: 100 },
|
|
278
|
+
{ id: "end", label: "Sell cap at the end", kind: "pct", def: 0.2, min: 0.05, max: 100 },
|
|
279
|
+
{ id: "at", label: "End reached when the curve has sold", kind: "pct", def: 80, min: 1, max: 100 },
|
|
280
|
+
],
|
|
281
|
+
apply: (v) => ({ taperStartBps: bps(v.start), taperEndBps: bps(v.end), taperSoldBps: bps(v.at) }),
|
|
282
|
+
describe: (r) => `Sell cap goes from ${pct(r.taperStartBps)} down to ${pct(r.taperEndBps)} as the curve sells ${pct(r.taperSoldBps)}.`,
|
|
283
|
+
live: (r, s) => `Sell cap now ${pct(r.taperStartBps - Math.floor(((r.taperStartBps - r.taperEndBps) * Math.min(s.soldBps, r.taperSoldBps)) / r.taperSoldBps))}.`,
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
kind: 14, id: "buys_unlock", name: "Buys unlock sells", group: "sell", scanner: HONEYPOT,
|
|
287
|
+
line: "A number of buys has to happen before the next sell.",
|
|
288
|
+
fields: [{ id: "buys", label: "Buys per sell", kind: "int", def: 3, min: 1, max: 20 }],
|
|
289
|
+
apply: (v) => ({ unlockBuys: n(v.buys) }),
|
|
290
|
+
describe: (r) => `${r.unlockBuys} ${r.unlockBuys === 1 ? "buy unlocks" : "buys unlock"} the next sell.`,
|
|
291
|
+
live: (r, s) => (s.buysSinceSell >= r.unlockBuys ? "A sell is unlocked." : `${r.unlockBuys - s.buysSinceSell} more ${r.unlockBuys - s.buysSinceSell === 1 ? "buy" : "buys"} until the next sell.`),
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
kind: 21, id: "dev_lock", name: "Dev lock", group: "sell", scanner: G("Only the creator is locked. Scanners see nothing."),
|
|
295
|
+
line: "The dev cannot sell or send for a while, then only in small pieces.",
|
|
296
|
+
fields: [
|
|
297
|
+
{ id: "lock", label: "Locked for", kind: "dur", def: DAY, max: 30 * DAY, zeroOk: true },
|
|
298
|
+
{ id: "cap", label: "Then max per sell", kind: "pct", def: 0.5, min: 0.01, max: 100 },
|
|
299
|
+
{ id: "gap", label: "Time between dev sells", kind: "dur", def: 3600, max: DAY, zeroOk: true },
|
|
300
|
+
],
|
|
301
|
+
apply: (v) => ({ devLockSeconds: n(v.lock), devCapBps: bps(v.cap), devGapSeconds: n(v.gap) }),
|
|
302
|
+
describe: (r) => `The dev is locked for ${dur(r.devLockSeconds)}, then sells at most ${pct(r.devCapBps)} at once${r.devGapSeconds ? `, ${dur(r.devGapSeconds)} apart` : ""}.`,
|
|
303
|
+
live: (r, s, now) => (age(s, now) < r.devLockSeconds ? `Dev unlocks in ${dur(r.devLockSeconds - age(s, now))}.` : "Dev lock is over."),
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
kind: 24, id: "diamond", name: "Diamond hands", group: "sell", scanner: HONEYPOT,
|
|
307
|
+
line: "A wallet holds for a while after its last buy before it can sell.",
|
|
308
|
+
fields: [{ id: "hold", label: "Hold after each buy", kind: "dur", def: 600, min: 1, max: DAY }],
|
|
309
|
+
apply: (v) => ({ diamondSeconds: n(v.hold) }),
|
|
310
|
+
describe: (r) => `A wallet holds ${dur(r.diamondSeconds)} after its last buy before it sells.`,
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
kind: 22, id: "cooldown", name: "Cooldown", group: "sell", scanner: RED('A fresh wallet cannot sell right after buying: scanners show "trading cooldown" and may mark it as a honeypot.'),
|
|
314
|
+
line: "A wallet trades once every so often.",
|
|
315
|
+
fields: [{ id: "gap", label: "Time between trades of one wallet", kind: "dur", def: 60, min: 1, max: DAY }],
|
|
316
|
+
apply: (v) => ({ cooldownSeconds: n(v.gap) }),
|
|
317
|
+
describe: (r) => `A wallet trades once every ${dur(r.cooldownSeconds)}.`,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
kind: 25, id: "sniper_jail", name: "Sniper jail", group: "sell", scanner: Y("Only wallets from the first seconds. A scanner that checks in that window sees a honeypot."),
|
|
321
|
+
line: "Wallets that get the coin in the first seconds sell later than everyone else.",
|
|
322
|
+
fields: [
|
|
323
|
+
{ id: "window", label: "Snipers are wallets that buy in the first", kind: "dur", def: 30, min: 1, max: 3600 },
|
|
324
|
+
{ id: "jail", label: "They wait after their buy", kind: "dur", def: 3600, min: 1, max: DAY },
|
|
325
|
+
],
|
|
326
|
+
apply: (v) => ({ jailWindow: n(v.window), jailSeconds: n(v.jail) }),
|
|
327
|
+
describe: (r) => `Wallets that bought in the first ${dur(r.jailWindow)} wait ${dur(r.jailSeconds)} before they sell or send.`,
|
|
328
|
+
live: (r, s, now) => (age(s, now) < r.jailWindow ? `Sniper window open for ${dur(r.jailWindow - age(s, now))}.` : "Sniper window closed."),
|
|
329
|
+
},
|
|
330
|
+
// when it trades
|
|
331
|
+
{
|
|
332
|
+
kind: 15, id: "hours", name: "Market hours", group: "time", scanner: Y("Outside hours a test buy fails. Red when sells close too."),
|
|
333
|
+
line: "Trades only in set hours and weekdays, in UTC. Sells can stay open.",
|
|
334
|
+
fields: [
|
|
335
|
+
{ id: "open", label: "Opens (UTC hour)", kind: "hour", def: 13 },
|
|
336
|
+
{ id: "close", label: "Closes (UTC hour)", kind: "hour", def: 20 },
|
|
337
|
+
{ id: "days", label: "Days", kind: "days", def: 0b0011111 },
|
|
338
|
+
{ id: "sells", label: "Sells always open", kind: "bool", def: true },
|
|
339
|
+
],
|
|
340
|
+
apply: (v) => ({ openHour: n(v.open), closeHour: n(v.close), weekdays: toChainDays(n(v.days)), sellsOpen: Boolean(v.sells) }),
|
|
341
|
+
describe: (r) => {
|
|
342
|
+
const days = fromChainDays(r.weekdays);
|
|
343
|
+
return `Open ${hh(r.openHour)} to ${hh(r.closeHour)} UTC${days === 127 ? ", every day" : `, ${DAYS.filter((_, i) => (days >> i) & 1).join(" ")}`}${r.sellsOpen ? ". Sells always open" : ""}.`;
|
|
344
|
+
},
|
|
345
|
+
live: (r, _s, now) => {
|
|
346
|
+
const h = Math.floor(now / 3600) % 24;
|
|
347
|
+
const day = (Math.floor(now / DAY) + 4) % 7;
|
|
348
|
+
const inHours = r.openHour < r.closeHour ? h >= r.openHour && h < r.closeHour : h >= r.openHour || h < r.closeHour;
|
|
349
|
+
return inHours && (r.weekdays >> day) & 1 ? "Open now." : "Closed now.";
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
kind: 16, id: "sell_window", name: "Sell window", group: "time", scanner: HONEYPOT,
|
|
354
|
+
line: "Sells only in the first part of every period.",
|
|
355
|
+
fields: [
|
|
356
|
+
{ id: "window", label: "Sells open for", kind: "dur", def: 300, min: 60, max: DAY },
|
|
357
|
+
{ id: "period", label: "Every", kind: "dur", def: 3600, min: 60, max: DAY },
|
|
358
|
+
],
|
|
359
|
+
apply: (v) => ({ sellWindow: n(v.window), sellPeriod: n(v.period) }),
|
|
360
|
+
describe: (r) => `Sells open for ${dur(r.sellWindow)} every ${dur(r.sellPeriod)}.`,
|
|
361
|
+
live: (r, s, now) => {
|
|
362
|
+
const t = age(s, now) % r.sellPeriod;
|
|
363
|
+
return t < r.sellWindow ? `Sells open, closing in ${dur(r.sellWindow - t)}.` : `Sells closed, open again in ${dur(r.sellPeriod - t)}.`;
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
kind: 20, id: "tick_tock", name: "Tick tock", group: "time", scanner: HONEYPOT,
|
|
368
|
+
line: "Buys only, then sells only, in turns.",
|
|
369
|
+
fields: [{ id: "turn", label: "Each turn lasts", kind: "dur", def: 300, min: 60, max: DAY }],
|
|
370
|
+
apply: (v) => ({ tickSeconds: n(v.turn) }),
|
|
371
|
+
describe: (r) => `Buy turn and sell turn swap every ${dur(r.tickSeconds)}.`,
|
|
372
|
+
live: (r, s, now) => {
|
|
373
|
+
const a = age(s, now);
|
|
374
|
+
return `${Math.floor(a / r.tickSeconds) % 2 === 0 ? "Buy" : "Sell"} turn, ${dur(r.tickSeconds - (a % r.tickSeconds))} left.`;
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
kind: 38, id: "opening", name: "Opening bell", group: "time", scanner: RED("While the bell has not rung, nobody can sell: scanners checking then see a honeypot."),
|
|
379
|
+
line: "No sells for the first while, earlier once enough of the curve is sold.",
|
|
380
|
+
fields: [
|
|
381
|
+
{ id: "secs", label: "No sells for", kind: "dur", def: 600, min: 1, max: DAY },
|
|
382
|
+
{ id: "sold", label: "Earlier once the curve sold (0 = never)", kind: "pct", def: 0, min: 0, max: 100, zeroOk: true },
|
|
383
|
+
],
|
|
384
|
+
apply: (v) => ({ bellSeconds: n(v.secs), bellSoldBps: Number(v.sold) > 0 ? bps(v.sold) : 10_000 }),
|
|
385
|
+
describe: (r) => `No sells for ${dur(r.bellSeconds)}${r.bellSoldBps < 10_000 ? `, or until the curve has sold ${pct(r.bellSoldBps)}` : ""}.`,
|
|
386
|
+
live: (r, s, now) => (age(s, now) >= r.bellSeconds || s.soldBps >= r.bellSoldBps ? "Sells are open." : `Bell rings in ${dur(r.bellSeconds - age(s, now))}.`),
|
|
387
|
+
},
|
|
388
|
+
// where it moves
|
|
389
|
+
{
|
|
390
|
+
kind: 7, id: "pool_only", name: "Pool only", alias: "Venue-locked", group: "move",
|
|
391
|
+
scanner: RED("Wallet to wallet is blocked. Aggregators that route through their own contract fail, and scanners may show transfers as blocked."),
|
|
392
|
+
line: "No wallet to wallet. The coin only moves through its pool.",
|
|
393
|
+
fields: [],
|
|
394
|
+
apply: () => ({}),
|
|
395
|
+
describe: () => "No wallet to wallet sends. Trade on PancakeSwap directly.",
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
kind: 9, id: "p2p_only", name: "P2P only", group: "move", scanner: RED('Nobody but the creator can buy from the pool: scanners show "cannot buy".'),
|
|
399
|
+
line: "Only the creator buys from the pool. Everyone else gets it hand to hand.",
|
|
400
|
+
fields: [],
|
|
401
|
+
apply: () => ({}),
|
|
402
|
+
describe: () => "Only the creator buys from the pool, everyone else gets it wallet to wallet. Selling back is open.",
|
|
403
|
+
},
|
|
404
|
+
// who gets paid
|
|
405
|
+
{
|
|
406
|
+
kind: 31, id: "rewards", name: "Buyer rewards", group: "pay", scanner: G(),
|
|
407
|
+
line: "Qualifying buys earn BNB from a pot you fill, or only every Nth buy.",
|
|
408
|
+
fields: [
|
|
409
|
+
{ id: "amount", label: "BNB per reward", kind: "bnb", def: 0.005, max: 10 },
|
|
410
|
+
{ id: "min", label: "Smallest qualifying buy (share of supply)", kind: "pct", def: 0.1, min: 0, max: 1, zeroOk: true },
|
|
411
|
+
{ id: "every", label: "Every Nth buy (1 = every buy)", kind: "int", def: 10, min: 1, max: 1000 },
|
|
412
|
+
{ id: "pot", label: "Fill the pot with", kind: "bnb", def: 0.05, max: 100, zeroOk: true, hint: "sent right after the launch" },
|
|
413
|
+
],
|
|
414
|
+
apply: (v) => ({ rewardPerBuy: wei(v.amount), rewardMinBuy: supplyShare(v.min), rewardEveryN: n(v.every) }),
|
|
415
|
+
describe: (r) => `${r.rewardEveryN > 1 ? `Every ${r.rewardEveryN}th buy` : "Every buy"} of ${tokens(fromRaw(r.rewardMinBuy))} or more earns ${bnb(Number(r.rewardPerBuy) / 1e18)}.`,
|
|
416
|
+
live: (r, s) => {
|
|
417
|
+
const left = r.rewardEveryN > 1 ? r.rewardEveryN - (s.qualifyingBuys % r.rewardEveryN) : 1;
|
|
418
|
+
return `Pot ${bnb(s.rewardsPot)}${r.rewardEveryN > 1 ? `, ${left === 1 ? "the next buy wins" : `${left} buys to the next reward`}` : ""}.`;
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
// games
|
|
422
|
+
{
|
|
423
|
+
kind: 36, id: "jackpot", name: "Jackpot", group: "games", scanner: G(),
|
|
424
|
+
line: "When nobody buys for a while, the last buyer takes the pot. You seed it, you can feed it a share of your creator fee, and anyone can top it up.",
|
|
425
|
+
fields: [
|
|
426
|
+
{ id: "quiet", label: "Pot pays after a quiet", kind: "dur", def: 300, min: 30, max: DAY },
|
|
427
|
+
{ id: "share", label: "Share of the pot paid out", kind: "pct", def: 80, min: 0.01, max: 100 },
|
|
428
|
+
{ id: "min", label: "Smallest buy that takes the lead (share of supply)", kind: "pct", def: 0.05, min: 0, max: 1, zeroOk: true },
|
|
429
|
+
{ id: "seed", label: "Seed the pot with", kind: "bnb", def: 0, max: 100, zeroOk: true, hint: "sent right after the launch" },
|
|
430
|
+
],
|
|
431
|
+
apply: (v) => ({ jackpotSilence: n(v.quiet), jackpotShareBps: bps(v.share), jackpotMinBuy: supplyShare(v.min), jackpotFeeBps: 0 }),
|
|
432
|
+
describe: (r) => `${dur(r.jackpotSilence)} without a buy and the last buyer takes ${pct(r.jackpotShareBps)} of the pot.`,
|
|
433
|
+
live: (r, s, now) => {
|
|
434
|
+
if (!s.jackpotLast)
|
|
435
|
+
return `Pot ${bnb(s.jackpotPot)}, waiting for the first buyer.`;
|
|
436
|
+
const quiet = now - s.jackpotTime;
|
|
437
|
+
return quiet >= r.jackpotSilence
|
|
438
|
+
? `Pot ${bnb(s.jackpotPot)}: ${short(s.jackpotLast)} won ${pct(r.jackpotShareBps)} of it.`
|
|
439
|
+
: `Pot ${bnb(s.jackpotPot)}, ${short(s.jackpotLast)} wins in ${dur(r.jackpotSilence - quiet)} unless someone buys.`;
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
kind: 37, id: "dip_only", name: "Dip only", group: "games", scanner: RED('Most of the time a buy fails: scanners show "cannot buy".'),
|
|
444
|
+
line: "Buys only while the price is a set share under its peak. The dev is the market maker.",
|
|
445
|
+
fields: [{ id: "dip", label: "Price under the peak by", kind: "pct", def: 20, min: 1, max: 99 }],
|
|
446
|
+
apply: (v) => ({ dipBps: bps(v.dip) }),
|
|
447
|
+
describe: (r) => `Buys only while the price is ${pct(r.dipBps)} or more under its peak. The dev can always buy.`,
|
|
448
|
+
live: (r, s) => {
|
|
449
|
+
const ratio = ((10_000 - s.peakBps) / Math.max(1, 10_000 - s.soldBps)) ** 2;
|
|
450
|
+
return ratio <= 1 - r.dipBps / 10_000 ? "In the dip: buys open." : `Price at ${Math.round(ratio * 100)}% of the peak: buys closed.`;
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
// linked coins
|
|
454
|
+
{
|
|
455
|
+
kind: 34, id: "entangled", name: "Entangled", alias: "Beacon", group: "linked",
|
|
456
|
+
scanner: RED('Buys fail until the beacon is reached: scanners show "cannot buy" until then, plus "external call".'),
|
|
457
|
+
line: "Locked until another coin of the pad has sold a set share of its supply.",
|
|
458
|
+
fields: [
|
|
459
|
+
{ id: "beacon", label: "Beacon coin (address of a coin on this pad)", kind: "address" },
|
|
460
|
+
{ id: "target", label: "Opens when the beacon has sold", kind: "pct", def: 20, min: 0.01, max: 100 },
|
|
461
|
+
],
|
|
462
|
+
apply: (v) => ({ beacon: String(v.beacon).trim(), beaconTargetBps: bps(v.target) }),
|
|
463
|
+
describe: (r) => `Buys locked until ${short(r.beacon)} has sold ${pct(r.beaconTargetBps)} of its supply.`,
|
|
464
|
+
live: (_r, s) => (s.entangledOpen ? "Unlocked for good." : "Locked, watching the beacon."),
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
kind: 35, id: "reactive", name: "Reactive", group: "linked", scanner: Y('Scanners show "anti whale" and "external call".'),
|
|
468
|
+
line: "The buy cap grows while a partner coin is bought and shrinks when it is sold.",
|
|
469
|
+
fields: [
|
|
470
|
+
{ id: "partner", label: "Partner coin (address of a coin on this pad)", kind: "address" },
|
|
471
|
+
{ id: "base", label: "Base buy cap", kind: "pct", def: 0.5, min: 0.01, max: 100 },
|
|
472
|
+
{ id: "gain", label: "Cap points per point the partner sold (%)", kind: "int", def: 10, min: 1, max: 1000 },
|
|
473
|
+
],
|
|
474
|
+
apply: (v) => ({ partner: String(v.partner).trim(), reactBaseBps: bps(v.base), reactSensitivity: n(v.gain) }),
|
|
475
|
+
describe: (r) => `Buy cap ${pct(r.reactBaseBps)}, moving ${r.reactSensitivity}% of every point ${short(r.partner)} sells or buys back.`,
|
|
476
|
+
},
|
|
477
|
+
// v2: hooks rules (each burns $hooks at launch)
|
|
478
|
+
{
|
|
479
|
+
kind: 39, id: "buyout", name: "Buyout", group: "hooks", burns: true,
|
|
480
|
+
scanner: G("Scanners see the rules that can be bought off. Once bought, their warning goes away."),
|
|
481
|
+
line: "Listed rules can be bought off for BNB. Half goes to you, half burns $hooks.",
|
|
482
|
+
fields: [
|
|
483
|
+
{ id: "rules", label: "Rules that can be bought off", kind: "rules", hint: "only rules that restrict trading" },
|
|
484
|
+
{ id: "price", label: "Price per rule", kind: "bnb", def: 1, min: 0.1, max: 100 },
|
|
485
|
+
],
|
|
486
|
+
apply: () => ({}),
|
|
487
|
+
extras: (v, picked) => {
|
|
488
|
+
const kinds = listedKinds(v.rules, picked);
|
|
489
|
+
const price = wei(v.price);
|
|
490
|
+
const prices = [0, 1, 2, 3, 4, 5, 6, 7].map((i) => (i < kinds.length ? price : BigInt(0)));
|
|
491
|
+
return { buyoutMask: maskOf(kinds), buyoutPrice: prices };
|
|
492
|
+
},
|
|
493
|
+
describe: (_r, x) => (x && x.buyoutMask ? `${namesOf(x.buyoutMask)} can be bought off for ${bnb(Number(x.buyoutPrice[0]) / 1e18)} each. Half of the price goes to the creator, half buys $hooks back and burns it. A bought rule never comes back.` : "Listed rules can be bought off for BNB. Half goes to the creator, half burns $hooks."),
|
|
494
|
+
live: (_r, s, _now, x) => {
|
|
495
|
+
if (!x || !s.v2)
|
|
496
|
+
return null;
|
|
497
|
+
const gone = kindsOf(x.buyoutMask).filter((k) => s.v2.off.includes(k));
|
|
498
|
+
return gone.length ? `Bought off so far: ${gone.map((k) => RULE_BY_KIND.get(k)?.name ?? k).join(", ")}.` : "Nothing bought off yet.";
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
kind: 40, id: "vote", name: "Vote", group: "hooks", burns: true,
|
|
503
|
+
scanner: G("Scanners see the rules that can be voted off, until they are."),
|
|
504
|
+
line: "Holders can vote listed rules off. Nobody changes the rules, except everyone together.",
|
|
505
|
+
fields: [
|
|
506
|
+
{ id: "rules", label: "Rules that can be voted off", kind: "rules", hint: "only rules that restrict trading" },
|
|
507
|
+
{ id: "quorum", label: "Quorum (share of the circulating supply on the yes side)", kind: "pct", def: 20, min: 5, max: 50 },
|
|
508
|
+
{ id: "time", label: "A vote runs for", kind: "dur", def: DAY, min: 3600, max: 7 * DAY },
|
|
509
|
+
],
|
|
510
|
+
apply: () => ({}),
|
|
511
|
+
extras: (v, picked) => ({ votable: maskOf(listedKinds(v.rules, picked)), quorumBps: bps(v.quorum), voteSeconds: n(v.time) }),
|
|
512
|
+
describe: (_r, x) => (x && x.votable ? `Holders can vote ${namesOf(x.votable)} off: coins go into the vote box for ${dur(x.voteSeconds)}, it passes with ${pct(x.quorumBps)} of the circulating supply on the yes side, a majority and at least 20 wallets. The creator's coins do not count. A lifted rule never comes back.` : "Holders can vote listed rules off."),
|
|
513
|
+
live: (_r, s, now, x) => {
|
|
514
|
+
if (!x || !s.v2)
|
|
515
|
+
return null;
|
|
516
|
+
const open = s.v2.votes.filter((v) => v.round > 0 && !v.done && now < v.end);
|
|
517
|
+
if (open.length)
|
|
518
|
+
return `Vote open on ${open.map((v) => RULE_BY_KIND.get(v.kind)?.name ?? v.kind).join(", ")}: ends in ${dur(Math.max(0, open[0].end - now))}.`;
|
|
519
|
+
const gone = kindsOf(x.votable).filter((k) => s.v2.off.includes(k));
|
|
520
|
+
return gone.length ? `Voted off so far: ${gone.map((k) => RULE_BY_KIND.get(k)?.name ?? k).join(", ")}.` : "No vote open right now.";
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
kind: 41, id: "duel", name: "Duel", group: "hooks", burns: true, scanner: G(),
|
|
525
|
+
line: "Two coins, one pot, 24 hours. The coin with more net buys wins both stakes.",
|
|
526
|
+
fields: [
|
|
527
|
+
{ id: "stake", label: "Stake", kind: "bnb", def: 0.1, min: 0.01, max: 10, hint: "the other coin stakes the same" },
|
|
528
|
+
{ id: "opponent", label: "Accept the open duel of (coin address)", kind: "address", optional: true, hint: "empty opens a new duel" },
|
|
529
|
+
],
|
|
530
|
+
apply: () => ({}),
|
|
531
|
+
extras: (v) => ({ duelStake: wei(v.stake), duelOpponent: (ADDRESS.test(String(v.opponent ?? "").trim()) ? String(v.opponent).trim() : ZERO) }),
|
|
532
|
+
describe: (_r, x) => {
|
|
533
|
+
if (!x)
|
|
534
|
+
return "Two coins, one pot, 24 hours.";
|
|
535
|
+
const base = `${bnb(Number(x.duelStake) / 1e18)} staked against another coin for 24 hours. The coin with more net buys through its pools wins both stakes: 95% buys the winner and burns it, 5% feeds the pickhooks token.`;
|
|
536
|
+
return x.duelOpponent !== ZERO ? `${base} This coin accepted the duel of ${short(x.duelOpponent)}.` : `${base} A duel nobody accepts within an hour pays the stake back.`;
|
|
537
|
+
},
|
|
538
|
+
live: (_r, s, now) => {
|
|
539
|
+
const d = s.v2?.duel;
|
|
540
|
+
if (!d)
|
|
541
|
+
return null;
|
|
542
|
+
if (d.settled)
|
|
543
|
+
return d.winner ? `Settled: ${short(d.winner)} won ${bnb(d.stake * 2 * 0.95)} of buys.` : "Settled with a tie or a refund.";
|
|
544
|
+
if (!d.opponent)
|
|
545
|
+
return now < d.openedAt + 3600 ? `Open for ${dur(d.openedAt + 3600 - now)}: another coin can accept.` : "Nobody accepted: the stake can be refunded.";
|
|
546
|
+
const end = d.startedAt + 86_400;
|
|
547
|
+
const lead = d.flowA === d.flowB ? "tied" : `${d.flowA > d.flowB ? "this coin" : short(d.opponent)} leads`;
|
|
548
|
+
return now < end ? `Against ${short(d.opponent)}, ${lead}, ${dur(end - now)} to go.` : d.closed ? `Over: ${d.winner ? `${short(d.winner)} won` : "a tie"}, waiting for the settle.` : "The 24 hours are over: anyone can close it.";
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
kind: 42, id: "sells_at", name: "Sells at", group: "hooks", burns: true,
|
|
553
|
+
scanner: RED("Until the target or the deadline nobody can sell. Scanners mark that as a honeypot."),
|
|
554
|
+
line: "Sells open when a price feed reaches a target, or at a deadline. Diamond hands, enforced by the market.",
|
|
555
|
+
fields: [
|
|
556
|
+
{ id: "feed", label: "Price feed (Chainlink)", kind: "choice", def: 0, options: [{ value: 0, label: "BNB" }, { value: 1, label: "BTC" }, { value: 2, label: "ETH" }, { value: 3, label: "DOGE" }] },
|
|
557
|
+
{ id: "target", label: "Target price", kind: "usd", def: 1000, min: 0.0001, max: 100_000_000, hint: "must be above the price at launch" },
|
|
558
|
+
{ id: "deadline", label: "Sells open at the latest after", kind: "dur", def: 7 * DAY, min: 3600, max: 7 * DAY },
|
|
559
|
+
],
|
|
560
|
+
apply: () => ({}),
|
|
561
|
+
extras: (v) => ({ feed: feedAddress(v.feed), target: BigInt(Math.round(Number(v.target) * 1e8)), sellsAtSeconds: n(v.deadline) }),
|
|
562
|
+
describe: (_r, x) => (x && x.feed !== ZERO ? `Nobody can sell until ${feedName(x.feed)} reaches ${usd(Number(x.target) / 1e8)}, or ${dur(x.sellsAtSeconds)} after launch at the latest. Once open, it stays open. A feed that stops updating opens sells too.` : "Sells open when a price feed reaches a target, or at a deadline."),
|
|
563
|
+
live: (_r, s, now, x) => {
|
|
564
|
+
if (!x || !s.v2)
|
|
565
|
+
return null;
|
|
566
|
+
if (s.v2.off.includes(42))
|
|
567
|
+
return "Open: the target was reached.";
|
|
568
|
+
if (now >= s.start + x.sellsAtSeconds)
|
|
569
|
+
return "Open: the deadline passed.";
|
|
570
|
+
return `${feedName(x.feed)} at ${s.v2.feedPrice === null ? "?" : usd(s.v2.feedPrice)} now, target ${usd(Number(x.target) / 1e8)}, deadline in ${dur(s.start + x.sellsAtSeconds - now)}.`;
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
// v2: trust rules (free)
|
|
574
|
+
{
|
|
575
|
+
kind: 43, id: "dead_man", name: "Dead man", group: "trust", scanner: G("Only the creator's bag can be burned. Scanners see nothing."),
|
|
576
|
+
line: "If the dev vanishes, the dev's bag burns.",
|
|
577
|
+
fields: [{ id: "time", label: "Burnable after this long without a sign of life", kind: "dur", def: 7 * DAY, min: 3 * DAY, max: 30 * DAY }],
|
|
578
|
+
apply: () => ({}),
|
|
579
|
+
extras: (v) => ({ deadManSeconds: n(v.time) }),
|
|
580
|
+
describe: (_r, x) => `If the creator shows no sign of life for ${x ? dur(x.deadManSeconds) : "a while"} (a trade, a send or a tap on "still here"), anyone can burn the creator's whole bag. The creator cannot park it elsewhere: sends count against the same lock as sells.`,
|
|
581
|
+
live: (_r, s, now, x) => {
|
|
582
|
+
if (!x || !s.v2)
|
|
583
|
+
return null;
|
|
584
|
+
const left = s.v2.devAlive + x.deadManSeconds - now;
|
|
585
|
+
return left > 0 ? `Creator last seen ${dur(Math.max(0, now - s.v2.devAlive))} ago. The bag can be burned in ${dur(left)}.` : "The creator went quiet: the bag can be burned now.";
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
kind: 44, id: "milestone", name: "Milestone unlock", group: "trust", scanner: G("Only the creator is locked. Scanners see nothing."),
|
|
590
|
+
line: "The dev gets paid when you do: the dev's bag unlocks with the BNB in the pool.",
|
|
591
|
+
fields: [],
|
|
592
|
+
apply: () => ({}),
|
|
593
|
+
extras: () => ({}),
|
|
594
|
+
describe: () => "The creator's bag unlocks with the BNB in the pool: a quarter at 2 BNB, half at 5, three quarters at 10, all of it at 20, and everything after 30 days. A level counts after it held for 10 minutes, and if the pool drains, the lock closes again.",
|
|
595
|
+
live: (_r, s) => (s.v2 ? `${s.v2.milestonePct}% of the creator's peak bag may leave, ${tokens(s.v2.devOutTokens)} left so far.` : null),
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
kind: 45, id: "floor_fund", name: "Floor fund", group: "trust", scanner: G(),
|
|
599
|
+
line: "The coin defends its own floor. Anyone can pull the trigger.",
|
|
600
|
+
fields: [
|
|
601
|
+
{ id: "deposit", label: "Deposit at launch", kind: "bnb", def: 0.5, min: 0.1, max: 10 },
|
|
602
|
+
{ id: "floor", label: "Floor, as a share of the peak price", kind: "choice", def: 5000, options: [{ value: 2500, label: "25%" }, { value: 5000, label: "50%" }, { value: 7500, label: "75%" }] },
|
|
603
|
+
],
|
|
604
|
+
apply: () => ({}),
|
|
605
|
+
extras: (v) => ({ floorDeposit: wei(v.deposit), floorBps: n(v.floor) }),
|
|
606
|
+
describe: (_r, x) => (x && x.floorBps ? `${bnb(Number(x.floorDeposit) / 1e18)} sit in a vault. When the half-hour average price falls under ${pct(x.floorBps)} of the peak hourly average, anyone can make the vault buy a tenth of its deposit and burn the coins, once an hour. The caller gets 0.5% of the buy.` : "The coin defends its own floor from a vault filled at launch."),
|
|
607
|
+
live: (_r, s, now) => {
|
|
608
|
+
const f = s.v2?.floor;
|
|
609
|
+
if (!f)
|
|
610
|
+
return null;
|
|
611
|
+
const rest = f.lastDefend + 3600 - now;
|
|
612
|
+
return `${bnb(f.left)} left of ${bnb(f.deposit)}.${f.peakPct === null ? " Waiting for the first hour of price history." : f.belowFloor ? (rest > 0 ? ` Under the floor, next defence in ${dur(rest)}.` : " Under the floor: the vault can buy now.") : " Above the floor."}`;
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
kind: 46, id: "payroll", name: "Payroll", group: "trust", scanner: G(),
|
|
617
|
+
line: "Hold your shift, get paid. Sell and you lose what is unpaid.",
|
|
618
|
+
fields: [
|
|
619
|
+
{ id: "shift", label: "Shift length", kind: "choice", def: 28_800, options: [{ value: 14_400, label: "4 hours" }, { value: 28_800, label: "8 hours" }, { value: 86_400, label: "24 hours" }] },
|
|
620
|
+
{ id: "min", label: "Smallest bag on the payroll (share of supply)", kind: "pct", def: 0.001, min: 0.0001, max: 1, hint: "0.0001% is 1,000 coins" },
|
|
621
|
+
{ id: "deposit", label: "Pot at launch", kind: "bnb", def: 0.1, max: 100, zeroOk: true, hint: "95% of your creator fee feeds it from then on" },
|
|
622
|
+
],
|
|
623
|
+
apply: () => ({}),
|
|
624
|
+
extras: (v) => ({ shiftSeconds: n(v.shift), payrollMin: supplyShare(v.min), payrollDeposit: wei(v.deposit) }),
|
|
625
|
+
describe: (_r, x) => (x && x.shiftSeconds ? `Hold at least ${pct(shareOf(x.payrollMin))} of supply through a whole ${SHIFT_LABEL[x.shiftSeconds] ?? dur(x.shiftSeconds)} shift and get paid in BNB: every shift pays a tenth of the pot over all coins on the payroll. Coins that join earn from the next shift. Sell or send, and the unpaid wages go back to the pot. 95% of the creator's fee feeds the pot.` : "Hold your shift, get paid in BNB."),
|
|
626
|
+
live: (_r, s, now) => {
|
|
627
|
+
const p = s.v2?.payroll;
|
|
628
|
+
if (!p)
|
|
629
|
+
return null;
|
|
630
|
+
const next = p.lastShift + p.shiftSeconds - now;
|
|
631
|
+
return `Pot ${bnb(p.pot)}, ${tokens(p.total)} on the payroll, shift ${p.shiftNo + 1} ${next > 0 ? `pays in ${dur(next)}` : "can be paid now"}.`;
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
];
|
|
635
|
+
/** the catalog the site shows: the eight v2 rules only once the v2 factory is set */
|
|
636
|
+
export const RULES_SHOWN = V2.factory ? RULES : RULES.filter((r) => r.kind < 39);
|
|
637
|
+
export const GROUPS_SHOWN = GROUPS.filter((g) => RULES_SHOWN.some((r) => r.group === g.id));
|
|
638
|
+
export const RULE_BY_KIND = new Map(RULES.map((r) => [r.kind, r]));
|
|
639
|
+
export const RULE_BY_ID = new Map(RULES.map((r) => [r.id, r]));
|
|
640
|
+
export function defaults(def) {
|
|
641
|
+
const v = {};
|
|
642
|
+
for (const f of def.fields)
|
|
643
|
+
v[f.id] = f.kind === "address" || f.kind === "rules" ? "" : f.def;
|
|
644
|
+
return v;
|
|
645
|
+
}
|
|
646
|
+
const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
647
|
+
/** Checks the values against the same limits the factory enforces. Returns a sentence or null. */
|
|
648
|
+
export function fieldProblem(def, v) {
|
|
649
|
+
for (const f of def.fields) {
|
|
650
|
+
const raw = v[f.id];
|
|
651
|
+
const val = Number(raw);
|
|
652
|
+
switch (f.kind) {
|
|
653
|
+
case "pct": {
|
|
654
|
+
const min = f.zeroOk ? 0 : (f.min ?? 0.01);
|
|
655
|
+
if (!Number.isFinite(val) || val < min || val > (f.max ?? 100) || (val === 0 && !f.zeroOk))
|
|
656
|
+
return `${f.label}: ${min} to ${f.max ?? 100}%.`;
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
case "dur":
|
|
660
|
+
if (!Number.isFinite(val) || val < (f.min ?? 0) || (val === 0 && !f.zeroOk) || val > f.max)
|
|
661
|
+
return `${f.label}: ${dur(f.min ?? 1)} to ${dur(f.max)}.`;
|
|
662
|
+
break;
|
|
663
|
+
case "int":
|
|
664
|
+
if (!Number.isInteger(val) || val < f.min || val > f.max)
|
|
665
|
+
return `${f.label}: ${f.min} to ${f.max}.`;
|
|
666
|
+
break;
|
|
667
|
+
case "bnb":
|
|
668
|
+
if (!Number.isFinite(val) || val < (f.min ?? 0) || (val === 0 && !f.zeroOk) || val > f.max)
|
|
669
|
+
return f.min ? `${f.label}: ${f.min} to ${f.max} BNB.` : `${f.label}: up to ${f.max} BNB.`;
|
|
670
|
+
break;
|
|
671
|
+
case "usd":
|
|
672
|
+
if (!Number.isFinite(val) || val < f.min || val > f.max)
|
|
673
|
+
return `${f.label}: ${f.min} to ${f.max.toLocaleString("en-US")} USD.`;
|
|
674
|
+
break;
|
|
675
|
+
case "choice":
|
|
676
|
+
if (!f.options.some((o) => o.value === val))
|
|
677
|
+
return `${f.label}: pick one.`;
|
|
678
|
+
break;
|
|
679
|
+
case "rules":
|
|
680
|
+
if (!String(raw ?? "").split(",").some((s) => Number(s.trim()) > 0))
|
|
681
|
+
return `${f.label}: pick at least one.`;
|
|
682
|
+
break;
|
|
683
|
+
case "gwei":
|
|
684
|
+
if (!Number.isFinite(val) || val < 0.05 || val > f.max)
|
|
685
|
+
return `${f.label}: 0.05 to ${f.max} gwei.`;
|
|
686
|
+
break;
|
|
687
|
+
case "address":
|
|
688
|
+
if (f.optional && String(raw ?? "").trim() === "")
|
|
689
|
+
break;
|
|
690
|
+
if (typeof raw !== "string" || !ADDRESS.test(raw.trim()))
|
|
691
|
+
return `${f.label}: paste a 0x address.`;
|
|
692
|
+
break;
|
|
693
|
+
case "hour":
|
|
694
|
+
if (!Number.isInteger(val) || val < 0 || val > 23)
|
|
695
|
+
return `${f.label}: an hour from 0 to 23.`;
|
|
696
|
+
break;
|
|
697
|
+
case "days":
|
|
698
|
+
if (!Number.isInteger(val) || val <= 0 || val > 127)
|
|
699
|
+
return "Pick at least one day.";
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const e = { ...emptyRules(), ...def.apply(v) };
|
|
704
|
+
if (def.id === "sell_curve" && e.curveFloorBps > e.curveSmallBps)
|
|
705
|
+
return "The big bag cap cannot be bigger than the small bag cap.";
|
|
706
|
+
if (def.id === "taper" && e.taperEndBps > e.taperStartBps)
|
|
707
|
+
return "The end cap cannot be bigger than the start cap.";
|
|
708
|
+
if (def.id === "sell_window" && e.sellWindow >= e.sellPeriod)
|
|
709
|
+
return "The window must be shorter than the period.";
|
|
710
|
+
if (def.id === "hours" && e.openHour === e.closeHour)
|
|
711
|
+
return "Opening and closing hour must differ.";
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
/** Checks a whole selection the way the factory does. */
|
|
715
|
+
export function selectionProblem(picked) {
|
|
716
|
+
if (!picked.length)
|
|
717
|
+
return null;
|
|
718
|
+
const kinds = picked.map((p) => RULE_BY_ID.get(p.id)?.kind ?? 0);
|
|
719
|
+
if (kinds.includes(0))
|
|
720
|
+
return "Unknown rule.";
|
|
721
|
+
if (picked.length > MAX_RULES)
|
|
722
|
+
return `Up to ${MAX_RULES} rules per coin.`;
|
|
723
|
+
if (new Set(kinds).size !== kinds.length)
|
|
724
|
+
return "Each rule once.";
|
|
725
|
+
if (kinds.includes(7) && kinds.includes(9))
|
|
726
|
+
return "Pool only and P2P only cannot be combined.";
|
|
727
|
+
if (kinds.includes(34) && kinds.includes(35))
|
|
728
|
+
return "One linked rule per coin.";
|
|
729
|
+
// time gates that together can shut sells for good: the live token factory accepts them, so the launcher refuses them
|
|
730
|
+
// (the pool factory refuses them on chain as well)
|
|
731
|
+
const sellsAlwaysOpen = picked.some((p) => p.id === "hours" && Boolean(p.values.sells));
|
|
732
|
+
if (kinds.includes(20) && kinds.includes(16))
|
|
733
|
+
return "Tick tock and Sell window cannot be combined: sells could stay shut for good.";
|
|
734
|
+
if (kinds.includes(15) && !sellsAlwaysOpen && kinds.includes(20))
|
|
735
|
+
return "Tick tock and Market hours only work together when sells stay open.";
|
|
736
|
+
if (kinds.includes(15) && !sellsAlwaysOpen && kinds.includes(16))
|
|
737
|
+
return "Sell window and Market hours only work together when sells stay open.";
|
|
738
|
+
// with P2P only nobody but the creator buys from the pools, so only the creator could ever unlock sells
|
|
739
|
+
if (kinds.includes(9) && kinds.includes(14))
|
|
740
|
+
return "P2P only and Buys unlock sells cannot be combined: only the creator could ever unlock sells.";
|
|
741
|
+
// a jackpot that ends keeps its pot, and the creator fee share keeps feeding it, with nobody left to win it
|
|
742
|
+
if (picked.some((p) => p.id === "jackpot" && p.endsAfter))
|
|
743
|
+
return "Jackpot cannot have an end time: its pot would keep filling with nobody left to win it.";
|
|
744
|
+
if (picked.some((p) => p.id === "rewards" && p.endsAfter))
|
|
745
|
+
return "Buyer rewards cannot have an end time: nobody could earn its pot after that.";
|
|
746
|
+
// the v2 factory's validator: traps that could stop every buy or sell for good, and what belongs together
|
|
747
|
+
const has = (k) => kinds.includes(k);
|
|
748
|
+
if (has(9) && has(14))
|
|
749
|
+
return "P2P only and Buys unlock sells cannot be combined: only the creator could ever unlock sells.";
|
|
750
|
+
if (has(14) && (has(34) || has(8)))
|
|
751
|
+
return "Buys unlock sells needs open buying: not with Entangled or Holder gated.";
|
|
752
|
+
if (has(14) && has(17) && has(23))
|
|
753
|
+
return "The club, Buy limit and Buys unlock sells together could lock sells for good.";
|
|
754
|
+
if (has(18)) {
|
|
755
|
+
const r = picked.reduce((acc, p) => Object.assign(acc, RULE_BY_ID.get(p.id).apply(p.values)), emptyRules());
|
|
756
|
+
let cap = 10_000;
|
|
757
|
+
if (has(1) && r.maxWalletBps < cap)
|
|
758
|
+
cap = r.maxWalletBps;
|
|
759
|
+
if (has(2) && r.maxTradeBps < cap)
|
|
760
|
+
cap = r.maxTradeBps;
|
|
761
|
+
if (has(3) && r.dumpBuyBps < cap)
|
|
762
|
+
cap = r.dumpBuyBps;
|
|
763
|
+
if (r.minBuy > (BigInt(SUPPLY) * E18 * BigInt(cap)) / BigInt(10_000))
|
|
764
|
+
return "Min buy is above the buy caps: nobody but the creator could ever buy.";
|
|
765
|
+
}
|
|
766
|
+
if (has(9) && has(43))
|
|
767
|
+
return "P2P only and Dead man cannot be combined: nobody but the creator could ever hold.";
|
|
768
|
+
if (has(44) && has(21))
|
|
769
|
+
return "Milestone unlock replaces Dev lock: pick one of them.";
|
|
770
|
+
if (has(46) && has(36))
|
|
771
|
+
return "Payroll and Jackpot cannot be combined: a coin has one pot.";
|
|
772
|
+
for (const p of picked) {
|
|
773
|
+
const def = RULE_BY_ID.get(p.id);
|
|
774
|
+
if (p.endsAfter && NO_UNTIL.includes(def.kind) && (def.kind >= 39 || V2.factory))
|
|
775
|
+
return `${def.name} cannot have an end time.`;
|
|
776
|
+
if ((p.id === "buyout" || p.id === "vote") && listedKinds(p.values.rules, picked).length === 0) {
|
|
777
|
+
return `${def.name}: pick at least one rule of this coin that restricts trading (not a trust rule, game, pot or linked coin).`;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
for (const p of picked) {
|
|
781
|
+
const def = RULE_BY_ID.get(p.id);
|
|
782
|
+
const problem = fieldProblem(def, p.values);
|
|
783
|
+
if (problem)
|
|
784
|
+
return `${def.name}: ${problem}`;
|
|
785
|
+
if (p.endsAfter !== undefined && p.endsAfter !== 0 && (!(p.endsAfter > 0) || p.endsAfter > 365 * DAY))
|
|
786
|
+
return `${def.name}: end time up to a year.`;
|
|
787
|
+
}
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
/** The launch arguments for the factory: the Rules struct, the end times, and for the v2 factory the Extras. */
|
|
791
|
+
export function buildRules(picked, nowSec, creatorKeepBps = 10_000) {
|
|
792
|
+
const rules = emptyRules();
|
|
793
|
+
const extras = { ...emptyExtras(), creatorKeepBps };
|
|
794
|
+
const untilIds = [];
|
|
795
|
+
const untilTimes = [];
|
|
796
|
+
for (const p of picked) {
|
|
797
|
+
const def = RULE_BY_ID.get(p.id);
|
|
798
|
+
Object.assign(rules, def.apply(p.values));
|
|
799
|
+
if (def.extras)
|
|
800
|
+
Object.assign(extras, def.extras(p.values, picked));
|
|
801
|
+
rules.mask |= bit(def.kind);
|
|
802
|
+
if (p.endsAfter) {
|
|
803
|
+
untilIds.push(def.kind);
|
|
804
|
+
untilTimes.push(nowSec + Math.round(p.endsAfter) + 120); // the launch lands a little later
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return { rules, untilIds, untilTimes, extras };
|
|
808
|
+
}
|
|
809
|
+
/** v2: how many hooks rules a selection carries (each burns $hooks at launch), and the BNB the launch deposits besides the seed. */
|
|
810
|
+
export function v2Costs(picked) {
|
|
811
|
+
let hooksRules = 0;
|
|
812
|
+
let stakeBnb = 0;
|
|
813
|
+
let floorBnb = 0;
|
|
814
|
+
let payrollBnb = 0;
|
|
815
|
+
let v2 = false;
|
|
816
|
+
for (const p of picked) {
|
|
817
|
+
const def = RULE_BY_ID.get(p.id);
|
|
818
|
+
if (!def)
|
|
819
|
+
continue;
|
|
820
|
+
if (def.kind >= 39)
|
|
821
|
+
v2 = true;
|
|
822
|
+
if (def.burns)
|
|
823
|
+
hooksRules++;
|
|
824
|
+
if (p.id === "duel")
|
|
825
|
+
stakeBnb += Number(p.values.stake || 0);
|
|
826
|
+
if (p.id === "floor_fund")
|
|
827
|
+
floorBnb += Number(p.values.deposit || 0);
|
|
828
|
+
if (p.id === "payroll")
|
|
829
|
+
payrollBnb += Number(p.values.deposit || 0);
|
|
830
|
+
}
|
|
831
|
+
return { hooksRules, stakeBnb, floorBnb, payrollBnb, v2 };
|
|
832
|
+
}
|
|
833
|
+
/** BNB the launcher sends into pots right after the launch. */
|
|
834
|
+
export function launchPots(picked) {
|
|
835
|
+
let jackpot = 0;
|
|
836
|
+
let rewards = 0;
|
|
837
|
+
for (const p of picked) {
|
|
838
|
+
if (p.id === "jackpot")
|
|
839
|
+
jackpot += Number(p.values.seed || 0);
|
|
840
|
+
if (p.id === "rewards")
|
|
841
|
+
rewards += Number(p.values.pot || 0);
|
|
842
|
+
}
|
|
843
|
+
return { jackpot, rewards };
|
|
844
|
+
}
|
|
845
|
+
/** The worst scanner level in a selection. */
|
|
846
|
+
export function worstScanner(kinds) {
|
|
847
|
+
const levels = kinds.map((k) => RULE_BY_KIND.get(k)?.scanner.level ?? "green");
|
|
848
|
+
return levels.includes("red") ? "red" : levels.includes("yellow") ? "yellow" : "green";
|
|
849
|
+
}
|
|
850
|
+
/** Rule kinds that are on in a mask. */
|
|
851
|
+
export const kindsOf = (mask) => RULES.filter((r) => (mask & bit(r.kind)) !== BigInt(0)).map((r) => r.kind);
|
|
852
|
+
/** The rule's current sentence, or that it ended. */
|
|
853
|
+
export function ruleNow(kind, r, s, until, now, x) {
|
|
854
|
+
const def = RULE_BY_KIND.get(kind);
|
|
855
|
+
if (!def)
|
|
856
|
+
return { name: `Rule ${kind}`, text: "Unknown rule.", live: null, ended: false, lifted: false, scanner: G() };
|
|
857
|
+
// v2: a rule that was bought off, voted off or (sells at) reached its target is over for good
|
|
858
|
+
const lifted = Boolean(s.v2?.off.includes(kind));
|
|
859
|
+
const ended = (until !== 0 && now >= until) || lifted;
|
|
860
|
+
let live = null;
|
|
861
|
+
if (!ended && def.live) {
|
|
862
|
+
try {
|
|
863
|
+
live = def.live(r, s, now, x);
|
|
864
|
+
}
|
|
865
|
+
catch {
|
|
866
|
+
live = null;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const ends = until && !ended ? ` Ends in ${dur(until - now)}.` : "";
|
|
870
|
+
return { name: def.name, text: def.describe(r, x) + ends, live, ended, lifted, scanner: def.scanner };
|
|
871
|
+
}
|
|
872
|
+
/** Names of the rules a token error refers to, for "rule: max wallet" messages. */
|
|
873
|
+
export function refusal(kind, r) {
|
|
874
|
+
const def = RULE_BY_KIND.get(kind);
|
|
875
|
+
if (!def)
|
|
876
|
+
return `Refused by rule ${kind}.`;
|
|
877
|
+
return `Refused by the rule "${def.name}". ${r ? def.describe(r) : def.line}`;
|
|
878
|
+
}
|