clockwork-press 0.0.1 → 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/dist/quote.js ADDED
@@ -0,0 +1,40 @@
1
+ // The curve exposes no quote function — this is the curve's own integer
2
+ // arithmetic, in the same order, copied from the PONS.md appendix. Buys charge every
3
+ // fee off the input before the constant-product step.
4
+ import { cfg } from './env.js';
5
+ import { pub } from './chain.js';
6
+ import { curveAbi } from './abis.js';
7
+ const ceilDiv = (a, b) => (a + b - 1n) / b;
8
+ const amountOut = (inAmt, rIn, rOut) => (inAmt * rOut) / (rIn + inAmt);
9
+ const amountIn = (outAmt, rIn, rOut) => (outAmt * rIn) / (rOut - outAmt) + 1n;
10
+ export async function quoteBuy(curve, quoteIn, recipient) {
11
+ const read = (functionName, args = []) => pub.readContract({ address: curve, abi: curveAbi, functionName, args });
12
+ const [reserves, sellable, feeBps, creatorTaxBps, rawSnipeBps] = await Promise.all([
13
+ read('getReserves'),
14
+ read('sellableTokens'),
15
+ read('feeBps'),
16
+ read('creatorTaxBps'),
17
+ read('currentSnipeTaxBps', [recipient]),
18
+ ]);
19
+ const [quoteReserve, tokenReserve] = reserves;
20
+ // The snipe tax is capped so the buyer always nets at least 1% of spend.
21
+ let snipeBps = rawSnipeBps;
22
+ if (snipeBps > 0n) {
23
+ const maxSnipeBps = cfg.BPS - feeBps - creatorTaxBps - 100n;
24
+ if (snipeBps > maxSnipeBps)
25
+ snipeBps = maxSnipeBps;
26
+ }
27
+ let spent = quoteIn;
28
+ const fee = (spent * feeBps) / cfg.BPS;
29
+ const tax = (spent * creatorTaxBps) / cfg.BPS;
30
+ const snipe = (spent * snipeBps) / cfg.BPS;
31
+ let tokensOut = amountOut(spent - fee - tax - snipe, quoteReserve, tokenReserve);
32
+ // A buy that would cross the reserved allocation fills to the edge.
33
+ if (tokensOut > sellable) {
34
+ tokensOut = sellable;
35
+ const net = amountIn(sellable, quoteReserve, tokenReserve);
36
+ const grossed = ceilDiv(net * cfg.BPS, cfg.BPS - feeBps - creatorTaxBps - snipeBps);
37
+ spent = grossed < quoteIn ? grossed : quoteIn;
38
+ }
39
+ return { tokensOut, spent, refund: quoteIn - spent, snipeBps };
40
+ }
@@ -0,0 +1,45 @@
1
+ import { cfg } from './env.js';
2
+ // The art the cards ride on comes from clockwork.json (brand.art); an empty slot means a text-only card.
3
+ export const ART = { press: cfg.art.claim || '', snack: cfg.art.slice || '', burn: cfg.art.percent || '', buy: cfg.art.buy || '', newHolder: cfg.art.newHolder || '' };
4
+ async function call(method, body) {
5
+ const res = await fetch(`https://api.telegram.org/bot${cfg.tgToken}/${method}`, {
6
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ chat_id: cfg.tgChat, ...body }),
7
+ });
8
+ if (!res.ok)
9
+ console.error(`[telegram] ${method} failed: ${res.status} ${await res.text()}`);
10
+ return res.ok;
11
+ }
12
+ export async function post(text) {
13
+ if (!cfg.tgToken || !cfg.tgChat) {
14
+ console.log('[telegram] not configured, would have posted:\n' + text);
15
+ return;
16
+ }
17
+ await call('sendMessage', { text, disable_web_page_preview: true });
18
+ }
19
+ // A photo (jpg/png url) or an animation (mp4 url) with the text as its caption; plain text if telegram refuses the media.
20
+ export async function postMedia(url, caption) {
21
+ if (!cfg.tgToken || !cfg.tgChat) {
22
+ console.log(`[telegram] not configured, would have posted ${url}:\n` + caption);
23
+ return;
24
+ }
25
+ const isVideo = /\.mp4($|\?)/i.test(url);
26
+ const ok = await call(isVideo ? 'sendAnimation' : 'sendPhoto', { [isVideo ? 'animation' : 'photo']: url, caption: caption.slice(0, 1000) });
27
+ if (!ok)
28
+ await call('sendMessage', { text: caption, disable_web_page_preview: true });
29
+ }
30
+ // A card: the art on top, clean lines under it, the first line bold. Values are escaped, so any text is safe.
31
+ const esc = (t) => t.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
32
+ export async function postCard(url, lines) {
33
+ const ls = lines.filter((l) => !!l).map(esc);
34
+ if (!ls.length)
35
+ return;
36
+ const html = [`<b>${ls[0]}</b>`, ...ls.slice(1)].join('\n');
37
+ if (!cfg.tgToken || !cfg.tgChat) {
38
+ console.log(`[telegram] not configured, would have posted ${url}:\n` + ls.join('\n'));
39
+ return;
40
+ }
41
+ const isVideo = /\.mp4($|\?)/i.test(url);
42
+ const ok = url ? await call(isVideo ? 'sendAnimation' : 'sendPhoto', { [isVideo ? 'animation' : 'photo']: url, caption: html.slice(0, 1000), parse_mode: 'HTML' }) : false;
43
+ if (!ok)
44
+ await call('sendMessage', { text: html, parse_mode: 'HTML', disable_web_page_preview: true });
45
+ }
package/dist/v4.js ADDED
@@ -0,0 +1,87 @@
1
+ // The burn leg in pool phase: GME -> $GMERALD through Uniswap v4 on Robinhood
2
+ // Chain via the Universal Router (V4_SWAP), quoted first with the v4 Quoter.
3
+ // Addresses from developers.uniswap.org/contracts/v4/deployments (chain 4663).
4
+ import { encodeAbiParameters, parseAbi, maxUint160 } from 'viem';
5
+ const MAX_UINT48 = 281474976710655; // 2^48 - 1, a JS number because viem maps uint48 to number
6
+ import { cfg } from './env.js';
7
+ import { pub, wallet, account } from './chain.js';
8
+ export const V4 = {
9
+ poolManager: '0x8366a39cc670b4001a1121b8f6a443a643e40951',
10
+ universalRouter: '0x8876789976decbfcbbbe364623c63652db8c0904',
11
+ quoter: '0x8dc178efb8111bb0973dd9d722ebeff267c98f94',
12
+ permit2: '0x000000000022D473030F116dDEE9F6B43aC78BA3',
13
+ tickSpacing: 200,
14
+ };
15
+ const quoterAbi = parseAbi([
16
+ 'struct PoolKey { address currency0; address currency1; uint24 fee; int24 tickSpacing; address hooks; }',
17
+ 'struct QuoteExactSingleParams { PoolKey poolKey; bool zeroForOne; uint128 exactAmount; bytes hookData; }',
18
+ 'function quoteExactInputSingle(QuoteExactSingleParams params) returns (uint256 amountOut, uint256 gasEstimate)',
19
+ ]);
20
+ const routerAbi = parseAbi(['function execute(bytes commands, bytes[] inputs, uint256 deadline) payable']);
21
+ const permit2Abi = parseAbi([
22
+ 'function allowance(address user, address token, address spender) view returns (uint160 amount, uint48 expiration, uint48 nonce)',
23
+ 'function approve(address token, address spender, uint160 amount, uint48 expiration)',
24
+ ]);
25
+ const erc20 = parseAbi(['function allowance(address,address) view returns (uint256)', 'function approve(address,uint256) returns (bool)']);
26
+ export function poolKey(token) {
27
+ const a = cfg.gme.toLowerCase(), b = token.toLowerCase();
28
+ const [currency0, currency1] = a < b ? [cfg.gme, token] : [token, cfg.gme];
29
+ return { currency0, currency1, fee: 0, tickSpacing: V4.tickSpacing, hooks: cfg.hook, zeroForOne: a < b };
30
+ }
31
+ export async function quoteGmeToToken(token, amountIn) {
32
+ const k = poolKey(token);
33
+ const { result } = await pub.simulateContract({
34
+ address: V4.quoter, abi: quoterAbi, functionName: 'quoteExactInputSingle',
35
+ args: [{ poolKey: { currency0: k.currency0, currency1: k.currency1, fee: k.fee, tickSpacing: k.tickSpacing, hooks: k.hooks }, zeroForOne: k.zeroForOne, exactAmount: amountIn, hookData: '0x' }],
36
+ });
37
+ return result[0];
38
+ }
39
+ // Gas: the chain's base fee moves fast and viem's default cap can land under the next block's base fee
40
+ // ("fee cap cannot be lower than the block base fee"). Cap at three times the current base fee; the chain
41
+ // only charges what the block actually costs.
42
+ async function fees() {
43
+ const b = await pub.getBlock();
44
+ const base = b.baseFeePerGas ?? 1000000000n;
45
+ let prio = 0n;
46
+ try {
47
+ prio = await pub.estimateMaxPriorityFeePerGas();
48
+ }
49
+ catch { }
50
+ return { maxFeePerGas: base * 3n + prio, maxPriorityFeePerGas: prio };
51
+ }
52
+ // Universal Router: command 0x10 = V4_SWAP; v4 actions 0x07 SWAP_EXACT_IN, 0x0c SETTLE_ALL, 0x0f TAKE_ALL.
53
+ export async function swapGmeToToken(token, amountIn, minOut) {
54
+ const w = wallet();
55
+ const me = account().address;
56
+ const k = poolKey(token);
57
+ // Permit2 is how the router pulls ERC-20 input: approve Permit2 once, then Permit2 -> router.
58
+ const erc20Allow = await pub.readContract({ address: cfg.gme, abi: erc20, functionName: 'allowance', args: [me, V4.permit2] });
59
+ if (erc20Allow < amountIn) {
60
+ const h = await w.writeContract({ address: cfg.gme, abi: erc20, functionName: 'approve', args: [V4.permit2, 2n ** 256n - 1n], ...(await fees()) });
61
+ await pub.waitForTransactionReceipt({ hash: h });
62
+ }
63
+ const [p2Amt, p2Exp] = await pub.readContract({ address: V4.permit2, abi: permit2Abi, functionName: 'allowance', args: [me, cfg.gme, V4.universalRouter] });
64
+ if (p2Amt < amountIn || Number(p2Exp) < Math.floor(Date.now() / 1000) + 600) {
65
+ const h = await w.writeContract({ address: V4.permit2, abi: permit2Abi, functionName: 'approve', args: [cfg.gme, V4.universalRouter, maxUint160, MAX_UINT48], ...(await fees()) });
66
+ await pub.waitForTransactionReceipt({ hash: h });
67
+ }
68
+ // This chain's Universal Router carries a non-standard swap struct: an extra `bytes`
69
+ // field sits between the path and the amounts (read off a live trade, tx 0x2ee2...c3a0).
70
+ // The standard single-hop layout reverts inside the router's decoder, so we use the
71
+ // path form with that field empty: SWAP_EXACT_IN (0x07), then SETTLE_ALL, TAKE_ALL.
72
+ const swapParams = encodeAbiParameters([{ type: 'tuple', components: [
73
+ { type: 'address', name: 'currencyIn' },
74
+ { type: 'tuple[]', name: 'path', components: [{ type: 'address', name: 'intermediateCurrency' }, { type: 'uint24', name: 'fee' }, { type: 'int24', name: 'tickSpacing' }, { type: 'address', name: 'hooks' }, { type: 'bytes', name: 'hookData' }] },
75
+ { type: 'bytes', name: 'extra' }, { type: 'uint128', name: 'amountIn' }, { type: 'uint128', name: 'amountOutMinimum' }
76
+ ] }], [{ currencyIn: cfg.gme, path: [{ intermediateCurrency: token, fee: k.fee, tickSpacing: k.tickSpacing, hooks: k.hooks, hookData: '0x' }], extra: '0x', amountIn, amountOutMinimum: minOut }]);
77
+ const settle = encodeAbiParameters([{ type: 'address' }, { type: 'uint256' }], [cfg.gme, amountIn]);
78
+ const take = encodeAbiParameters([{ type: 'address' }, { type: 'uint256' }], [token, minOut]);
79
+ const actions = '0x070c0f';
80
+ const input = encodeAbiParameters([{ type: 'bytes' }, { type: 'bytes[]' }], [actions, [swapParams, settle, take]]);
81
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + 600);
82
+ const hash = await w.writeContract({ address: V4.universalRouter, abi: routerAbi, functionName: 'execute', args: ['0x10', [input], deadline], ...(await fees()) });
83
+ const rcpt = await pub.waitForTransactionReceipt({ hash });
84
+ if (rcpt.status !== 'success')
85
+ throw new Error(`v4 swap reverted: ${hash}`);
86
+ return hash;
87
+ }