nat-cli 0.0.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +50 -1
- package/SKILL.md +100 -0
- package/assets/deployment.json +8 -0
- package/i18n/en.json +283 -0
- package/i18n/revert.json +98 -0
- package/i18n/zh.json +283 -0
- package/lib/baw.mjs +191 -0
- package/lib/chain.mjs +238 -0
- package/lib/i18n.mjs +96 -0
- package/lib/journal.mjs +59 -0
- package/lib/tui.mjs +671 -0
- package/llms.txt +26 -0
- package/package.json +38 -4
- package/scripts/cli.mjs +570 -0
package/llms.txt
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# bNAT
|
|
2
|
+
|
|
3
|
+
> One NFT per BNB Smart Chain block (tokenId = block number), carrying the BNB that block burned under BEP-95, in gwei. Minting is limited to addresses holding a SPACE ID Agent ID (.ai) and is done by AI agents through the Binance Agentic Wallet with the bnat CLI. Wrap / unwrap and the bid-only market need no .ai and can be done on the website with an ordinary browser wallet, or by the agent with the bnat CLI.
|
|
4
|
+
|
|
5
|
+
- Mint: any finalized BSC block since 13,082,000 with burn >= 1 gwei, once, first come first served. `mint(blk, burnGwei, sig, to)`: the caller must hold a .ai and is recorded as minter; the NFT goes to `to`, which can be any address. A signer attests the burn from the site's burn index (BSC finalized blocks, a few seconds behind); the CLI checks the signature is for this contract and recovers to its current signer.
|
|
6
|
+
- Wrap: an NFT can be wrapped into the ERC-20 BNAT (bNAT), 1 BNAT = 1 BNB burned (1e9 gwei). The NFT moves into the BNAT vault; the wrapper gets 99%, 1% stays in the BNAT contract as a fee the developer can withdraw.
|
|
7
|
+
- Unwrap: reversible. Anyone can take any block out of the vault by paying its burn + 1% in BNAT (the burn part is burned, the 1% stays as the fee); the NFT goes to the payer. `BNAT.quote(blk)` returns (amount, fee).
|
|
8
|
+
- Market: bid-only. Buyers lock BNB at a per-BNAT price; holders sell into any bid, fully or partly; 1% of each fill goes to the developer address; unfilled BNB can be withdrawn by cancelling.
|
|
9
|
+
- On-chain art: tokenURI returns metadata with an SVG sigil generated by the contract.
|
|
10
|
+
|
|
11
|
+
## Agent
|
|
12
|
+
|
|
13
|
+
- Install: `npm install -g nat-cli` (installs baw as a dependency), then the user runs `bnat` in a terminal and scans the QR with the Binance App, then `bnat wallet`.
|
|
14
|
+
- Skill: SKILL.md (shipped in the nat-cli package and served at /skill/SKILL.md).
|
|
15
|
+
- Commands: `bnat status | wallet | blocks | top | block --block N | holdings | vault | bids | history`, `bnat mint --block N | --best [--to ADDRESS]`, `bnat wrap --block N[,N2,…]` (up to 50 blocks in one transaction; BNAT always goes to the wrapping wallet), `bnat unwrap --block N[,N2,…]` (pays burn + 1% per block in BNAT, no approval; the NFTs go to the unwrapping wallet), `bnat bid --price P --amount A`, `bnat sell --bid ID --amount A | --all`, `bnat cancel --bid ID`, `bnat resume --hash 0x… | --abandon ID`, `bnat lang [zh|en]`, `bnat config [mintTo ADDRESS|self]`.
|
|
16
|
+
- Writes run only with `--yes`, after the user approves the printed plan. Never resend; settle with `bnat resume`.
|
|
17
|
+
|
|
18
|
+
## Read API (same origin as the site)
|
|
19
|
+
|
|
20
|
+
- /api/v1/status (contract: chainId and contract addresses; heads and index progress; contract.lastEvent changes only when a contract event was indexed: refetch other data when it moves), /api/v1/stats
|
|
21
|
+
- /api/v1/recent?n=96[&since=N] (the newest blocks of the burn index with burn and minted flag; signableTo: the newest block the signer will sign; since: only rows after N, mintedBlks covers the window)
|
|
22
|
+
- /api/v1/mints?filter=all|held|wrapped&sort=recent|burn&limit=&offset=
|
|
23
|
+
- /api/v1/top?filter=unminted|minted|all&limit=&offset= (blocks by burn, highest first; filter defaults to unminted, limit to 50, at most 100 per request; ranked 10,000 deep: offset ≤ 9999, a page past rank 10,000 is cut short and the returned limit says so)
|
|
24
|
+
- /api/v1/blk/{N}, /api/v1/burn/{N} (burn of one indexed block; 503 until the index reaches it), /api/v1/addr/{address}?limit=&offset=
|
|
25
|
+
- /api/v1/candidates (the 8 newest indexed blocks with burn and minted flag)
|
|
26
|
+
- /api/v1/market, /api/v1/market/trades?limit=&offset=, /api/v1/market/buyer/{address}[?status=open]
|
package/package.json
CHANGED
|
@@ -1,8 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nat-cli",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Mint NAT, wrap / unwrap BNAT and trade it through the Binance Agentic Wallet. Ships SKILL.md for AI agents.",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"homepage": "https://bnat.live",
|
|
6
|
-
"
|
|
7
|
-
|
|
7
|
+
"keywords": [
|
|
8
|
+
"bnat",
|
|
9
|
+
"nat",
|
|
10
|
+
"bsc",
|
|
11
|
+
"bep-95",
|
|
12
|
+
"binance",
|
|
13
|
+
"agentic-wallet",
|
|
14
|
+
"cli"
|
|
15
|
+
],
|
|
16
|
+
"bin": {
|
|
17
|
+
"bnat": "scripts/cli.mjs"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"scripts/cli.mjs",
|
|
21
|
+
"lib",
|
|
22
|
+
"i18n",
|
|
23
|
+
"assets",
|
|
24
|
+
"SKILL.md",
|
|
25
|
+
"llms.txt",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "BNAT_LANG=zh node --no-warnings --test test/*.test.mjs",
|
|
33
|
+
"assets": "node scripts/assets.mjs",
|
|
34
|
+
"prepublishOnly": "node scripts/prepublish.mjs && npm test"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@binance/agentic-wallet": "1.12.0",
|
|
38
|
+
"qrcode": "1.5.4",
|
|
39
|
+
"viem": "2.57.10"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT"
|
|
8
42
|
}
|
package/scripts/cli.mjs
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bnat: mint NAT, wrap / unwrap it into BNAT and trade BNAT through the Binance Agentic Wallet (baw).
|
|
3
|
+
// Every write: local checks -> on-chain simulation -> wallet preview checked field by field
|
|
4
|
+
// -> journal -> execute once -> receipt and event verified. See SKILL.md.
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { parseArgs } from 'node:util';
|
|
7
|
+
import { getAddress, isAddress } from 'viem';
|
|
8
|
+
import {
|
|
9
|
+
ABI, ELEMENT_ACTIVATION_BLOCK, ZERO, api, attest, calldata, costOf, eventsOf, explainRevert,
|
|
10
|
+
fmt18, loadConfig, makeClient, marketFee, nativeBalance, redact, parseBlk, BATCH_MAX, parseUnits18, payOf, readBid, REVERT_TEXT, wrapAmounts, walletState,
|
|
11
|
+
} from '../lib/chain.mjs';
|
|
12
|
+
import * as wallet from '../lib/baw.mjs';
|
|
13
|
+
import { configFile, lang, saveSetting, settings, t, tIn } from '../lib/i18n.mjs';
|
|
14
|
+
import { Journal } from '../lib/journal.mjs';
|
|
15
|
+
|
|
16
|
+
const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
17
|
+
const HELP = t('cli.help', { VERSION, BATCH_MAX });
|
|
18
|
+
|
|
19
|
+
const { values: o, positionals } = parseArgs({
|
|
20
|
+
allowPositionals: true,
|
|
21
|
+
options: {
|
|
22
|
+
yes: { type: 'boolean' }, 'preview-only': { type: 'boolean' }, json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
|
|
23
|
+
block: { type: 'string' }, best: { type: 'boolean' }, to: { type: 'string' }, size: { type: 'string' },
|
|
24
|
+
price: { type: 'string' }, amount: { type: 'string' }, all: { type: 'boolean' }, bid: { type: 'string' },
|
|
25
|
+
hash: { type: 'string' }, id: { type: 'string' }, abandon: { type: 'string' }, version: { type: 'boolean', short: 'v' },
|
|
26
|
+
filter: { type: 'string' }, offset: { type: 'string' }, address: { type: 'string' }, sort: { type: 'string' }, mine: { type: 'boolean' },
|
|
27
|
+
lang: { type: 'string' }, // read by lib/i18n.mjs at import
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const json = (v) => JSON.stringify(v, (_, x) => (typeof x === 'bigint' ? x.toString() : x), 2);
|
|
32
|
+
const lines = [];
|
|
33
|
+
const say = (s = '') => { if (!o.json) console.log(redact(s)); else lines.push(s); };
|
|
34
|
+
const { Stop } = wallet; // a refused precondition: exit 2, nothing was sent
|
|
35
|
+
|
|
36
|
+
let cfg, client; // loaded in main() so a bad config is reported like any other error
|
|
37
|
+
const short = (a) => `${a.slice(0, 6)}…${a.slice(-4)}`;
|
|
38
|
+
const bnb = (wei) => `${fmt18(wei)} BNB`;
|
|
39
|
+
const UNKNOWN_RESULT = t('cli.outcomeUnknownDoNotResend');
|
|
40
|
+
const FROM_INDEX = t('cli.fromTheSiteIndexChecked');
|
|
41
|
+
|
|
42
|
+
async function main() {
|
|
43
|
+
const cmd = positionals[0];
|
|
44
|
+
if (o.version) return say(VERSION);
|
|
45
|
+
if (o.lang !== undefined && o.lang !== 'zh' && o.lang !== 'en') throw new Stop(t('cli.langMustBeZhOr'));
|
|
46
|
+
// Bare `bnat` in a terminal opens the TUI; piped or under an agent (no TTY) it stays the help text.
|
|
47
|
+
if ((!cmd && !o.help && !o.json && process.stdin.isTTY && process.stdout.isTTY) || cmd === 'gui') {
|
|
48
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Stop(t('cli.theInteractiveScreenNeedsA'));
|
|
49
|
+
const { runTui } = await import('../lib/tui.mjs');
|
|
50
|
+
return runTui();
|
|
51
|
+
}
|
|
52
|
+
if (!cmd || o.help || cmd === 'help') return say(HELP);
|
|
53
|
+
if (cmd === 'lang') return langCmd();
|
|
54
|
+
if (cmd === 'config') return configCmd();
|
|
55
|
+
const run = { status, wallet: walletCmd, blocks, top, block: blockCmd, holdings, vault, bids, history, mint, wrap, unwrap, bid, sell, cancel, resume }[cmd];
|
|
56
|
+
if (!run) throw new Stop(`${t('cli.unknownCommand')} ${cmd}\n\n${HELP}`);
|
|
57
|
+
cfg = loadConfig();
|
|
58
|
+
client = makeClient(cfg);
|
|
59
|
+
return run();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------- read-only ----------------
|
|
63
|
+
|
|
64
|
+
/** `bnat config [mintTo <address|self>]`: show the settings, or set the default mint recipient. */
|
|
65
|
+
function configCmd() {
|
|
66
|
+
const [, key, value] = positionals;
|
|
67
|
+
if (key === undefined) {
|
|
68
|
+
const c = settings();
|
|
69
|
+
say(t('cli.configShow', { lang, mintTo: c.mintTo ?? t('cli.thisWallet'), file: configFile() }));
|
|
70
|
+
return { lang, mintTo: c.mintTo ?? null, file: configFile() };
|
|
71
|
+
}
|
|
72
|
+
if (key !== 'mintTo' || value === undefined) throw new Stop(t('cli.configUsage'));
|
|
73
|
+
if (value !== 'self' && !isAddress(value)) throw new Stop(t('tui.badAddress', { v: value }));
|
|
74
|
+
const v = value === 'self' ? undefined : getAddress(value);
|
|
75
|
+
saveSetting('mintTo', v);
|
|
76
|
+
say(t('cli.configSaved', { key, value: v ?? t('cli.thisWallet'), file: configFile() }));
|
|
77
|
+
return { mintTo: v ?? null, file: configFile() };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `bnat lang [zh|en]`: show or save the language ("lang" in config.json). */
|
|
81
|
+
function langCmd() {
|
|
82
|
+
const v = positionals[1];
|
|
83
|
+
if (v === undefined) {
|
|
84
|
+
say(t('cli.languageEnglishSettingsFile', { v0: configFile() }));
|
|
85
|
+
return { lang, file: configFile() };
|
|
86
|
+
}
|
|
87
|
+
if (v !== 'zh' && v !== 'en') throw new Stop(t('cli.usageBnatLangZhEn'));
|
|
88
|
+
saveSetting('lang', v);
|
|
89
|
+
say(tIn(v, 'cli.langSaved', { file: configFile() })); // in the newly chosen language
|
|
90
|
+
return { lang: v, file: configFile() };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function status() {
|
|
94
|
+
const out = { chainId: cfg.d.chainId, nat: cfg.d.nat, bnat: cfg.d.bnat, market: cfg.d.market, agentIdRegistry: cfg.d.agentId };
|
|
95
|
+
say(t('cli.chainNatBnatMarket', { chainId: cfg.d.chainId, nat: cfg.d.nat, bnat: cfg.d.bnat, market: cfg.d.market }));
|
|
96
|
+
if (cfg.site) {
|
|
97
|
+
const [s, m] = await Promise.all([api(cfg, '/api/v1/stats'), api(cfg, '/api/v1/market')]);
|
|
98
|
+
Object.assign(out, { stats: s, market: m.stats });
|
|
99
|
+
const sealed = fmt18(BigInt(s.totalBurnGwei) * 10n ** 9n);
|
|
100
|
+
say(t('cli.mintedOfThemInThe', { minted: s.minted, wrapped: s.wrapped, sealed, holders: s.holders }));
|
|
101
|
+
const price = m.stats.bestPrice ? fmt18(BigInt(m.stats.bestPrice)) : '—';
|
|
102
|
+
const vol = fmt18(BigInt(m.stats.volumePaid));
|
|
103
|
+
say(t('cli.marketBidsCurrentPriceBnb', { openBids: m.stats.openBids, price, vol }));
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function walletCmd() {
|
|
109
|
+
const a = await wallet.walletAddress();
|
|
110
|
+
const w = await walletState(cfg, client, a);
|
|
111
|
+
say(`${t('cli.wallet')} ${a}`);
|
|
112
|
+
say(`Agent ID ${w.agentId ? t('cli.yes') : t('cli.noneClaimOneInThe')}`);
|
|
113
|
+
say(`BNB ${fmt18(w.bnb)}`);
|
|
114
|
+
say(`NAT ${w.nfts}${t('cli.countUnit')}`);
|
|
115
|
+
say(`BNAT ${fmt18(w.bnat)}`);
|
|
116
|
+
return w;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function candidates() {
|
|
120
|
+
const r = await api(cfg, '/api/v1/recent?n=96');
|
|
121
|
+
if (!Array.isArray(r.rows) || !Number.isInteger(r.signableTo)) throw Error(t('cli.unexpectedReplyFromRecent'));
|
|
122
|
+
// Only blocks the signer's burn index has reached (it answers 503 above signableTo).
|
|
123
|
+
return r.rows.filter((x) => x.blk <= r.signableTo && !x.minted && BigInt(x.burnGwei) >= 1n).sort((a, b) => (BigInt(b.burnGwei) > BigInt(a.burnGwei) ? 1 : -1));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function blocks() {
|
|
127
|
+
const size = intOpt('size', 20, 1, 96);
|
|
128
|
+
const rows = (await candidates()).slice(0, size);
|
|
129
|
+
say(t('cli.blockBurnGwei'));
|
|
130
|
+
for (const r of rows) say(`#${String(r.blk).padEnd(14)} ${r.burnGwei}`);
|
|
131
|
+
if (!rows.length) say(t('cli.theLast96FinalizedBlocks'));
|
|
132
|
+
say(FROM_INDEX);
|
|
133
|
+
return rows;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** --name as an integer in [lo, hi], or the default; anything else stops. */
|
|
137
|
+
function intOpt(name, dflt, lo, hi) {
|
|
138
|
+
const v = o[name];
|
|
139
|
+
if (v === undefined) return dflt;
|
|
140
|
+
if (!/^\d+$/.test(v) || Number(v) < lo || Number(v) > hi) throw new Stop(t('cli.mustBeAnIntegerFrom', { name, lo, hi }));
|
|
141
|
+
return Number(v);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function top() {
|
|
145
|
+
const filter = o.filter ?? 'unminted';
|
|
146
|
+
if (!['unminted', 'minted', 'all'].includes(filter)) throw new Stop(t('cli.filterMustBeUnmintedMinted'));
|
|
147
|
+
const size = intOpt('size', 20, 1, 100), offset = intOpt('offset', 0, 0, 9_999);
|
|
148
|
+
const r = await api(cfg, `/api/v1/top?filter=${filter}&limit=${size}&offset=${offset}`);
|
|
149
|
+
if (!Array.isArray(r.rows)) throw Error(t('cli.unexpectedReplyFromTop'));
|
|
150
|
+
say(t('cli.rankBlockBurnGweiStatus'));
|
|
151
|
+
r.rows.forEach((x, i) => say(`${String(offset + i + 1).padEnd(7)} #${String(x.blk).padEnd(14)} ${x.burnGwei.padEnd(18)} ${x.minted ? t('cli.minted') : t('cli.unminted')}`));
|
|
152
|
+
if (!r.rows.length) say(t('cli.noMatchingBlocks'));
|
|
153
|
+
say(FROM_INDEX);
|
|
154
|
+
return r.rows;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function blockCmd() {
|
|
158
|
+
const blk = parseBlk(o.block);
|
|
159
|
+
if (blk === null) throw new Stop(t('cli.nameTheBlockWithBlock'));
|
|
160
|
+
const b = await api(cfg, `/api/v1/blk/${blk}`);
|
|
161
|
+
say(`${t('cli.block')} #${b.blk}`);
|
|
162
|
+
say(`${t('cli.burn')} ${b.burnGwei ?? '—'} gwei${b.bscBurnGwei ? t('cli.bscRecomputesWhichDiffersFrom', { bscBurnGwei: b.bscBurnGwei }) : ''}`);
|
|
163
|
+
say(`${t('cli.final')} ${b.finalized ? t('cli.yes2') : t('cli.no')}`);
|
|
164
|
+
say(`${t('cli.minted2')} ${b.minted
|
|
165
|
+
? t('cli.mintedHeldBy', { owner: b.owner, where: b.wrapped ? t('cli.inTheBnatVaultCan') : '' })
|
|
166
|
+
: t('cli.no2')}`);
|
|
167
|
+
return b;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function holdings() {
|
|
171
|
+
const who = o.address ?? await wallet.walletAddress();
|
|
172
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(who)) throw new Stop(t('cli.addressMustBeAFull'));
|
|
173
|
+
const size = intOpt('size', 100, 1, 100), offset = intOpt('offset', 0, 0, 1e9);
|
|
174
|
+
const r = await api(cfg, `/api/v1/addr/${who}?limit=${size}&offset=${offset}`);
|
|
175
|
+
if (!Array.isArray(r.held)) throw Error(t('cli.unexpectedReplyFromAddr'));
|
|
176
|
+
const range = r.heldTotal > r.held.length ? t('cli.showing', { v0: offset + 1, v1: offset + r.held.length }) : '';
|
|
177
|
+
say(t('cli.holdsNat', { who, heldTotal: r.heldTotal, range }));
|
|
178
|
+
for (const h of r.held) say(`#${String(h.blk).padEnd(14)} ${h.burnGwei} gwei`);
|
|
179
|
+
return r;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function vault() {
|
|
183
|
+
const sort = o.sort ?? 'burn';
|
|
184
|
+
if (!['burn', 'recent'].includes(sort)) throw new Stop(t('cli.sortMustBeBurnOr'));
|
|
185
|
+
const size = intOpt('size', 20, 1, 100), offset = intOpt('offset', 0, 0, 1e9);
|
|
186
|
+
const r = await api(cfg, `/api/v1/mints?filter=wrapped&sort=${sort}&limit=${size}&offset=${offset}`);
|
|
187
|
+
if (!Array.isArray(r.rows)) throw Error(t('cli.unexpectedReplyFromMints'));
|
|
188
|
+
const range = r.total > r.rows.length ? t('cli.showing', { v0: offset + 1, v1: offset + r.rows.length }) : '';
|
|
189
|
+
say(t('cli.natInTheVault', { total: r.total, range }));
|
|
190
|
+
say(t('cli.blockBurnGweiUnwrapCost'));
|
|
191
|
+
for (const x of r.rows) say(`#${String(x.blk).padEnd(14)} ${x.burnGwei.padEnd(18)} ${fmt18(wrapAmounts(BigInt(x.burnGwei)).unwrapCost)}`);
|
|
192
|
+
return r;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function bids() {
|
|
196
|
+
const size = intOpt('size', 100, 1, 100), offset = intOpt('offset', 0, 0, 1e9);
|
|
197
|
+
const m = o.mine
|
|
198
|
+
? await api(cfg, `/api/v1/market/buyer/${await wallet.walletAddress()}?status=open&limit=${size}&offset=${offset}`)
|
|
199
|
+
: await api(cfg, `/api/v1/market?limit=${size}&offset=${offset}`);
|
|
200
|
+
if (!Array.isArray(m.bids)) throw Error(t('cli.unexpectedReplyFromMarket'));
|
|
201
|
+
const total = m.bidsTotal ?? m.total;
|
|
202
|
+
if (total > m.bids.length) say(t('cli.bidsShowing', { total, v0: offset + 1, v1: offset + m.bids.length }));
|
|
203
|
+
say(t('cli.idPriceBnbBnatLeft'));
|
|
204
|
+
for (const b of m.bids) say(`#${String(b.id).padEnd(5)} ${fmt18(BigInt(b.price)).padEnd(18)} ${fmt18(BigInt(b.remaining)).padEnd(17)} ${fmt18(BigInt(b.escrow)).padEnd(13)} ${short(b.buyer)}`);
|
|
205
|
+
if (!m.bids.length) say(o.mine ? t('cli.thisWalletHasNoOpen') : t('cli.thereAreNoBidsRight'));
|
|
206
|
+
return m.bids;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function history() {
|
|
210
|
+
const all = new Journal().entries();
|
|
211
|
+
for (const e of all) say(`${e.at} ${e.status.padEnd(10)} ${e.action} ${e.summary ?? ''}${e.hash ? ` ${e.hash}` : ''} [${e.id}]`);
|
|
212
|
+
if (!all.length) say(t('cli.noEntriesYet'));
|
|
213
|
+
return all;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---------------- the write pipeline ----------------
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* plan: { action, summary, address, abi, functionName, args, value, assets, native, approve?, verify(receipt) }
|
|
220
|
+
*/
|
|
221
|
+
const loopback = (u) => { try { return ['127.0.0.1', 'localhost', '[::1]'].includes(new URL(u).hostname); } catch { return false; } };
|
|
222
|
+
function fakeWallet() {
|
|
223
|
+
const bin = process.env.BNAT_BAW_BIN;
|
|
224
|
+
return !!bin && !/agentic-wallet|(^|[/\\])baw(\.\w+)?$/.test(bin) && loopback(cfg.rpc) && loopback(process.env.FAKE_BAW_RPC);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function send(from, plan) {
|
|
228
|
+
// A dev deployment lives on a local chain, but the real wallet signs on BSC mainnet, where these
|
|
229
|
+
// addresses mean something else. Only the fake wallet may send for it: an override that is not
|
|
230
|
+
// the packaged baw, with both this CLI and the fake wallet on a loopback RPC.
|
|
231
|
+
if (cfg.d.mockAgentId && !fakeWallet()) {
|
|
232
|
+
throw new Stop(t('cli.thisIsADevDeployment'));
|
|
233
|
+
}
|
|
234
|
+
const j = new Journal();
|
|
235
|
+
const open = j.open();
|
|
236
|
+
if (open.length) {
|
|
237
|
+
const list = open.map((e) => `${e.action} ${e.hash ?? t('cli.noHash')} [${e.id}]`).join(t('common.listSep'));
|
|
238
|
+
throw new Stop(t('cli.transactionSWithAnUnknown', { length: open.length, list }));
|
|
239
|
+
}
|
|
240
|
+
const value = plan.value ?? 0n;
|
|
241
|
+
const data = calldata(plan.abi, plan.functionName, plan.args);
|
|
242
|
+
const errs = [...plan.abi, ...ABI.bnat, ...ABI.nat].filter((x) => x.type === 'error');
|
|
243
|
+
try {
|
|
244
|
+
await client.simulateContract({ address: plan.address, abi: [...plan.abi, ...errs], functionName: plan.functionName, args: plan.args, account: from, value });
|
|
245
|
+
} catch (e) {
|
|
246
|
+
throw new Stop(t('cli.onChainSimulationFailed') + explainRevert(e));
|
|
247
|
+
}
|
|
248
|
+
const [gas, gasPrice, bal] = await Promise.all([
|
|
249
|
+
client.estimateGas({ account: from, to: plan.address, data, value }), client.getGasPrice(), nativeBalance(client, from),
|
|
250
|
+
]);
|
|
251
|
+
const need = value + gas * gasPrice;
|
|
252
|
+
if (bal < need) throw new Stop(t('cli.notEnoughBnbNeedIncluding', { need: bnb(need), v0: bnb(gas * gasPrice), bal: bnb(bal), short: bnb(need - bal) }));
|
|
253
|
+
|
|
254
|
+
say(t('cli.plan') + plan.summary);
|
|
255
|
+
say(t('cli.contractSendsGasAbout', { address: plan.address, functionName: plan.functionName, value: bnb(value), v0: bnb(gas * gasPrice) }));
|
|
256
|
+
say(t('cli.onChainSimulationPassed'));
|
|
257
|
+
if (!o.yes && !o['preview-only']) {
|
|
258
|
+
say(t('cli.withoutYesOnlyCheckedNothing'));
|
|
259
|
+
return { planned: true, data };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const tx = { from, to: plan.address, data, value };
|
|
263
|
+
const pv = wallet.validatePreview(await wallet.preview(cfg.d.chainId, tx), {
|
|
264
|
+
...tx, assets: plan.assets ?? {}, native: plan.native ?? {}, approve: plan.approve,
|
|
265
|
+
});
|
|
266
|
+
say(t('cli.walletPreviewEveryFieldChecked'));
|
|
267
|
+
if (o['preview-only']) return { previewed: true, requestId: pv.requestId, data };
|
|
268
|
+
|
|
269
|
+
const id = j.begin({ action: plan.action, summary: plan.summary, from, to: plan.address, data, value: value.toString() });
|
|
270
|
+
let res;
|
|
271
|
+
try {
|
|
272
|
+
res = await wallet.execute(pv.requestId, () => say(t('cli.stillWaitingForTheWallet', { id })));
|
|
273
|
+
} catch (e) {
|
|
274
|
+
// The request may or may not have reached the chain: keep the entry open.
|
|
275
|
+
j.update(id, { error: String(e.message ?? e) });
|
|
276
|
+
throw Error(`${t('cli.executeErrorPrefix', { message: e.message })}${UNKNOWN_RESULT}`);
|
|
277
|
+
}
|
|
278
|
+
if (!res.hash) {
|
|
279
|
+
j.update(id, { status: 'sent', orderId: res.orderId, walletStatus: res.status });
|
|
280
|
+
throw Error(t('cli.theWalletAcceptedItBut', { v0: res.orderId ?? '?', status: res.status }) + UNKNOWN_RESULT);
|
|
281
|
+
}
|
|
282
|
+
j.update(id, { status: 'sent', hash: res.hash });
|
|
283
|
+
say(t('cli.sentWaitingForTheReceipt', { hash: res.hash }));
|
|
284
|
+
return settle(j, id, res.hash, plan.verify);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function settle(j, id, hash, verify) {
|
|
288
|
+
let rc;
|
|
289
|
+
try {
|
|
290
|
+
rc = await client.waitForTransactionReceipt({ hash, timeout: 180_000 });
|
|
291
|
+
} catch (e) {
|
|
292
|
+
// Still pending or the RPC failed: the entry stays open (status "sent"), nothing is resent.
|
|
293
|
+
const why = e.shortMessage ?? e.message;
|
|
294
|
+
throw Error(t('cli.transactionWasSentButHas', { hash, why }));
|
|
295
|
+
}
|
|
296
|
+
if (rc.status !== 'success') {
|
|
297
|
+
j.update(id, { status: 'failed', hash, block: Number(rc.blockNumber) });
|
|
298
|
+
throw Error(t('cli.transactionIsOnChainBut', { hash, blockNumber: rc.blockNumber }));
|
|
299
|
+
}
|
|
300
|
+
const result = verify ? verify(rc) : {};
|
|
301
|
+
j.update(id, { status: 'confirmed', hash, block: Number(rc.blockNumber), result });
|
|
302
|
+
say(t('cli.doneBlock', { hash, blockNumber: rc.blockNumber }));
|
|
303
|
+
return { hash, block: Number(rc.blockNumber), ...result };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const one = (evs, name, ok) => {
|
|
307
|
+
const e = evs.find((x) => x.eventName === name && ok(x.args));
|
|
308
|
+
if (!e) throw Error(t('cli.theReceiptLacksTheExpected', { name }));
|
|
309
|
+
return e.args;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// ---------------- writes ----------------
|
|
313
|
+
|
|
314
|
+
async function mint() {
|
|
315
|
+
const from = await wallet.walletAddress();
|
|
316
|
+
// Recipient: --to, else the mintTo setting (`bnat config mintTo …`), else this wallet.
|
|
317
|
+
const saved = o.to === undefined ? settings().mintTo : undefined;
|
|
318
|
+
if (saved !== undefined && (!isAddress(String(saved)) || saved === ZERO)) throw new Stop(t('cli.mintToBad', { v: saved }));
|
|
319
|
+
const to = o.to ? (isAddress(o.to) ? getAddress(o.to) : null) : saved ? getAddress(saved) : from;
|
|
320
|
+
if (!to || to === ZERO) throw new Stop(t('cli.toIsNotAValid'));
|
|
321
|
+
const self = to.toLowerCase() === from.toLowerCase();
|
|
322
|
+
const w = await walletState(cfg, client, from);
|
|
323
|
+
if (!w.agentId) {
|
|
324
|
+
throw new Stop(t('cli.walletHasNoAiAgent', { from }));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let blk;
|
|
328
|
+
if (o.best) {
|
|
329
|
+
const c = await candidates();
|
|
330
|
+
for (const r of c) {
|
|
331
|
+
// burnOf is nonzero exactly when the block has been minted
|
|
332
|
+
const b = await client.readContract({ address: cfg.d.nat, abi: ABI.nat, functionName: 'burnOf', args: [BigInt(r.blk)] });
|
|
333
|
+
if (b === 0n) { blk = r.blk; break; }
|
|
334
|
+
}
|
|
335
|
+
if (blk === undefined) throw new Stop(t('cli.nothingLeftToMintIn'));
|
|
336
|
+
} else {
|
|
337
|
+
blk = parseBlk(o.block);
|
|
338
|
+
if (blk === null) throw new Stop(t('cli.nameTheBlockWithBlock2'));
|
|
339
|
+
}
|
|
340
|
+
if (blk < ELEMENT_ACTIVATION_BLOCK) throw new Stop(t('cli.isBeforeTheBep95', { blk, ELEMENT_ACTIVATION_BLOCK }));
|
|
341
|
+
const onChain = await client.readContract({ address: cfg.d.nat, abi: ABI.nat, functionName: 'burnOf', args: [BigInt(blk)] });
|
|
342
|
+
if (onChain !== 0n) throw new Stop(t('cli.isAlreadyMintedFirstCome', { blk }));
|
|
343
|
+
// The signer refuses blocks that are not in its burn index yet or burned < 1 gwei.
|
|
344
|
+
const { sig, burn } = await attest(cfg, client, blk);
|
|
345
|
+
|
|
346
|
+
return send(from, {
|
|
347
|
+
action: 'mint', summary: t('cli.mintSummary', { blk, burn, dest: self ? t('cli.thisWallet') : to }),
|
|
348
|
+
address: cfg.d.nat, abi: ABI.nat, functionName: 'mint',
|
|
349
|
+
// To self: the 3-arg overload (simplest calldata for the wallet); otherwise name the recipient.
|
|
350
|
+
args: self ? [BigInt(blk), burn, sig] : [BigInt(blk), burn, sig, to],
|
|
351
|
+
// Minting to another address moves nothing in or out of this wallet: no asset may change.
|
|
352
|
+
assets: self ? { [cfg.d.nat]: { in: true } } : {},
|
|
353
|
+
verify: (rc) => {
|
|
354
|
+
const a = one(eventsOf(rc, ABI.nat, cfg.d.nat), 'Minted', (x) => x.blk === BigInt(blk)
|
|
355
|
+
&& x.minter.toLowerCase() === from.toLowerCase() && x.to.toLowerCase() === to.toLowerCase());
|
|
356
|
+
return { blk, to: a.to, burnGwei: a.burnGwei };
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function wrap() {
|
|
362
|
+
const blks = blockList('wrap');
|
|
363
|
+
if (o.to !== undefined) throw new Stop(t('cli.wrapHasNoToBnat'));
|
|
364
|
+
const from = await wallet.walletAddress();
|
|
365
|
+
// Each step reads every block at once (one aggregate3), then checks them in block order, so the
|
|
366
|
+
// first failing block is the one reported.
|
|
367
|
+
const read = (functionName, args, abi = ABI.nat, address = cfg.d.nat) => client.readContract({ address, abi, functionName, args });
|
|
368
|
+
const owners = await Promise.allSettled(blks.map((blk) => read('ownerOf', [BigInt(blk)])));
|
|
369
|
+
const foreign = [];
|
|
370
|
+
blks.forEach((blk, i) => {
|
|
371
|
+
const r = owners[i];
|
|
372
|
+
if (r.status === 'rejected') throw new Stop(t('cli.hasNoHolderNotMinted', { blk }) + explainRevert(r.reason));
|
|
373
|
+
if (r.value.toLowerCase() === cfg.d.bnat.toLowerCase()) throw new Stop(t('cli.isAlreadyInTheBnat', { blk }));
|
|
374
|
+
if (r.value.toLowerCase() !== from.toLowerCase()) foreign.push({ blk, owner: r.value });
|
|
375
|
+
});
|
|
376
|
+
const approvals = await Promise.all(foreign.map(({ blk, owner }) => Promise.all([read('getApproved', [BigInt(blk)]), read('isApprovedForAll', [owner, from])])));
|
|
377
|
+
foreign.forEach(({ blk, owner }, i) => {
|
|
378
|
+
const [ap, all] = approvals[i];
|
|
379
|
+
if (ap.toLowerCase() !== from.toLowerCase() && !all) throw new Stop(t('cli.belongsToNotThisWallet', { blk, owner }));
|
|
380
|
+
});
|
|
381
|
+
const burns = await Promise.all(blks.map((blk) => read('burnOf', [BigInt(blk)])));
|
|
382
|
+
let toHolder = 0n, fee = 0n;
|
|
383
|
+
for (const burn of burns) {
|
|
384
|
+
const s = wrapAmounts(burn); // fee floors per block, as the contract does
|
|
385
|
+
toHolder += s.toHolder;
|
|
386
|
+
fee += s.fee;
|
|
387
|
+
}
|
|
388
|
+
const batch = blks.length > 1;
|
|
389
|
+
const names = blks.map((b) => `#${b}`).join(' ');
|
|
390
|
+
return send(from, {
|
|
391
|
+
action: 'wrap',
|
|
392
|
+
summary: t('cli.wrapBnatToThisWallet', { names, toHolder: fmt18(toHolder), fee: fmt18(fee) }),
|
|
393
|
+
address: cfg.d.nat, abi: ABI.nat,
|
|
394
|
+
...(batch ? { functionName: 'wrapBatch', args: [blks.map(BigInt)] } : { functionName: 'wrap', args: [BigInt(blks[0])] }),
|
|
395
|
+
assets: { [cfg.d.nat]: { maxOut: BigInt(blks.length) }, [cfg.d.bnat]: { in: true } },
|
|
396
|
+
verify: (rc) => {
|
|
397
|
+
const evs = eventsOf(rc, ABI.nat, cfg.d.nat);
|
|
398
|
+
const got = blks.map((blk) => one(evs, 'Wrapped', (x) => x.blk === BigInt(blk)));
|
|
399
|
+
const sum = (k) => got.reduce((acc, a) => acc + a[k], 0n);
|
|
400
|
+
return batch ? { blks, to: got[0].to, amount: sum('amount'), fee: sum('fee') } : { blk: blks[0], to: got[0].to, amount: got[0].amount, fee: got[0].fee };
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function unwrap() {
|
|
406
|
+
const blks = blockList('unwrap');
|
|
407
|
+
if (o.to !== undefined) throw new Stop(t('cli.unwrapHasNoToThe'));
|
|
408
|
+
const from = await wallet.walletAddress();
|
|
409
|
+
// ownerOf of every block, the contract's own quote of every block (amount = burnOf * 1e9, fee =
|
|
410
|
+
// 1% floored per block) and the balance: one aggregate3, then checked in block order.
|
|
411
|
+
const [owners, quotes, bal] = await Promise.all([
|
|
412
|
+
Promise.allSettled(blks.map((blk) => client.readContract({ address: cfg.d.nat, abi: ABI.nat, functionName: 'ownerOf', args: [BigInt(blk)] }))),
|
|
413
|
+
Promise.allSettled(blks.map((blk) => client.readContract({ address: cfg.d.bnat, abi: ABI.bnat, functionName: 'quote', args: [BigInt(blk)] }))),
|
|
414
|
+
client.readContract({ address: cfg.d.bnat, abi: ABI.bnat, functionName: 'balanceOf', args: [from] }),
|
|
415
|
+
]);
|
|
416
|
+
let amount = 0n, fee = 0n;
|
|
417
|
+
blks.forEach((blk, i) => {
|
|
418
|
+
const r = owners[i];
|
|
419
|
+
if (r.status === 'rejected') throw new Stop(`#${blk}${t('common.colon')}${REVERT_TEXT.NotInVault}${t('cli.notMintedYet', { reason: explainRevert(r.reason) })}`);
|
|
420
|
+
if (r.value.toLowerCase() !== cfg.d.bnat.toLowerCase()) throw new Stop(`#${blk}${t('common.colon')}${REVERT_TEXT.NotInVault}${t('cli.heldBy', { value: r.value })}`);
|
|
421
|
+
if (quotes[i].status === 'rejected') throw quotes[i].reason;
|
|
422
|
+
amount += quotes[i].value[0];
|
|
423
|
+
fee += quotes[i].value[1];
|
|
424
|
+
});
|
|
425
|
+
const total = amount + fee;
|
|
426
|
+
if (bal < total) {
|
|
427
|
+
throw new Stop(t('cli.notEnoughBnatNeedIncluding', { total: fmt18(total), fee: fmt18(fee), bal: fmt18(bal), v0: fmt18(total - bal) }));
|
|
428
|
+
}
|
|
429
|
+
const batch = blks.length > 1;
|
|
430
|
+
const names = blks.map((b) => `#${b}`).join(' ');
|
|
431
|
+
return send(from, {
|
|
432
|
+
action: 'unwrap',
|
|
433
|
+
summary: t('cli.unwrapPayBnatBurnFee', { names, total: fmt18(total), amount: fmt18(amount), fee: fmt18(fee), length: blks.length }),
|
|
434
|
+
address: cfg.d.bnat, abi: ABI.bnat,
|
|
435
|
+
...(batch ? { functionName: 'unwrapBatch', args: [blks.map(BigInt)] } : { functionName: 'unwrap', args: [BigInt(blks[0])] }),
|
|
436
|
+
// No approval: BNAT burns and moves the caller's own tokens.
|
|
437
|
+
assets: { [cfg.d.bnat]: { maxOut: total }, [cfg.d.nat]: { in: true } },
|
|
438
|
+
verify: (rc) => {
|
|
439
|
+
const evs = eventsOf(rc, ABI.bnat, cfg.d.bnat);
|
|
440
|
+
const got = blks.map((blk) => one(evs, 'Unwrapped', (x) => x.blk === BigInt(blk) && x.by.toLowerCase() === from.toLowerCase()));
|
|
441
|
+
const sum = (k) => got.reduce((acc, a) => acc + a[k], 0n);
|
|
442
|
+
return batch ? { blks, amount: sum('amount'), fee: sum('fee') } : { blk: blks[0], amount: got[0].amount, fee: got[0].fee };
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** --block N1,N2,… for wrap / unwrap: valid, distinct, at most BATCH_MAX. */
|
|
448
|
+
function blockList(verb) {
|
|
449
|
+
const parts = String(o.block ?? '').split(',').map((x) => x.trim()).filter(Boolean);
|
|
450
|
+
const blks = parts.map(parseBlk);
|
|
451
|
+
if (blks.length === 0 || blks.includes(null)) throw new Stop(t('cli.nameTheNatToWith', { verb }));
|
|
452
|
+
if (new Set(blks).size !== blks.length) throw new Stop(t('cli.blockListsABlockTwice'));
|
|
453
|
+
if (blks.length > BATCH_MAX) throw new Stop(t('cli.atMostPerSplitIt', { verb, BATCH_MAX }));
|
|
454
|
+
return blks;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function bid() {
|
|
458
|
+
const price = parseUnits18(o.price), amount = parseUnits18(o.amount);
|
|
459
|
+
if (price === null || amount === null) throw new Stop(t('cli.priceBnbPerBnatAnd'));
|
|
460
|
+
if (price === 0n || amount === 0n) throw new Stop(t('cli.priceAndAmountMustBoth'));
|
|
461
|
+
const from = await wallet.walletAddress();
|
|
462
|
+
const cost = costOf(price, amount);
|
|
463
|
+
return send(from, {
|
|
464
|
+
action: 'bid', summary: t('cli.bidBnatAtBnbLocking', { amount: fmt18(amount), price: fmt18(price), cost: bnb(cost) }),
|
|
465
|
+
address: cfg.d.market, abi: ABI.market, functionName: 'bid', args: [price, amount], value: cost,
|
|
466
|
+
native: { maxOut: cost },
|
|
467
|
+
verify: (rc) => {
|
|
468
|
+
const a = one(eventsOf(rc, ABI.market, cfg.d.market), 'BidPlaced', (x) => x.buyer.toLowerCase() === from.toLowerCase());
|
|
469
|
+
say(t('cli.bid', { id: a.id }));
|
|
470
|
+
return { bid: Number(a.id), price, amount, escrow: a.escrow };
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function sell() {
|
|
476
|
+
if (!/^\d+$/.test(o.bid ?? '')) throw new Stop(t('cli.nameTheBidWithBid'));
|
|
477
|
+
if (!o.all && parseUnits18(o.amount) === null) throw new Stop(t('cli.giveTheAmountWithAmount'));
|
|
478
|
+
const from = await wallet.walletAddress();
|
|
479
|
+
const id = BigInt(o.bid);
|
|
480
|
+
const b = await readBid(cfg, client, id);
|
|
481
|
+
if (b.remaining === 0n) throw new Stop(t('cli.bidIsAlreadyFilledOr', { id }));
|
|
482
|
+
const bal = await client.readContract({ address: cfg.d.bnat, abi: ABI.bnat, functionName: 'balanceOf', args: [from] });
|
|
483
|
+
const amount = o.all ? (bal < b.remaining ? bal : b.remaining) : parseUnits18(o.amount);
|
|
484
|
+
if (amount === null) throw new Stop(t('cli.giveTheAmountWithAmount'));
|
|
485
|
+
if (amount === 0n) throw new Stop(t('cli.theSellAmountMustBe'));
|
|
486
|
+
if (amount > b.remaining) throw new Stop(t('cli.bidHasOnlyBnatLeft', { id, remaining: fmt18(b.remaining), amount: fmt18(amount) }));
|
|
487
|
+
if (bal < amount) throw new Stop(t('cli.notEnoughBnatNeedHave', { amount: fmt18(amount), bal: fmt18(bal), short: fmt18(amount - bal) }));
|
|
488
|
+
const pay = payOf(b, amount);
|
|
489
|
+
if (pay === 0n) throw new Stop(t('cli.amountTooSmallWorthLess'));
|
|
490
|
+
const fee = marketFee(pay);
|
|
491
|
+
const summary = t('cli.sellBnatToBidReceive', { amount: fmt18(amount), id, v0: bnb(pay - fee), fee: bnb(fee) });
|
|
492
|
+
|
|
493
|
+
const allowance = await client.readContract({ address: cfg.d.bnat, abi: ABI.bnat, functionName: 'allowance', args: [from, cfg.d.market] });
|
|
494
|
+
let approval = null;
|
|
495
|
+
if (allowance < amount) {
|
|
496
|
+
say(t('cli.plan') + summary); // the sale runs after the approval: show it now, not only then
|
|
497
|
+
say(t('cli.theMarketSBnatAllowance', { allowance: fmt18(allowance), amount: fmt18(amount) }));
|
|
498
|
+
approval = await send(from, {
|
|
499
|
+
action: 'approve', summary: t('cli.approveTheMarketToUse', { amount: fmt18(amount) }),
|
|
500
|
+
address: cfg.d.bnat, abi: ABI.bnat, functionName: 'approve', args: [cfg.d.market, amount],
|
|
501
|
+
approve: { token: cfg.d.bnat, spender: cfg.d.market, max: amount },
|
|
502
|
+
});
|
|
503
|
+
if (approval.planned || approval.previewed) {
|
|
504
|
+
say(t('cli.theApprovalHasNotRun'));
|
|
505
|
+
return { approval };
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const r = await send(from, {
|
|
509
|
+
action: 'sell', summary,
|
|
510
|
+
address: cfg.d.market, abi: ABI.market, functionName: 'fill', args: [id, amount],
|
|
511
|
+
assets: { [cfg.d.bnat]: { maxOut: amount } }, native: { in: true },
|
|
512
|
+
verify: (rc) => {
|
|
513
|
+
const a = one(eventsOf(rc, ABI.market, cfg.d.market), 'BidFilled', (x) => x.id === id && x.seller.toLowerCase() === from.toLowerCase());
|
|
514
|
+
return { bid: Number(id), amount: a.amount, paid: a.paid, fee: a.fee };
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
return approval ? { approval, ...r } : r;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function cancel() {
|
|
521
|
+
if (!/^\d+$/.test(o.bid ?? '')) throw new Stop(t('cli.nameTheBidToCancel'));
|
|
522
|
+
const from = await wallet.walletAddress();
|
|
523
|
+
const id = BigInt(o.bid);
|
|
524
|
+
const b = await readBid(cfg, client, id);
|
|
525
|
+
if (b.buyer.toLowerCase() !== from.toLowerCase()) throw new Stop(t('cli.bidWasPlacedByNot', { id, buyer: b.buyer }));
|
|
526
|
+
if (b.remaining === 0n) throw new Stop(t('cli.bidIsAlreadyFilledOr', { id }));
|
|
527
|
+
return send(from, {
|
|
528
|
+
action: 'cancel', summary: t('cli.cancelBidRefunding', { id, escrow: bnb(b.escrow) }),
|
|
529
|
+
address: cfg.d.market, abi: ABI.market, functionName: 'cancel', args: [id], native: { in: true },
|
|
530
|
+
verify: (rc) => {
|
|
531
|
+
const a = one(eventsOf(rc, ABI.market, cfg.d.market), 'BidCancelled', (x) => x.id === id);
|
|
532
|
+
return { bid: Number(id), refund: a.refund };
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function resume() {
|
|
538
|
+
const j = new Journal();
|
|
539
|
+
const open = j.open();
|
|
540
|
+
if (o.abandon) {
|
|
541
|
+
const e = open.find((x) => x.id === o.abandon);
|
|
542
|
+
if (!e) throw new Stop(t('cli.noOpenEntry', { abandon: o.abandon }));
|
|
543
|
+
if (e.hash) throw new Stop(t('cli.thisEntryHasAHash'));
|
|
544
|
+
j.update(e.id, { status: 'abandoned' });
|
|
545
|
+
say(t('cli.voided', { action: e.action, id: e.id }));
|
|
546
|
+
return { abandoned: e.id };
|
|
547
|
+
}
|
|
548
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(o.hash ?? '')) throw new Stop(t('cli.giveTheTransactionHashWith'));
|
|
549
|
+
const e = o.id ? open.find((x) => x.id === o.id) : open.find((x) => x.hash?.toLowerCase() === o.hash.toLowerCase()) ?? (open.length === 1 ? open[0] : null);
|
|
550
|
+
if (!e) {
|
|
551
|
+
throw new Stop(open.length
|
|
552
|
+
? t('cli.openEntriesPickOneWith', { length: open.length }) + open.map((x) => x.id).join(', ')
|
|
553
|
+
: t('cli.noOpenEntries'));
|
|
554
|
+
}
|
|
555
|
+
const tx = await client.getTransaction({ hash: o.hash });
|
|
556
|
+
if (tx.from.toLowerCase() !== e.from.toLowerCase() || tx.to?.toLowerCase() !== e.to.toLowerCase()
|
|
557
|
+
|| tx.input.toLowerCase() !== e.data.toLowerCase() || tx.value !== BigInt(e.value ?? 0)) {
|
|
558
|
+
throw new Stop(t('cli.thisTransactionDoesNotMatch'));
|
|
559
|
+
}
|
|
560
|
+
return settle(j, e.id, o.hash, null);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
main().then((r) => {
|
|
564
|
+
if (o.json) console.log(redact(json({ ok: true, result: r ?? null, log: lines })));
|
|
565
|
+
}).catch((e) => {
|
|
566
|
+
const stop = e instanceof Stop;
|
|
567
|
+
if (o.json) console.log(redact(json({ ok: false, stopped: stop, error: e.message, log: lines })));
|
|
568
|
+
else console.error(redact(`${stop ? t('cli.stopped') : t('cli.error')}${t('common.colon')}${e.message}`));
|
|
569
|
+
process.exit(stop ? 2 : 1);
|
|
570
|
+
});
|