clockwork-press 0.0.1 → 0.2.1

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/dist/ledger.js ADDED
@@ -0,0 +1,97 @@
1
+ // The site's live data: press-ledger.json (every press, forever) and
2
+ // press-stats.json (the status-bar numbers). The workflow commits both after
3
+ // a press, Vercel redeploys, and the terminal reads them — nothing is
4
+ // reported, it is read.
5
+ import { readFileSync, writeFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { cfg } from './env.js';
8
+ import { boardSeats } from './holders.js';
9
+ import { readRank } from './gmerank.js';
10
+ export function costBasis() {
11
+ let sc = 0, sg = 0, fc = 0, tg = 0;
12
+ for (const p of readArchive()) {
13
+ const st = Number(p.stashedGme || 0), fed = Number(p.burnGmeSpent || 0), px = Number(p.gmeUsd || 0);
14
+ tg += st;
15
+ if (px > 0) {
16
+ sc += st * px;
17
+ sg += st;
18
+ fc += fed * px;
19
+ }
20
+ }
21
+ const r = (x) => Math.round(x * 100) / 100;
22
+ return { stashCostUsd: r(sc), stashAvgUsd: sg > 0 ? r(sc / sg) : 0, fedCostUsd: r(fc), pricedGme: r(sg), totalGme: r(tg) };
23
+ }
24
+ const ledgerPath = () => join(cfg.siteDir, 'press-ledger.json');
25
+ export const statsPath = () => join(cfg.siteDir, "press-stats.json");
26
+ const archivePath = () => join(cfg.siteDir, 'press-ledger-all.json');
27
+ function readArchive() {
28
+ try {
29
+ return JSON.parse(readFileSync(archivePath(), 'utf8')).presses;
30
+ }
31
+ catch {
32
+ return readLedger().presses;
33
+ }
34
+ }
35
+ function sumTotals(all) {
36
+ const t = { press: 0, snack: 0, burnGme: 0, stashedGme: 0, burnedGmerald: 0 };
37
+ for (const p of all) {
38
+ t[(p.kind ?? 'press')]++;
39
+ t.burnGme += Number(p.burnGmeSpent || 0);
40
+ t.stashedGme += Number(p.stashedGme || 0);
41
+ t.burnedGmerald += Number(p.burnedGmerald || 0);
42
+ }
43
+ return t;
44
+ }
45
+ export const totals = () => readLedger().totals ?? sumTotals(readArchive());
46
+ // Per-day rollup of snacks (utc days), last 7 days, for the site's today/yesterday lines.
47
+ export function dailyRollup() {
48
+ const out = {};
49
+ for (const p of readArchive()) {
50
+ if (p.kind !== 'snack')
51
+ continue;
52
+ const d = p.ts.slice(0, 10);
53
+ out[d] ??= { snacks: 0, gme: 0, burned: 0 };
54
+ out[d].snacks++;
55
+ out[d].gme += Number(p.burnGmeSpent || 0);
56
+ out[d].burned += Number(p.burnedGmerald || 0);
57
+ }
58
+ return Object.fromEntries(Object.entries(out).sort().slice(-7));
59
+ }
60
+ const RECENT_SNACKS = 48, RECENT_PRESSES = 12;
61
+ export function readLedger() {
62
+ try {
63
+ return JSON.parse(readFileSync(ledgerPath(), 'utf8'));
64
+ }
65
+ catch {
66
+ return { presses: [] };
67
+ }
68
+ }
69
+ export function writeLedger(ledger) { writeFileSync(ledgerPath(), JSON.stringify(ledger, null, 1)); }
70
+ export const countKind = (kind) => totals()[kind];
71
+ export const hasTx = (tx) => readArchive().some((p) => p.stashTx === tx || p.burnTx === tx);
72
+ export function appendPress(entry) {
73
+ const all = readArchive();
74
+ const kind = entry.kind ?? 'press';
75
+ const full = {
76
+ n: all.length + 1,
77
+ ts: new Date().toISOString(),
78
+ explorer: cfg.explorer,
79
+ ...entry,
80
+ kind,
81
+ k: all.filter((p) => (p.kind ?? 'press') === kind).length + 1,
82
+ };
83
+ all.push(full);
84
+ writeFileSync(archivePath(), JSON.stringify({ presses: all }, null, 1));
85
+ const ledger = readLedger();
86
+ const presses = all.filter((p) => (p.kind ?? 'press') === 'press').slice(-RECENT_PRESSES);
87
+ const snacks = all.filter((p) => p.kind === 'snack').slice(-RECENT_SNACKS);
88
+ ledger.presses = [...presses, ...snacks].sort((a, b) => a.n - b.n);
89
+ ledger.totals = sumTotals(all);
90
+ writeFileSync(ledgerPath(), JSON.stringify(ledger, null, 1));
91
+ return full;
92
+ }
93
+ export function writeStats(stats) {
94
+ writeFileSync(statsPath(), JSON.stringify({ ...stats, seats: boardSeats(), gmeRank: readRank(), basis: costBasis() }, null, 1));
95
+ }
96
+ // Cumulative GME the burns pushed into the curve/pool — part of "GME sunk".
97
+ export function burnGmeTotal() { return totals().burnGme; }
package/dist/market.js ADDED
@@ -0,0 +1,19 @@
1
+ export async function readMarket(poolId) {
2
+ try {
3
+ const res = await fetch(`https://api.dexscreener.com/latest/dex/pairs/robinhood/${poolId}`);
4
+ if (!res.ok)
5
+ return null;
6
+ const j = (await res.json());
7
+ const p = (j.pairs ?? [j.pair])[0];
8
+ if (!p?.priceNative)
9
+ return null;
10
+ const now = Number(p.priceNative);
11
+ const ch = p.priceChange ?? {};
12
+ const at = (k) => (ch[k] == null ? now : now / (1 + Number(ch[k]) / 100));
13
+ const high = Math.max(now, at('h1'), at('h6'), at('h24'));
14
+ return { priceGme: now, recentHighGme: high, vsHighBps: Math.round((now / high - 1) * 10000), h1: Number(ch.h1 ?? 0) };
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
package/dist/peg.js ADDED
@@ -0,0 +1,56 @@
1
+ // The ops-leg gate: "never a forced seller." Fair value comes from the
2
+ // Chainlink tokenized-equity feed; the on-chain token price comes from
3
+ // dexscreener (keyless). When either half is unavailable the answer is
4
+ // 'unchecked' and the caller HOLDS the ops slice — the conservative default.
5
+ import { cfg } from './env.js';
6
+ import { pub } from './chain.js';
7
+ import { feedAbi } from './abis.js';
8
+ // the pools that can set the token price: the stock token as base, a dollar as quote, real depth
9
+ const USD = new Set(['USDG', 'USDC', 'USDT', 'USD1', 'DAI']);
10
+ export async function checkPeg() {
11
+ if (!cfg.pegFeed)
12
+ return { status: 'unchecked', note: 'PEG_FEED_ADDRESS not set' };
13
+ try {
14
+ const [round, dec] = await Promise.all([
15
+ pub.readContract({ address: cfg.pegFeed, abi: feedAbi, functionName: 'latestRoundData' }),
16
+ pub.readContract({ address: cfg.pegFeed, abi: feedAbi, functionName: 'decimals' }),
17
+ ]);
18
+ const fairUsd = Number(round[1]) / 10 ** Number(dec);
19
+ const feedAt = Number(round[3]);
20
+ if (!(fairUsd > 0))
21
+ return { status: 'unchecked', note: 'feed returned no price' };
22
+ const res = await fetch(`https://api.dexscreener.com/latest/dex/tokens/${cfg.gme}`);
23
+ if (!res.ok)
24
+ return { status: 'unchecked', note: `dexscreener ${res.status}`, fairUsd };
25
+ const data = (await res.json());
26
+ const pairs = (data.pairs ?? []).filter((p) => p.priceUsd && p.baseToken?.address?.toLowerCase() === cfg.gme.toLowerCase()
27
+ && USD.has((p.quoteToken?.symbol ?? '').toUpperCase()) && (p.liquidity?.usd ?? 0) >= 20000);
28
+ if (!pairs.length)
29
+ return { status: 'unchecked', note: 'no usd pools for the stock token', fairUsd, feedAt };
30
+ // weighted by the hour's volume, so a pool nobody traded in cannot set the price; the deepest pool if nothing traded
31
+ let num = 0, den = 0, n = 0;
32
+ for (const p of pairs) {
33
+ const w = p.volume?.h1 ?? 0;
34
+ if (w > 0) {
35
+ num += Number(p.priceUsd) * w;
36
+ den += w;
37
+ n++;
38
+ }
39
+ }
40
+ pairs.sort((a, b) => (b.liquidity?.usd ?? 0) - (a.liquidity?.usd ?? 0));
41
+ const tokenUsd = den > 0 ? num / den : Number(pairs[0].priceUsd);
42
+ const source = den > 0 ? `${n} pool${n === 1 ? '' : 's'}, weighted by the hour's volume` : 'deepest pool, no trades this hour';
43
+ const premiumBps = Math.round((tokenUsd / fairUsd - 1) * 10000);
44
+ const status = premiumBps >= -cfg.pegToleranceBps ? 'at-or-above' : 'below';
45
+ return {
46
+ status,
47
+ fairUsd,
48
+ tokenUsd,
49
+ premiumBps, feedAt, source,
50
+ note: `token $${tokenUsd.toFixed(2)} (${source}) vs fair $${fairUsd.toFixed(2)} (chainlink ${new Date(feedAt * 1000).toISOString().slice(11, 16)} utc): ${premiumBps >= 0 ? '+' : ''}${(premiumBps / 100).toFixed(2)}%`,
51
+ };
52
+ }
53
+ catch (e) {
54
+ return { status: 'unchecked', note: `peg check failed: ${e.message}` };
55
+ }
56
+ }
package/dist/pons.js ADDED
@@ -0,0 +1,203 @@
1
+ // pons v2, read and written from one place: where the launch is, who the fees point at, what is unswept
2
+ // on the curve or the hook, what the escrow has credited, the buyback vest, and the handback.
3
+ // money in and out is raw units of the asset (bigint); callers format with cfg.pairDecimals.
4
+ // nothing here signs without a key: every writer checks cfg.key and cfg.dry before touching wallet().
5
+ import { keccak256, encodeAbiParameters, BaseError, ContractFunctionRevertedError } from 'viem';
6
+ import { cfg } from './env.js';
7
+ import { pub, wallet } from './chain.js';
8
+ import { factoryAbi, curveAbi, hookAbi, escrowAbi, vaultAbi } from './abis.js';
9
+ import { poolKey } from './v4.js';
10
+ // the hook and the vault come from env.ts (clockwork.json's launch.hook / launch.vault, or the shared pons v2
11
+ // addresses on robinhood chain when the file does not name them), the same place the factory and escrow live.
12
+ const ZERO = '0x0000000000000000000000000000000000000000';
13
+ const token = () => cfg.token;
14
+ const escrow = () => cfg.escrow;
15
+ // a launch quoted in native eth has the zero address as its pair; the escrow keeps a separate ledger for it.
16
+ const pairNative = () => cfg.pairNative;
17
+ const PHASE = ['curve', 'swept', 'pool', 'rescued'];
18
+ // the standard uniswap v4 pool id: keccak of the abi-encoded PoolKey. the key is the one v4.ts swaps
19
+ // through (currencies sorted, fee 0, the record's tick spacing, the pons hook), so the id the hook
20
+ // reports pendings under is the id the press trades on.
21
+ export function poolIdFor(t) {
22
+ const k = poolKey(t);
23
+ return keccak256(encodeAbiParameters([{ type: 'address' }, { type: 'address' }, { type: 'uint24' }, { type: 'int24' }, { type: 'address' }], [k.currency0, k.currency1, k.fee, k.tickSpacing, k.hooks]));
24
+ }
25
+ // a revert, in words a founder can act on. viem decodes the custom error when the abi names it and
26
+ // otherwise hands back the bare selector.
27
+ export function reason(e) {
28
+ let name = '', sig = '';
29
+ if (e instanceof BaseError) {
30
+ const r = e.walk((x) => x instanceof ContractFunctionRevertedError);
31
+ name = r?.data?.errorName ?? '';
32
+ sig = r?.signature ?? '';
33
+ }
34
+ if (name === 'InternalSwapRequiresOperator')
35
+ return 'waiting on pons: the sweep needs the operator';
36
+ if (name === 'NotCreatorFeeRecipient')
37
+ return 'the machine wallet is not the fee recipient';
38
+ if (name === 'NotVestBeneficiary')
39
+ return 'the machine wallet is not the vest beneficiary';
40
+ if (name)
41
+ return `reverted: ${name}`;
42
+ if (sig)
43
+ return `reverted with ${sig} (the contract refused; the caller is likely not the creator or the operator)`;
44
+ const m = e?.shortMessage || e?.message || String(e);
45
+ return String(m).split('\n')[0];
46
+ }
47
+ export async function readLaunch() {
48
+ const [lt, pend] = await Promise.all([
49
+ pub.readContract({ address: cfg.factory, abi: factoryAbi, functionName: 'getLaunchedToken', args: [token()] }),
50
+ pub.readContract({ address: cfg.factory, abi: factoryAbi, functionName: 'pendingCreatorFeeRecipient', args: [token()] }),
51
+ ]);
52
+ const phase = (lt.phase <= 3 ? lt.phase : 3);
53
+ let graduationPct = null;
54
+ if (lt.exists && phase === 0) {
55
+ // progress along the curve: what has really been paid in against the threshold, capped at 100
56
+ const raised = await pub.readContract({ address: lt.curve, abi: curveAbi, functionName: 'realQuoteReserve' });
57
+ graduationPct = lt.graduationThreshold > 0n ? Math.min(100, Number((raised * 10000n) / lt.graduationThreshold) / 100) : 0;
58
+ }
59
+ const [to, effectiveAt, expiresAt] = pend;
60
+ return {
61
+ exists: lt.exists, phase, phaseWord: PHASE[phase], graduationPct,
62
+ curve: lt.curve, recipient: lt.creatorFeeRecipient,
63
+ pending: to.toLowerCase() === ZERO && effectiveAt === 0n ? null : { to, effectiveAt: Number(effectiveAt), expiresAt: Number(expiresAt) },
64
+ buybackEnabled: lt.buybackEnabled, pairToken: lt.pairToken,
65
+ };
66
+ }
67
+ // what has been earned but not yet swept into the escrow, in the pair currency. quote is the pool or curve
68
+ // fee before pons splits it (protocol / buyback / creator), tax is the creator tax, all of which is the
69
+ // creator's. tokenSide is fees denominated in the launch token: only the operator can convert those.
70
+ export async function unswept(s) {
71
+ const zero = { quote: 0n, tax: 0n, total: 0n, tokenSide: 0n };
72
+ try {
73
+ if (!s.exists)
74
+ return zero;
75
+ if (s.phase === 0) {
76
+ const [quote, tax] = await Promise.all([
77
+ pub.readContract({ address: s.curve, abi: curveAbi, functionName: 'quoteFeeBalance' }),
78
+ pub.readContract({ address: s.curve, abi: curveAbi, functionName: 'creatorTaxBalance' }),
79
+ ]);
80
+ return { quote, tax, total: quote + tax, tokenSide: 0n };
81
+ }
82
+ if (s.phase === 2) {
83
+ const id = poolIdFor(token());
84
+ const read = (fn, cur) => pub.readContract({ address: cfg.hook, abi: hookAbi, functionName: fn, args: [id, cur] });
85
+ const [quote, tax, tf, tt] = await Promise.all([read('pendingFees', cfg.gme), read('pendingCreatorTax', cfg.gme), read('pendingFees', token()), read('pendingCreatorTax', token())]);
86
+ return { quote, tax, total: quote + tax, tokenSide: tf + tt };
87
+ }
88
+ return zero;
89
+ }
90
+ catch (e) {
91
+ console.log(`[pons] unswept read failed: ${reason(e)}`);
92
+ return zero;
93
+ }
94
+ }
95
+ async function waitTx(hash) {
96
+ const rcpt = await pub.waitForTransactionReceipt({ hash });
97
+ if (rcpt.status !== 'success')
98
+ throw new Error(`tx reverted: ${hash}`);
99
+ return hash;
100
+ }
101
+ // can this run sign? dry runs and keyless runs read and simulate only.
102
+ const canWrite = () => !!cfg.key && !cfg.dry;
103
+ // the sweep the creator may do itself: sweepFees(0) on the curve, sweepPoolFees(poolId, 0, 0) on the hook.
104
+ // simulated first from the machine wallet; pons refuses with InternalSwapRequiresOperator when a buyback
105
+ // or a launch-token conversion is involved, and that is not a failure, it is a wait.
106
+ // send: false is the diagnostic path (claimcheck): it says whether the sweep would pass and never signs.
107
+ // onSend runs once the simulation passed and a transaction is about to go out (the press stamps lastRunAt there).
108
+ export async function trySweep(s, opts = {}) {
109
+ if (!s.exists)
110
+ return { swept: false, reason: 'no launch record' };
111
+ const call = s.phase === 0
112
+ ? { address: s.curve, abi: curveAbi, functionName: 'sweepFees', args: [0n] }
113
+ : s.phase === 2
114
+ ? { address: cfg.hook, abi: hookAbi, functionName: 'sweepPoolFees', args: [poolIdFor(token()), 0n, 0n] }
115
+ : null;
116
+ if (!call)
117
+ return { swept: false, reason: `nothing to sweep in phase ${s.phaseWord}` };
118
+ try {
119
+ await pub.simulateContract({ ...call, account: cfg.pressWallet });
120
+ }
121
+ catch (e) {
122
+ return { swept: false, reason: reason(e) };
123
+ }
124
+ if (opts.send === false)
125
+ return { swept: false, reason: 'the sweep would pass; this job only reads, so it is not sent' };
126
+ if (!canWrite())
127
+ return { swept: false, reason: cfg.dry ? 'dry: the sweep would pass, not sent' : 'no key: the sweep would pass, not sent' };
128
+ try {
129
+ opts.onSend?.();
130
+ const tx = await waitTx(await wallet().writeContract({ ...call }));
131
+ return { swept: true, tx };
132
+ }
133
+ catch (e) {
134
+ return { swept: false, reason: reason(e) };
135
+ }
136
+ }
137
+ // what the escrow has credited to a recipient: the pair ledger (native or per-token) and the launch-token
138
+ // ledger, where released vests land.
139
+ export async function owed(recipient) {
140
+ if (!cfg.escrow)
141
+ return { pair: 0n, token: 0n };
142
+ const [pair, tok] = await Promise.all([
143
+ pairNative()
144
+ ? pub.readContract({ address: escrow(), abi: escrowAbi, functionName: 'balanceOf', args: [recipient] })
145
+ : pub.readContract({ address: escrow(), abi: escrowAbi, functionName: 'balanceOfToken', args: [recipient, cfg.gme] }),
146
+ pub.readContract({ address: escrow(), abi: escrowAbi, functionName: 'balanceOfToken', args: [recipient, token()] }),
147
+ ]);
148
+ return { pair, token: tok };
149
+ }
150
+ const claimCall = () => pairNative()
151
+ ? { address: escrow(), abi: escrowAbi, functionName: 'claim' }
152
+ : { address: escrow(), abi: escrowAbi, functionName: 'claimToken', args: [cfg.gme] };
153
+ const claimTokenCall = () => ({ address: escrow(), abi: escrowAbi, functionName: 'claimToken', args: [token()] });
154
+ // a write, or in dry mode the simulation of it. null when nothing was sent. the simulation runs first in
155
+ // every mode, so a call the contract would refuse (NotVestBeneficiary, NotCreatorFeeRecipient) throws a
156
+ // decoded reason before anything is signed; callers on the press path catch it and carry on.
157
+ async function write(call, what) {
158
+ await pub.simulateContract({ ...call, account: cfg.pressWallet });
159
+ if (!canWrite()) {
160
+ console.log(`[pons] ${cfg.dry ? 'dry' : 'no key'}: would ${what}`);
161
+ return null;
162
+ }
163
+ return waitTx(await wallet().writeContract({ ...call }));
164
+ }
165
+ export async function claimPair() {
166
+ if (!cfg.escrow)
167
+ return null;
168
+ return write(claimCall(), `claim the ${cfg.pairSymbol} the escrow holds for the machine wallet`);
169
+ }
170
+ // the buyback vest: zeros when the launch has buybacks off, else what the vault can release now and what
171
+ // an earlier release already credited in the escrow under the launch token.
172
+ export async function vestStatus(s) {
173
+ const st = s ?? await readLaunch();
174
+ if (!st.exists || !st.buybackEnabled)
175
+ return { releasable: 0n, escrowToken: 0n };
176
+ const [releasable, o] = await Promise.all([
177
+ pub.readContract({ address: cfg.vault, abi: vaultAbi, functionName: 'releasable', args: [token()] }),
178
+ owed(cfg.pressWallet),
179
+ ]);
180
+ return { releasable, escrowToken: o.token };
181
+ }
182
+ export async function releaseVest() {
183
+ return write({ address: cfg.vault, abi: vaultAbi, functionName: 'release', args: [token()] }, 'release the buyback vest');
184
+ }
185
+ export async function claimVestTokens() {
186
+ if (!cfg.escrow)
187
+ return null;
188
+ return write(claimTokenCall(), `claim the ${cfg.tokenSymbol} the escrow holds from released vests`);
189
+ }
190
+ // hand the fees back: claim what is already credited (a transfer does not move it), then move the
191
+ // recipient. only the current recipient may call; the machine must be it.
192
+ export async function handback(to) {
193
+ const o = cfg.escrow ? await owed(cfg.pressWallet) : { pair: 0n, token: 0n };
194
+ let claimed = null;
195
+ if (o.pair > 0n)
196
+ claimed = await claimPair();
197
+ if (o.token > 0n) {
198
+ const t = await claimVestTokens();
199
+ claimed = claimed ?? t;
200
+ }
201
+ const moved = await write({ address: cfg.factory, abi: factoryAbi, functionName: 'transferCreatorFeeRecipient', args: [token(), to] }, `move the fee recipient from ${cfg.pressWallet} to ${to}`);
202
+ return { claimed, moved };
203
+ }