imm-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +315 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +112 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +251 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/immbook.d.ts +16 -0
- package/dist/commands/immbook.d.ts.map +1 -0
- package/dist/commands/immbook.js +795 -0
- package/dist/commands/immbook.js.map +1 -0
- package/dist/commands/jaine.d.ts +3 -0
- package/dist/commands/jaine.d.ts.map +1 -0
- package/dist/commands/jaine.js +1397 -0
- package/dist/commands/jaine.js.map +1 -0
- package/dist/commands/send.d.ts +3 -0
- package/dist/commands/send.d.ts.map +1 -0
- package/dist/commands/send.js +229 -0
- package/dist/commands/send.js.map +1 -0
- package/dist/commands/setup.d.ts +3 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +83 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/slop-app.d.ts +9 -0
- package/dist/commands/slop-app.d.ts.map +1 -0
- package/dist/commands/slop-app.js +793 -0
- package/dist/commands/slop-app.js.map +1 -0
- package/dist/commands/slop.d.ts +3 -0
- package/dist/commands/slop.d.ts.map +1 -0
- package/dist/commands/slop.js +1053 -0
- package/dist/commands/slop.js.map +1 -0
- package/dist/commands/wallet.d.ts +3 -0
- package/dist/commands/wallet.d.ts.map +1 -0
- package/dist/commands/wallet.js +298 -0
- package/dist/commands/wallet.js.map +1 -0
- package/dist/config/paths.d.ts +6 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +24 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/store.d.ts +44 -0
- package/dist/config/store.d.ts.map +1 -0
- package/dist/config/store.js +109 -0
- package/dist/config/store.js.map +1 -0
- package/dist/constants/chain.d.ts +56 -0
- package/dist/constants/chain.d.ts.map +1 -0
- package/dist/constants/chain.js +50 -0
- package/dist/constants/chain.js.map +1 -0
- package/dist/errors.d.ts +86 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +100 -0
- package/dist/errors.js.map +1 -0
- package/dist/immbook/api.d.ts +38 -0
- package/dist/immbook/api.d.ts.map +1 -0
- package/dist/immbook/api.js +86 -0
- package/dist/immbook/api.js.map +1 -0
- package/dist/immbook/auth.d.ts +31 -0
- package/dist/immbook/auth.d.ts.map +1 -0
- package/dist/immbook/auth.js +93 -0
- package/dist/immbook/auth.js.map +1 -0
- package/dist/immbook/comments.d.ts +26 -0
- package/dist/immbook/comments.d.ts.map +1 -0
- package/dist/immbook/comments.js +20 -0
- package/dist/immbook/comments.js.map +1 -0
- package/dist/immbook/follows.d.ts +19 -0
- package/dist/immbook/follows.d.ts.map +1 -0
- package/dist/immbook/follows.js +21 -0
- package/dist/immbook/follows.js.map +1 -0
- package/dist/immbook/jwtCache.d.ts +15 -0
- package/dist/immbook/jwtCache.d.ts.map +1 -0
- package/dist/immbook/jwtCache.js +63 -0
- package/dist/immbook/jwtCache.js.map +1 -0
- package/dist/immbook/points.d.ts +35 -0
- package/dist/immbook/points.d.ts.map +1 -0
- package/dist/immbook/points.js +20 -0
- package/dist/immbook/points.js.map +1 -0
- package/dist/immbook/posts.d.ts +46 -0
- package/dist/immbook/posts.d.ts.map +1 -0
- package/dist/immbook/posts.js +43 -0
- package/dist/immbook/posts.js.map +1 -0
- package/dist/immbook/profile.d.ts +29 -0
- package/dist/immbook/profile.d.ts.map +1 -0
- package/dist/immbook/profile.js +14 -0
- package/dist/immbook/profile.js.map +1 -0
- package/dist/immbook/submolts.d.ts +22 -0
- package/dist/immbook/submolts.d.ts.map +1 -0
- package/dist/immbook/submolts.js +24 -0
- package/dist/immbook/submolts.js.map +1 -0
- package/dist/immbook/tradeProof.d.ts +21 -0
- package/dist/immbook/tradeProof.d.ts.map +1 -0
- package/dist/immbook/tradeProof.js +14 -0
- package/dist/immbook/tradeProof.js.map +1 -0
- package/dist/immbook/votes.d.ts +17 -0
- package/dist/immbook/votes.d.ts.map +1 -0
- package/dist/immbook/votes.js +20 -0
- package/dist/immbook/votes.js.map +1 -0
- package/dist/intents/store.d.ts +22 -0
- package/dist/intents/store.d.ts.map +1 -0
- package/dist/intents/store.js +76 -0
- package/dist/intents/store.js.map +1 -0
- package/dist/intents/types.d.ts +21 -0
- package/dist/intents/types.d.ts.map +1 -0
- package/dist/intents/types.js +2 -0
- package/dist/intents/types.js.map +1 -0
- package/dist/jaine/abi/erc20.d.ts +90 -0
- package/dist/jaine/abi/erc20.d.ts.map +1 -0
- package/dist/jaine/abi/erc20.js +65 -0
- package/dist/jaine/abi/erc20.js.map +1 -0
- package/dist/jaine/abi/factory.d.ts +38 -0
- package/dist/jaine/abi/factory.d.ts.map +1 -0
- package/dist/jaine/abi/factory.js +26 -0
- package/dist/jaine/abi/factory.js.map +1 -0
- package/dist/jaine/abi/index.d.ts +11 -0
- package/dist/jaine/abi/index.d.ts.map +1 -0
- package/dist/jaine/abi/index.js +11 -0
- package/dist/jaine/abi/index.js.map +1 -0
- package/dist/jaine/abi/nftManager.d.ts +282 -0
- package/dist/jaine/abi/nftManager.d.ts.map +1 -0
- package/dist/jaine/abi/nftManager.js +182 -0
- package/dist/jaine/abi/nftManager.js.map +1 -0
- package/dist/jaine/abi/pool.d.ts +77 -0
- package/dist/jaine/abi/pool.d.ts.map +1 -0
- package/dist/jaine/abi/pool.js +56 -0
- package/dist/jaine/abi/pool.js.map +1 -0
- package/dist/jaine/abi/quoter.d.ts +84 -0
- package/dist/jaine/abi/quoter.d.ts.map +1 -0
- package/dist/jaine/abi/quoter.js +53 -0
- package/dist/jaine/abi/quoter.js.map +1 -0
- package/dist/jaine/abi/router.d.ts +135 -0
- package/dist/jaine/abi/router.d.ts.map +1 -0
- package/dist/jaine/abi/router.js +88 -0
- package/dist/jaine/abi/router.js.map +1 -0
- package/dist/jaine/abi/w0g.d.ts +41 -0
- package/dist/jaine/abi/w0g.d.ts.map +1 -0
- package/dist/jaine/abi/w0g.js +34 -0
- package/dist/jaine/abi/w0g.js.map +1 -0
- package/dist/jaine/allowance.d.ts +48 -0
- package/dist/jaine/allowance.d.ts.map +1 -0
- package/dist/jaine/allowance.js +192 -0
- package/dist/jaine/allowance.js.map +1 -0
- package/dist/jaine/coreTokens.d.ts +32 -0
- package/dist/jaine/coreTokens.d.ts.map +1 -0
- package/dist/jaine/coreTokens.js +91 -0
- package/dist/jaine/coreTokens.js.map +1 -0
- package/dist/jaine/pathEncoding.d.ts +39 -0
- package/dist/jaine/pathEncoding.d.ts.map +1 -0
- package/dist/jaine/pathEncoding.js +98 -0
- package/dist/jaine/pathEncoding.js.map +1 -0
- package/dist/jaine/paths.d.ts +11 -0
- package/dist/jaine/paths.d.ts.map +1 -0
- package/dist/jaine/paths.js +20 -0
- package/dist/jaine/paths.js.map +1 -0
- package/dist/jaine/poolCache.d.ts +42 -0
- package/dist/jaine/poolCache.d.ts.map +1 -0
- package/dist/jaine/poolCache.js +164 -0
- package/dist/jaine/poolCache.js.map +1 -0
- package/dist/jaine/routing.d.ts +41 -0
- package/dist/jaine/routing.d.ts.map +1 -0
- package/dist/jaine/routing.js +247 -0
- package/dist/jaine/routing.js.map +1 -0
- package/dist/jaine/userTokens.d.ts +27 -0
- package/dist/jaine/userTokens.d.ts.map +1 -0
- package/dist/jaine/userTokens.js +89 -0
- package/dist/jaine/userTokens.js.map +1 -0
- package/dist/slop/abi/factory.d.ts +128 -0
- package/dist/slop/abi/factory.d.ts.map +1 -0
- package/dist/slop/abi/factory.js +70 -0
- package/dist/slop/abi/factory.js.map +1 -0
- package/dist/slop/abi/feeCollector.d.ts +95 -0
- package/dist/slop/abi/feeCollector.d.ts.map +1 -0
- package/dist/slop/abi/feeCollector.js +71 -0
- package/dist/slop/abi/feeCollector.js.map +1 -0
- package/dist/slop/abi/index.d.ts +5 -0
- package/dist/slop/abi/index.d.ts.map +1 -0
- package/dist/slop/abi/index.js +5 -0
- package/dist/slop/abi/index.js.map +1 -0
- package/dist/slop/abi/registry.d.ts +135 -0
- package/dist/slop/abi/registry.d.ts.map +1 -0
- package/dist/slop/abi/registry.js +90 -0
- package/dist/slop/abi/registry.js.map +1 -0
- package/dist/slop/abi/token.d.ts +320 -0
- package/dist/slop/abi/token.d.ts.map +1 -0
- package/dist/slop/abi/token.js +251 -0
- package/dist/slop/abi/token.js.map +1 -0
- package/dist/slop/quote.d.ts +80 -0
- package/dist/slop/quote.d.ts.map +1 -0
- package/dist/slop/quote.js +174 -0
- package/dist/slop/quote.js.map +1 -0
- package/dist/utils/canonicalJson.d.ts +8 -0
- package/dist/utils/canonicalJson.d.ts.map +1 -0
- package/dist/utils/canonicalJson.js +20 -0
- package/dist/utils/canonicalJson.js.map +1 -0
- package/dist/utils/env.d.ts +11 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +20 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/http.d.ts +19 -0
- package/dist/utils/http.d.ts.map +1 -0
- package/dist/utils/http.js +61 -0
- package/dist/utils/http.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +21 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/output.d.ts +19 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +37 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/respond.d.ts +19 -0
- package/dist/utils/respond.d.ts.map +1 -0
- package/dist/utils/respond.js +25 -0
- package/dist/utils/respond.js.map +1 -0
- package/dist/utils/ui.d.ts +38 -0
- package/dist/utils/ui.d.ts.map +1 -0
- package/dist/utils/ui.js +126 -0
- package/dist/utils/ui.js.map +1 -0
- package/dist/wallet/client.d.ts +4 -0
- package/dist/wallet/client.d.ts.map +1 -0
- package/dist/wallet/client.js +53 -0
- package/dist/wallet/client.js.map +1 -0
- package/dist/wallet/keystore.d.ts +21 -0
- package/dist/wallet/keystore.d.ts.map +1 -0
- package/dist/wallet/keystore.js +111 -0
- package/dist/wallet/keystore.js.map +1 -0
- package/package.json +56 -0
- package/skills/imm/SKILL.md +617 -0
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { isAddress, getAddress, parseUnits, formatUnits, decodeEventLog, } from "viem";
|
|
4
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
5
|
+
import { createWalletClient, http } from "viem";
|
|
6
|
+
import { loadConfig } from "../config/store.js";
|
|
7
|
+
import { getPublicClient } from "../wallet/client.js";
|
|
8
|
+
import { loadKeystore, decryptPrivateKey } from "../wallet/keystore.js";
|
|
9
|
+
import { requireKeystorePassword } from "../utils/env.js";
|
|
10
|
+
import { ImmError, ErrorCodes } from "../errors.js";
|
|
11
|
+
import { isHeadless, writeJsonSuccess, writeStderr } from "../utils/output.js";
|
|
12
|
+
import { spinner, successBox, infoBox, colors, formatBalance, createTable } from "../utils/ui.js";
|
|
13
|
+
import { SLOP_FACTORY_ABI } from "../slop/abi/factory.js";
|
|
14
|
+
import { SLOP_TOKEN_ABI } from "../slop/abi/token.js";
|
|
15
|
+
import { SLOP_REGISTRY_ABI } from "../slop/abi/registry.js";
|
|
16
|
+
import { SLOP_FEE_COLLECTOR_ABI } from "../slop/abi/feeCollector.js";
|
|
17
|
+
import { calculateOgOut, calculatePartialFill, applySlippage, calculateGraduationProgress, } from "../slop/quote.js";
|
|
18
|
+
// ============ HELPERS ============
|
|
19
|
+
function parseIntSafe(value, name) {
|
|
20
|
+
const n = parseInt(value, 10);
|
|
21
|
+
if (!Number.isFinite(n)) {
|
|
22
|
+
throw new ImmError(ErrorCodes.INVALID_AMOUNT, `Invalid ${name}: ${value}`);
|
|
23
|
+
}
|
|
24
|
+
return n;
|
|
25
|
+
}
|
|
26
|
+
function parseUnitsSafe(value, decimals, name) {
|
|
27
|
+
try {
|
|
28
|
+
const result = parseUnits(value, decimals);
|
|
29
|
+
if (result < 0n) {
|
|
30
|
+
throw new ImmError(ErrorCodes.INVALID_AMOUNT, `${name} must be >= 0`);
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
if (err instanceof ImmError)
|
|
36
|
+
throw err;
|
|
37
|
+
throw new ImmError(ErrorCodes.INVALID_AMOUNT, `Invalid ${name}: ${value}`, "Must be a valid decimal number (e.g., 0.01, 100)");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function validateUserSalt(salt) {
|
|
41
|
+
// Check format: 0x + 64 hex chars
|
|
42
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(salt)) {
|
|
43
|
+
throw new ImmError(ErrorCodes.INVALID_AMOUNT, "Invalid userSalt format", "Must be 32 bytes hex (0x + 64 hex characters)");
|
|
44
|
+
}
|
|
45
|
+
// Check non-zero (contract requires non-zero salt)
|
|
46
|
+
if (BigInt(salt) === 0n) {
|
|
47
|
+
throw new ImmError(ErrorCodes.INVALID_AMOUNT, "userSalt cannot be zero", "Provide a non-zero 32-byte hex value");
|
|
48
|
+
}
|
|
49
|
+
return salt;
|
|
50
|
+
}
|
|
51
|
+
function validateSlippage(bps) {
|
|
52
|
+
if (bps < 0 || bps > 5000) {
|
|
53
|
+
throw new ImmError(ErrorCodes.INVALID_SLIPPAGE, `Invalid slippage: ${bps} bps`, "Slippage must be between 0 and 5000 bps (0-50%)");
|
|
54
|
+
}
|
|
55
|
+
return bps;
|
|
56
|
+
}
|
|
57
|
+
function requireWalletAndKeystore() {
|
|
58
|
+
const cfg = loadConfig();
|
|
59
|
+
if (!cfg.wallet.address) {
|
|
60
|
+
throw new ImmError(ErrorCodes.WALLET_NOT_CONFIGURED, "No wallet configured.", "Run: imm wallet create --json");
|
|
61
|
+
}
|
|
62
|
+
const password = requireKeystorePassword();
|
|
63
|
+
const keystore = loadKeystore();
|
|
64
|
+
if (!keystore) {
|
|
65
|
+
throw new ImmError(ErrorCodes.KEYSTORE_NOT_FOUND, "Keystore not found.", "Run: imm wallet create --json");
|
|
66
|
+
}
|
|
67
|
+
const privateKey = decryptPrivateKey(keystore, password);
|
|
68
|
+
return { address: cfg.wallet.address, privateKey };
|
|
69
|
+
}
|
|
70
|
+
function createSlopWalletClient(privateKey) {
|
|
71
|
+
const cfg = loadConfig();
|
|
72
|
+
const account = privateKeyToAccount(privateKey);
|
|
73
|
+
return createWalletClient({
|
|
74
|
+
account,
|
|
75
|
+
chain: {
|
|
76
|
+
id: cfg.chain.chainId,
|
|
77
|
+
name: "0G",
|
|
78
|
+
nativeCurrency: { name: "0G", symbol: "0G", decimals: 18 },
|
|
79
|
+
rpcUrls: { default: { http: [cfg.chain.rpcUrl] } },
|
|
80
|
+
},
|
|
81
|
+
transport: http(cfg.chain.rpcUrl, {
|
|
82
|
+
timeout: 30_000,
|
|
83
|
+
retryCount: 2,
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async function validateOfficialToken(tokenAddr) {
|
|
88
|
+
const cfg = loadConfig();
|
|
89
|
+
const client = getPublicClient();
|
|
90
|
+
const isValid = await client.readContract({
|
|
91
|
+
address: cfg.slop.tokenRegistry,
|
|
92
|
+
abi: SLOP_REGISTRY_ABI,
|
|
93
|
+
functionName: "isValidToken",
|
|
94
|
+
args: [tokenAddr],
|
|
95
|
+
});
|
|
96
|
+
if (!isValid) {
|
|
97
|
+
throw new ImmError(ErrorCodes.SLOP_TOKEN_NOT_OFFICIAL, "Not an official slop.money token", `Token ${tokenAddr} is not registered in TokenRegistry`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function checkNotGraduated(tokenAddr) {
|
|
101
|
+
const client = getPublicClient();
|
|
102
|
+
const isGraduated = await client.readContract({
|
|
103
|
+
address: tokenAddr,
|
|
104
|
+
abi: SLOP_TOKEN_ABI,
|
|
105
|
+
functionName: "isGraduated",
|
|
106
|
+
});
|
|
107
|
+
if (isGraduated) {
|
|
108
|
+
throw new ImmError(ErrorCodes.SLOP_TOKEN_GRADUATED, "Token has graduated - bonding curve trading disabled", "Use: imm jaine swap to trade on the DEX");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function checkTradingEnabled(tokenAddr) {
|
|
112
|
+
const client = getPublicClient();
|
|
113
|
+
const isTradingEnabled = await client.readContract({
|
|
114
|
+
address: tokenAddr,
|
|
115
|
+
abi: SLOP_TOKEN_ABI,
|
|
116
|
+
functionName: "isTradingEnabled",
|
|
117
|
+
});
|
|
118
|
+
if (!isTradingEnabled) {
|
|
119
|
+
throw new ImmError(ErrorCodes.SLOP_TRADE_DISABLED, "Trading is disabled for this token");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function getTokenState(tokenAddr) {
|
|
123
|
+
const client = getPublicClient();
|
|
124
|
+
const [ogReserves, tokenReserves, virtualOgReserves, virtualTokenReserves, k, curveSupply, buyFeeBps, sellFeeBps, isGraduated,] = await Promise.all([
|
|
125
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "ogReserves" }),
|
|
126
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "tokenReserves" }),
|
|
127
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "virtualOgReserves" }),
|
|
128
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "virtualTokenReserves" }),
|
|
129
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "k" }),
|
|
130
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "CURVE_SUPPLY" }),
|
|
131
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "buyFeeBps" }),
|
|
132
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "sellFeeBps" }),
|
|
133
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "isGraduated" }),
|
|
134
|
+
]);
|
|
135
|
+
return {
|
|
136
|
+
ogReserves,
|
|
137
|
+
tokenReserves,
|
|
138
|
+
virtualOgReserves,
|
|
139
|
+
virtualTokenReserves,
|
|
140
|
+
k,
|
|
141
|
+
curveSupply,
|
|
142
|
+
buyFeeBps: BigInt(buyFeeBps),
|
|
143
|
+
sellFeeBps: BigInt(sellFeeBps),
|
|
144
|
+
isGraduated,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// ============ COMMAND FACTORY ============
|
|
148
|
+
export function createSlopCommand() {
|
|
149
|
+
const slop = new Command("slop")
|
|
150
|
+
.description("Slop.money bonding curve operations")
|
|
151
|
+
.exitOverride();
|
|
152
|
+
// ============ TOKEN SUBCOMMAND ============
|
|
153
|
+
const token = new Command("token")
|
|
154
|
+
.description("Token management")
|
|
155
|
+
.exitOverride();
|
|
156
|
+
token
|
|
157
|
+
.command("create")
|
|
158
|
+
.description("Create a new bonding curve token")
|
|
159
|
+
.requiredOption("--name <name>", "Token name")
|
|
160
|
+
.requiredOption("--symbol <symbol>", "Token symbol")
|
|
161
|
+
.option("--description <text>", "Token description", "")
|
|
162
|
+
.option("--image-url <url>", "Token image URL", "")
|
|
163
|
+
.option("--twitter <handle>", "Twitter handle", "")
|
|
164
|
+
.option("--telegram <handle>", "Telegram handle", "")
|
|
165
|
+
.option("--website <url>", "Website URL", "")
|
|
166
|
+
.option("--user-salt <hex>", "User-provided salt (32 bytes hex, default: random)")
|
|
167
|
+
.requiredOption("--yes", "Confirm the transaction")
|
|
168
|
+
.action(async (options) => {
|
|
169
|
+
if (!options.yes) {
|
|
170
|
+
throw new ImmError(ErrorCodes.CONFIRMATION_REQUIRED, "Add --yes to confirm");
|
|
171
|
+
}
|
|
172
|
+
const { privateKey } = requireWalletAndKeystore();
|
|
173
|
+
const cfg = loadConfig();
|
|
174
|
+
// Generate or validate userSalt
|
|
175
|
+
let userSalt;
|
|
176
|
+
if (options.userSalt) {
|
|
177
|
+
userSalt = validateUserSalt(options.userSalt);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
userSalt = `0x${randomBytes(32).toString("hex")}`;
|
|
181
|
+
}
|
|
182
|
+
const spin = spinner("Creating token...");
|
|
183
|
+
spin.start();
|
|
184
|
+
const walletClient = createSlopWalletClient(privateKey);
|
|
185
|
+
const publicClient = getPublicClient();
|
|
186
|
+
try {
|
|
187
|
+
const txHash = await walletClient.writeContract({
|
|
188
|
+
address: cfg.slop.factory,
|
|
189
|
+
abi: SLOP_FACTORY_ABI,
|
|
190
|
+
functionName: "createToken",
|
|
191
|
+
args: [
|
|
192
|
+
options.name,
|
|
193
|
+
options.symbol,
|
|
194
|
+
options.description,
|
|
195
|
+
options.imageUrl,
|
|
196
|
+
options.twitter,
|
|
197
|
+
options.telegram,
|
|
198
|
+
options.website,
|
|
199
|
+
userSalt,
|
|
200
|
+
],
|
|
201
|
+
});
|
|
202
|
+
spin.text = "Waiting for confirmation...";
|
|
203
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
204
|
+
// Decode TokenCreated event (filter by factory address for safety)
|
|
205
|
+
let tokenAddress;
|
|
206
|
+
let tokenId;
|
|
207
|
+
for (const log of receipt.logs) {
|
|
208
|
+
// Only process logs from the factory contract
|
|
209
|
+
if (log.address.toLowerCase() !== cfg.slop.factory.toLowerCase()) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const decoded = decodeEventLog({
|
|
214
|
+
abi: SLOP_FACTORY_ABI,
|
|
215
|
+
data: log.data,
|
|
216
|
+
topics: log.topics,
|
|
217
|
+
});
|
|
218
|
+
if (decoded.eventName === "TokenCreated") {
|
|
219
|
+
tokenAddress = decoded.args.tokenAddress;
|
|
220
|
+
tokenId = decoded.args.tokenId;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Not a TokenCreated event
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (!tokenAddress) {
|
|
229
|
+
throw new ImmError(ErrorCodes.SLOP_CREATE_FAILED, "Failed to decode TokenCreated event from receipt");
|
|
230
|
+
}
|
|
231
|
+
spin.succeed("Token created");
|
|
232
|
+
const explorerUrl = `${cfg.chain.explorerUrl}/tx/${txHash}`;
|
|
233
|
+
if (isHeadless()) {
|
|
234
|
+
writeJsonSuccess({
|
|
235
|
+
txHash,
|
|
236
|
+
explorerUrl,
|
|
237
|
+
tokenAddress,
|
|
238
|
+
tokenId: tokenId?.toString(),
|
|
239
|
+
creator: walletClient.account.address,
|
|
240
|
+
name: options.name,
|
|
241
|
+
symbol: options.symbol,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
successBox("Token Created", `Name: ${colors.info(options.name)}\n` +
|
|
246
|
+
`Symbol: ${colors.info(options.symbol)}\n` +
|
|
247
|
+
`Address: ${colors.address(tokenAddress)}\n` +
|
|
248
|
+
`Token ID: ${tokenId?.toString()}\n` +
|
|
249
|
+
`Tx: ${colors.info(txHash)}\n` +
|
|
250
|
+
`Explorer: ${colors.muted(explorerUrl)}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
spin.fail("Token creation failed");
|
|
255
|
+
if (err instanceof ImmError)
|
|
256
|
+
throw err;
|
|
257
|
+
throw new ImmError(ErrorCodes.SLOP_CREATE_FAILED, `Token creation failed: ${err instanceof Error ? err.message : err}`);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
token
|
|
261
|
+
.command("info <token>")
|
|
262
|
+
.description("Show token information")
|
|
263
|
+
.action(async (tokenArg) => {
|
|
264
|
+
if (!isAddress(tokenArg)) {
|
|
265
|
+
throw new ImmError(ErrorCodes.INVALID_ADDRESS, `Invalid address: ${tokenArg}`);
|
|
266
|
+
}
|
|
267
|
+
const tokenAddr = getAddress(tokenArg);
|
|
268
|
+
await validateOfficialToken(tokenAddr);
|
|
269
|
+
const client = getPublicClient();
|
|
270
|
+
const cfg = loadConfig();
|
|
271
|
+
const spin = spinner("Fetching token info...");
|
|
272
|
+
spin.start();
|
|
273
|
+
const [name, symbol, metadata, creator, creationTime, state, tradeInfo, [price, priceSource],] = await Promise.all([
|
|
274
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "name" }),
|
|
275
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "symbol" }),
|
|
276
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "metadata" }),
|
|
277
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "creator" }),
|
|
278
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "creationTime" }),
|
|
279
|
+
getTokenState(tokenAddr),
|
|
280
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "tradeInfo" }),
|
|
281
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "getCurrentPrice" }),
|
|
282
|
+
]);
|
|
283
|
+
const graduationProgress = calculateGraduationProgress(state.tokenReserves, state.virtualTokenReserves, state.curveSupply);
|
|
284
|
+
spin.succeed("Token info loaded");
|
|
285
|
+
const priceSourceStr = priceSource === 0 ? "bonding" : "pool";
|
|
286
|
+
if (isHeadless()) {
|
|
287
|
+
writeJsonSuccess({
|
|
288
|
+
token: tokenAddr,
|
|
289
|
+
name,
|
|
290
|
+
symbol,
|
|
291
|
+
creator,
|
|
292
|
+
creationTime: creationTime.toString(),
|
|
293
|
+
isGraduated: state.isGraduated,
|
|
294
|
+
price: price.toString(),
|
|
295
|
+
priceSource: priceSourceStr,
|
|
296
|
+
priceFormatted: formatUnits(price, 18),
|
|
297
|
+
graduationProgressBps: graduationProgress.toString(),
|
|
298
|
+
graduationProgressPct: (Number(graduationProgress) / 100).toFixed(2),
|
|
299
|
+
reserves: {
|
|
300
|
+
og: state.ogReserves.toString(),
|
|
301
|
+
token: state.tokenReserves.toString(),
|
|
302
|
+
virtualOg: state.virtualOgReserves.toString(),
|
|
303
|
+
virtualToken: state.virtualTokenReserves.toString(),
|
|
304
|
+
},
|
|
305
|
+
fees: {
|
|
306
|
+
buyBps: Number(state.buyFeeBps),
|
|
307
|
+
sellBps: Number(state.sellFeeBps),
|
|
308
|
+
},
|
|
309
|
+
tradeInfo: {
|
|
310
|
+
totalVolume: tradeInfo[0].toString(),
|
|
311
|
+
totalTransactions: tradeInfo[1].toString(),
|
|
312
|
+
buyCount: tradeInfo[2].toString(),
|
|
313
|
+
sellCount: tradeInfo[3].toString(),
|
|
314
|
+
uniqueTraders: tradeInfo[4].toString(),
|
|
315
|
+
},
|
|
316
|
+
metadata: {
|
|
317
|
+
description: metadata[0],
|
|
318
|
+
imageUrl: metadata[1],
|
|
319
|
+
twitter: metadata[2],
|
|
320
|
+
telegram: metadata[3],
|
|
321
|
+
website: metadata[4],
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
const statusStr = state.isGraduated
|
|
327
|
+
? colors.success("Graduated")
|
|
328
|
+
: `${colors.info("Active")} (${(Number(graduationProgress) / 100).toFixed(2)}% to graduation)`;
|
|
329
|
+
infoBox(`${name} (${symbol})`, `Address: ${colors.address(tokenAddr)}\n` +
|
|
330
|
+
`Creator: ${colors.address(creator)}\n` +
|
|
331
|
+
`Status: ${statusStr}\n` +
|
|
332
|
+
`Price: ${colors.value(formatUnits(price, 18))} 0G (${priceSourceStr})\n` +
|
|
333
|
+
`Buy Fee: ${(Number(state.buyFeeBps) / 100).toFixed(2)}%\n` +
|
|
334
|
+
`Sell Fee: ${(Number(state.sellFeeBps) / 100).toFixed(2)}%\n` +
|
|
335
|
+
`Trades: ${tradeInfo[1].toString()} (${tradeInfo[4].toString()} unique traders)\n` +
|
|
336
|
+
`Volume: ${colors.value(formatBalance(tradeInfo[0], 18))} 0G`);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
slop.addCommand(token);
|
|
340
|
+
// ============ TOKENS SUBCOMMAND ============
|
|
341
|
+
const tokens = new Command("tokens")
|
|
342
|
+
.description("List tokens")
|
|
343
|
+
.exitOverride();
|
|
344
|
+
tokens
|
|
345
|
+
.command("mine")
|
|
346
|
+
.description("List tokens created by a specific address")
|
|
347
|
+
.option("--creator <address>", "Creator address (default: wallet from config)")
|
|
348
|
+
.action(async (options) => {
|
|
349
|
+
const cfg = loadConfig();
|
|
350
|
+
const client = getPublicClient();
|
|
351
|
+
let creatorAddr;
|
|
352
|
+
if (options.creator) {
|
|
353
|
+
if (!isAddress(options.creator)) {
|
|
354
|
+
throw new ImmError(ErrorCodes.INVALID_ADDRESS, `Invalid address: ${options.creator}`);
|
|
355
|
+
}
|
|
356
|
+
creatorAddr = getAddress(options.creator);
|
|
357
|
+
}
|
|
358
|
+
else if (cfg.wallet.address) {
|
|
359
|
+
creatorAddr = cfg.wallet.address;
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
throw new ImmError(ErrorCodes.WALLET_NOT_CONFIGURED, "No creator specified and no wallet configured", "Use --creator <address> or configure a wallet");
|
|
363
|
+
}
|
|
364
|
+
const spin = spinner("Fetching tokens...");
|
|
365
|
+
spin.start();
|
|
366
|
+
const tokenAddresses = await client.readContract({
|
|
367
|
+
address: cfg.slop.tokenRegistry,
|
|
368
|
+
abi: SLOP_REGISTRY_ABI,
|
|
369
|
+
functionName: "getCreatorTokens",
|
|
370
|
+
args: [creatorAddr],
|
|
371
|
+
});
|
|
372
|
+
if (tokenAddresses.length === 0) {
|
|
373
|
+
spin.succeed("No tokens found");
|
|
374
|
+
if (isHeadless()) {
|
|
375
|
+
writeJsonSuccess({ creator: creatorAddr, tokens: [], count: 0, truncated: false });
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
infoBox("Creator Tokens", `No tokens found for ${colors.address(creatorAddr)}`);
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
// Fetch token info
|
|
383
|
+
const tokenInfos = await client.readContract({
|
|
384
|
+
address: cfg.slop.tokenRegistry,
|
|
385
|
+
abi: SLOP_REGISTRY_ABI,
|
|
386
|
+
functionName: "getTokensInfo",
|
|
387
|
+
args: [tokenAddresses],
|
|
388
|
+
});
|
|
389
|
+
spin.succeed(`Found ${tokenAddresses.length} tokens`);
|
|
390
|
+
const tokensData = tokenAddresses.map((addr, i) => ({
|
|
391
|
+
address: addr,
|
|
392
|
+
name: tokenInfos[i].name,
|
|
393
|
+
symbol: tokenInfos[i].symbol,
|
|
394
|
+
createdAt: tokenInfos[i].createdAt.toString(),
|
|
395
|
+
isGraduated: tokenInfos[i].isGraduated,
|
|
396
|
+
}));
|
|
397
|
+
const count = await client.readContract({
|
|
398
|
+
address: cfg.slop.tokenRegistry,
|
|
399
|
+
abi: SLOP_REGISTRY_ABI,
|
|
400
|
+
functionName: "creatorTokenCount",
|
|
401
|
+
args: [creatorAddr],
|
|
402
|
+
});
|
|
403
|
+
const truncated = Number(count) > 100;
|
|
404
|
+
if (isHeadless()) {
|
|
405
|
+
writeJsonSuccess({
|
|
406
|
+
creator: creatorAddr,
|
|
407
|
+
tokens: tokensData,
|
|
408
|
+
count: tokensData.length,
|
|
409
|
+
truncated,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
const table = createTable([
|
|
414
|
+
{ header: "Symbol", width: 12 },
|
|
415
|
+
{ header: "Name", width: 20 },
|
|
416
|
+
{ header: "Address", width: 45 },
|
|
417
|
+
{ header: "Status", width: 12 },
|
|
418
|
+
]);
|
|
419
|
+
for (const t of tokensData) {
|
|
420
|
+
table.push([
|
|
421
|
+
t.symbol,
|
|
422
|
+
t.name.slice(0, 18),
|
|
423
|
+
t.address,
|
|
424
|
+
t.isGraduated ? colors.success("Graduated") : colors.info("Active"),
|
|
425
|
+
]);
|
|
426
|
+
}
|
|
427
|
+
writeStderr(table.toString());
|
|
428
|
+
if (truncated) {
|
|
429
|
+
writeStderr(colors.muted(`\nShowing first 100 of ${count} tokens`));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
slop.addCommand(tokens);
|
|
434
|
+
// ============ TRADE SUBCOMMAND ============
|
|
435
|
+
const trade = new Command("trade")
|
|
436
|
+
.description("Trade on bonding curve (pre-graduation)")
|
|
437
|
+
.exitOverride();
|
|
438
|
+
trade
|
|
439
|
+
.command("buy <token>")
|
|
440
|
+
.description("Buy tokens with 0G")
|
|
441
|
+
.requiredOption("--amount-og <amount>", "Amount of 0G to spend")
|
|
442
|
+
.option("--slippage-bps <bps>", "Slippage tolerance in basis points", "50")
|
|
443
|
+
.option("--dry-run", "Show quote without executing")
|
|
444
|
+
.option("--yes", "Confirm the transaction")
|
|
445
|
+
.action(async (tokenArg, options) => {
|
|
446
|
+
if (!isAddress(tokenArg)) {
|
|
447
|
+
throw new ImmError(ErrorCodes.INVALID_ADDRESS, `Invalid address: ${tokenArg}`);
|
|
448
|
+
}
|
|
449
|
+
const tokenAddr = getAddress(tokenArg);
|
|
450
|
+
await validateOfficialToken(tokenAddr);
|
|
451
|
+
await checkNotGraduated(tokenAddr);
|
|
452
|
+
await checkTradingEnabled(tokenAddr);
|
|
453
|
+
const slippageBps = validateSlippage(parseIntSafe(options.slippageBps, "slippageBps"));
|
|
454
|
+
const ogAmountWei = parseUnitsSafe(options.amountOg, 18, "amount-og");
|
|
455
|
+
if (ogAmountWei <= 0n) {
|
|
456
|
+
throw new ImmError(ErrorCodes.INVALID_AMOUNT, "Amount must be > 0");
|
|
457
|
+
}
|
|
458
|
+
const client = getPublicClient();
|
|
459
|
+
const state = await getTokenState(tokenAddr);
|
|
460
|
+
// Calculate quote with partial fill logic
|
|
461
|
+
let quote;
|
|
462
|
+
try {
|
|
463
|
+
quote = calculatePartialFill(state.ogReserves, state.tokenReserves, state.virtualTokenReserves, state.curveSupply, ogAmountWei, state.buyFeeBps);
|
|
464
|
+
}
|
|
465
|
+
catch (err) {
|
|
466
|
+
throw new ImmError(ErrorCodes.SLOP_QUOTE_FAILED, `Quote failed: ${err instanceof Error ? err.message : err}`);
|
|
467
|
+
}
|
|
468
|
+
const minTokensOut = applySlippage(quote.tokensOut, BigInt(slippageBps));
|
|
469
|
+
// Fetch token symbol for display
|
|
470
|
+
const symbol = await client.readContract({
|
|
471
|
+
address: tokenAddr,
|
|
472
|
+
abi: SLOP_TOKEN_ABI,
|
|
473
|
+
functionName: "symbol",
|
|
474
|
+
});
|
|
475
|
+
// Dry run output
|
|
476
|
+
if (options.dryRun) {
|
|
477
|
+
if (isHeadless()) {
|
|
478
|
+
writeJsonSuccess({
|
|
479
|
+
dryRun: true,
|
|
480
|
+
token: tokenAddr,
|
|
481
|
+
symbol,
|
|
482
|
+
amountOgWei: ogAmountWei.toString(),
|
|
483
|
+
tokensOut: quote.tokensOut.toString(),
|
|
484
|
+
minTokensOut: minTokensOut.toString(),
|
|
485
|
+
ogUsed: quote.ogUsed.toString(),
|
|
486
|
+
feeUsed: quote.feeUsed.toString(),
|
|
487
|
+
refund: quote.refund.toString(),
|
|
488
|
+
hitCap: quote.hitCap,
|
|
489
|
+
slippageBps,
|
|
490
|
+
formatted: {
|
|
491
|
+
amountOg: options.amountOg,
|
|
492
|
+
tokensOut: formatUnits(quote.tokensOut, 18),
|
|
493
|
+
minTokensOut: formatUnits(minTokensOut, 18),
|
|
494
|
+
refund: formatUnits(quote.refund, 18),
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
infoBox("Buy Quote (Dry Run)", `Spend: ${colors.value(options.amountOg)} 0G\n` +
|
|
500
|
+
`Receive: ~${colors.value(formatUnits(quote.tokensOut, 18))} ${symbol}\n` +
|
|
501
|
+
`Min receive: ${colors.value(formatUnits(minTokensOut, 18))} ${symbol}\n` +
|
|
502
|
+
`Fee: ${colors.muted(formatUnits(quote.feeUsed, 18))} 0G\n` +
|
|
503
|
+
(quote.hitCap ? `${colors.warn("Partial fill")} - refund: ${formatUnits(quote.refund, 18)} 0G\n` : "") +
|
|
504
|
+
`Slippage: ${(slippageBps / 100).toFixed(2)}%`);
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
// Require --yes for execution
|
|
509
|
+
if (!options.yes) {
|
|
510
|
+
throw new ImmError(ErrorCodes.CONFIRMATION_REQUIRED, "Add --yes to confirm (or --dry-run to preview)");
|
|
511
|
+
}
|
|
512
|
+
const { privateKey } = requireWalletAndKeystore();
|
|
513
|
+
const cfg = loadConfig();
|
|
514
|
+
const spin = spinner("Executing buy...");
|
|
515
|
+
spin.start();
|
|
516
|
+
const walletClient = createSlopWalletClient(privateKey);
|
|
517
|
+
try {
|
|
518
|
+
const txHash = await walletClient.writeContract({
|
|
519
|
+
address: tokenAddr,
|
|
520
|
+
abi: SLOP_TOKEN_ABI,
|
|
521
|
+
functionName: "buyWithSlippage",
|
|
522
|
+
args: [minTokensOut],
|
|
523
|
+
value: ogAmountWei,
|
|
524
|
+
});
|
|
525
|
+
spin.succeed("Buy executed");
|
|
526
|
+
const explorerUrl = `${cfg.chain.explorerUrl}/tx/${txHash}`;
|
|
527
|
+
if (isHeadless()) {
|
|
528
|
+
writeJsonSuccess({
|
|
529
|
+
txHash,
|
|
530
|
+
explorerUrl,
|
|
531
|
+
token: tokenAddr,
|
|
532
|
+
symbol,
|
|
533
|
+
quote: {
|
|
534
|
+
tokensOut: quote.tokensOut.toString(),
|
|
535
|
+
minTokensOut: minTokensOut.toString(),
|
|
536
|
+
ogUsed: quote.ogUsed.toString(),
|
|
537
|
+
feeUsed: quote.feeUsed.toString(),
|
|
538
|
+
refund: quote.refund.toString(),
|
|
539
|
+
hitCap: quote.hitCap,
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
successBox("Buy Executed", `Spent: ${colors.value(options.amountOg)} 0G\n` +
|
|
545
|
+
`Expected: ~${colors.value(formatUnits(quote.tokensOut, 18))} ${symbol}\n` +
|
|
546
|
+
`Tx: ${colors.info(txHash)}\n` +
|
|
547
|
+
`Explorer: ${colors.muted(explorerUrl)}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
spin.fail("Buy failed");
|
|
552
|
+
throw new ImmError(ErrorCodes.SLOP_TX_FAILED, `Buy failed: ${err instanceof Error ? err.message : err}`);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
trade
|
|
556
|
+
.command("sell <token>")
|
|
557
|
+
.description("Sell tokens for 0G")
|
|
558
|
+
.requiredOption("--amount-tokens <amount>", "Amount of tokens to sell")
|
|
559
|
+
.option("--slippage-bps <bps>", "Slippage tolerance in basis points", "50")
|
|
560
|
+
.option("--dry-run", "Show quote without executing")
|
|
561
|
+
.option("--yes", "Confirm the transaction")
|
|
562
|
+
.action(async (tokenArg, options) => {
|
|
563
|
+
if (!isAddress(tokenArg)) {
|
|
564
|
+
throw new ImmError(ErrorCodes.INVALID_ADDRESS, `Invalid address: ${tokenArg}`);
|
|
565
|
+
}
|
|
566
|
+
const tokenAddr = getAddress(tokenArg);
|
|
567
|
+
await validateOfficialToken(tokenAddr);
|
|
568
|
+
await checkNotGraduated(tokenAddr);
|
|
569
|
+
await checkTradingEnabled(tokenAddr);
|
|
570
|
+
const slippageBps = validateSlippage(parseIntSafe(options.slippageBps, "slippageBps"));
|
|
571
|
+
const tokenAmountWei = parseUnitsSafe(options.amountTokens, 18, "amount-tokens");
|
|
572
|
+
if (tokenAmountWei <= 0n) {
|
|
573
|
+
throw new ImmError(ErrorCodes.INVALID_AMOUNT, "Amount must be > 0");
|
|
574
|
+
}
|
|
575
|
+
const client = getPublicClient();
|
|
576
|
+
const cfg = loadConfig();
|
|
577
|
+
const state = await getTokenState(tokenAddr);
|
|
578
|
+
// Calculate quote
|
|
579
|
+
let ogOutGross;
|
|
580
|
+
try {
|
|
581
|
+
ogOutGross = calculateOgOut(state.k, state.ogReserves, state.tokenReserves, tokenAmountWei);
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
throw new ImmError(ErrorCodes.SLOP_QUOTE_FAILED, `Quote failed: ${err instanceof Error ? err.message : err}`);
|
|
585
|
+
}
|
|
586
|
+
const fee = (ogOutGross * state.sellFeeBps) / 10000n;
|
|
587
|
+
const ogOutNet = ogOutGross - fee;
|
|
588
|
+
const minOgOut = applySlippage(ogOutNet, BigInt(slippageBps));
|
|
589
|
+
// Fetch token symbol for display
|
|
590
|
+
const symbol = await client.readContract({
|
|
591
|
+
address: tokenAddr,
|
|
592
|
+
abi: SLOP_TOKEN_ABI,
|
|
593
|
+
functionName: "symbol",
|
|
594
|
+
});
|
|
595
|
+
// Dry run output
|
|
596
|
+
if (options.dryRun) {
|
|
597
|
+
if (isHeadless()) {
|
|
598
|
+
writeJsonSuccess({
|
|
599
|
+
dryRun: true,
|
|
600
|
+
token: tokenAddr,
|
|
601
|
+
symbol,
|
|
602
|
+
tokenAmountWei: tokenAmountWei.toString(),
|
|
603
|
+
ogOutGross: ogOutGross.toString(),
|
|
604
|
+
ogOutNet: ogOutNet.toString(),
|
|
605
|
+
minOgOut: minOgOut.toString(),
|
|
606
|
+
fee: fee.toString(),
|
|
607
|
+
slippageBps,
|
|
608
|
+
formatted: {
|
|
609
|
+
amountTokens: options.amountTokens,
|
|
610
|
+
ogOutNet: formatUnits(ogOutNet, 18),
|
|
611
|
+
minOgOut: formatUnits(minOgOut, 18),
|
|
612
|
+
fee: formatUnits(fee, 18),
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
infoBox("Sell Quote (Dry Run)", `Sell: ${colors.value(options.amountTokens)} ${symbol}\n` +
|
|
618
|
+
`Receive: ~${colors.value(formatUnits(ogOutNet, 18))} 0G\n` +
|
|
619
|
+
`Min receive: ${colors.value(formatUnits(minOgOut, 18))} 0G\n` +
|
|
620
|
+
`Fee: ${colors.muted(formatUnits(fee, 18))} 0G\n` +
|
|
621
|
+
`Slippage: ${(slippageBps / 100).toFixed(2)}%`);
|
|
622
|
+
}
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
// Require --yes for execution
|
|
626
|
+
if (!options.yes) {
|
|
627
|
+
throw new ImmError(ErrorCodes.CONFIRMATION_REQUIRED, "Add --yes to confirm (or --dry-run to preview)");
|
|
628
|
+
}
|
|
629
|
+
// Check balance
|
|
630
|
+
const { address, privateKey } = requireWalletAndKeystore();
|
|
631
|
+
const balance = await client.readContract({
|
|
632
|
+
address: tokenAddr,
|
|
633
|
+
abi: SLOP_TOKEN_ABI,
|
|
634
|
+
functionName: "balanceOf",
|
|
635
|
+
args: [address],
|
|
636
|
+
});
|
|
637
|
+
if (balance < tokenAmountWei) {
|
|
638
|
+
throw new ImmError(ErrorCodes.SLOP_INSUFFICIENT_BALANCE, `Insufficient balance: ${formatUnits(balance, 18)} ${symbol}`, `You need ${options.amountTokens} ${symbol}`);
|
|
639
|
+
}
|
|
640
|
+
const spin = spinner("Executing sell...");
|
|
641
|
+
spin.start();
|
|
642
|
+
const walletClient = createSlopWalletClient(privateKey);
|
|
643
|
+
try {
|
|
644
|
+
const txHash = await walletClient.writeContract({
|
|
645
|
+
address: tokenAddr,
|
|
646
|
+
abi: SLOP_TOKEN_ABI,
|
|
647
|
+
functionName: "sellWithSlippage",
|
|
648
|
+
args: [tokenAmountWei, minOgOut],
|
|
649
|
+
});
|
|
650
|
+
spin.succeed("Sell executed");
|
|
651
|
+
const explorerUrl = `${cfg.chain.explorerUrl}/tx/${txHash}`;
|
|
652
|
+
if (isHeadless()) {
|
|
653
|
+
writeJsonSuccess({
|
|
654
|
+
txHash,
|
|
655
|
+
explorerUrl,
|
|
656
|
+
token: tokenAddr,
|
|
657
|
+
symbol,
|
|
658
|
+
quote: {
|
|
659
|
+
tokensSold: tokenAmountWei.toString(),
|
|
660
|
+
ogOutNet: ogOutNet.toString(),
|
|
661
|
+
minOgOut: minOgOut.toString(),
|
|
662
|
+
fee: fee.toString(),
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
successBox("Sell Executed", `Sold: ${colors.value(options.amountTokens)} ${symbol}\n` +
|
|
668
|
+
`Expected: ~${colors.value(formatUnits(ogOutNet, 18))} 0G\n` +
|
|
669
|
+
`Tx: ${colors.info(txHash)}\n` +
|
|
670
|
+
`Explorer: ${colors.muted(explorerUrl)}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
catch (err) {
|
|
674
|
+
spin.fail("Sell failed");
|
|
675
|
+
throw new ImmError(ErrorCodes.SLOP_TX_FAILED, `Sell failed: ${err instanceof Error ? err.message : err}`);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
slop.addCommand(trade);
|
|
679
|
+
// ============ PRICE COMMAND ============
|
|
680
|
+
slop
|
|
681
|
+
.command("price <token>")
|
|
682
|
+
.description("Get current token price")
|
|
683
|
+
.action(async (tokenArg) => {
|
|
684
|
+
if (!isAddress(tokenArg)) {
|
|
685
|
+
throw new ImmError(ErrorCodes.INVALID_ADDRESS, `Invalid address: ${tokenArg}`);
|
|
686
|
+
}
|
|
687
|
+
const tokenAddr = getAddress(tokenArg);
|
|
688
|
+
await validateOfficialToken(tokenAddr);
|
|
689
|
+
const client = getPublicClient();
|
|
690
|
+
const [[price, priceSource], symbol] = await Promise.all([
|
|
691
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "getCurrentPrice" }),
|
|
692
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "symbol" }),
|
|
693
|
+
]);
|
|
694
|
+
const sourceStr = priceSource === 0 ? "bonding" : "pool";
|
|
695
|
+
if (isHeadless()) {
|
|
696
|
+
writeJsonSuccess({
|
|
697
|
+
token: tokenAddr,
|
|
698
|
+
symbol,
|
|
699
|
+
price: price.toString(),
|
|
700
|
+
priceFormatted: formatUnits(price, 18),
|
|
701
|
+
source: sourceStr,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
infoBox(`${symbol} Price`, `${colors.value(formatUnits(price, 18))} 0G per token\n` +
|
|
706
|
+
`Source: ${sourceStr}`);
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
// ============ CURVE COMMAND ============
|
|
710
|
+
slop
|
|
711
|
+
.command("curve <token>")
|
|
712
|
+
.description("Show bonding curve state and graduation progress")
|
|
713
|
+
.action(async (tokenArg) => {
|
|
714
|
+
if (!isAddress(tokenArg)) {
|
|
715
|
+
throw new ImmError(ErrorCodes.INVALID_ADDRESS, `Invalid address: ${tokenArg}`);
|
|
716
|
+
}
|
|
717
|
+
const tokenAddr = getAddress(tokenArg);
|
|
718
|
+
await validateOfficialToken(tokenAddr);
|
|
719
|
+
const client = getPublicClient();
|
|
720
|
+
const state = await getTokenState(tokenAddr);
|
|
721
|
+
const symbol = await client.readContract({
|
|
722
|
+
address: tokenAddr,
|
|
723
|
+
abi: SLOP_TOKEN_ABI,
|
|
724
|
+
functionName: "symbol",
|
|
725
|
+
});
|
|
726
|
+
const graduationProgress = calculateGraduationProgress(state.tokenReserves, state.virtualTokenReserves, state.curveSupply);
|
|
727
|
+
// Clamp to >= 0 for defensive safety (matches on-chain getRealOgReserves behavior)
|
|
728
|
+
const realOg = state.ogReserves > state.virtualOgReserves
|
|
729
|
+
? state.ogReserves - state.virtualOgReserves
|
|
730
|
+
: 0n;
|
|
731
|
+
const tokensSold = state.virtualTokenReserves > state.tokenReserves
|
|
732
|
+
? state.virtualTokenReserves - state.tokenReserves
|
|
733
|
+
: 0n;
|
|
734
|
+
if (isHeadless()) {
|
|
735
|
+
writeJsonSuccess({
|
|
736
|
+
token: tokenAddr,
|
|
737
|
+
symbol,
|
|
738
|
+
isGraduated: state.isGraduated,
|
|
739
|
+
reserves: {
|
|
740
|
+
og: state.ogReserves.toString(),
|
|
741
|
+
token: state.tokenReserves.toString(),
|
|
742
|
+
virtualOg: state.virtualOgReserves.toString(),
|
|
743
|
+
virtualToken: state.virtualTokenReserves.toString(),
|
|
744
|
+
realOg: realOg.toString(),
|
|
745
|
+
k: state.k.toString(),
|
|
746
|
+
},
|
|
747
|
+
curveSupply: state.curveSupply.toString(),
|
|
748
|
+
tokensSold: tokensSold.toString(),
|
|
749
|
+
graduationProgressBps: graduationProgress.toString(),
|
|
750
|
+
graduationProgressPct: (Number(graduationProgress) / 100).toFixed(2),
|
|
751
|
+
graduationThresholdBps: "8000",
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
const progressBar = (pct) => {
|
|
756
|
+
const filled = Math.round(pct / 5);
|
|
757
|
+
const empty = 20 - filled;
|
|
758
|
+
return `[${"=".repeat(filled)}${" ".repeat(empty)}] ${pct.toFixed(2)}%`;
|
|
759
|
+
};
|
|
760
|
+
const progressPct = Number(graduationProgress) / 100;
|
|
761
|
+
infoBox(`${symbol} Bonding Curve`, `Status: ${state.isGraduated ? colors.success("Graduated") : colors.info("Active")}\n` +
|
|
762
|
+
`\nReserves:\n` +
|
|
763
|
+
` 0G: ${colors.value(formatUnits(state.ogReserves, 18))} (real: ${formatUnits(realOg, 18)})\n` +
|
|
764
|
+
` Token: ${colors.value(formatUnits(state.tokenReserves, 18))}\n` +
|
|
765
|
+
` K: ${state.k.toString()}\n` +
|
|
766
|
+
`\nProgress to Graduation (80%):\n` +
|
|
767
|
+
` ${progressBar(progressPct)}\n` +
|
|
768
|
+
` Tokens sold: ${colors.value(formatUnits(tokensSold, 18))} / ${formatUnits(state.curveSupply, 18)}`);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
// ============ FEES SUBCOMMAND ============
|
|
772
|
+
const fees = new Command("fees")
|
|
773
|
+
.description("Fee management")
|
|
774
|
+
.exitOverride();
|
|
775
|
+
fees
|
|
776
|
+
.command("stats <token>")
|
|
777
|
+
.description("Show fee statistics for a token")
|
|
778
|
+
.action(async (tokenArg) => {
|
|
779
|
+
if (!isAddress(tokenArg)) {
|
|
780
|
+
throw new ImmError(ErrorCodes.INVALID_ADDRESS, `Invalid address: ${tokenArg}`);
|
|
781
|
+
}
|
|
782
|
+
const tokenAddr = getAddress(tokenArg);
|
|
783
|
+
await validateOfficialToken(tokenAddr);
|
|
784
|
+
const cfg = loadConfig();
|
|
785
|
+
const client = getPublicClient();
|
|
786
|
+
const [feeStats, symbol] = await Promise.all([
|
|
787
|
+
client.readContract({
|
|
788
|
+
address: cfg.slop.feeCollector,
|
|
789
|
+
abi: SLOP_FEE_COLLECTOR_ABI,
|
|
790
|
+
functionName: "getTokenFeeStats",
|
|
791
|
+
args: [tokenAddr],
|
|
792
|
+
}),
|
|
793
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "symbol" }),
|
|
794
|
+
]);
|
|
795
|
+
const [totalCreator, totalPlatform, pendingCreator, pendingPlatform, volume] = feeStats;
|
|
796
|
+
if (isHeadless()) {
|
|
797
|
+
writeJsonSuccess({
|
|
798
|
+
token: tokenAddr,
|
|
799
|
+
symbol,
|
|
800
|
+
totalCreatorFees: totalCreator.toString(),
|
|
801
|
+
totalPlatformFees: totalPlatform.toString(),
|
|
802
|
+
pendingCreatorFees: pendingCreator.toString(),
|
|
803
|
+
pendingPlatformFees: pendingPlatform.toString(),
|
|
804
|
+
totalVolume: volume.toString(),
|
|
805
|
+
formatted: {
|
|
806
|
+
totalCreatorFees: formatUnits(totalCreator, 18),
|
|
807
|
+
totalPlatformFees: formatUnits(totalPlatform, 18),
|
|
808
|
+
pendingCreatorFees: formatUnits(pendingCreator, 18),
|
|
809
|
+
pendingPlatformFees: formatUnits(pendingPlatform, 18),
|
|
810
|
+
totalVolume: formatUnits(volume, 18),
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
infoBox(`${symbol} Fee Stats`, `Total Volume: ${colors.value(formatUnits(volume, 18))} 0G\n` +
|
|
816
|
+
`\nCreator Fees:\n` +
|
|
817
|
+
` Total: ${colors.value(formatUnits(totalCreator, 18))} 0G\n` +
|
|
818
|
+
` Pending: ${colors.value(formatUnits(pendingCreator, 18))} 0G\n` +
|
|
819
|
+
`\nPlatform Fees:\n` +
|
|
820
|
+
` Total: ${colors.value(formatUnits(totalPlatform, 18))} 0G`);
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
fees
|
|
824
|
+
.command("claim-creator <token>")
|
|
825
|
+
.description("Withdraw pending creator fees")
|
|
826
|
+
.requiredOption("--yes", "Confirm the transaction")
|
|
827
|
+
.action(async (tokenArg, options) => {
|
|
828
|
+
if (!options.yes) {
|
|
829
|
+
throw new ImmError(ErrorCodes.CONFIRMATION_REQUIRED, "Add --yes to confirm");
|
|
830
|
+
}
|
|
831
|
+
if (!isAddress(tokenArg)) {
|
|
832
|
+
throw new ImmError(ErrorCodes.INVALID_ADDRESS, `Invalid address: ${tokenArg}`);
|
|
833
|
+
}
|
|
834
|
+
const tokenAddr = getAddress(tokenArg);
|
|
835
|
+
await validateOfficialToken(tokenAddr);
|
|
836
|
+
const { privateKey } = requireWalletAndKeystore();
|
|
837
|
+
const cfg = loadConfig();
|
|
838
|
+
const spin = spinner("Withdrawing creator fees...");
|
|
839
|
+
spin.start();
|
|
840
|
+
const walletClient = createSlopWalletClient(privateKey);
|
|
841
|
+
try {
|
|
842
|
+
const txHash = await walletClient.writeContract({
|
|
843
|
+
address: cfg.slop.feeCollector,
|
|
844
|
+
abi: SLOP_FEE_COLLECTOR_ABI,
|
|
845
|
+
functionName: "withdrawCreatorFees",
|
|
846
|
+
args: [tokenAddr],
|
|
847
|
+
});
|
|
848
|
+
spin.succeed("Creator fees withdrawn");
|
|
849
|
+
const explorerUrl = `${cfg.chain.explorerUrl}/tx/${txHash}`;
|
|
850
|
+
if (isHeadless()) {
|
|
851
|
+
writeJsonSuccess({ txHash, explorerUrl, token: tokenAddr });
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
successBox("Creator Fees Withdrawn", `Token: ${colors.address(tokenAddr)}\n` +
|
|
855
|
+
`Tx: ${colors.info(txHash)}\n` +
|
|
856
|
+
`Explorer: ${colors.muted(explorerUrl)}`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
catch (err) {
|
|
860
|
+
spin.fail("Withdrawal failed");
|
|
861
|
+
throw new ImmError(ErrorCodes.SLOP_TX_FAILED, `Withdrawal failed: ${err instanceof Error ? err.message : err}`);
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
const lpFees = new Command("lp")
|
|
865
|
+
.description("LP fees (post-graduation)")
|
|
866
|
+
.exitOverride();
|
|
867
|
+
lpFees
|
|
868
|
+
.command("pending <token>")
|
|
869
|
+
.description("Show pending LP fees")
|
|
870
|
+
.action(async (tokenArg) => {
|
|
871
|
+
if (!isAddress(tokenArg)) {
|
|
872
|
+
throw new ImmError(ErrorCodes.INVALID_ADDRESS, `Invalid address: ${tokenArg}`);
|
|
873
|
+
}
|
|
874
|
+
const tokenAddr = getAddress(tokenArg);
|
|
875
|
+
await validateOfficialToken(tokenAddr);
|
|
876
|
+
const client = getPublicClient();
|
|
877
|
+
const [isGraduated, symbol] = await Promise.all([
|
|
878
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "isGraduated" }),
|
|
879
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "symbol" }),
|
|
880
|
+
]);
|
|
881
|
+
if (!isGraduated) {
|
|
882
|
+
if (isHeadless()) {
|
|
883
|
+
writeJsonSuccess({
|
|
884
|
+
token: tokenAddr,
|
|
885
|
+
symbol,
|
|
886
|
+
isGraduated: false,
|
|
887
|
+
pendingW0G: "0",
|
|
888
|
+
pendingToken: "0",
|
|
889
|
+
note: "Token not graduated - no LP fees yet",
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
infoBox(`${symbol} LP Fees`, "Token not graduated - no LP fees yet");
|
|
894
|
+
}
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const [pendingW0G, pendingToken] = await client.readContract({
|
|
898
|
+
address: tokenAddr,
|
|
899
|
+
abi: SLOP_TOKEN_ABI,
|
|
900
|
+
functionName: "getPendingLPFees",
|
|
901
|
+
});
|
|
902
|
+
if (isHeadless()) {
|
|
903
|
+
writeJsonSuccess({
|
|
904
|
+
token: tokenAddr,
|
|
905
|
+
symbol,
|
|
906
|
+
isGraduated: true,
|
|
907
|
+
pendingW0G: pendingW0G.toString(),
|
|
908
|
+
pendingToken: pendingToken.toString(),
|
|
909
|
+
formatted: {
|
|
910
|
+
pendingW0G: formatUnits(pendingW0G, 18),
|
|
911
|
+
pendingToken: formatUnits(pendingToken, 18),
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
infoBox(`${symbol} LP Fees`, `Pending W0G: ${colors.value(formatUnits(pendingW0G, 18))}\n` +
|
|
917
|
+
`Pending ${symbol}: ${colors.value(formatUnits(pendingToken, 18))}`);
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
lpFees
|
|
921
|
+
.command("collect <token>")
|
|
922
|
+
.description("Collect LP fees (creator only)")
|
|
923
|
+
.option("--recipient <address>", "Recipient address (default: wallet)")
|
|
924
|
+
.requiredOption("--yes", "Confirm the transaction")
|
|
925
|
+
.action(async (tokenArg, options) => {
|
|
926
|
+
if (!options.yes) {
|
|
927
|
+
throw new ImmError(ErrorCodes.CONFIRMATION_REQUIRED, "Add --yes to confirm");
|
|
928
|
+
}
|
|
929
|
+
if (!isAddress(tokenArg)) {
|
|
930
|
+
throw new ImmError(ErrorCodes.INVALID_ADDRESS, `Invalid address: ${tokenArg}`);
|
|
931
|
+
}
|
|
932
|
+
const tokenAddr = getAddress(tokenArg);
|
|
933
|
+
await validateOfficialToken(tokenAddr);
|
|
934
|
+
const { address, privateKey } = requireWalletAndKeystore();
|
|
935
|
+
const cfg = loadConfig();
|
|
936
|
+
const recipient = options.recipient ? getAddress(options.recipient) : address;
|
|
937
|
+
const spin = spinner("Collecting LP fees...");
|
|
938
|
+
spin.start();
|
|
939
|
+
const walletClient = createSlopWalletClient(privateKey);
|
|
940
|
+
try {
|
|
941
|
+
const txHash = await walletClient.writeContract({
|
|
942
|
+
address: tokenAddr,
|
|
943
|
+
abi: SLOP_TOKEN_ABI,
|
|
944
|
+
functionName: "collectLPFees",
|
|
945
|
+
args: [recipient],
|
|
946
|
+
});
|
|
947
|
+
spin.succeed("LP fees collected");
|
|
948
|
+
const explorerUrl = `${cfg.chain.explorerUrl}/tx/${txHash}`;
|
|
949
|
+
if (isHeadless()) {
|
|
950
|
+
writeJsonSuccess({ txHash, explorerUrl, token: tokenAddr, recipient });
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
successBox("LP Fees Collected", `Token: ${colors.address(tokenAddr)}\n` +
|
|
954
|
+
`Recipient: ${colors.address(recipient)}\n` +
|
|
955
|
+
`Tx: ${colors.info(txHash)}\n` +
|
|
956
|
+
`Explorer: ${colors.muted(explorerUrl)}`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
catch (err) {
|
|
960
|
+
spin.fail("Collection failed");
|
|
961
|
+
throw new ImmError(ErrorCodes.SLOP_TX_FAILED, `LP fee collection failed: ${err instanceof Error ? err.message : err}`);
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
fees.addCommand(lpFees);
|
|
965
|
+
slop.addCommand(fees);
|
|
966
|
+
// ============ REWARD SUBCOMMAND ============
|
|
967
|
+
const reward = new Command("reward")
|
|
968
|
+
.description("Creator graduation reward")
|
|
969
|
+
.exitOverride();
|
|
970
|
+
reward
|
|
971
|
+
.command("pending <token>")
|
|
972
|
+
.description("Show pending creator graduation reward")
|
|
973
|
+
.action(async (tokenArg) => {
|
|
974
|
+
if (!isAddress(tokenArg)) {
|
|
975
|
+
throw new ImmError(ErrorCodes.INVALID_ADDRESS, `Invalid address: ${tokenArg}`);
|
|
976
|
+
}
|
|
977
|
+
const tokenAddr = getAddress(tokenArg);
|
|
978
|
+
await validateOfficialToken(tokenAddr);
|
|
979
|
+
const client = getPublicClient();
|
|
980
|
+
const [pendingReward, totalReward, symbol, isGraduated] = await Promise.all([
|
|
981
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "pendingCreatorReward" }),
|
|
982
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "CREATOR_GRADUATION_REWARD" }),
|
|
983
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "symbol" }),
|
|
984
|
+
client.readContract({ address: tokenAddr, abi: SLOP_TOKEN_ABI, functionName: "isGraduated" }),
|
|
985
|
+
]);
|
|
986
|
+
if (isHeadless()) {
|
|
987
|
+
writeJsonSuccess({
|
|
988
|
+
token: tokenAddr,
|
|
989
|
+
symbol,
|
|
990
|
+
isGraduated,
|
|
991
|
+
pendingReward: pendingReward.toString(),
|
|
992
|
+
totalReward: totalReward.toString(),
|
|
993
|
+
formatted: {
|
|
994
|
+
pendingReward: formatUnits(pendingReward, 18),
|
|
995
|
+
totalReward: formatUnits(totalReward, 18),
|
|
996
|
+
},
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
const statusNote = isGraduated
|
|
1001
|
+
? pendingReward > 0n
|
|
1002
|
+
? colors.success("Claimable")
|
|
1003
|
+
: colors.muted("Already claimed")
|
|
1004
|
+
: colors.muted("Not graduated yet");
|
|
1005
|
+
infoBox(`${symbol} Creator Reward`, `Status: ${statusNote}\n` +
|
|
1006
|
+
`Pending: ${colors.value(formatUnits(pendingReward, 18))} 0G\n` +
|
|
1007
|
+
`Total Reward: ${formatUnits(totalReward, 18)} 0G`);
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
reward
|
|
1011
|
+
.command("claim <token>")
|
|
1012
|
+
.description("Claim creator graduation reward")
|
|
1013
|
+
.requiredOption("--yes", "Confirm the transaction")
|
|
1014
|
+
.action(async (tokenArg, options) => {
|
|
1015
|
+
if (!options.yes) {
|
|
1016
|
+
throw new ImmError(ErrorCodes.CONFIRMATION_REQUIRED, "Add --yes to confirm");
|
|
1017
|
+
}
|
|
1018
|
+
if (!isAddress(tokenArg)) {
|
|
1019
|
+
throw new ImmError(ErrorCodes.INVALID_ADDRESS, `Invalid address: ${tokenArg}`);
|
|
1020
|
+
}
|
|
1021
|
+
const tokenAddr = getAddress(tokenArg);
|
|
1022
|
+
await validateOfficialToken(tokenAddr);
|
|
1023
|
+
const { privateKey } = requireWalletAndKeystore();
|
|
1024
|
+
const cfg = loadConfig();
|
|
1025
|
+
const spin = spinner("Claiming creator reward...");
|
|
1026
|
+
spin.start();
|
|
1027
|
+
const walletClient = createSlopWalletClient(privateKey);
|
|
1028
|
+
try {
|
|
1029
|
+
const txHash = await walletClient.writeContract({
|
|
1030
|
+
address: tokenAddr,
|
|
1031
|
+
abi: SLOP_TOKEN_ABI,
|
|
1032
|
+
functionName: "claimCreatorReward",
|
|
1033
|
+
});
|
|
1034
|
+
spin.succeed("Creator reward claimed");
|
|
1035
|
+
const explorerUrl = `${cfg.chain.explorerUrl}/tx/${txHash}`;
|
|
1036
|
+
if (isHeadless()) {
|
|
1037
|
+
writeJsonSuccess({ txHash, explorerUrl, token: tokenAddr });
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
successBox("Creator Reward Claimed", `Token: ${colors.address(tokenAddr)}\n` +
|
|
1041
|
+
`Tx: ${colors.info(txHash)}\n` +
|
|
1042
|
+
`Explorer: ${colors.muted(explorerUrl)}`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
catch (err) {
|
|
1046
|
+
spin.fail("Claim failed");
|
|
1047
|
+
throw new ImmError(ErrorCodes.SLOP_TX_FAILED, `Reward claim failed: ${err instanceof Error ? err.message : err}`);
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
slop.addCommand(reward);
|
|
1051
|
+
return slop;
|
|
1052
|
+
}
|
|
1053
|
+
//# sourceMappingURL=slop.js.map
|