@0xwork/cli 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/0xwork.js +1272 -6
- package/package.json +1 -1
- package/src/commands/post.js +55 -1
- package/src/sdk.js +13 -4
package/package.json
CHANGED
package/src/commands/post.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const { requireSDK, getEthers, isDryRun, normalizeError } = require('../sdk');
|
|
5
|
-
const { success, fail, header, keyValue, blank, hint } = require('../output');
|
|
5
|
+
const { success, fail, header, keyValue, blank, hint, warn } = require('../output');
|
|
6
6
|
const { createSpinner } = require('../spinner');
|
|
7
7
|
const { fmtBounty, fmtDeadline, fmtAxobotl } = require('../format');
|
|
8
|
+
const { fetchWithTimeout } = require('../http');
|
|
9
|
+
const { API_URL } = require('../config');
|
|
8
10
|
|
|
9
11
|
const VALID_CATEGORIES = ['Writing', 'Research', 'Code', 'Creative', 'Data', 'Social'];
|
|
10
12
|
|
|
@@ -16,6 +18,7 @@ function register(program) {
|
|
|
16
18
|
.requiredOption('--bounty <amount>', 'Bounty in USDC (e.g. 10)')
|
|
17
19
|
.option('--category <cat>', 'Category: Writing, Research, Code, Creative, Data, Social', 'Code')
|
|
18
20
|
.option('--deadline <dur>', 'Deadline: 7d, 24h, 30m (default: 7d)', '7d')
|
|
21
|
+
.option('--skip-duplicate-check', 'Skip the duplicate task check')
|
|
19
22
|
.action(async (opts) => {
|
|
20
23
|
try {
|
|
21
24
|
await run(opts);
|
|
@@ -79,6 +82,38 @@ async function run(opts) {
|
|
|
79
82
|
const ethers = getEthers();
|
|
80
83
|
const address = sdk.address;
|
|
81
84
|
|
|
85
|
+
// ── Duplicate detection ─────────────────────────────────────────────
|
|
86
|
+
if (!opts.skipDuplicateCheck) {
|
|
87
|
+
try {
|
|
88
|
+
const resp = await fetchWithTimeout(`${API_URL}/tasks?poster=${address}&status=Open&limit=50`);
|
|
89
|
+
if (resp.ok) {
|
|
90
|
+
const data = await resp.json();
|
|
91
|
+
const tasks = data.tasks || [];
|
|
92
|
+
const descNorm = opts.description.trim().toLowerCase();
|
|
93
|
+
const dupes = tasks.filter(t =>
|
|
94
|
+
t.description && t.description.trim().toLowerCase() === descNorm
|
|
95
|
+
);
|
|
96
|
+
if (dupes.length > 0) {
|
|
97
|
+
const dupeIds = dupes.map(t => `#${t.chain_task_id}`).join(', ');
|
|
98
|
+
fail(`Duplicate detected — you already have an open task with this exact description (${dupeIds})`, {
|
|
99
|
+
suggestion: 'Use --skip-duplicate-check to post anyway, or cancel the existing task first.',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// Also warn if posting same category with similar bounty (likely retry)
|
|
103
|
+
const sameCategory = tasks.filter(t =>
|
|
104
|
+
t.category === opts.category &&
|
|
105
|
+
Math.abs(Number(t.bounty_amount) - bountyNum) < 0.01
|
|
106
|
+
);
|
|
107
|
+
if (sameCategory.length >= 3) {
|
|
108
|
+
warn(`You already have ${sameCategory.length} open "${opts.category}" tasks with similar bounties. Use --skip-duplicate-check to post anyway.`);
|
|
109
|
+
fail('Too many similar open tasks — likely a retry loop', {
|
|
110
|
+
suggestion: `Check existing tasks: 0xwork status`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch { /* API check failed — continue anyway */ }
|
|
115
|
+
}
|
|
116
|
+
|
|
82
117
|
// Pre-flight checks
|
|
83
118
|
const s1 = createSpinner('Checking balances…');
|
|
84
119
|
s1.start();
|
|
@@ -121,6 +156,21 @@ async function run(opts) {
|
|
|
121
156
|
|
|
122
157
|
s2.succeed('Posted');
|
|
123
158
|
|
|
159
|
+
// ── Post-action verification ──────────────────────────────────────
|
|
160
|
+
// Fetch the task back from the API to confirm it's live
|
|
161
|
+
let verified = false;
|
|
162
|
+
if (result.taskId != null) {
|
|
163
|
+
try {
|
|
164
|
+
const resp = await fetchWithTimeout(`${API_URL}/tasks/${result.taskId}`);
|
|
165
|
+
if (resp.ok) {
|
|
166
|
+
const data = await resp.json();
|
|
167
|
+
if (data.task && data.task.status === 'Open') {
|
|
168
|
+
verified = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch { /* verification failed — still report success from chain */ }
|
|
172
|
+
}
|
|
173
|
+
|
|
124
174
|
success({
|
|
125
175
|
command: 'post',
|
|
126
176
|
dryRun: false,
|
|
@@ -133,14 +183,18 @@ async function run(opts) {
|
|
|
133
183
|
deadlineHuman: new Date(deadlineSecs * 1000).toISOString(),
|
|
134
184
|
posterStake: posterStake > 0n ? ethers.formatUnits(posterStake, 18) : '0',
|
|
135
185
|
address,
|
|
186
|
+
verified,
|
|
136
187
|
message: `Posted task #${result.taskId} — ${fmtBounty(bountyNum)} bounty`,
|
|
137
188
|
}, () => {
|
|
138
189
|
header('✔', `Task #${result.taskId} Posted`);
|
|
190
|
+
keyValue('Task ID', chalk.white.bold(`#${result.taskId}`));
|
|
139
191
|
keyValue('Bounty', chalk.green.bold(`${fmtBounty(bountyNum)} USDC`));
|
|
140
192
|
keyValue('Category', opts.category);
|
|
141
193
|
keyValue('Deadline', fmtDeadline(deadlineSecs, true));
|
|
142
194
|
if (posterStake > 0n) keyValue('Poster stake', `${fmtAxobotl(ethers.formatUnits(posterStake, 18))} AXOBOTL`);
|
|
195
|
+
keyValue('Status', verified ? chalk.green('✓ Verified on-chain & API') : chalk.yellow('⚠ On-chain confirmed, API sync pending'));
|
|
143
196
|
blank();
|
|
197
|
+
hint(`View: 0xwork task ${result.taskId}`);
|
|
144
198
|
hint(`tx: ${result.txHash}`);
|
|
145
199
|
blank();
|
|
146
200
|
});
|
package/src/sdk.js
CHANGED
|
@@ -105,24 +105,33 @@ function normalizeError(err) {
|
|
|
105
105
|
if (err.code === 'INSUFFICIENT_FUNDS') {
|
|
106
106
|
return 'Insufficient ETH for gas. Fund the wallet with a small amount of ETH on Base.';
|
|
107
107
|
}
|
|
108
|
+
if (msg.includes('insufficient allowance') || msg.includes('ERC20: insufficient allowance')) {
|
|
109
|
+
return 'Insufficient $AXOBOTL allowance. The token approval may have failed — try again.';
|
|
110
|
+
}
|
|
111
|
+
if (msg.includes('transfer amount exceeds balance') || msg.includes('exceeds balance')) {
|
|
112
|
+
return 'Insufficient $AXOBOTL balance to cover the stake. Buy more $AXOBOTL to claim this task.';
|
|
113
|
+
}
|
|
114
|
+
if (msg.includes('Not open') || msg.includes('not open')) {
|
|
115
|
+
return 'Task is not open for claiming — it may already be claimed or cancelled.';
|
|
116
|
+
}
|
|
108
117
|
if (err.code === 'CALL_EXCEPTION') {
|
|
109
118
|
const match = msg.match(/reason="([^"]+)"/);
|
|
110
119
|
return match
|
|
111
|
-
? `Contract
|
|
112
|
-
: 'Contract call failed —
|
|
120
|
+
? `Contract reverted: ${match[1]}`
|
|
121
|
+
: 'Contract call failed — check task state, token balance, and allowance.';
|
|
113
122
|
}
|
|
114
123
|
if (err.code === 'UNPREDICTABLE_GAS_LIMIT' || msg.includes('cannot estimate gas')) {
|
|
115
124
|
const match = msg.match(/reason="([^"]+)"/);
|
|
116
125
|
return match
|
|
117
126
|
? `Transaction would revert: ${match[1]}`
|
|
118
|
-
: 'Transaction would revert
|
|
127
|
+
: 'Transaction would revert — likely insufficient $AXOBOTL for stake, or task is not claimable.';
|
|
119
128
|
}
|
|
120
129
|
if (msg.includes('execution reverted')) {
|
|
121
130
|
const match = msg.match(/reason="([^"]+)"/);
|
|
122
131
|
if (match) return `Contract reverted: ${match[1]}`;
|
|
123
132
|
}
|
|
124
133
|
if (msg.includes('missing revert data') || msg.includes('BAD_DATA')) {
|
|
125
|
-
return 'Contract
|
|
134
|
+
return 'Contract call failed — the task may not exist on-chain or has already been claimed.';
|
|
126
135
|
}
|
|
127
136
|
if (msg.includes('ECONNREFUSED') || msg.includes('ETIMEDOUT') || msg.includes('ENETUNREACH')) {
|
|
128
137
|
return 'Network error: could not reach API or RPC. Check your connection.';
|