diff --git a/DEVELOPERS.md b/DEVELOPERS.md index ec3e32a..ddd8395 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -95,6 +95,18 @@ Each command prints one JSON object on stdout. The exceptions: `gang watch` prin New fields can appear at any time, so ignore what you don't know. Other fields can change with a minor version: read `gang capabilities` and test before you upgrade. +### Pad 3.0 token addresses + +After the owner enables the `abcd` factory on Pad 3.0, the CLI automatically searches a CREATE2 salt for every curve, +fair-mint and fly launch, including vault and promoter variants. The commands and flags stay the same. The create preview +includes `addressSuffix: "abcd"` and `tokenIfNext`: this address is fixed for that transaction, including its final metadata, +and does not change when another creator launches first. Confirm the deployed token from the `Created` event. + +Direct integrations read `vanityDeployer()` on the pad. A nonzero result requires wrapping the original create calldata in +`createWithSalt(bytes32 salt, bytes data)`. Obtain the exact `tokenInitCodeHash` or `mintInitCodeHash` from that factory and +search with the effective salt `keccak256(abi.encode(creator, salt))`. The address must end in `abcd` (hex, ignoring checksum +capitalization). Existing tokens keep their addresses. A suffix alone does not prove a token was launched on Gang. + ### Failure codes | `code` | Meaning | diff --git a/DEVELOPERS.zh-CN.md b/DEVELOPERS.zh-CN.md index 37c4392..e57a203 100644 --- a/DEVELOPERS.zh-CN.md +++ b/DEVELOPERS.zh-CN.md @@ -95,6 +95,16 @@ gang execute --request-id 发送 随时可能增加新字段,不认识的忽略即可。其他字段可能随小版本变化:升级前看 `gang capabilities` 并先测试。 +### Pad 3.0 代币地址 + +Pad 3.0 的 owner 启用 `abcd` 部署器后,CLI 会为曲线、打新、铸币飞轮发币自动搜索 CREATE2 salt,金库和推广员版本也包含在内。 +命令和参数不变。发币预览增加 `addressSuffix: "abcd"` 和 `tokenIfNext`:地址按这笔交易的最终元数据确定,其他人先发币也不会改变它。 +实际部署结果仍以 `Created` 事件为准。 + +直接对接合约时,先读 pad 的 `vanityDeployer()`。返回非零地址就必须用 `createWithSalt(bytes32 salt, bytes data)` 包住原来的发币 calldata。 +从该部署器读取准确的 `tokenInitCodeHash` 或 `mintInitCodeHash`,按有效 salt `keccak256(abi.encode(creator, salt))` 搜索地址。 +十六进制地址后四位必须为 `abcd`,忽略校验和带来的大小写。旧币地址不变;仅凭后缀不能认定是 Gang 发行的币。 + ### 失败代码 | `code` | 含义 | diff --git a/README.md b/README.md index 8672b8e..3343200 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Operate [gang.fun](https://gang.fun) from a Binance **Agentic Wallet**. Curve bu **One command per action (0.12.0+):** `gang buy` / `sell` / `info` / `status` / `claim` / `mint` / `withdraw` / `release` work on every token: the CLI reads the token's kind and runs the V3, 打新 or V2 / Soft code. `gang v3 …` / `gang fair …` stay as aliases. +**Pad 3.0 address suffix (0.38.0+):** after the owner activates the `abcd` deployer, all new curve, fair-mint and flywheel tokens end in `abcd` (ignoring checksum capitalization). The CLI automatically finds the salt after metadata is finalized, including `--grad`, `--vault` and `--promote` launches, and shows the predicted address in `tokenIfNext`. Update the CLI before activation: old direct create calls are then refused. Existing tokens keep their addresses and trade normally. Before activation the new CLI uses the existing create flow. + **铸币飞轮 / Mint Flywheel (0.18.0+):** `gang create --fly` starts a sale (kind 3): shares with a quota per wallet (invites add quota), a pool at the mint price once it fills, and minting that goes on after launch. The unified commands detect these tokens (`kind: "fly"`); `gang fly …` is the alias. Live on BSC mainnet since 2026-10-30 (V1 pad `0x631e66560F4FAE4e486B65349edE48189E48B029`); from 0.21.0 new tokens went to the V2 pad `0x281889ad915c1A95ffc4922235c6314b3Cbd8a5a`, and from 0.30.0 they go to GangLaunchpad 3.0 (below). Every other command follows the token's own pad, so V1 and V2 tokens keep working (`padVersion` in `gang info`: 1, 2 or 3). `--testnet` uses BSC testnet (V1 set 2, V2 `0x7069…E3d6`, 3.0). Rules: `skill/references/fly.md`. **V3 tax tokens and 打新 (0.9.0+):** on GangLaunchpad, live on BSC mainnet since 2026-10-27 (1.0: pad `0x15CFD2dc8278Fb87a1783ef42111618E20874365`, GangRouter `0xE040445690Aa56C8b68B7f7038C7b2562B5079C4`; 2.0 since 2026-10-31: pad `0x770331a9accdb53ACb4a45B6c5B7D151fADCe557`, GangRouter `0xa83209d6bfC2B220cD86410cCDDC71FEb354844a`; new tokens go to 3.0 since CLI 0.30.0, see below; all built in); `--testnet` uses BSC testnet (3.0 for new tokens; 2.0 and set 10 = 1.0 for their tokens). diff --git a/README.zh-CN.md b/README.zh-CN.md index eca6077..e265b6e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,5 +1,7 @@ # Gang CLI(`gang`) +**Pad 3.0 地址后缀(0.38.0+):** owner 启用 `abcd` 部署器后,曲线、打新和铸币飞轮新币地址统一以 `abcd` 结尾(忽略校验和大小写)。CLI 会在最终元数据确定后自动计算 salt,`--grad`、`--vault`、`--promote` 发币也包含在内,`tokenIfNext` 显示确定的预测地址。请在启用前升级 CLI:启用后旧的直接发币调用会被拒绝。旧币地址和交易不变;启用前,新 CLI 仍走原有发币流程。 + 用 Binance **Agentic Wallet** 操作 [gang.fun](https://gang.fun)。内盘买入/发币需要 SPACE ID `.ai` Agent ID。本 CLI 只组 calldata,签名走 `baw`,不持有私钥。 **`gang create`:** 在 GangLaunchpad 上发 V3 币(`gang create --fair` 发打新);不带 `--buy-tax` / `--sell-tax` 就是无税币。LaunchpadV2 / Soft / LaunchpadU / LaunchpadS **不再发新币**,存量币照常交易(`gang buy` / `gang sell`)。V3 新币在 GangLaunchpad 1.0 上 **15 BNB** 毕业;2.0 和 3.0 上由 creator 用 `--grad` 选 **6 / 9 / 12 / 15 BNB**(默认 9)。Soft 存量链上仍为 20 BNB 毕业。 diff --git a/package-lock.json b/package-lock.json index 912ead2..6354dbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gangfun/cli", - "version": "0.37.0", + "version": "0.38.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gangfun/cli", - "version": "0.37.0", + "version": "0.38.0", "license": "UNLICENSED", "dependencies": { "viem": "^2.0.0" diff --git a/package.json b/package.json index f1a612a..b934f47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gangfun/cli", - "version": "0.37.0", + "version": "0.38.0", "mcpName": "io.github.gangfun/gang", "description": "Gang launchpad CLI for Binance Agentic Wallet agents: create on GangLaunchpad (untaxed or tax tokens), 打新 fair mint, PancakeSwap pool trades, LaunchpadV2 and legacy Soft/U/S trade.", "type": "module", diff --git a/server.json b/server.json index 62a9335..cf33fc1 100644 --- a/server.json +++ b/server.json @@ -8,12 +8,12 @@ "url": "https://github.com/gangfun/cli", "source": "github" }, - "version": "0.37.0", + "version": "0.38.0", "packages": [ { "registryType": "npm", "identifier": "@gangfun/cli", - "version": "0.37.0", + "version": "0.38.0", "transport": { "type": "stdio" }, diff --git a/src/fair.js b/src/fair.js index dde9bc7..08c4f2c 100644 --- a/src/fair.js +++ b/src/fair.js @@ -1,4 +1,5 @@ import { createPublicClient, parseAbi, encodeFunctionData, decodeEventLog, formatUnits, parseEther, parseUnits, isAddress, zeroAddress } from 'viem' +import { VANITY_ABI, vanityCreate, vanityDeployer } from './vanity.js' import { bawAddress } from './baw.js' import { defaultRefPool, dividendOf, fundingStep as fund, parseAmount, parseIntIn, parseNum, poolSellQuote, POOL_SELL_HINT, revertOf as revertIn, ROUTER_ABI, ROUTER_HINTS, routerDeadline, step, swapBackState, units, viaRouter, GAS_RESERVE } from './steps.js' import { API } from './discover.js' @@ -125,7 +126,7 @@ export function walletFor(ctx, from) { } /** Revert name (+ BadConfig reason) of a failed eth_call, or null when it was the RPC. */ -const revertOf = (e) => revertIn(e, [PAD_ABI, ROUTER_ABI, MERGED_ERRORS_ABI], { ...MERGED_HINTS, ...ROUTER_HINTS, ...HINTS }) +const revertOf = (e) => revertIn(e, [PAD_ABI, VANITY_ABI, ROUTER_ABI, MERGED_ERRORS_ABI], { ...MERGED_HINTS, ...ROUTER_HINTS, ...HINTS }) /** Dry-run `data` from `from`; null when it would pass. */ async function dryRun(ctx, from, to, data, value = 0n) { @@ -329,15 +330,21 @@ export async function runFair(sub, { opt, flag, out, fail, summarize, resolveLog const createData = (logoURI, metaURI) => encodeFunctionData({ abi: PAD_ABI, functionName: 'create', args: [name, symbol, logoURI, metaURI, quote, refPool, cfg, BigInt(devShares)], }) + const vanity = await vanityDeployer(ctx) + const handler = vanity ? await ctx.read(ctx.pad, PAD_ABI, 'taxHandler') : zeroAddress + const prepareCreate = (logoURI, metaURI) => vanityCreate(ctx, me, createData(logoURI, metaURI), { + args: [name, symbol, metaURI, handler, 100, 100, true], + }) // the logo / metadata strings never decide a revert: dry-run without them, so a create that would fail uploads nothing - const dry = await dryRun(ctx, me, ctx.pad, createData('', '')) + const dry = await dryRun(ctx, me, ctx.pad, (await prepareCreate('', '')).data) if (dry) return out({ success: false, step: 'create', ...dry }) const logo = await resolveLogo() const meta = await resolveMeta({ name, symbol, logoURI: logo.logoURI }) - const data = createData(logo.logoURI, meta.metaURI) - return out(notes.step(ctx, { from: me, to: ctx.pad, data }, { + const launch = await prepareCreate(logo.logoURI, meta.metaURI) + return out(notes.step(ctx, { from: me, to: ctx.pad, data: launch.data }, { step: 'create', chainId: Number(ctx.chainId), name, symbol, logoURI: logo.logoURI, ...(logo.uploaded && { uploaded: true }), metaURI: meta.metaURI, metadata: meta.metadata, quote: sym || quote, + ...(launch.token && { tokenIfNext: launch.token, addressSuffix: 'abcd' }), raise: await units(ctx, quote, raiseQuote), sharePrice: await units(ctx, quote, sharePrice), capShares: cfg.capBps * 2, minWallets: Math.ceil(10_000 / (cfg.capBps * 2)), holdPct, mintFeePct, exitFeePct, devShares, devCost: await units(ctx, quote, need), diff --git a/src/fly.js b/src/fly.js index d78a93d..ce060c9 100644 --- a/src/fly.js +++ b/src/fly.js @@ -1,4 +1,5 @@ import { createPublicClient, parseAbi, encodeFunctionData, decodeEventLog, formatUnits, parseEther, parseUnits, isAddress, zeroAddress } from 'viem' +import { VANITY_ABI, vanityCreate, vanityDeployer } from './vanity.js' import { defaultRefPool, dividendOf, VAULT_ABI, fundingStep as fund, parseAmount, parseIntIn, parseNum, poolSellQuote, POOL_SELL_HINT, revertOf as revertIn, roomyGas, ROUTER_ABI, ROUTER_HINTS, step, swapBackState, units, GAS_RESERVE } from './steps.js' import { API, get as apiGet } from './discover.js' import { walletFor } from './fair.js' @@ -191,7 +192,7 @@ const HINTS = { FeeOnTransferQuote: 'the quote token takes a fee on transfer; it cannot be used', RouterNeedsGate: 'this sale has no Agent gate, so router entries are off', } -const revertOf = (e) => revertIn(e, [FLY_PAD_ABI, FLY_HANDLER_ABI, TOKEN_ABI, ROUTER_ABI, MERGED_ERRORS_ABI], { ...MERGED_HINTS, ...ROUTER_HINTS, ...HINTS }) +const revertOf = (e) => revertIn(e, [FLY_PAD_ABI, VANITY_ABI, FLY_HANDLER_ABI, TOKEN_ABI, ROUTER_ABI, MERGED_ERRORS_ABI], { ...MERGED_HINTS, ...ROUTER_HINTS, ...HINTS }) /** * Network settings: GangLaunchpad's chain, RPCs, WBNB and GangRouter (pool trades), with the fly pads. ctx.pad / @@ -827,16 +828,24 @@ export async function runFly(sub, { opt, flag, out, fail, summarize, resolveLogo const sym = await symbolOf(ctx, quote) const pre = await fund(ctx, { me: who, token: quote, need, spender: ctx.pad, sym }, rawOnly, summarize) if (pre) return out(pre) - const createData = (logoURI, metaURI) => tagged(encodeFunctionData({ + const createData = (logoURI, metaURI) => encodeFunctionData({ abi: padAbi, functionName: 'create', args: [[name, symbol, logoURI, metaURI], quote, refPool, cfg, BigInt(devShares)], - })) - const dry = await dryRun(ctx, who, ctx.pad, createData('', '')) + }) + const vanity = await vanityDeployer(ctx) + const handler = vanity ? await handlerOf() : zeroAddress + const maxSupply = parseEther('2000000000') * BigInt(cfg.postMint ? cfg.supplyCapMul : 1) + const prepareCreate = (logoURI, metaURI) => vanityCreate(ctx, who, createData(logoURI, metaURI), { + mint: true, args: [name, symbol, metaURI, handler, 100, cfg.sellTaxBps, 0n, maxSupply], + }) + const dry = await dryRun(ctx, who, ctx.pad, tagged((await prepareCreate('', '')).data)) if (dry) return out({ success: false, step: 'create', ...dry }) const logo = await resolveLogo() const meta = await resolveMeta({ name, symbol, logoURI: logo.logoURI }) - return out(write({ from: who, to: ctx.pad, data: createData(logo.logoURI, meta.metaURI) }, { + const launch = await prepareCreate(logo.logoURI, meta.metaURI) + return out(write({ from: who, to: ctx.pad, data: tagged(launch.data) }, { step: 'create', kind: 'fly', padVersion: ctx.version, ...(ctx.version === 3 && { padGen: PAD3_GEN }), pad: ctx.pad, name, symbol, logoURI: logo.logoURI, ...(logo.uploaded && { uploaded: true }), metaURI: meta.metaURI, metadata: meta.metadata, quote: sym || quote, preset: presetName, config: cfgView(cfg), + ...(launch.token && { tokenIfNext: launch.token, addressSuffix: 'abcd' }), sharePrice: await units(ctx, quote, quoteWbnb ? price : (price * 100n) / 102n), devShares, devCost: await units(ctx, quote, need), ...(v2 && v.postMint && { guardNote: cfg.guardBps diff --git a/src/v3.js b/src/v3.js index 49ddfb0..af95ae7 100644 --- a/src/v3.js +++ b/src/v3.js @@ -14,6 +14,7 @@ import { splitOf } from './split.js' import { lpOf } from './lp.js' import { comboOf, decoyWarning } from './combo.js' import { cappedOf, decoyWarning as cappedDecoyWarning } from './capped.js' +import { VANITY_ABI, vanityCreate, vanityDeployer } from './vanity.js' /** * `gang v3 …` — LaunchpadV3 (tax recipient): a token with its own buy / sell tax (1–5% each), a dividend share and a @@ -160,7 +161,7 @@ const VIRTUAL_TOKEN = 1_066_666_666_666_666_666_666_666_667n const ceilDiv = (a, b) => (a + b - 1n) / b /** Revert name (+ BadTaxConfig reason) of a failed call, or null when it was the RPC. */ -const revertOf = (e) => revertIn(e, [PAD_ABI, PROMOTE_PAD_ABI, CREATOR_FEE_ABI, GRAD_TARGET_ABI, TAX_ROUTER_ABI, GANG_ROUTER_ABI, MERGED_ERRORS_ABI], { BadPromoteTerms: PROMOTE_HINTS.BadPromoteTerms, TraderNotOrigin: PROMOTE_HINTS.TraderNotOrigin, ...MERGED_HINTS, ...ROUTER_HINTS, ...HINTS }) +const revertOf = (e) => revertIn(e, [PAD_ABI, VANITY_ABI, PROMOTE_PAD_ABI, CREATOR_FEE_ABI, GRAD_TARGET_ABI, TAX_ROUTER_ABI, GANG_ROUTER_ABI, MERGED_ERRORS_ABI], { BadPromoteTerms: PROMOTE_HINTS.BadPromoteTerms, TraderNotOrigin: PROMOTE_HINTS.TraderNotOrigin, ...MERGED_HINTS, ...ROUTER_HINTS, ...HINTS }) /** eth_call `data` from `from`: { result } when it passes, { failed } with the revert (or RPC error) otherwise. */ async function simulate(ctx, from, to, abi, functionName, args, value = 0n) { @@ -557,11 +558,19 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo, const argsOf = (logoURI, metaURI) => [name, symbol, logoURI, metaURI, quote, refPool, tax, ...(vault ? [parseUnits(String(targetBnb), 18), vault] : promote ? [parseUnits(String(targetBnb), 18), promote.shareBps, promote.window] : grad !== null ? [parseUnits(String(grad), 18)] : [])] const createData = (logoURI, metaURI) => encodeFunctionData({ abi: PAD_ABI, functionName: fn, args: argsOf(logoURI, metaURI) }) + const vanity = await vanityDeployer(ctx) + const handler = vanity && taxed ? (await taxContracts(ctx)).handler : zeroAddress + const prepareCreate = (logoURI, metaURI) => vanityCreate(ctx, me, createData(logoURI, metaURI), { + args: [name, symbol, metaURI, handler, tax.buyTaxBps, tax.sellTaxBps, false], + }) // the logo / metadata strings never decide a revert: dry-run without them, so a create that would fail uploads nothing - const dry = await simulate(ctx, me, ctx.pad, PAD_ABI, fn, argsOf('', '')) + const probe = await prepareCreate('', '') + const dry = await simulate(ctx, me, ctx.pad, probe.salt ? VANITY_ABI : PAD_ABI, + probe.salt ? 'createWithSalt' : fn, probe.salt ? [probe.salt, createData('', '')] : argsOf('', '')) if (dry.failed) return out({ success: false, step: 'create', ...dry.failed }) const logo = await resolveLogo() const meta = await resolveMeta({ name, symbol, logoURI: logo.logoURI }) + const launch = await prepareCreate(logo.logoURI, meta.metaURI) let label = null const selfRecipient = recipient !== zeroAddress && recipient.toLowerCase() === String(me).toLowerCase() if (recipient !== zeroAddress && !selfRecipient) { @@ -572,14 +581,15 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo, const baseCreatorFee = await createNote(ctx, net) const creatorFee = baseCreatorFee && promote ? `${baseCreatorFee} Less the ${promote.shareBps / 100}% of it you give promoters on their bound wallets' trades.` : baseCreatorFee const poolModule = promote && !!launchPoolModule - return out(notes.step(ctx, { from: me, to: ctx.pad, data: createData(logo.logoURI, meta.metaURI) }, { + return out(notes.step(ctx, { from: me, to: ctx.pad, data: launch.data }, { step: 'create', chainId: Number(ctx.chainId), name, symbol, logoURI: logo.logoURI, ...(logo.uploaded && { uploaded: true }), metaURI: meta.metaURI, metadata: meta.metadata, quote: (await symbolOf(ctx, quote)) || quote, graduationTargetBnb: targetBnb, graduationAt: await units(ctx, quote, gradAmt), graduation: `${gradLine(targetBnb, quote.toLowerCase() === String(ctx.wbnb).toLowerCase() ? null : `${await units(ctx, quote, gradAmt)} ${(await symbolOf(ctx, quote)) || 'of the quote'}`)}${tiers ? `; ${grad !== null ? 'picked with --grad' : 'the default'}, --grad picks ${tiers.join(' / ')}` : ''}`, ...(refPool !== zeroAddress && quote.toLowerCase() !== String(ctx.wbnb).toLowerCase() && { refPool }), buyTaxPct: buyTax, sellTaxPct: sellTax, dividendPct: dividend, - recipient: recipient === zeroAddress ? null : recipient, recipientLabel: selfRecipient ? 'you (the creator)' : label, tokenIfNext: dry.result, + recipient: recipient === zeroAddress ? null : recipient, recipientLabel: selfRecipient ? 'you (the creator)' : label, tokenIfNext: launch.token ?? dry.result, + ...(launch.token && { addressSuffix: 'abcd' }), split: taxed ? `of each ${buyTax}% buy / ${sellTax}% sell tax in the PancakeSwap pool (after graduation; the curve charges no tax): ${fees.platformTaxPct}% of volume to the platform, then ${dividend}% of the rest to holders, ${rest}% to the recipient in the quote` + (dividend > 0 ? '; if no holder has 10,000+ tokens when a tax is paid, the holders\' part goes to the recipient too' : '') @@ -597,7 +607,7 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo, padGen: ctx.gen, pad: ctx.pad, vault, vaultNote: 'the token is bound to this vault for good. Until graduation the token moves only through the pad: the vault locks a holder\'s tokens (counted in that holder\'s 1% cap) and releases them through the pad, for the holder that sends the tx and holds an Agent ID; after graduation it is a plain token. This create uses up the wallet\'s naming for the vault: one token per naming. A vault-bound token takes no GangBond (gang bond post refuses it): a lock moves tokens out of the creator\'s wallet, which would forfeit a bond', }), - note: `${taxed ? 'tax rates and recipient are' : 'untaxed is'} fixed at create. Curve trades pay only the ${fees.curveFeePct}% fee, no tax${taxed ? ': the tax starts in the pool after graduation' : ''}. tokenIfNext changes if another create lands first: the real address is the Created event from \`gang tx --hash …\`.`, + note: `${taxed ? 'tax rates and recipient are' : 'untaxed is'} fixed at create. Curve trades pay only the ${fees.curveFeePct}% fee, no tax${taxed ? ': the tax starts in the pool after graduation' : ''}. ${launch.token ? 'The CREATE2 address ends in abcd and is fixed for this transaction.' : 'tokenIfNext changes if another create lands first: the real address is the Created event from `gang tx --hash …`.'}`, }, 'create')) } diff --git a/src/vanity.js b/src/vanity.js new file mode 100644 index 0000000..4839f50 --- /dev/null +++ b/src/vanity.js @@ -0,0 +1,57 @@ +import { randomBytes } from 'node:crypto' +import { encodeFunctionData, hexToBytes, keccak256, parseAbi, toHex, zeroAddress } from 'viem' +import { PAD3_GEN } from './gangpad.js' + +export const VANITY_ABI = parseAbi([ + 'function vanityDeployer() view returns (address)', + 'function createWithSalt(bytes32 salt, bytes data) returns (address token)', + 'function tokenInitCodeHash(string name, string symbol, string uri, address handler, uint16 buyTaxBps, uint16 sellTaxBps, bool reclaimable) view returns (bytes32)', + 'function mintInitCodeHash(string name, string symbol, string uri, address handler, uint16 buyTaxBps, uint16 sellTaxBps, uint256 startSupply, uint256 maxSupply) view returns (bytes32)', + 'error InvalidSuffix()', 'error VanityContextRequired()', 'error BadVanityCall()', +]) + +/** No RPC calls per attempt. Only the chosen candidate is checked for existing code. */ +export async function mineSalt(deployer, creator, initCodeHash, start = BigInt(toHex(randomBytes(32)))) { + const bound = new Uint8Array(64) + bound.set(hexToBytes(creator), 12) + const preimage = new Uint8Array(85) + preimage[0] = 0xff + preimage.set(hexToBytes(deployer), 1) + preimage.set(hexToBytes(initCodeHash), 53) + for (let i = 0n; ; i++) { + const salt = toHex((start + i) % (1n << 256n), { size: 32 }) + bound.set(hexToBytes(salt), 32) + preimage.set(hexToBytes(keccak256(bound)), 21) + const hash = keccak256(preimage) + if (hash.endsWith('abcd')) return { salt, token: `0x${hash.slice(-40)}` } + if (i % 4096n === 4095n) await new Promise((resolve) => setImmediate(resolve)) + } +} + +/** Probe only 3.0; a transport failure must never silently build an unsalted launch. */ +export async function vanityDeployer(ctx) { + if (ctx.gen !== PAD3_GEN && ctx.version !== 3) return null + if (!ctx._vanityDeployer) ctx._vanityDeployer = (async () => { + let deployer + try { deployer = await ctx.read(ctx.pad, VANITY_ABI, 'vanityDeployer') } catch (e) { + const reverted = e?.walk?.((x) => x?.name === 'ContractFunctionRevertedError') + if (reverted && !reverted.data && (!reverted.raw || reverted.raw === '0x')) return null + throw e + } + return deployer.toLowerCase() === zeroAddress ? null : deployer + })() + return ctx._vanityDeployer +} + +export async function vanityCreate(ctx, creator, data, { mint = false, args }) { + const deployer = await vanityDeployer(ctx) + if (!deployer) return { data } + const initCodeHash = await ctx.read(deployer, VANITY_ABI, mint ? 'mintInitCodeHash' : 'tokenInitCodeHash', args) + let found + do { found = await mineSalt(deployer, creator, initCodeHash) } + while (await ctx.client.getBytecode({ address: found.token }).then((code) => code && code !== '0x')) + return { + ...found, + data: encodeFunctionData({ abi: VANITY_ABI, functionName: 'createWithSalt', args: [found.salt, data] }), + } +} diff --git a/test/vanity-create.mjs b/test/vanity-create.mjs new file mode 100644 index 0000000..d8682cb --- /dev/null +++ b/test/vanity-create.mjs @@ -0,0 +1,110 @@ +// Exercise the real three create commands, with metadata resolution and an activated factory, against a local RPC. +import assert from 'node:assert/strict' +import { createServer } from 'node:http' +import { decodeFunctionData, encodeAbiParameters, encodeFunctionResult, getCreate2Address, keccak256, parseAbi, parseAbiParameters, toFunctionSelector, toHex } from 'viem' +import { runV3, V3_EVENT_ABIS, V3_NETS } from '../src/v3.js' +import { runFair, FAIR_EVENT_ABIS, FAIR_NETS } from '../src/fair.js' +import { runFly, FLY_PAD_ABI, FLY_PAD_V2_ABI } from '../src/fly.js' +import { CREATOR_FEE_ABI, GRAD_TARGET_ABI } from '../src/gangpad.js' +import { VANITY_ABI } from '../src/vanity.js' + +const FACTORY = '0x1111111111111111111111111111111111111111' +const CREATOR = '0x2222222222222222222222222222222222222222' +const HANDLER = '0x3333333333333333333333333333333333333333' +const abi = [...V3_EVENT_ABIS[0], ...FAIR_EVENT_ABIS[0], ...FLY_PAD_ABI, ...FLY_PAD_V2_ABI, + ...CREATOR_FEE_ABI, ...GRAD_TARGET_ABI, ...VANITY_ABI, + ...parseAbi(['function symbol() view returns (string)', 'function decimals() view returns (uint8)', + 'function balanceOf(address) view returns (uint256)', 'function allowance(address,address) view returns (uint256)', + 'function implementation() view returns (address)'])] +const functions = new Map(abi.filter((f) => f.type === 'function').map((f) => [toFunctionSelector(f), f])) +const hashOf = (args) => keccak256(toHex(JSON.stringify(args, (_key, x) => typeof x === 'bigint' ? x.toString() : x))) +const predicted = (salt, hash) => getCreate2Address({ from: FACTORY, bytecodeHash: hash, + salt: keccak256(encodeAbiParameters(parseAbiParameters('address, bytes32'), [CREATOR, salt])), +}).toLowerCase() +let hashes = [], simulations = 0 + +const answer = (b) => { + if (b.method === 'eth_chainId') return { result: '0x61' } + if (b.method === 'eth_getCode') return { result: '0x' } + assert.equal(b.method, 'eth_call') + const { data } = b.params[0] + const f = functions.get(data.slice(0, 10)) + if (!f) return { error: { code: 3, message: 'execution reverted', data: '0x' } } + const { args = [] } = decodeFunctionData({ abi: [f], data }) + let result + switch (f.name) { + case 'vanityDeployer': result = FACTORY; break + case 'taxHandler': case 'flyTaxHandler': case 'router': case 'recipientRegistry': case 'migrator': case 'implementation': result = HANDLER; break + case 'tokenInitCodeHash': case 'mintInitCodeHash': + result = hashOf(args); hashes.push({ name: f.name, args, hash: result }); break + case 'createWithSalt': { + const inner = decodeFunctionData({ abi, data: args[1] }) + assert.ok(['create', 'createWithGradTarget', 'createWithVault', 'createWithPromote'].includes(inner.functionName)) + result = predicted(args[0], hashes.at(-1).hash) + assert.ok(result.endsWith('abcd')) + simulations++ + break + } + case 'create': assert.fail('the CLI must wrap every create after activation'); break + case 'checkTaxConfigFor': case 'checkTaxConfig': case 'checkConfig': result = [true, '']; break + case 'isAgent': case 'agentGateEnabled': result = true; break + case 'GRAD_BNB_TIERS': result = [6n, 9n, 12n, 15n].map((x) => x * 10n ** 18n); break + case 'TARGET_GRAD_BNB': case 'graduationAmountOf': result = 9n * 10n ** 18n; break + case 'graduationAmountFor': result = args[2]; break + case 'previewCreate': result = [15n * 10n ** 18n, 15n * 10n ** 14n]; break + case 'PROTOCOL_FEE_BPS': result = 100n; break + case 'PLATFORM_TAX_BPS': result = 50n; break + case 'CREATOR_FEE_SHARE_BPS': result = 7000n; break + case 'symbol': result = 'WBNB'; break + case 'decimals': result = 18; break + case 'balanceOf': case 'allowance': result = 10n ** 30n; break + default: return { error: { code: 3, message: 'execution reverted', data: '0x' } } + } + return { result: encodeFunctionResult({ abi: [f], functionName: f.name, result }) } +} +const server = createServer((req, res) => { + let body = '' + req.on('data', (c) => (body += c)) + req.on('end', () => { + const input = JSON.parse(body) + const one = (b) => ({ jsonrpc: '2.0', id: b.id, ...answer(b) }) + res.writeHead(200, { 'content-type': 'application/json' }) + res.end(JSON.stringify(Array.isArray(input) ? input.map(one) : one(input))) + }) +}) +await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) +const rpc = `http://127.0.0.1:${server.address().port}` +process.env.GANG_TESTNET_RPC_URL = rpc +V3_NETS.testnet.rpcs = [rpc] +FAIR_NETS.testnet.rpcs = [rpc] +const io = (flags) => ({ + opt: (key, fallback) => flags[key] ?? fallback, + flag: (key) => Object.hasOwn(flags, key), + out: (x) => x, + fail: (error, details) => { throw new Error(`${error} ${JSON.stringify(details ?? {})}`) }, + summarize: () => assert.fail('raw calldata needs no wallet preview'), + checkMeta: () => {}, + resolveLogo: async () => ({ logoURI: 'ipfs://logo' }), + resolveMeta: async () => ({ metaURI: 'ipfs://final-metadata', metadata: { name: 'Vanity' } }), + notes: { step: (_ctx, tx, extra) => ({ success: true, ...extra, tx }), skipped: () => ({}) }, +}) +try { + for (const [run, extra, mint] of [[runV3, {}, false], [runV3, { 'buy-tax': '2', 'sell-tax': '3' }, false], + [runFair, {}, false], [runFly, {}, true]]) { + hashes = []; simulations = 0 + const output = await run('create', io({ name: 'Vanity', symbol: 'VAN', from: CREATOR, testnet: true, calldata: true, ...extra })) + assert.equal(output.success, true, JSON.stringify(output)) + assert.equal(output.addressSuffix, 'abcd') + assert.equal(simulations, 1, 'a salted dry run happens before metadata upload') + assert.equal(hashes[0].args[2], '') + assert.equal(hashes.at(-1).args[2], 'ipfs://final-metadata') + assert.equal(hashes.at(-1).name, mint ? 'mintInitCodeHash' : 'tokenInitCodeHash') + const { args } = decodeFunctionData({ abi: VANITY_ABI, data: output.tx.data }) + assert.equal(output.tokenIfNext, predicted(args[0], hashes.at(-1).hash)) + const inner = decodeFunctionData({ abi, data: args[1] }) + assert.equal(mint ? inner.args[0][3] : inner.args[3], 'ipfs://final-metadata') + if (mint) assert.equal(hashes.at(-1).args[7], 200_000_000_000n * 10n ** 18n) + else if (extra['buy-tax']) assert.deepEqual(hashes.at(-1).args.slice(3), [HANDLER, 200, 300, false]) + } + console.log('vanity-create: ok') +} finally { server.closeAllConnections(); server.close() } diff --git a/test/vanity.mjs b/test/vanity.mjs new file mode 100644 index 0000000..c3505dd --- /dev/null +++ b/test/vanity.mjs @@ -0,0 +1,55 @@ +import assert from 'node:assert/strict' +import { ContractFunctionRevertedError, decodeFunctionData, encodeAbiParameters, getCreate2Address, keccak256, parseAbiParameters, zeroAddress } from 'viem' +import { mineSalt, VANITY_ABI, vanityCreate, vanityDeployer } from '../src/vanity.js' +import { PAD3_GEN } from '../src/gangpad.js' + +const FACTORY = '0x1111111111111111111111111111111111111111' +const CREATOR = '0x2222222222222222222222222222222222222222' +const HASH = `0x${'33'.repeat(32)}` +const effectiveSalt = (creator, salt) => keccak256(encodeAbiParameters(parseAbiParameters('address, bytes32'), [creator, salt])) +const predicted = (creator, hash, salt) => getCreate2Address({ from: FACTORY, salt: effectiveSalt(creator, salt), bytecodeHash: hash }).toLowerCase() + +const found = await mineSalt(FACTORY, CREATOR, HASH, 0n) +assert.equal(found.salt, `0x${'2d4e'.padStart(64, '0')}`) +assert.equal(found.token, predicted(CREATOR, HASH, found.salt)) +assert.ok(found.token.endsWith('abcd')) +assert.notEqual(found.token, predicted(zeroAddress, HASH, found.salt), 'creator binding prevents copying a salt to take its address') +assert.notEqual(found.token, predicted(CREATOR, `0x${'44'.repeat(32)}`, found.salt), 'constructor parameters are part of the address') + +// A previous 3.0 deployment reverts for the missing getter; RPC failures and empty data do not mean disabled. +const missing = new ContractFunctionRevertedError({ abi: VANITY_ABI, functionName: 'vanityDeployer', data: '0x' }) +assert.equal(await vanityDeployer({ gen: PAD3_GEN, read: async () => { throw missing } }), null) +await assert.rejects(vanityDeployer({ gen: PAD3_GEN, read: async () => { throw new Error('RPC offline') } }), /RPC offline/) +assert.equal(await vanityDeployer({ gen: '2.0', read: async () => { assert.fail('must not probe older pads') } }), null) + +const reads = [], checked = [] +const ctx = { + gen: PAD3_GEN, pad: CREATOR, + read: async (to, _abi, fn, args) => { + reads.push({ to, fn, args }) + if (fn === 'vanityDeployer') return FACTORY + assert.equal(to, FACTORY) + assert.equal(fn, 'tokenInitCodeHash') + return HASH + }, + client: { getBytecode: async ({ address }) => { checked.push(address); return checked.length === 1 ? '0x6000' : undefined } }, +} +const args = ['Name', 'SYM', 'ipfs://metadata', zeroAddress, 0, 0, false] +const launch = await vanityCreate(ctx, CREATOR, '0x12345678', { args }) +assert.equal(checked.length, 2, 'occupied candidates are skipped') +assert.equal(launch.token, predicted(CREATOR, HASH, launch.salt)) +const decoded = decodeFunctionData({ abi: VANITY_ABI, data: launch.data }) +assert.equal(decoded.functionName, 'createWithSalt') +assert.deepEqual(decoded.args, [launch.salt, '0x12345678']) +assert.deepEqual(reads[1].args, args) + +// The mint path uses the final metadata and supply cap; a disabled factory preserves legacy calldata exactly. +ctx.read = async (_to, _abi, fn, actual) => { + assert.equal(fn, 'mintInitCodeHash') + assert.equal(actual[2], 'ipfs://final-metadata') + assert.equal(actual[7], 200_000_000_000n * 10n ** 18n) + return HASH +} +await vanityCreate(ctx, CREATOR, '0x87654321', { mint: true, args: ['Fly', 'FLY', 'ipfs://final-metadata', FACTORY, 100, 200, 0n, 200_000_000_000n * 10n ** 18n] }) +assert.deepEqual(await vanityCreate({ gen: PAD3_GEN, read: async () => zeroAddress }, CREATOR, '0x12345678', { args }), { data: '0x12345678' }) +console.log('vanity: ok')