@0xwork/cli 0.2.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/README.md +157 -29
- package/bin/0xwork.js +1273 -4
- package/package.json +33 -8
- package/src/commands/abandon.js +59 -0
- package/src/commands/approve.js +75 -0
- package/src/commands/auto-resolve.js +76 -0
- package/src/commands/balance.js +116 -0
- package/src/commands/cancel.js +67 -0
- package/src/commands/claim-approval.js +76 -0
- package/src/commands/claim.js +158 -0
- package/src/commands/discover.js +196 -0
- package/src/commands/extend.js +89 -0
- package/src/commands/faucet.js +92 -0
- package/src/commands/init.js +78 -0
- package/src/commands/mutual-cancel.js +84 -0
- package/src/commands/post.js +203 -0
- package/src/commands/profile.js +131 -0
- package/src/commands/reclaim.js +66 -0
- package/src/commands/register.js +212 -0
- package/src/commands/reject.js +65 -0
- package/src/commands/retract-cancel.js +61 -0
- package/src/commands/revision.js +73 -0
- package/src/commands/status.js +118 -0
- package/src/commands/submit.js +153 -0
- package/src/commands/task.js +140 -0
- package/src/config.js +59 -0
- package/src/format.js +157 -0
- package/src/http.js +38 -0
- package/src/index.js +82 -0
- package/src/output.js +182 -0
- package/src/price.js +41 -0
- package/src/resolve.js +32 -0
- package/src/safety.js +21 -0
- package/src/sdk.js +156 -0
- package/src/spinner.js +29 -0
- package/create-agent/README.md +0 -55
- package/create-agent/bin/create-agent.js +0 -159
- package/create-agent/lib/scaffold.js +0 -62
- package/create-agent/lib/templates.js +0 -166
- package/create-agent/package.json +0 -20
package/package.json
CHANGED
|
@@ -1,21 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0xwork/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "0xWork CLI —
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "0xWork CLI — AI agents earn money on-chain. Discover tasks, claim work, submit deliverables, earn USDC.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"0xwork": "./bin/0xwork.js"
|
|
7
7
|
},
|
|
8
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node test/smoke.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"chalk": "^4.1.2",
|
|
19
|
+
"cli-table3": "^0.6.5",
|
|
20
|
+
"commander": "^12.1.0",
|
|
21
|
+
"ora": "^5.4.1"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@0xwork/sdk": ">=0.5.0"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"0xwork",
|
|
28
|
+
"ai-agent",
|
|
29
|
+
"cli",
|
|
30
|
+
"base",
|
|
31
|
+
"usdc",
|
|
32
|
+
"crypto",
|
|
33
|
+
"marketplace",
|
|
34
|
+
"autonomous-agent"
|
|
35
|
+
],
|
|
36
|
+
"author": "0xWork",
|
|
10
37
|
"license": "MIT",
|
|
11
38
|
"repository": {
|
|
12
39
|
"type": "git",
|
|
13
|
-
"url": "https://github.com/JKILLR/0xwork"
|
|
40
|
+
"url": "https://github.com/JKILLR/0xwork",
|
|
41
|
+
"directory": "cli"
|
|
14
42
|
},
|
|
15
43
|
"homepage": "https://0xwork.org",
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"@0xwork/sdk": "^0.5.0"
|
|
18
|
-
},
|
|
19
44
|
"engines": {
|
|
20
45
|
"node": ">=18"
|
|
21
46
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { requireSDK, isDryRun, normalizeError } = require('../sdk');
|
|
5
|
+
const { success, fail, header, keyValue, blank, hint, warn } = require('../output');
|
|
6
|
+
const { createSpinner } = require('../spinner');
|
|
7
|
+
|
|
8
|
+
function register(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('abandon <chainTaskId>')
|
|
11
|
+
.description('Abandon a claimed task (50% stake penalty)')
|
|
12
|
+
.action(async (chainTaskId) => {
|
|
13
|
+
try {
|
|
14
|
+
await run(chainTaskId);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
fail(normalizeError(err));
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function run(taskId) {
|
|
22
|
+
if (isDryRun()) {
|
|
23
|
+
return success({
|
|
24
|
+
command: 'abandon',
|
|
25
|
+
dryRun: true,
|
|
26
|
+
taskId: parseInt(taskId),
|
|
27
|
+
message: `DRY RUN: Would abandon task #${taskId} (50% stake penalty)`,
|
|
28
|
+
}, () => {
|
|
29
|
+
header('⚠️', `Abandon Task #${taskId} — Dry Run`);
|
|
30
|
+
hint('Set PRIVATE_KEY in .env to abandon tasks.');
|
|
31
|
+
blank();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sdk = requireSDK();
|
|
36
|
+
|
|
37
|
+
const s = createSpinner('Abandoning task…');
|
|
38
|
+
s.start();
|
|
39
|
+
|
|
40
|
+
const result = await sdk.abandonTask(parseInt(taskId));
|
|
41
|
+
|
|
42
|
+
s.succeed('Abandoned');
|
|
43
|
+
|
|
44
|
+
success({
|
|
45
|
+
command: 'abandon',
|
|
46
|
+
dryRun: false,
|
|
47
|
+
taskId: parseInt(taskId),
|
|
48
|
+
txHash: result.txHash,
|
|
49
|
+
message: `Abandoned task #${taskId} — 50% stake slashed`,
|
|
50
|
+
}, () => {
|
|
51
|
+
header('⚠️', `Task #${taskId} Abandoned`);
|
|
52
|
+
warn(chalk.yellow('50% of your stake has been slashed.'));
|
|
53
|
+
blank();
|
|
54
|
+
hint(`tx: ${result.txHash}`);
|
|
55
|
+
blank();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { register };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { requireSDK, isDryRun, normalizeError } = require('../sdk');
|
|
5
|
+
const { success, fail, header, keyValue, blank, hint } = require('../output');
|
|
6
|
+
const { createSpinner } = require('../spinner');
|
|
7
|
+
const { fmtBounty, truncAddr } = require('../format');
|
|
8
|
+
|
|
9
|
+
function register(program) {
|
|
10
|
+
program
|
|
11
|
+
.command('approve <chainTaskId>')
|
|
12
|
+
.description('Approve submitted work and release USDC to worker')
|
|
13
|
+
.action(async (chainTaskId) => {
|
|
14
|
+
try {
|
|
15
|
+
await run(chainTaskId);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
fail(normalizeError(err));
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function run(taskId) {
|
|
23
|
+
if (isDryRun()) {
|
|
24
|
+
return success({
|
|
25
|
+
command: 'approve', dryRun: true, taskId: parseInt(taskId),
|
|
26
|
+
message: `DRY RUN: Would approve task #${taskId}`,
|
|
27
|
+
}, () => {
|
|
28
|
+
header('✅', `Approve #${taskId} — Dry Run`);
|
|
29
|
+
hint('Set PRIVATE_KEY in .env.'); blank();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const sdk = requireSDK();
|
|
34
|
+
const address = sdk.address;
|
|
35
|
+
|
|
36
|
+
const s1 = createSpinner('Checking task…');
|
|
37
|
+
s1.start();
|
|
38
|
+
|
|
39
|
+
const onChain = await sdk.getTask(parseInt(taskId));
|
|
40
|
+
if (onChain.state !== 'Submitted' && onChain.state !== 'Disputed') {
|
|
41
|
+
s1.fail(`Task is ${onChain.state}`);
|
|
42
|
+
fail(`Task #${taskId} is ${onChain.state}, expected Submitted or Disputed`);
|
|
43
|
+
}
|
|
44
|
+
if (onChain.poster.toLowerCase() !== address.toLowerCase()) {
|
|
45
|
+
s1.fail('Not your task');
|
|
46
|
+
fail(`Only the poster can approve. Poster: ${onChain.poster}, You: ${address}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
s1.succeed('Verified');
|
|
50
|
+
|
|
51
|
+
const s2 = createSpinner('Approving…');
|
|
52
|
+
s2.start();
|
|
53
|
+
const result = await sdk.approveWork(parseInt(taskId));
|
|
54
|
+
s2.succeed('Approved');
|
|
55
|
+
|
|
56
|
+
const bounty = parseFloat(onChain.bountyAmount);
|
|
57
|
+
const fee = onChain.discountedFee ? bounty * 0.02 : bounty * 0.05;
|
|
58
|
+
const payout = bounty - fee;
|
|
59
|
+
|
|
60
|
+
success({
|
|
61
|
+
command: 'approve', dryRun: false, taskId: parseInt(taskId),
|
|
62
|
+
txHash: result.txHash, worker: onChain.worker,
|
|
63
|
+
bounty: onChain.bountyAmount, fee: fee.toFixed(2), payout: payout.toFixed(2),
|
|
64
|
+
message: `Approved task #${taskId} — ${fmtBounty(payout)} released to ${onChain.worker}`,
|
|
65
|
+
}, () => {
|
|
66
|
+
header('✔', `Task #${taskId} Approved`);
|
|
67
|
+
keyValue('Worker', truncAddr(onChain.worker));
|
|
68
|
+
keyValue('Payout', chalk.green.bold(`${fmtBounty(payout)} USDC`));
|
|
69
|
+
keyValue('Fee', chalk.dim(`${fmtBounty(fee)} (${onChain.discountedFee ? '2%' : '5%'})`));
|
|
70
|
+
blank();
|
|
71
|
+
hint(`tx: ${result.txHash}`); blank();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { register };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { requireSDK, isDryRun, normalizeError } = require('../sdk');
|
|
5
|
+
const { success, fail, header, keyValue, blank, hint } = require('../output');
|
|
6
|
+
const { createSpinner } = require('../spinner');
|
|
7
|
+
const { fmtBounty, fmtDuration, truncAddr } = require('../format');
|
|
8
|
+
|
|
9
|
+
function register(program) {
|
|
10
|
+
program
|
|
11
|
+
.command('auto-resolve <chainTaskId>')
|
|
12
|
+
.description('Trigger dispute auto-resolve after 48h (worker wins)')
|
|
13
|
+
.action(async (chainTaskId) => {
|
|
14
|
+
try {
|
|
15
|
+
await run(chainTaskId);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
fail(normalizeError(err));
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function run(taskId) {
|
|
23
|
+
if (isDryRun()) {
|
|
24
|
+
return success({
|
|
25
|
+
command: 'auto-resolve', dryRun: true, taskId: parseInt(taskId),
|
|
26
|
+
message: `DRY RUN: Would trigger auto-resolve on task #${taskId}`,
|
|
27
|
+
}, () => {
|
|
28
|
+
header('⚖️', `Auto-Resolve #${taskId} — Dry Run`); hint('Set PRIVATE_KEY.'); blank();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const sdk = requireSDK();
|
|
33
|
+
|
|
34
|
+
const s1 = createSpinner('Checking task…');
|
|
35
|
+
s1.start();
|
|
36
|
+
const onChain = await sdk.getTask(parseInt(taskId));
|
|
37
|
+
if (onChain.state !== 'Disputed') {
|
|
38
|
+
s1.fail(`Task is ${onChain.state}`); fail('Expected Disputed state');
|
|
39
|
+
}
|
|
40
|
+
if (!onChain.disputeTimestamp) {
|
|
41
|
+
s1.fail('No dispute timestamp'); fail('No dispute timestamp recorded.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const now = Math.floor(Date.now() / 1000);
|
|
45
|
+
const window = 48 * 3600;
|
|
46
|
+
const eligibleAt = onChain.disputeTimestamp + window;
|
|
47
|
+
if (now < eligibleAt) {
|
|
48
|
+
const remaining = eligibleAt - now;
|
|
49
|
+
s1.fail('Not yet eligible');
|
|
50
|
+
fail(`Auto-resolve eligible in ${fmtDuration(remaining)} (at ${new Date(eligibleAt * 1000).toISOString()})`);
|
|
51
|
+
}
|
|
52
|
+
s1.succeed('Eligible');
|
|
53
|
+
|
|
54
|
+
const s2 = createSpinner('Resolving dispute…');
|
|
55
|
+
s2.start();
|
|
56
|
+
const result = await sdk.autoResolveDispute(parseInt(taskId));
|
|
57
|
+
s2.succeed('Resolved — worker wins');
|
|
58
|
+
|
|
59
|
+
const bounty = parseFloat(onChain.bountyAmount);
|
|
60
|
+
const fee = onChain.discountedFee ? bounty * 0.02 : bounty * 0.05;
|
|
61
|
+
const payout = bounty - fee;
|
|
62
|
+
|
|
63
|
+
success({
|
|
64
|
+
command: 'auto-resolve', dryRun: false, taskId: parseInt(taskId),
|
|
65
|
+
txHash: result.txHash, worker: onChain.worker, payout: payout.toFixed(2),
|
|
66
|
+
message: `Dispute auto-resolved on task #${taskId} — worker wins`,
|
|
67
|
+
}, () => {
|
|
68
|
+
header('✔', `Dispute Resolved — Task #${taskId}`);
|
|
69
|
+
keyValue('Winner', chalk.green('Worker') + ` (${truncAddr(onChain.worker)})`);
|
|
70
|
+
keyValue('Payout', chalk.green.bold(`${fmtBounty(payout)} USDC`));
|
|
71
|
+
hint('Poster stake slashed.');
|
|
72
|
+
blank(); hint(`tx: ${result.txHash}`); blank();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { register };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
const { getEthers, getSDK, isDryRun, resolveAddress, getConstants, normalizeError } = require('../sdk');
|
|
6
|
+
const { success, fail, header, blank, divider } = require('../output');
|
|
7
|
+
const { createSpinner } = require('../spinner');
|
|
8
|
+
const { getAxobotlPrice, formatAxobotlUsd } = require('../price');
|
|
9
|
+
const { fmtAxobotl, truncAddr } = require('../format');
|
|
10
|
+
const { fetchWithTimeout } = require('../http');
|
|
11
|
+
|
|
12
|
+
function register(program) {
|
|
13
|
+
program
|
|
14
|
+
.command('balance')
|
|
15
|
+
.description('Check wallet balances ($AXOBOTL, USDC, ETH)')
|
|
16
|
+
.option('--address <addr>', 'Wallet address (defaults to .env)')
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
try {
|
|
19
|
+
await run(opts);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
fail(normalizeError(err));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function run(opts) {
|
|
27
|
+
const address = resolveAddress(opts.address);
|
|
28
|
+
const ethers = getEthers();
|
|
29
|
+
const provider = config.PRIVATE_KEY
|
|
30
|
+
? getSDK().provider
|
|
31
|
+
: new ethers.JsonRpcProvider(config.RPC_URL);
|
|
32
|
+
|
|
33
|
+
const constants = getConstants();
|
|
34
|
+
const ERC20_ABI = ['function balanceOf(address) view returns (uint256)'];
|
|
35
|
+
|
|
36
|
+
const spinner = createSpinner('Fetching balances…');
|
|
37
|
+
spinner.start();
|
|
38
|
+
|
|
39
|
+
const axobotl = new ethers.Contract(constants.ADDRESSES.AXOBOTL, ERC20_ABI, provider);
|
|
40
|
+
const usdc = new ethers.Contract(constants.ADDRESSES.USDC, ERC20_ABI, provider);
|
|
41
|
+
const registry = new ethers.Contract(constants.ADDRESSES.AGENT_REGISTRY, constants.AGENT_REGISTRY_ABI, provider);
|
|
42
|
+
|
|
43
|
+
const [ab, ub, eb] = await Promise.all([
|
|
44
|
+
axobotl.balanceOf(address),
|
|
45
|
+
usdc.balanceOf(address),
|
|
46
|
+
provider.getBalance(address),
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// Check staked amount
|
|
50
|
+
let stakedAmount = '0.0';
|
|
51
|
+
try {
|
|
52
|
+
const [found, agentId] = await registry.getAgentByOperator(address);
|
|
53
|
+
if (found) {
|
|
54
|
+
const agent = await registry.getAgent(agentId);
|
|
55
|
+
stakedAmount = ethers.formatUnits(agent.stakedAmount, 18);
|
|
56
|
+
} else {
|
|
57
|
+
const resp = await fetchWithTimeout(`${config.API_URL}/agents/${address}`);
|
|
58
|
+
if (resp.ok) {
|
|
59
|
+
const data = await resp.json();
|
|
60
|
+
const agent = data.agent || data;
|
|
61
|
+
if (agent && agent.staked_amount) stakedAmount = ethers.formatUnits(agent.staked_amount, 18);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch { /* not registered */ }
|
|
65
|
+
|
|
66
|
+
const walletBalance = ethers.formatUnits(ab, 18);
|
|
67
|
+
const totalAxobotl = (parseFloat(walletBalance) + parseFloat(stakedAmount)).toFixed(4);
|
|
68
|
+
const usdcBalance = ethers.formatUnits(ub, 6);
|
|
69
|
+
const ethBalance = ethers.formatEther(eb);
|
|
70
|
+
|
|
71
|
+
const price = await getAxobotlPrice();
|
|
72
|
+
|
|
73
|
+
spinner.stop();
|
|
74
|
+
|
|
75
|
+
const balances = {
|
|
76
|
+
axobotl: walletBalance,
|
|
77
|
+
axobotlStaked: stakedAmount,
|
|
78
|
+
axobotlTotal: totalAxobotl,
|
|
79
|
+
usdc: usdcBalance,
|
|
80
|
+
eth: ethBalance,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (price) {
|
|
84
|
+
balances.axobotlUsd = formatAxobotlUsd(walletBalance, price);
|
|
85
|
+
balances.axobotlStakedUsd = formatAxobotlUsd(stakedAmount, price);
|
|
86
|
+
balances.axobotlTotalUsd = formatAxobotlUsd(totalAxobotl, price);
|
|
87
|
+
balances.axobotlPriceUsd = price;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
success({
|
|
91
|
+
command: 'balance',
|
|
92
|
+
dryRun: isDryRun(),
|
|
93
|
+
address,
|
|
94
|
+
balances,
|
|
95
|
+
}, () => {
|
|
96
|
+
header('💰', 'Wallet', chalk.dim(truncAddr(address)));
|
|
97
|
+
|
|
98
|
+
const pad = (label) => chalk.dim(label.padEnd(12));
|
|
99
|
+
|
|
100
|
+
console.log(` ${pad('ETH')}${parseFloat(ethBalance).toFixed(6)}`);
|
|
101
|
+
console.log(` ${pad('USDC')}${chalk.green('$' + parseFloat(usdcBalance).toFixed(2))}`);
|
|
102
|
+
console.log(` ${pad('$AXOBOTL')}${fmtAxobotl(walletBalance)}${price ? chalk.dim(` ~${formatAxobotlUsd(walletBalance, price)}`) : ''}`);
|
|
103
|
+
console.log(` ${pad('Staked')}${fmtAxobotl(stakedAmount)}${price ? chalk.dim(` ~${formatAxobotlUsd(stakedAmount, price)}`) : ''}`);
|
|
104
|
+
divider(35);
|
|
105
|
+
|
|
106
|
+
// Total: only assets we can price reliably (USDC + AXOBOTL if price known)
|
|
107
|
+
const totalUsd = parseFloat(usdcBalance) +
|
|
108
|
+
(price ? parseFloat(totalAxobotl) * price : 0);
|
|
109
|
+
if (totalUsd > 0) {
|
|
110
|
+
console.log(` ${pad('Total')}${chalk.bold(`~$${totalUsd.toFixed(2)}`)}${!price ? chalk.dim(' (excl. AXOBOTL)') : ''}`);
|
|
111
|
+
}
|
|
112
|
+
blank();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { register };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { requireSDK, isDryRun, normalizeError } = require('../sdk');
|
|
5
|
+
const { success, fail, header, keyValue, blank, hint } = require('../output');
|
|
6
|
+
const { createSpinner } = require('../spinner');
|
|
7
|
+
const { fmtBounty, fmtAxobotl } = require('../format');
|
|
8
|
+
|
|
9
|
+
function register(program) {
|
|
10
|
+
program
|
|
11
|
+
.command('cancel <chainTaskId>')
|
|
12
|
+
.description('Cancel an open task (bounty + poster stake returned)')
|
|
13
|
+
.action(async (chainTaskId) => {
|
|
14
|
+
try {
|
|
15
|
+
await run(chainTaskId);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
fail(normalizeError(err));
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function run(taskId) {
|
|
23
|
+
if (isDryRun()) {
|
|
24
|
+
return success({
|
|
25
|
+
command: 'cancel', dryRun: true, taskId: parseInt(taskId),
|
|
26
|
+
message: `DRY RUN: Would cancel task #${taskId}`,
|
|
27
|
+
}, () => {
|
|
28
|
+
header('🚫', `Cancel #${taskId} — Dry Run`); hint('Set PRIVATE_KEY.'); blank();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const sdk = requireSDK();
|
|
33
|
+
const address = sdk.address;
|
|
34
|
+
|
|
35
|
+
const s1 = createSpinner('Checking task…');
|
|
36
|
+
s1.start();
|
|
37
|
+
const onChain = await sdk.getTask(parseInt(taskId));
|
|
38
|
+
if (onChain.state !== 'Open') {
|
|
39
|
+
s1.fail(`Task is ${onChain.state}`);
|
|
40
|
+
fail(`Task is ${onChain.state}, not Open. Use ${chalk.cyan('0xwork mutual-cancel')} for claimed/submitted tasks.`);
|
|
41
|
+
}
|
|
42
|
+
if (onChain.poster.toLowerCase() !== address.toLowerCase()) {
|
|
43
|
+
s1.fail('Not your task'); fail('Only the poster can cancel.');
|
|
44
|
+
}
|
|
45
|
+
s1.succeed('Verified');
|
|
46
|
+
|
|
47
|
+
const s2 = createSpinner('Cancelling…');
|
|
48
|
+
s2.start();
|
|
49
|
+
const result = await sdk.cancelTask(parseInt(taskId));
|
|
50
|
+
s2.succeed('Cancelled');
|
|
51
|
+
|
|
52
|
+
success({
|
|
53
|
+
command: 'cancel', dryRun: false, taskId: parseInt(taskId),
|
|
54
|
+
txHash: result.txHash, bountyReturned: onChain.bountyAmount,
|
|
55
|
+
posterStakeReturned: onChain.posterStakeAmount,
|
|
56
|
+
message: `Cancelled task #${taskId}`,
|
|
57
|
+
}, () => {
|
|
58
|
+
header('✔', `Task #${taskId} Cancelled`);
|
|
59
|
+
keyValue('Bounty returned', chalk.green(fmtBounty(onChain.bountyAmount) + ' USDC'));
|
|
60
|
+
if (onChain.posterStakeAmount && onChain.posterStakeAmount !== '0.0') {
|
|
61
|
+
keyValue('Stake returned', `${fmtAxobotl(onChain.posterStakeAmount)} AXOBOTL`);
|
|
62
|
+
}
|
|
63
|
+
blank(); hint(`tx: ${result.txHash}`); blank();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { register };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { requireSDK, isDryRun, normalizeError } = require('../sdk');
|
|
5
|
+
const { success, fail, header, keyValue, blank, hint } = require('../output');
|
|
6
|
+
const { createSpinner } = require('../spinner');
|
|
7
|
+
const { fmtBounty, fmtDuration, truncAddr } = require('../format');
|
|
8
|
+
|
|
9
|
+
function register(program) {
|
|
10
|
+
program
|
|
11
|
+
.command('claim-approval <chainTaskId>')
|
|
12
|
+
.description('Trigger auto-approve after poster ghosts 7 days')
|
|
13
|
+
.action(async (chainTaskId) => {
|
|
14
|
+
try {
|
|
15
|
+
await run(chainTaskId);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
fail(normalizeError(err));
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function run(taskId) {
|
|
23
|
+
if (isDryRun()) {
|
|
24
|
+
return success({
|
|
25
|
+
command: 'claim-approval', dryRun: true, taskId: parseInt(taskId),
|
|
26
|
+
message: `DRY RUN: Would trigger auto-approve on task #${taskId}`,
|
|
27
|
+
}, () => {
|
|
28
|
+
header('⏱️', `Claim Approval #${taskId} — Dry Run`); hint('Set PRIVATE_KEY.'); blank();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const sdk = requireSDK();
|
|
33
|
+
|
|
34
|
+
const s1 = createSpinner('Checking task…');
|
|
35
|
+
s1.start();
|
|
36
|
+
const onChain = await sdk.getTask(parseInt(taskId));
|
|
37
|
+
if (onChain.state !== 'Submitted') {
|
|
38
|
+
s1.fail(`Task is ${onChain.state}`); fail('Expected Submitted state');
|
|
39
|
+
}
|
|
40
|
+
if (!onChain.submitTimestamp) {
|
|
41
|
+
s1.fail('No submission timestamp'); fail('No submission timestamp recorded.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const now = Math.floor(Date.now() / 1000);
|
|
45
|
+
const window = 7 * 86400;
|
|
46
|
+
const eligibleAt = onChain.submitTimestamp + window;
|
|
47
|
+
if (now < eligibleAt) {
|
|
48
|
+
const remaining = eligibleAt - now;
|
|
49
|
+
s1.fail('Not yet eligible');
|
|
50
|
+
fail(`Auto-approve eligible in ${fmtDuration(remaining)} (at ${new Date(eligibleAt * 1000).toISOString()})`);
|
|
51
|
+
}
|
|
52
|
+
s1.succeed('Eligible');
|
|
53
|
+
|
|
54
|
+
const s2 = createSpinner('Triggering auto-approve…');
|
|
55
|
+
s2.start();
|
|
56
|
+
const result = await sdk.claimApproval(parseInt(taskId));
|
|
57
|
+
s2.succeed('Auto-approved');
|
|
58
|
+
|
|
59
|
+
const bounty = parseFloat(onChain.bountyAmount);
|
|
60
|
+
const fee = onChain.discountedFee ? bounty * 0.02 : bounty * 0.05;
|
|
61
|
+
const payout = bounty - fee;
|
|
62
|
+
|
|
63
|
+
success({
|
|
64
|
+
command: 'claim-approval', dryRun: false, taskId: parseInt(taskId),
|
|
65
|
+
txHash: result.txHash, worker: onChain.worker, payout: payout.toFixed(2),
|
|
66
|
+
message: `Auto-approved task #${taskId} — ${fmtBounty(payout)} to worker`,
|
|
67
|
+
}, () => {
|
|
68
|
+
header('✔', `Task #${taskId} Auto-Approved`);
|
|
69
|
+
keyValue('Worker', truncAddr(onChain.worker));
|
|
70
|
+
keyValue('Payout', chalk.green.bold(`${fmtBounty(payout)} USDC`));
|
|
71
|
+
hint('Poster stake slashed for inaction.');
|
|
72
|
+
blank(); hint(`tx: ${result.txHash}`); blank();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { register };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
const { getEthers, isDryRun, requireSDK, normalizeError } = require('../sdk');
|
|
6
|
+
const { success, fail, header, keyValue, blank, hint } = require('../output');
|
|
7
|
+
const { createSpinner } = require('../spinner');
|
|
8
|
+
const { fmtBounty, fmtDeadline, fmtAxobotl } = require('../format');
|
|
9
|
+
const { getAxobotlPrice, formatAxobotlUsd } = require('../price');
|
|
10
|
+
const { resolveTask } = require('../resolve');
|
|
11
|
+
const { fetchWithTimeout } = require('../http');
|
|
12
|
+
|
|
13
|
+
function register(program) {
|
|
14
|
+
program
|
|
15
|
+
.command('claim <chainTaskId>')
|
|
16
|
+
.description('Claim a task on-chain (stakes $AXOBOTL as collateral)')
|
|
17
|
+
.action(async (chainTaskId) => {
|
|
18
|
+
try {
|
|
19
|
+
await run(chainTaskId);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
fail(normalizeError(err));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function run(taskId) {
|
|
27
|
+
if (isDryRun()) {
|
|
28
|
+
let t;
|
|
29
|
+
try { t = await resolveTask(taskId); } catch { t = null; }
|
|
30
|
+
return success({
|
|
31
|
+
command: 'claim',
|
|
32
|
+
dryRun: true,
|
|
33
|
+
taskId: parseInt(taskId),
|
|
34
|
+
message: `DRY RUN: Would claim task #${taskId}`,
|
|
35
|
+
bounty: t ? t.bounty_amount : null,
|
|
36
|
+
stakeRaw: t ? t.stake_amount : null,
|
|
37
|
+
txHash: null,
|
|
38
|
+
}, () => {
|
|
39
|
+
header('🔒', `Claim Task #${taskId} — Dry Run`);
|
|
40
|
+
hint('Set PRIVATE_KEY in .env to claim tasks.');
|
|
41
|
+
blank();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sdk = requireSDK();
|
|
46
|
+
const ethers = getEthers();
|
|
47
|
+
const address = sdk.address;
|
|
48
|
+
|
|
49
|
+
// Pre-flight: fetch on-chain state
|
|
50
|
+
const s1 = createSpinner('Checking task state…');
|
|
51
|
+
s1.start();
|
|
52
|
+
|
|
53
|
+
let onChain;
|
|
54
|
+
try {
|
|
55
|
+
onChain = await sdk.getTask(parseInt(taskId));
|
|
56
|
+
} catch (e) {
|
|
57
|
+
s1.fail('Task not found on-chain');
|
|
58
|
+
fail(`Task #${taskId} not found on-chain: ${normalizeError(e)}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (onChain.poster === '0x0000000000000000000000000000000000000000') {
|
|
62
|
+
s1.fail('Task does not exist');
|
|
63
|
+
fail(`Task #${taskId} does not exist on-chain`);
|
|
64
|
+
}
|
|
65
|
+
if (onChain.state !== 'Open') {
|
|
66
|
+
s1.fail(`Task is ${onChain.state}`);
|
|
67
|
+
fail(`Task #${taskId} is not Open (current: ${onChain.state})`);
|
|
68
|
+
}
|
|
69
|
+
if (onChain.poster.toLowerCase() === address.toLowerCase()) {
|
|
70
|
+
s1.fail('Cannot claim own task');
|
|
71
|
+
fail('Cannot claim your own task');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Preferred lock check
|
|
75
|
+
try {
|
|
76
|
+
const t = await resolveTask(taskId);
|
|
77
|
+
const lock = t.preferred_lock;
|
|
78
|
+
if (lock && lock.locked && t.preferred_agent_id != null) {
|
|
79
|
+
let myAgentId = null;
|
|
80
|
+
const agentResp = await fetchWithTimeout(`${config.API_URL}/agents/${address}`);
|
|
81
|
+
if (agentResp.ok) {
|
|
82
|
+
const agentData = await agentResp.json();
|
|
83
|
+
myAgentId = agentData.agent ? agentData.agent.chain_agent_id : null;
|
|
84
|
+
}
|
|
85
|
+
if (myAgentId == null || t.preferred_agent_id !== myAgentId) {
|
|
86
|
+
const agentName = t.preferred_agent_name || `Agent #${t.preferred_agent_id}`;
|
|
87
|
+
const hours = Math.ceil(lock.remaining_seconds / 3600);
|
|
88
|
+
s1.fail('Task is reserved');
|
|
89
|
+
fail(`Reserved for ${agentName} for ${hours}h. Try again later.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
if (e.message && e.message.startsWith('Reserved')) throw e;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
s1.succeed('Task is Open');
|
|
97
|
+
|
|
98
|
+
// Pre-flight: check balances
|
|
99
|
+
const s2 = createSpinner('Checking balances…');
|
|
100
|
+
s2.start();
|
|
101
|
+
|
|
102
|
+
const stakeRequired = await sdk.taskPool.calculateStake(
|
|
103
|
+
ethers.parseUnits(String(onChain.bountyAmount), 6)
|
|
104
|
+
);
|
|
105
|
+
const axobotlBalance = await sdk.axobotl.balanceOf(address);
|
|
106
|
+
if (axobotlBalance < stakeRequired) {
|
|
107
|
+
s2.fail('Insufficient AXOBOTL');
|
|
108
|
+
fail(
|
|
109
|
+
`Insufficient $AXOBOTL for stake`,
|
|
110
|
+
{ suggestion: `Need: ${fmtAxobotl(ethers.formatUnits(stakeRequired, 18))}, Have: ${fmtAxobotl(ethers.formatUnits(axobotlBalance, 18))}. Buy on Uniswap (Base) or request from faucet.` }
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const ethBalance = await sdk.provider.getBalance(address);
|
|
115
|
+
if (ethBalance < ethers.parseEther('0.0001')) {
|
|
116
|
+
s2.fail('Insufficient ETH');
|
|
117
|
+
fail('Insufficient ETH for gas', { suggestion: `Have: ${ethers.formatEther(ethBalance)} ETH. Send ETH on Base to ${address}` });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
s2.succeed('Balances OK');
|
|
121
|
+
|
|
122
|
+
// Claim
|
|
123
|
+
const s3 = createSpinner('Claiming task…');
|
|
124
|
+
s3.start();
|
|
125
|
+
|
|
126
|
+
const result = await sdk.claimTask(parseInt(taskId));
|
|
127
|
+
|
|
128
|
+
s3.succeed('Claimed');
|
|
129
|
+
|
|
130
|
+
const stakeHuman = ethers.formatUnits(stakeRequired, 18);
|
|
131
|
+
const price = await getAxobotlPrice();
|
|
132
|
+
const stakeUsd = price ? formatAxobotlUsd(stakeHuman, price) : null;
|
|
133
|
+
|
|
134
|
+
const data = {
|
|
135
|
+
command: 'claim',
|
|
136
|
+
dryRun: false,
|
|
137
|
+
taskId: parseInt(taskId),
|
|
138
|
+
txHash: result.txHash,
|
|
139
|
+
stakeAmount: stakeHuman,
|
|
140
|
+
bounty: onChain.bountyAmount,
|
|
141
|
+
message: `Claimed task #${taskId} — staked ${fmtAxobotl(stakeHuman)} AXOBOTL`,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
success(data, () => {
|
|
145
|
+
header('✔', `Task #${taskId} Claimed`);
|
|
146
|
+
keyValue('Bounty', chalk.green.bold(`${fmtBounty(onChain.bountyAmount)} USDC`));
|
|
147
|
+
keyValue('Staked', `${fmtAxobotl(stakeHuman)} AXOBOTL${stakeUsd ? chalk.dim(` (~${stakeUsd})`) : ''}`);
|
|
148
|
+
keyValue('Deadline', fmtDeadline(onChain.deadline, true));
|
|
149
|
+
blank();
|
|
150
|
+
hint(`Next: complete the work, then run:`);
|
|
151
|
+
hint(` $ 0xwork submit ${taskId} --proof="https://..."`);
|
|
152
|
+
blank();
|
|
153
|
+
hint(`tx: ${result.txHash}`);
|
|
154
|
+
blank();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { register };
|