@poa-box/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -0
- package/dist/abis/DirectDemocracyVotingNew.d.ts +749 -0
- package/dist/abis/DirectDemocracyVotingNew.js +961 -0
- package/dist/abis/ERC20.d.ts +90 -0
- package/dist/abis/ERC20.js +125 -0
- package/dist/abis/EducationHubNew.d.ts +590 -0
- package/dist/abis/EducationHubNew.js +762 -0
- package/dist/abis/EligibilityModuleNew.d.ts +1615 -0
- package/dist/abis/EligibilityModuleNew.js +2082 -0
- package/dist/abis/Executor.d.ts +619 -0
- package/dist/abis/Executor.js +796 -0
- package/dist/abis/HybridVotingNew.d.ts +898 -0
- package/dist/abis/HybridVotingNew.js +1149 -0
- package/dist/abis/ImplementationRegistry.d.ts +270 -0
- package/dist/abis/ImplementationRegistry.js +355 -0
- package/dist/abis/OrgDeployerNew.d.ts +1275 -0
- package/dist/abis/OrgDeployerNew.js +1630 -0
- package/dist/abis/OrgRegistry.d.ts +670 -0
- package/dist/abis/OrgRegistry.js +868 -0
- package/dist/abis/ParticipationToken.d.ts +1086 -0
- package/dist/abis/ParticipationToken.js +1415 -0
- package/dist/abis/PasskeyAccount.d.ts +788 -0
- package/dist/abis/PasskeyAccount.js +1019 -0
- package/dist/abis/PasskeyAccountFactory.d.ts +344 -0
- package/dist/abis/PasskeyAccountFactory.js +449 -0
- package/dist/abis/PaymasterHub.d.ts +1603 -0
- package/dist/abis/PaymasterHub.js +2048 -0
- package/dist/abis/PaymentManager.d.ts +520 -0
- package/dist/abis/PaymentManager.js +671 -0
- package/dist/abis/PoaManager.d.ts +344 -0
- package/dist/abis/PoaManager.js +449 -0
- package/dist/abis/QuickJoinNew.d.ts +855 -0
- package/dist/abis/QuickJoinNew.js +1098 -0
- package/dist/abis/TaskManagerNew.d.ts +1236 -0
- package/dist/abis/TaskManagerNew.js +1573 -0
- package/dist/abis/ToggleModule.d.ts +193 -0
- package/dist/abis/ToggleModule.js +255 -0
- package/dist/abis/UniversalAccountRegistry.d.ts +577 -0
- package/dist/abis/UniversalAccountRegistry.js +750 -0
- package/dist/abis/ZkEmailInvites.d.ts +823 -0
- package/dist/abis/ZkEmailInvites.js +1060 -0
- package/dist/abis/external/AaveGovernanceV2.d.ts +128 -0
- package/dist/abis/external/AaveGovernanceV2.js +177 -0
- package/dist/abis/external/AaveGovernanceV3.d.ts +160 -0
- package/dist/abis/external/AaveGovernanceV3.js +220 -0
- package/dist/abis/external/AragonVoting.d.ts +107 -0
- package/dist/abis/external/AragonVoting.js +150 -0
- package/dist/abis/external/CurveGaugeController.d.ts +95 -0
- package/dist/abis/external/CurveGaugeController.js +130 -0
- package/dist/abis/external/CurveVotingEscrow.d.ts +86 -0
- package/dist/abis/external/CurveVotingEscrow.js +116 -0
- package/dist/abis/external/GovernorAlpha.d.ts +172 -0
- package/dist/abis/external/GovernorAlpha.js +228 -0
- package/dist/abis/external/MakerDAOChief.d.ts +92 -0
- package/dist/abis/external/MakerDAOChief.js +129 -0
- package/dist/abis/external/OZGovernor.d.ts +227 -0
- package/dist/abis/external/OZGovernor.js +317 -0
- package/dist/abis/external/SolidlyVotingEscrow.d.ts +196 -0
- package/dist/abis/external/SolidlyVotingEscrow.js +260 -0
- package/dist/abis/index.d.ts +62 -0
- package/dist/abis/index.js +98 -0
- package/dist/chains.d.ts +150 -0
- package/dist/chains.js +320 -0
- package/dist/context.d.ts +48 -0
- package/dist/context.js +38 -0
- package/dist/contracts.d.ts +24 -0
- package/dist/contracts.js +41 -0
- package/dist/encoding.d.ts +58 -0
- package/dist/encoding.js +243 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.js +4 -0
- package/dist/error-catalog.d.ts +41 -0
- package/dist/error-catalog.js +941 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +60 -0
- package/dist/execute/ethers.d.ts +93 -0
- package/dist/execute/ethers.js +323 -0
- package/dist/execute/index.d.ts +7 -0
- package/dist/execute/index.js +23 -0
- package/dist/execute/sponsored.d.ts +68 -0
- package/dist/execute/sponsored.js +277 -0
- package/dist/exit-codes.d.ts +18 -0
- package/dist/exit-codes.js +21 -0
- package/dist/format.d.ts +21 -0
- package/dist/format.js +89 -0
- package/dist/graph/client.d.ts +298 -0
- package/dist/graph/client.js +702 -0
- package/dist/graph/documents/activity.d.ts +9 -0
- package/dist/graph/documents/activity.js +164 -0
- package/dist/graph/documents/beacons.d.ts +172 -0
- package/dist/graph/documents/beacons.js +345 -0
- package/dist/graph/documents/index.d.ts +21 -0
- package/dist/graph/documents/index.js +57 -0
- package/dist/graph/documents/infrastructure.d.ts +30 -0
- package/dist/graph/documents/infrastructure.js +34 -0
- package/dist/graph/documents/org.d.ts +15 -0
- package/dist/graph/documents/org.js +249 -0
- package/dist/graph/documents/paymaster.d.ts +95 -0
- package/dist/graph/documents/paymaster.js +101 -0
- package/dist/graph/documents/role.d.ts +6 -0
- package/dist/graph/documents/role.js +41 -0
- package/dist/graph/documents/roles.d.ts +50 -0
- package/dist/graph/documents/roles.js +112 -0
- package/dist/graph/documents/task.d.ts +84 -0
- package/dist/graph/documents/task.js +236 -0
- package/dist/graph/documents/token.d.ts +13 -0
- package/dist/graph/documents/token.js +126 -0
- package/dist/graph/documents/treasury.d.ts +39 -0
- package/dist/graph/documents/treasury.js +136 -0
- package/dist/graph/documents/user.d.ts +120 -0
- package/dist/graph/documents/user.js +302 -0
- package/dist/graph/documents/voting-classes.d.ts +69 -0
- package/dist/graph/documents/voting-classes.js +149 -0
- package/dist/graph/documents/voting.d.ts +28 -0
- package/dist/graph/documents/voting.js +163 -0
- package/dist/graph/documents/vouch.d.ts +79 -0
- package/dist/graph/documents/vouch.js +171 -0
- package/dist/graph/documents/zkemail.d.ts +63 -0
- package/dist/graph/documents/zkemail.js +188 -0
- package/dist/graph/index.d.ts +2 -0
- package/dist/graph/index.js +41 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +92 -0
- package/dist/ipfs.d.ts +39 -0
- package/dist/ipfs.js +214 -0
- package/dist/label-aliases.d.ts +31 -0
- package/dist/label-aliases.js +70 -0
- package/dist/metadata/education.d.ts +30 -0
- package/dist/metadata/education.js +31 -0
- package/dist/metadata/index.d.ts +12 -0
- package/dist/metadata/index.js +48 -0
- package/dist/metadata/org.d.ts +92 -0
- package/dist/metadata/org.js +93 -0
- package/dist/metadata/proposal.d.ts +44 -0
- package/dist/metadata/proposal.js +45 -0
- package/dist/metadata/role.d.ts +38 -0
- package/dist/metadata/role.js +35 -0
- package/dist/metadata/task.d.ts +108 -0
- package/dist/metadata/task.js +81 -0
- package/dist/metadata/token.d.ts +32 -0
- package/dist/metadata/token.js +36 -0
- package/dist/metadata/user.d.ts +63 -0
- package/dist/metadata/user.js +70 -0
- package/dist/multicall.d.ts +32 -0
- package/dist/multicall.js +96 -0
- package/dist/payout.d.ts +59 -0
- package/dist/payout.js +96 -0
- package/dist/perms.d.ts +47 -0
- package/dist/perms.js +120 -0
- package/dist/preflight.d.ts +72 -0
- package/dist/preflight.js +269 -0
- package/dist/reads/education.d.ts +79 -0
- package/dist/reads/education.js +122 -0
- package/dist/reads/eligibility.d.ts +377 -0
- package/dist/reads/eligibility.js +687 -0
- package/dist/reads/index.d.ts +16 -0
- package/dist/reads/index.js +55 -0
- package/dist/reads/org.d.ts +258 -0
- package/dist/reads/org.js +220 -0
- package/dist/reads/paymaster.d.ts +147 -0
- package/dist/reads/paymaster.js +235 -0
- package/dist/reads/project.d.ts +33 -0
- package/dist/reads/project.js +62 -0
- package/dist/reads/resolve.d.ts +34 -0
- package/dist/reads/resolve.js +64 -0
- package/dist/reads/task.d.ts +286 -0
- package/dist/reads/task.js +280 -0
- package/dist/reads/token.d.ts +94 -0
- package/dist/reads/token.js +75 -0
- package/dist/reads/treasury.d.ts +222 -0
- package/dist/reads/treasury.js +241 -0
- package/dist/reads/user.d.ts +229 -0
- package/dist/reads/user.js +155 -0
- package/dist/reads/vote.d.ts +296 -0
- package/dist/reads/vote.js +392 -0
- package/dist/reads/zkemail.d.ts +117 -0
- package/dist/reads/zkemail.js +217 -0
- package/dist/similarity.d.ts +36 -0
- package/dist/similarity.js +67 -0
- package/dist/sponsorship-config.d.ts +47 -0
- package/dist/sponsorship-config.js +41 -0
- package/dist/stats.d.ts +11 -0
- package/dist/stats.js +27 -0
- package/dist/task-lens.d.ts +113 -0
- package/dist/task-lens.js +210 -0
- package/dist/tx/education.d.ts +169 -0
- package/dist/tx/education.js +344 -0
- package/dist/tx/eligibility.d.ts +511 -0
- package/dist/tx/eligibility.js +818 -0
- package/dist/tx/governance.d.ts +85 -0
- package/dist/tx/governance.js +87 -0
- package/dist/tx/index.d.ts +17 -0
- package/dist/tx/index.js +56 -0
- package/dist/tx/intent.d.ts +67 -0
- package/dist/tx/intent.js +48 -0
- package/dist/tx/org.d.ts +321 -0
- package/dist/tx/org.js +544 -0
- package/dist/tx/paymaster.d.ts +140 -0
- package/dist/tx/paymaster.js +251 -0
- package/dist/tx/project.d.ts +146 -0
- package/dist/tx/project.js +217 -0
- package/dist/tx/task.d.ts +480 -0
- package/dist/tx/task.js +1149 -0
- package/dist/tx/token.d.ts +96 -0
- package/dist/tx/token.js +165 -0
- package/dist/tx/treasury.d.ts +474 -0
- package/dist/tx/treasury.js +911 -0
- package/dist/tx/user.d.ts +177 -0
- package/dist/tx/user.js +292 -0
- package/dist/tx/vote.d.ts +332 -0
- package/dist/tx/vote.js +762 -0
- package/dist/tx/zkemail.d.ts +90 -0
- package/dist/tx/zkemail.js +210 -0
- package/dist/validation.d.ts +8 -0
- package/dist/validation.js +39 -0
- package/dist/version.d.ts +142 -0
- package/dist/version.js +257 -0
- package/dist/zkemail.d.ts +161 -0
- package/dist/zkemail.js +299 -0
- package/package.json +130 -0
package/dist/tx/task.js
ADDED
|
@@ -0,0 +1,1149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Task-domain transaction builders (TaskManagerNew).
|
|
4
|
+
*
|
|
5
|
+
* Level 1 — pure builders (sync, zero I/O). One per contract write; they take
|
|
6
|
+
* resolved addresses + prepared values and return a TxIntent. ALL argument
|
|
7
|
+
* encoding lives here, byte-for-byte as the CLI encodes it: stringToBytes
|
|
8
|
+
* titles, ipfsCidToBytes32 hashes, parseUnits amounts (18d PT payouts, token
|
|
9
|
+
* registry decimals for bounties), and the v6/legacy createTask arity switch
|
|
10
|
+
* via TaskManagerFeatures + LEGACY_TM_FRAGMENTS.
|
|
11
|
+
*
|
|
12
|
+
* Level 2 — resolved builders (async, `<action>Intent(ctx, params)`). They
|
|
13
|
+
* resolve the org's TaskManager via reads/resolve, replicate the CLI's
|
|
14
|
+
* payout/pricing derivation and read-merge logic, pin metadata via ../ipfs,
|
|
15
|
+
* then delegate to the Level-1 builder. Duplicate checks, idempotency caches,
|
|
16
|
+
* confirmation prompts and lens pre-flights are CLI UX and are deliberately
|
|
17
|
+
* NOT ported.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.CONFIG_KEY_ROLE_PERM = void 0;
|
|
21
|
+
exports.parseUpdateDeadline = parseUpdateDeadline;
|
|
22
|
+
exports.parseFoldersRoot = parseFoldersRoot;
|
|
23
|
+
exports.parseHatId = parseHatId;
|
|
24
|
+
exports.buildCreateTask = buildCreateTask;
|
|
25
|
+
exports.buildCreateTasksBatch = buildCreateTasksBatch;
|
|
26
|
+
exports.buildClaimTask = buildClaimTask;
|
|
27
|
+
exports.buildUnclaimTask = buildUnclaimTask;
|
|
28
|
+
exports.buildSubmitTask = buildSubmitTask;
|
|
29
|
+
exports.buildCompleteTask = buildCompleteTask;
|
|
30
|
+
exports.buildRejectTask = buildRejectTask;
|
|
31
|
+
exports.buildCancelTask = buildCancelTask;
|
|
32
|
+
exports.buildAssignTask = buildAssignTask;
|
|
33
|
+
exports.buildApplyForTask = buildApplyForTask;
|
|
34
|
+
exports.buildApproveApplication = buildApproveApplication;
|
|
35
|
+
exports.buildUpdateTask = buildUpdateTask;
|
|
36
|
+
exports.buildUpdateTaskMetadata = buildUpdateTaskMetadata;
|
|
37
|
+
exports.buildSetProjectRolePerm = buildSetProjectRolePerm;
|
|
38
|
+
exports.buildSetGlobalRolePermCall = buildSetGlobalRolePermCall;
|
|
39
|
+
exports.buildProposeGlobalRolePerm = buildProposeGlobalRolePerm;
|
|
40
|
+
exports.buildSetFolders = buildSetFolders;
|
|
41
|
+
exports.createTaskIntent = createTaskIntent;
|
|
42
|
+
exports.createTasksBatchIntents = createTasksBatchIntents;
|
|
43
|
+
exports.claimTaskIntent = claimTaskIntent;
|
|
44
|
+
exports.unclaimTaskIntent = unclaimTaskIntent;
|
|
45
|
+
exports.submitTaskIntent = submitTaskIntent;
|
|
46
|
+
exports.completeTaskIntent = completeTaskIntent;
|
|
47
|
+
exports.rejectTaskIntent = rejectTaskIntent;
|
|
48
|
+
exports.cancelTaskIntent = cancelTaskIntent;
|
|
49
|
+
exports.assignTaskIntent = assignTaskIntent;
|
|
50
|
+
exports.applyForTaskIntent = applyForTaskIntent;
|
|
51
|
+
exports.approveApplicationIntent = approveApplicationIntent;
|
|
52
|
+
exports.updateTaskIntent = updateTaskIntent;
|
|
53
|
+
exports.editTaskMetadataIntent = editTaskMetadataIntent;
|
|
54
|
+
exports.setProjectRolePermIntent = setProjectRolePermIntent;
|
|
55
|
+
exports.proposeGlobalRolePermIntent = proposeGlobalRolePermIntent;
|
|
56
|
+
exports.setFoldersIntent = setFoldersIntent;
|
|
57
|
+
const ethers_1 = require("ethers");
|
|
58
|
+
const governance_1 = require("./governance");
|
|
59
|
+
const contracts_1 = require("../contracts");
|
|
60
|
+
const encoding_1 = require("../encoding");
|
|
61
|
+
const version_1 = require("../version");
|
|
62
|
+
const chains_1 = require("../chains");
|
|
63
|
+
const payout_1 = require("../payout");
|
|
64
|
+
const perms_1 = require("../perms");
|
|
65
|
+
const ipfs_1 = require("../ipfs");
|
|
66
|
+
const errors_1 = require("../errors");
|
|
67
|
+
const exit_codes_1 = require("../exit-codes");
|
|
68
|
+
const validation_1 = require("../validation");
|
|
69
|
+
const resolve_1 = require("../reads/resolve");
|
|
70
|
+
const org_1 = require("../graph/documents/org");
|
|
71
|
+
const task_1 = require("../graph/documents/task");
|
|
72
|
+
const task_2 = require("../reads/task");
|
|
73
|
+
const task_3 = require("../metadata/task");
|
|
74
|
+
const proposal_1 = require("../metadata/proposal");
|
|
75
|
+
const task_lens_1 = require("../task-lens");
|
|
76
|
+
// ─────────────────────────── shared encoding helpers ───────────────────────────
|
|
77
|
+
/** TaskManager.ConfigKey.ROLE_PERM — verified enum index (src/commands/task/perms.ts:49). */
|
|
78
|
+
exports.CONFIG_KEY_ROLE_PERM = 2;
|
|
79
|
+
/**
|
|
80
|
+
* Bounty payout in raw token units, exactly as the CLI computes it
|
|
81
|
+
* (task/create.ts, task/create-batch.ts): parsed only when an amount > 0 was
|
|
82
|
+
* given AND the token is not the zero address; otherwise the literal 0.
|
|
83
|
+
*/
|
|
84
|
+
function computeBountyPayoutWei(bountyToken, bountyAmount) {
|
|
85
|
+
if (bountyAmount && Number(bountyAmount) > 0 && bountyToken !== ethers_1.ethers.constants.AddressZero) {
|
|
86
|
+
return ethers_1.ethers.utils.parseUnits(bountyAmount.toString(), (0, chains_1.getTokenDecimals)(bountyToken));
|
|
87
|
+
}
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
/** Accepts the CLI's string forms ('7d', ISO, unix) or a pre-resolved number. */
|
|
91
|
+
function parseDeadlineInput(value) {
|
|
92
|
+
return (0, encoding_1.parseDeadline)(String(value));
|
|
93
|
+
}
|
|
94
|
+
/** Accepts the CLI's string forms ('48h', '3600') or a pre-resolved number. */
|
|
95
|
+
function parseWindowInput(value) {
|
|
96
|
+
return (0, encoding_1.parseDurationSeconds)(String(value));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* parseDeadline for the update path: the contract deliberately accepts a
|
|
100
|
+
* PAST absolute deadline here (admin lever — immediately opens a CLAIMED
|
|
101
|
+
* task to takeover), so downgrade parseDeadline's past-check to a flag.
|
|
102
|
+
* Only absolute forms (unix seconds / ISO date) can resolve to the past —
|
|
103
|
+
* relative forms ("7d") are always future — so re-parsing with now=0 never
|
|
104
|
+
* changes relative math. Port of src/commands/task/update.ts parseUpdateDeadline.
|
|
105
|
+
*/
|
|
106
|
+
function parseUpdateDeadline(input) {
|
|
107
|
+
try {
|
|
108
|
+
return { value: (0, encoding_1.parseDeadline)(input), isPast: false };
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (err instanceof errors_1.CliError && /is in the past/.test(err.message)) {
|
|
112
|
+
return { value: (0, encoding_1.parseDeadline)(input, 0), isPast: true };
|
|
113
|
+
}
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Parse a folders-root input: CIDv0 (Qm…), 0x-prefixed bytes32, or "clear".
|
|
119
|
+
* Deliberately stricter than ipfsCidToBytes32 (which keccaks arbitrary
|
|
120
|
+
* strings — a silent footgun for a root pointer).
|
|
121
|
+
* Port of src/commands/task/folders.ts parseFoldersRoot.
|
|
122
|
+
*/
|
|
123
|
+
function parseFoldersRoot(input) {
|
|
124
|
+
const trimmed = (input || '').trim();
|
|
125
|
+
if (trimmed.toLowerCase() === 'clear')
|
|
126
|
+
return ethers_1.ethers.constants.HashZero;
|
|
127
|
+
if (trimmed.startsWith('0x')) {
|
|
128
|
+
if (trimmed.length === 66 && ethers_1.ethers.utils.isHexString(trimmed, 32))
|
|
129
|
+
return trimmed.toLowerCase();
|
|
130
|
+
throw new errors_1.CliError(`Invalid bytes32 root "${input}".`, exit_codes_1.EXIT.USAGE, 'Pass a 32-byte 0x-prefixed hex value, a Qm… CIDv0, or "clear".');
|
|
131
|
+
}
|
|
132
|
+
if (trimmed.startsWith('Qm')) {
|
|
133
|
+
const converted = (0, encoding_1.ipfsCidToBytes32)(trimmed);
|
|
134
|
+
if (converted === ethers_1.ethers.constants.HashZero) {
|
|
135
|
+
throw new errors_1.CliError(`Could not decode CIDv0 "${input}".`, exit_codes_1.EXIT.USAGE, 'Check the CID — it must be a base58 CIDv0 (Qm…, 46 chars).');
|
|
136
|
+
}
|
|
137
|
+
return converted;
|
|
138
|
+
}
|
|
139
|
+
throw new errors_1.CliError(`Unparseable --new-root "${input}".`, exit_codes_1.EXIT.USAGE, 'Pass a Qm… CIDv0, a 0x-prefixed bytes32, or "clear".');
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Parse a hat-id input (decimal or 0x hex) into a BigNumber.
|
|
143
|
+
* Port of src/commands/task/perms.ts parseHatId (same message).
|
|
144
|
+
*/
|
|
145
|
+
function parseHatId(input) {
|
|
146
|
+
try {
|
|
147
|
+
return ethers_1.ethers.BigNumber.from(String(input).trim());
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
throw new errors_1.CliError(`Invalid --hat "${input}".`, exit_codes_1.EXIT.USAGE, 'Pass the hat ID as a decimal or 0x-hex integer (see pop org roles).');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Port of `pop task create` — src/commands/task/create.ts (createTask send).
|
|
155
|
+
* v6 9-arg: [payoutWei, titleBytes, metadataHash, pid, bountyToken,
|
|
156
|
+
* bountyPayoutWei, requiresApplication, absoluteDeadline, completionWindow];
|
|
157
|
+
* legacy 7-arg drops the two deadline words and uses LEGACY_TM_FRAGMENTS.
|
|
158
|
+
*/
|
|
159
|
+
function buildCreateTask(a) {
|
|
160
|
+
const payoutWei = ethers_1.ethers.utils.parseUnits(a.payout.toString(), 18);
|
|
161
|
+
const titleBytes = (0, encoding_1.stringToBytes)(a.title);
|
|
162
|
+
const metadataHash = (0, encoding_1.ipfsCidToBytes32)(a.metadataHash);
|
|
163
|
+
const bountyToken = a.bountyToken || ethers_1.ethers.constants.AddressZero;
|
|
164
|
+
const bountyPayoutWei = computeBountyPayoutWei(bountyToken, a.bountyAmount);
|
|
165
|
+
const requiresApp = a.requiresApplication || false;
|
|
166
|
+
const v6 = a.features.deadlines;
|
|
167
|
+
return {
|
|
168
|
+
to: a.taskManagerAddress,
|
|
169
|
+
abi: v6 ? (0, contracts_1.getAbi)('TaskManagerNew') : version_1.LEGACY_TM_FRAGMENTS,
|
|
170
|
+
method: 'createTask',
|
|
171
|
+
args: v6
|
|
172
|
+
? [payoutWei, titleBytes, metadataHash, a.projectId, bountyToken, bountyPayoutWei, requiresApp, a.absoluteDeadline ?? 0, a.completionWindow ?? 0]
|
|
173
|
+
: [payoutWei, titleBytes, metadataHash, a.projectId, bountyToken, bountyPayoutWei, requiresApp],
|
|
174
|
+
meta: {
|
|
175
|
+
domain: 'task',
|
|
176
|
+
action: 'create',
|
|
177
|
+
orgId: a.orgId,
|
|
178
|
+
summary: { name: a.title, projectId: a.projectId, payout: String(a.payout), signature: v6 ? 'v6' : 'legacy7' },
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Port of `pop task create-batch` (v6 path) — src/commands/task/create-batch.ts.
|
|
184
|
+
* ONE all-or-nothing createTasksBatch(pid, CreateTaskInput[]) transaction.
|
|
185
|
+
* CreateTaskInput tuple field order must match the createTasksBatch components
|
|
186
|
+
* in the synced ABI (NOTE: no pid inside): payout, title, metadataHash,
|
|
187
|
+
* bountyToken, bountyPayout, requiresApplication, absoluteDeadline,
|
|
188
|
+
* completionWindow.
|
|
189
|
+
*/
|
|
190
|
+
function buildCreateTasksBatch(a) {
|
|
191
|
+
const inputs = a.tasks.map((task) => {
|
|
192
|
+
const bountyToken = task.bountyToken || ethers_1.ethers.constants.AddressZero;
|
|
193
|
+
return [
|
|
194
|
+
ethers_1.ethers.utils.parseUnits(task.payout.toString(), 18),
|
|
195
|
+
(0, encoding_1.stringToBytes)(task.title),
|
|
196
|
+
(0, encoding_1.ipfsCidToBytes32)(task.metadataHash),
|
|
197
|
+
bountyToken,
|
|
198
|
+
computeBountyPayoutWei(bountyToken, task.bountyAmount),
|
|
199
|
+
task.requiresApplication || false,
|
|
200
|
+
task.absoluteDeadline ?? 0,
|
|
201
|
+
task.completionWindow ?? 0,
|
|
202
|
+
];
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
to: a.taskManagerAddress,
|
|
206
|
+
abi: (0, contracts_1.getAbi)('TaskManagerNew'),
|
|
207
|
+
method: 'createTasksBatch',
|
|
208
|
+
args: [a.projectId, inputs],
|
|
209
|
+
meta: {
|
|
210
|
+
domain: 'task',
|
|
211
|
+
action: 'create-batch',
|
|
212
|
+
orgId: a.orgId,
|
|
213
|
+
summary: { projectId: a.projectId, tasks: a.tasks.length },
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/** Port of `pop task claim` — src/commands/task/claim.ts (claimTask send). */
|
|
218
|
+
function buildClaimTask(a) {
|
|
219
|
+
const id = (0, encoding_1.parseTaskId)(a.taskId);
|
|
220
|
+
return {
|
|
221
|
+
to: a.taskManagerAddress,
|
|
222
|
+
abi: (0, contracts_1.getAbi)('TaskManagerNew'),
|
|
223
|
+
method: 'claimTask',
|
|
224
|
+
args: [id],
|
|
225
|
+
meta: { domain: 'task', action: 'claim', orgId: a.orgId, summary: { taskId: id } },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Port of `pop task unclaim` — src/commands/task/unclaim.ts (unclaimTask send).
|
|
230
|
+
* TaskManager v7 only; the feature gate lives in unclaimTaskIntent (the
|
|
231
|
+
* encoding itself is version-independent).
|
|
232
|
+
*/
|
|
233
|
+
function buildUnclaimTask(a) {
|
|
234
|
+
const id = (0, encoding_1.parseTaskId)(a.taskId);
|
|
235
|
+
return {
|
|
236
|
+
to: a.taskManagerAddress,
|
|
237
|
+
abi: (0, contracts_1.getAbi)('TaskManagerNew'),
|
|
238
|
+
method: 'unclaimTask',
|
|
239
|
+
args: [id],
|
|
240
|
+
meta: { domain: 'task', action: 'unclaim', orgId: a.orgId, summary: { taskId: id } },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/** Port of `pop task submit` — src/commands/task/submit.ts (submitTask send). */
|
|
244
|
+
function buildSubmitTask(a) {
|
|
245
|
+
const id = (0, encoding_1.parseTaskId)(a.taskId);
|
|
246
|
+
return {
|
|
247
|
+
to: a.taskManagerAddress,
|
|
248
|
+
abi: (0, contracts_1.getAbi)('TaskManagerNew'),
|
|
249
|
+
method: 'submitTask',
|
|
250
|
+
args: [id, (0, encoding_1.ipfsCidToBytes32)(a.submissionHash)],
|
|
251
|
+
meta: { domain: 'task', action: 'submit', orgId: a.orgId, summary: { taskId: id } },
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Port of `pop task review --action approve` — src/commands/task/review.ts
|
|
256
|
+
* (completeTask send; releases payout + bounty to the claimer).
|
|
257
|
+
*/
|
|
258
|
+
function buildCompleteTask(a) {
|
|
259
|
+
const id = (0, encoding_1.parseTaskId)(a.taskId);
|
|
260
|
+
return {
|
|
261
|
+
to: a.taskManagerAddress,
|
|
262
|
+
abi: (0, contracts_1.getAbi)('TaskManagerNew'),
|
|
263
|
+
method: 'completeTask',
|
|
264
|
+
args: [id],
|
|
265
|
+
meta: { domain: 'task', action: 'complete', orgId: a.orgId, summary: { taskId: id } },
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Port of `pop task review --action reject` — src/commands/task/review.ts
|
|
270
|
+
* (rejectTask send; no payout, task returns to CLAIMED).
|
|
271
|
+
*/
|
|
272
|
+
function buildRejectTask(a) {
|
|
273
|
+
const id = (0, encoding_1.parseTaskId)(a.taskId);
|
|
274
|
+
return {
|
|
275
|
+
to: a.taskManagerAddress,
|
|
276
|
+
abi: (0, contracts_1.getAbi)('TaskManagerNew'),
|
|
277
|
+
method: 'rejectTask',
|
|
278
|
+
args: [id, (0, encoding_1.ipfsCidToBytes32)(a.rejectionHash)],
|
|
279
|
+
meta: { domain: 'task', action: 'reject', orgId: a.orgId, summary: { taskId: id } },
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/** Port of `pop task cancel` — src/commands/task/cancel.ts (cancelTask send). */
|
|
283
|
+
function buildCancelTask(a) {
|
|
284
|
+
const id = (0, encoding_1.parseTaskId)(a.taskId);
|
|
285
|
+
return {
|
|
286
|
+
to: a.taskManagerAddress,
|
|
287
|
+
abi: (0, contracts_1.getAbi)('TaskManagerNew'),
|
|
288
|
+
method: 'cancelTask',
|
|
289
|
+
args: [id],
|
|
290
|
+
meta: { domain: 'task', action: 'cancel', orgId: a.orgId, summary: { taskId: id } },
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/** Port of `pop task assign` — src/commands/task/assign.ts (assignTask send). */
|
|
294
|
+
function buildAssignTask(a) {
|
|
295
|
+
const id = (0, encoding_1.parseTaskId)(a.taskId);
|
|
296
|
+
return {
|
|
297
|
+
to: a.taskManagerAddress,
|
|
298
|
+
abi: (0, contracts_1.getAbi)('TaskManagerNew'),
|
|
299
|
+
method: 'assignTask',
|
|
300
|
+
args: [id, a.assignee],
|
|
301
|
+
meta: { domain: 'task', action: 'assign', orgId: a.orgId, summary: { taskId: id, assignee: a.assignee } },
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/** Port of `pop task apply` — src/commands/task/apply.ts (applyForTask send). */
|
|
305
|
+
function buildApplyForTask(a) {
|
|
306
|
+
const id = (0, encoding_1.parseTaskId)(a.taskId);
|
|
307
|
+
return {
|
|
308
|
+
to: a.taskManagerAddress,
|
|
309
|
+
abi: (0, contracts_1.getAbi)('TaskManagerNew'),
|
|
310
|
+
method: 'applyForTask',
|
|
311
|
+
args: [id, (0, encoding_1.ipfsCidToBytes32)(a.applicationHash)],
|
|
312
|
+
meta: { domain: 'task', action: 'apply', orgId: a.orgId, summary: { taskId: id } },
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Port of `pop task approve-app` — src/commands/task/approve-application.ts
|
|
317
|
+
* (approveApplication send).
|
|
318
|
+
*/
|
|
319
|
+
function buildApproveApplication(a) {
|
|
320
|
+
const id = (0, encoding_1.parseTaskId)(a.taskId);
|
|
321
|
+
return {
|
|
322
|
+
to: a.taskManagerAddress,
|
|
323
|
+
abi: (0, contracts_1.getAbi)('TaskManagerNew'),
|
|
324
|
+
method: 'approveApplication',
|
|
325
|
+
args: [id, a.applicant],
|
|
326
|
+
meta: { domain: 'task', action: 'approve-app', orgId: a.orgId, summary: { taskId: id, applicant: a.applicant } },
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Port of `pop task update` — src/commands/task/update.ts (updateTask send).
|
|
331
|
+
* updateTask is a FULL OVERWRITE on-chain (8-arg v6 signature): every field is
|
|
332
|
+
* the merged FINAL value, in wei — use updateTaskIntent for the read-merge.
|
|
333
|
+
* The CLI refuses the pre-v6 6-field signature entirely (features.deadlines
|
|
334
|
+
* gate), so this builder only ever emits the v6 shape.
|
|
335
|
+
*/
|
|
336
|
+
function buildUpdateTask(a) {
|
|
337
|
+
const id = (0, encoding_1.parseTaskId)(a.taskId);
|
|
338
|
+
return {
|
|
339
|
+
to: a.taskManagerAddress,
|
|
340
|
+
abi: (0, contracts_1.getAbi)('TaskManagerNew'),
|
|
341
|
+
method: 'updateTask',
|
|
342
|
+
args: [
|
|
343
|
+
id,
|
|
344
|
+
a.payoutWei,
|
|
345
|
+
(0, encoding_1.stringToBytes)(a.title),
|
|
346
|
+
(0, encoding_1.ipfsCidToBytes32)(a.metadataHash),
|
|
347
|
+
a.bountyToken,
|
|
348
|
+
a.bountyPayoutWei,
|
|
349
|
+
a.absoluteDeadline,
|
|
350
|
+
a.completionWindow,
|
|
351
|
+
],
|
|
352
|
+
meta: { domain: 'task', action: 'update', orgId: a.orgId, summary: { taskId: id, title: a.title } },
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Port of `pop task edit-meta` — src/commands/task/edit-meta.ts
|
|
357
|
+
* (updateTaskMetadata send). v5+; preserves payout/bounty on-chain but fully
|
|
358
|
+
* overwrites the title + metadata pointer pair.
|
|
359
|
+
*/
|
|
360
|
+
function buildUpdateTaskMetadata(a) {
|
|
361
|
+
const id = (0, encoding_1.parseTaskId)(a.taskId);
|
|
362
|
+
return {
|
|
363
|
+
to: a.taskManagerAddress,
|
|
364
|
+
abi: (0, contracts_1.getAbi)('TaskManagerNew'),
|
|
365
|
+
method: 'updateTaskMetadata',
|
|
366
|
+
args: [id, (0, encoding_1.stringToBytes)(a.title), (0, encoding_1.ipfsCidToBytes32)(a.metadataHash)],
|
|
367
|
+
meta: { domain: 'task', action: 'edit-meta', orgId: a.orgId, summary: { taskId: id, title: a.title } },
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Port of `pop task perms set` — src/commands/task/perms.ts:309
|
|
372
|
+
* (setProjectRolePerm send; creator-hat/executor gate stays on-chain).
|
|
373
|
+
*/
|
|
374
|
+
function buildSetProjectRolePerm(a) {
|
|
375
|
+
const hatId = ethers_1.ethers.BigNumber.from(a.hatId);
|
|
376
|
+
return {
|
|
377
|
+
to: a.taskManagerAddress,
|
|
378
|
+
abi: (0, contracts_1.getAbi)('TaskManagerNew'),
|
|
379
|
+
method: 'setProjectRolePerm',
|
|
380
|
+
args: [a.projectId, hatId, a.mask],
|
|
381
|
+
meta: {
|
|
382
|
+
domain: 'task',
|
|
383
|
+
action: 'perms-set',
|
|
384
|
+
orgId: a.orgId,
|
|
385
|
+
summary: { projectId: a.projectId, hatId: hatId.toString(), mask: a.mask, permissions: (0, perms_1.formatMask)(a.mask) },
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* The executor-gated setConfig(ROLE_PERM, abi.encode(uint256 hatId, uint8
|
|
391
|
+
* mask)) call `pop task perms propose-global` wraps in a proposal —
|
|
392
|
+
* src/commands/task/perms.ts:418. The CLI encodes via an inline
|
|
393
|
+
* `setConfig(uint8,bytes)` interface; the synced TaskManagerNew ABI carries
|
|
394
|
+
* the same signature, so the calldata is byte-identical.
|
|
395
|
+
*/
|
|
396
|
+
function buildSetGlobalRolePermCall(taskManagerAddress, hatId, mask) {
|
|
397
|
+
const encodedValue = ethers_1.ethers.utils.defaultAbiCoder.encode(['uint256', 'uint8'], [ethers_1.ethers.BigNumber.from(hatId), mask]);
|
|
398
|
+
return (0, governance_1.encodeExecutorCall)('TaskManagerNew', taskManagerAddress, 'setConfig', [exports.CONFIG_KEY_ROLE_PERM, encodedValue]);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Port of `pop task perms propose-global` — src/commands/task/perms.ts:400.
|
|
402
|
+
* HybridVoting.createProposal(titleBytes, descriptionHash, duration, 2,
|
|
403
|
+
* [[setConfig call], []], []) — option 0 applies the mask, option 1 keeps
|
|
404
|
+
* current. Title matches the CLI's deterministic string.
|
|
405
|
+
*/
|
|
406
|
+
function buildProposeGlobalRolePerm(a) {
|
|
407
|
+
const hatId = ethers_1.ethers.BigNumber.from(a.hatId);
|
|
408
|
+
const permsLabel = (0, perms_1.formatMask)(a.mask);
|
|
409
|
+
const title = `Set global task permissions for hat ${hatId.toString()} to ${permsLabel}`;
|
|
410
|
+
return (0, governance_1.buildGovernanceProposal)({
|
|
411
|
+
votingAddress: a.hybridVotingAddress,
|
|
412
|
+
votingAbiName: 'HybridVotingNew',
|
|
413
|
+
title,
|
|
414
|
+
descriptionHash: (0, encoding_1.ipfsCidToBytes32)(a.descriptionHash),
|
|
415
|
+
durationMinutes: a.durationMinutes,
|
|
416
|
+
numOptions: 2,
|
|
417
|
+
batches: [
|
|
418
|
+
[buildSetGlobalRolePermCall(a.taskManagerAddress, hatId, a.mask)], // option 0: apply
|
|
419
|
+
[], // option 1: keep current
|
|
420
|
+
],
|
|
421
|
+
hatIds: [],
|
|
422
|
+
orgId: a.orgId,
|
|
423
|
+
domain: 'task',
|
|
424
|
+
action: 'perms-propose-global',
|
|
425
|
+
summary: { hatId: hatId.toString(), mask: a.mask, permissions: permsLabel },
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Port of `pop task folders set` — src/commands/task/folders.ts:286
|
|
430
|
+
* (setFolders send). CAS-guarded: reverts FoldersRootStale when
|
|
431
|
+
* expectedCurrentRoot no longer matches; the CLI's read-retry-once loop is
|
|
432
|
+
* execution-time behavior and stays host-side.
|
|
433
|
+
*/
|
|
434
|
+
function buildSetFolders(a) {
|
|
435
|
+
return {
|
|
436
|
+
to: a.taskManagerAddress,
|
|
437
|
+
abi: (0, contracts_1.getAbi)('TaskManagerNew'),
|
|
438
|
+
method: 'setFolders',
|
|
439
|
+
args: [a.expectedCurrentRoot, a.newRoot],
|
|
440
|
+
meta: {
|
|
441
|
+
domain: 'task',
|
|
442
|
+
action: 'folders-set',
|
|
443
|
+
orgId: a.orgId,
|
|
444
|
+
summary: { expectedCurrentRoot: a.expectedCurrentRoot, newRoot: a.newRoot },
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
async function resolveTaskManager(ctx, org) {
|
|
449
|
+
const modules = await (0, resolve_1.resolveOrgModules)(ctx.client, org, ctx.chainId);
|
|
450
|
+
return {
|
|
451
|
+
orgId: modules.orgId,
|
|
452
|
+
taskManagerAddress: (0, resolve_1.requireModule)(modules, 'taskManagerAddress'),
|
|
453
|
+
hybridVotingAddress: modules.hybridVotingAddress,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Feature probe for builders whose ENCODING depends on it. `features` override
|
|
458
|
+
* wins; otherwise detection needs ctx.provider (the CLI always has one — a
|
|
459
|
+
* subgraph-served implementation could pick the wrong calldata shape, see
|
|
460
|
+
* version.ts).
|
|
461
|
+
*/
|
|
462
|
+
async function requireFeatures(ctx, taskManagerAddress, orgId, override, purpose) {
|
|
463
|
+
if (override)
|
|
464
|
+
return override;
|
|
465
|
+
if (!ctx.provider) {
|
|
466
|
+
throw new errors_1.CliError(`${purpose} needs the org's TaskManager feature set. Provide ctx.provider (for detection) or params.features.`, exit_codes_1.EXIT.USAGE);
|
|
467
|
+
}
|
|
468
|
+
return (0, version_1.detectTaskManagerFeatures)(ctx.provider, taskManagerAddress, ctx.chainId, {
|
|
469
|
+
orgId,
|
|
470
|
+
client: ctx.client,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
/** Best-effort probe for gates that do not change the encoding (e.g. unclaim). */
|
|
474
|
+
async function maybeFeatures(ctx, taskManagerAddress, orgId, override) {
|
|
475
|
+
if (override)
|
|
476
|
+
return override;
|
|
477
|
+
if (!ctx.provider)
|
|
478
|
+
return null;
|
|
479
|
+
return (0, version_1.detectTaskManagerFeatures)(ctx.provider, taskManagerAddress, ctx.chainId, {
|
|
480
|
+
orgId,
|
|
481
|
+
client: ctx.client,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Read the org's payout convention, distinguishing "no pricing configured"
|
|
486
|
+
* (org row with null metadata ⇒ default pricing is correct) from "org not
|
|
487
|
+
* indexed / subgraph down" (refuse-to-guess). Port of the config read in
|
|
488
|
+
* task/create.ts:213 / create-batch.ts:167.
|
|
489
|
+
*/
|
|
490
|
+
async function fetchOrgPayoutConfig(ctx, orgId) {
|
|
491
|
+
let config = {
|
|
492
|
+
hoursOnly: false, hourlyRate: null, useTokenSymbol: false,
|
|
493
|
+
};
|
|
494
|
+
let error = null;
|
|
495
|
+
try {
|
|
496
|
+
const cfg = await ctx.client.query(org_1.FETCH_ORG_PAYOUT_CONFIG, { orgId }, ctx.chainId);
|
|
497
|
+
if (!cfg.organization) {
|
|
498
|
+
// A successful query with NO org row (indexer lag on a fresh org) is
|
|
499
|
+
// indistinguishable from "no pricing configured" only by accident —
|
|
500
|
+
// treat it like a failed fetch so derivation refuses to guess.
|
|
501
|
+
throw new Error('organization not indexed');
|
|
502
|
+
}
|
|
503
|
+
config = (0, payout_1.payoutConfigFromMetadata)(cfg.organization?.metadata);
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
// Only fatal when a payout must be DERIVED — explicit payouts do not
|
|
507
|
+
// depend on org pricing.
|
|
508
|
+
error = err;
|
|
509
|
+
}
|
|
510
|
+
return { config, error };
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Resolve a project input the way `pop task create` does (task/create.ts:178):
|
|
514
|
+
* bytes32 hex as-is → subgraph title match → numeric index → error listing
|
|
515
|
+
* the available titles. NOTE the order differs from project delete's resolver
|
|
516
|
+
* (reads/project resolveProjectInput), which tries numerics before titles.
|
|
517
|
+
*/
|
|
518
|
+
async function resolveCreateProjectPid(ctx, orgId, projectInput) {
|
|
519
|
+
if (projectInput.startsWith('0x') && projectInput.length === 66)
|
|
520
|
+
return projectInput;
|
|
521
|
+
const projResult = await ctx.client.query(task_1.FETCH_PROJECTS_DATA, { orgId }, ctx.chainId);
|
|
522
|
+
const projects = projResult.organization?.taskManager?.projects || [];
|
|
523
|
+
const match = projects.find((p) => (p.title || '').toLowerCase() === projectInput.toLowerCase());
|
|
524
|
+
if (match) {
|
|
525
|
+
// Extract bytes32 project ID from subgraph composite ID: "{contractAddress}-{projectIdHex}"
|
|
526
|
+
return (0, encoding_1.parseProjectId)(match.id);
|
|
527
|
+
}
|
|
528
|
+
const num = parseInt(projectInput, 10);
|
|
529
|
+
if (!isNaN(num)) {
|
|
530
|
+
return ethers_1.ethers.utils.hexZeroPad(ethers_1.ethers.utils.hexlify(num), 32);
|
|
531
|
+
}
|
|
532
|
+
const available = projects.map((p) => p.title).filter(Boolean).join(', ');
|
|
533
|
+
throw new Error(`Project "${projectInput}" not found. Available: ${available || 'none'}`);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Port of `pop task create` — src/commands/task/create.ts. Resolves the org +
|
|
537
|
+
* project, feature-gates the deadline flags, derives the payout exactly as
|
|
538
|
+
* the web app would when omitted, pins the canonical metadata document, and
|
|
539
|
+
* delegates to buildCreateTask. Duplicate-check and idempotency are CLI UX
|
|
540
|
+
* and are not ported.
|
|
541
|
+
*/
|
|
542
|
+
async function createTaskIntent(ctx, p) {
|
|
543
|
+
// Parse deadline flags up-front so bad input fails before any network work.
|
|
544
|
+
const deadlineFlagsSet = p.deadline !== undefined || p.completionWindow !== undefined;
|
|
545
|
+
const absoluteDeadline = p.deadline !== undefined ? parseDeadlineInput(p.deadline) : 0;
|
|
546
|
+
const completionWindow = p.completionWindow !== undefined ? parseWindowInput(p.completionWindow) : 0;
|
|
547
|
+
const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
|
|
548
|
+
// Feature-gate: pre-v6 TaskManagers only expose the 7-arg createTask.
|
|
549
|
+
const features = await requireFeatures(ctx, taskManagerAddress, orgId, p.features, 'createTask');
|
|
550
|
+
if (!features.deadlines && deadlineFlagsSet) {
|
|
551
|
+
throw new errors_1.CliError((0, version_1.featureUnavailable)('task deadlines', 'TaskManager v6', 'Re-run without --deadline/--completion-window, or upgrade the org TaskManager beacon.'), exit_codes_1.EXIT.PRECONDITION);
|
|
552
|
+
}
|
|
553
|
+
const pid = await resolveCreateProjectPid(ctx, orgId, p.project);
|
|
554
|
+
// Price the task. TaskManager stores whatever payout it is handed, so pricing is a
|
|
555
|
+
// convention shared with the web app rather than an on-chain rule — when payout is
|
|
556
|
+
// omitted, derive it exactly as the UI does.
|
|
557
|
+
const { config: payoutConfig, error: payoutConfigError } = await fetchOrgPayoutConfig(ctx, orgId);
|
|
558
|
+
let derivedPayout = null;
|
|
559
|
+
if (p.payout === undefined) {
|
|
560
|
+
if (payoutConfigError) {
|
|
561
|
+
// Deriving from the hard-coded default config would silently misprice the
|
|
562
|
+
// task for any org with hours-only or custom hourly pricing — and the
|
|
563
|
+
// payout goes on-chain. Refuse rather than guess.
|
|
564
|
+
throw new errors_1.CliError('Could not read the org payout config (subgraph unreachable), so a payout cannot be derived safely.', exit_codes_1.EXIT.PRECONDITION, 'Pass --payout explicitly, or retry when the subgraph is reachable.');
|
|
565
|
+
}
|
|
566
|
+
derivedPayout = (0, payout_1.calculatePayout)(p.difficulty || 'medium', p.estHours || 0, payoutConfig);
|
|
567
|
+
if (derivedPayout <= 0) {
|
|
568
|
+
throw new errors_1.CliError('Could not derive a payout: the org pays by hours only and --est-hours is 0.', exit_codes_1.EXIT.USAGE, 'Pass --est-hours, or set --payout explicitly.');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const payoutAmount = p.payout ?? derivedPayout;
|
|
572
|
+
// Metadata JSON (key order must match the frontend exactly — see metadata/task).
|
|
573
|
+
const metadata = (0, task_3.buildTaskMetadata)({
|
|
574
|
+
name: p.name,
|
|
575
|
+
description: p.description,
|
|
576
|
+
location: p.location || '',
|
|
577
|
+
difficulty: p.difficulty || 'medium',
|
|
578
|
+
estHours: p.estHours || 0,
|
|
579
|
+
submission: '',
|
|
580
|
+
});
|
|
581
|
+
const cid = await (0, ipfs_1.pinJson)((0, task_3.serializeTaskMetadata)(metadata), ctx.ipfs);
|
|
582
|
+
const intent = buildCreateTask({
|
|
583
|
+
taskManagerAddress,
|
|
584
|
+
payout: payoutAmount,
|
|
585
|
+
title: p.name,
|
|
586
|
+
metadataHash: cid,
|
|
587
|
+
projectId: pid,
|
|
588
|
+
bountyToken: p.bountyToken,
|
|
589
|
+
bountyAmount: p.bountyAmount,
|
|
590
|
+
requiresApplication: p.requiresApplication,
|
|
591
|
+
absoluteDeadline,
|
|
592
|
+
completionWindow,
|
|
593
|
+
features,
|
|
594
|
+
orgId,
|
|
595
|
+
});
|
|
596
|
+
intent.meta.ipfs = { cid, metadata };
|
|
597
|
+
if (derivedPayout !== null && intent.meta.summary)
|
|
598
|
+
intent.meta.summary.derivedPayout = derivedPayout;
|
|
599
|
+
return intent;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Port of `pop task create-batch` — src/commands/task/create-batch.ts.
|
|
603
|
+
* Returns ONE createTasksBatch intent (all-or-nothing) on v6 orgs, or one
|
|
604
|
+
* legacy 7-arg createTask intent PER ROW on pre-v6 orgs (the CLI sends those
|
|
605
|
+
* sequentially; --continue-on-error is host-side execution policy). Per-row
|
|
606
|
+
* metadata is pinned in input order so receipt TaskCreated logs line up.
|
|
607
|
+
*/
|
|
608
|
+
async function createTasksBatchIntents(ctx, p) {
|
|
609
|
+
// Batch-wide defaults parsed up-front so bad input fails before network work.
|
|
610
|
+
const defaultDeadline = p.deadline !== undefined ? parseDeadlineInput(p.deadline) : 0;
|
|
611
|
+
const defaultWindow = p.completionWindow !== undefined ? parseWindowInput(p.completionWindow) : 0;
|
|
612
|
+
const batchDeadlineSet = p.deadline !== undefined || p.completionWindow !== undefined;
|
|
613
|
+
const tasks = [];
|
|
614
|
+
for (let i = 0; i < p.tasks.length; i++) {
|
|
615
|
+
const task = p.tasks[i];
|
|
616
|
+
if (!task.name || !task.description) {
|
|
617
|
+
throw new errors_1.CliError(`Row ${i + 1}: Missing required fields: name, description`, exit_codes_1.EXIT.USAGE);
|
|
618
|
+
}
|
|
619
|
+
// Rows go through String() + parse exactly like the CLI (create-batch.ts:129).
|
|
620
|
+
const absoluteDeadline = task.deadline !== undefined
|
|
621
|
+
? (0, encoding_1.parseDeadline)(String(task.deadline))
|
|
622
|
+
: defaultDeadline;
|
|
623
|
+
const completionWindowSecs = task.completionWindow !== undefined
|
|
624
|
+
? (0, encoding_1.parseDurationSeconds)(String(task.completionWindow))
|
|
625
|
+
: defaultWindow;
|
|
626
|
+
tasks.push({
|
|
627
|
+
...task,
|
|
628
|
+
payout: task.payout ?? 0,
|
|
629
|
+
payoutProvided: task.payout !== undefined,
|
|
630
|
+
absoluteDeadline,
|
|
631
|
+
completionWindowSecs,
|
|
632
|
+
deadlineSet: batchDeadlineSet || task.deadline !== undefined || task.completionWindow !== undefined,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
// Client-side EmptyBatch guard (the v6 contract reverts on empty input).
|
|
636
|
+
if (tasks.length === 0) {
|
|
637
|
+
throw new errors_1.CliError('EmptyBatch: file parsed to 0 valid tasks — nothing to create.', exit_codes_1.EXIT.USAGE);
|
|
638
|
+
}
|
|
639
|
+
const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
|
|
640
|
+
// Same pricing convention as `pop task create`.
|
|
641
|
+
const { config: payoutConfig, error: payoutConfigError } = await fetchOrgPayoutConfig(ctx, orgId);
|
|
642
|
+
const needsDerivation = tasks.filter((t) => !t.payoutProvided);
|
|
643
|
+
if (needsDerivation.length > 0 && payoutConfigError) {
|
|
644
|
+
throw new errors_1.CliError(`Could not read the org payout config (subgraph unreachable), and ${needsDerivation.length} `
|
|
645
|
+
+ `row(s) omit "payout": ${needsDerivation.map((t) => t.name).join(', ')}. `
|
|
646
|
+
+ 'Add explicit payouts to those rows, or retry when the subgraph is reachable.', exit_codes_1.EXIT.PRECONDITION);
|
|
647
|
+
}
|
|
648
|
+
for (const t of tasks) {
|
|
649
|
+
if (!t.payoutProvided) {
|
|
650
|
+
t.payout = (0, payout_1.calculatePayout)(t.difficulty || 'medium', t.estHours || 0, payoutConfig);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const unpriced = tasks.filter((t) => !t.payoutProvided && t.payout <= 0);
|
|
654
|
+
if (unpriced.length > 0) {
|
|
655
|
+
throw new errors_1.CliError(`${unpriced.length} row(s) could not be priced (no "payout", and difficulty/estHours `
|
|
656
|
+
+ `derive to 0): ${unpriced.map((t) => t.name).join(', ')}`, exit_codes_1.EXIT.USAGE);
|
|
657
|
+
}
|
|
658
|
+
const pid = (0, encoding_1.parseProjectId)(p.project);
|
|
659
|
+
const features = await requireFeatures(ctx, taskManagerAddress, orgId, p.features, 'createTasksBatch');
|
|
660
|
+
if (tasks.some((t) => t.deadlineSet) && !features.deadlines) {
|
|
661
|
+
throw new errors_1.CliError((0, version_1.featureUnavailable)('task deadlines', 'TaskManager v6', 'Re-run without --deadline/--completion-window, or upgrade the org TaskManager beacon.'), exit_codes_1.EXIT.PRECONDITION);
|
|
662
|
+
}
|
|
663
|
+
// Per-row metadata: exactly buildMetadata() from create-batch.ts, pinned in
|
|
664
|
+
// input order.
|
|
665
|
+
const pinned = [];
|
|
666
|
+
for (const row of tasks) {
|
|
667
|
+
const metadata = (0, task_3.buildTaskMetadata)({
|
|
668
|
+
name: row.name,
|
|
669
|
+
description: row.description,
|
|
670
|
+
location: row.location || '',
|
|
671
|
+
difficulty: row.difficulty || 'medium',
|
|
672
|
+
estHours: row.estHours || 0,
|
|
673
|
+
submission: '',
|
|
674
|
+
});
|
|
675
|
+
const cid = await (0, ipfs_1.pinJson)((0, task_3.serializeTaskMetadata)(metadata), ctx.ipfs);
|
|
676
|
+
pinned.push({ row, cid, metadata });
|
|
677
|
+
}
|
|
678
|
+
if (features.batchCreate) {
|
|
679
|
+
const intent = buildCreateTasksBatch({
|
|
680
|
+
taskManagerAddress,
|
|
681
|
+
projectId: pid,
|
|
682
|
+
tasks: pinned.map(({ row, cid }) => ({
|
|
683
|
+
payout: row.payout,
|
|
684
|
+
title: row.name,
|
|
685
|
+
metadataHash: cid,
|
|
686
|
+
bountyToken: row.bountyToken,
|
|
687
|
+
bountyAmount: row.bountyAmount,
|
|
688
|
+
requiresApplication: row.requiresApplication,
|
|
689
|
+
absoluteDeadline: row.absoluteDeadline,
|
|
690
|
+
completionWindow: row.completionWindowSecs,
|
|
691
|
+
})),
|
|
692
|
+
orgId,
|
|
693
|
+
});
|
|
694
|
+
intent.meta.ipfs = { cid: pinned[0].cid, metadata: pinned.map((x) => x.metadata) };
|
|
695
|
+
return [intent];
|
|
696
|
+
}
|
|
697
|
+
// Legacy pre-v6 org: per-task 7-arg createTask intents (sequential sends).
|
|
698
|
+
return pinned.map(({ row, cid, metadata }) => {
|
|
699
|
+
const intent = buildCreateTask({
|
|
700
|
+
taskManagerAddress,
|
|
701
|
+
payout: row.payout,
|
|
702
|
+
title: row.name,
|
|
703
|
+
metadataHash: cid,
|
|
704
|
+
projectId: pid,
|
|
705
|
+
bountyToken: row.bountyToken,
|
|
706
|
+
bountyAmount: row.bountyAmount,
|
|
707
|
+
requiresApplication: row.requiresApplication,
|
|
708
|
+
features: { deadlines: false },
|
|
709
|
+
orgId,
|
|
710
|
+
});
|
|
711
|
+
intent.meta.ipfs = { cid, metadata };
|
|
712
|
+
return intent;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
/** Port of `pop task claim` — src/commands/task/claim.ts. */
|
|
716
|
+
async function claimTaskIntent(ctx, p) {
|
|
717
|
+
const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
|
|
718
|
+
return buildClaimTask({ taskManagerAddress, taskId: p.task, orgId });
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Port of `pop task unclaim` — src/commands/task/unclaim.ts (TaskManager v7).
|
|
722
|
+
* Feature-gated like the CLI when a provider (or features override) is
|
|
723
|
+
* available; without either the gate is skipped — the encoding is identical
|
|
724
|
+
* and the contract is the authority. The self-vs-force consent tiering and
|
|
725
|
+
* the lens gate are host UX.
|
|
726
|
+
*/
|
|
727
|
+
async function unclaimTaskIntent(ctx, p) {
|
|
728
|
+
const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
|
|
729
|
+
const features = await maybeFeatures(ctx, taskManagerAddress, orgId, p.features);
|
|
730
|
+
if (features && !features.unclaim) {
|
|
731
|
+
throw new errors_1.CliError((0, version_1.featureUnavailable)('releasing a claimed task', 'TaskManager v7', 'Upgrade the org\'s TaskManager beacon, or wait for the claim deadline to expire so it can be taken over.'), exit_codes_1.EXIT.PRECONDITION);
|
|
732
|
+
}
|
|
733
|
+
return buildUnclaimTask({ taskManagerAddress, taskId: p.task, orgId });
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Port of `pop task submit` — src/commands/task/submit.ts. The submission is
|
|
737
|
+
* a FULL OVERWRITE of the task's metadata pointer, so the current document is
|
|
738
|
+
* read first (subgraph → IPFS fallback) and merged; when the base is
|
|
739
|
+
* unrecoverable this REFUSES (exit INFRA) rather than blanking the task's
|
|
740
|
+
* name/description/dueDate. Pre-flight (CLAIMED + claimer) is host UX.
|
|
741
|
+
*/
|
|
742
|
+
async function submitTaskIntent(ctx, p) {
|
|
743
|
+
const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
|
|
744
|
+
const parsedTaskId = (0, encoding_1.parseTaskId)(p.task);
|
|
745
|
+
// Fetch existing metadata so the submission preserves it. Match on the
|
|
746
|
+
// PARSED numeric id, fall back to IPFS before giving up — same ladder as
|
|
747
|
+
// task update / edit-meta.
|
|
748
|
+
let subgraphTask = null;
|
|
749
|
+
try {
|
|
750
|
+
const taskData = await ctx.client.query(task_1.FETCH_PROJECTS_DATA, { orgId }, ctx.chainId);
|
|
751
|
+
subgraphTask = (0, task_2.findSubgraphTask)(taskData.organization?.taskManager?.projects || [], parsedTaskId);
|
|
752
|
+
}
|
|
753
|
+
catch { /* handled below */ }
|
|
754
|
+
let existingMeta = subgraphTask?.metadata || null;
|
|
755
|
+
if (!existingMeta && subgraphTask?.metadataHash) {
|
|
756
|
+
try {
|
|
757
|
+
existingMeta = await (0, ipfs_1.fetchJson)(subgraphTask.metadataHash, ctx.ipfs);
|
|
758
|
+
}
|
|
759
|
+
catch { /* handled below */ }
|
|
760
|
+
}
|
|
761
|
+
if (!existingMeta) {
|
|
762
|
+
throw new errors_1.CliError(`Task ${p.task} metadata is not indexed yet (subgraph lag) — submitting now would `
|
|
763
|
+
+ 'overwrite the task\'s name, description and due date with blanks.', exit_codes_1.EXIT.INFRA, 'Retry in a few seconds. The submission itself is unaffected once metadata resolves.');
|
|
764
|
+
}
|
|
765
|
+
// Merge submission into existing metadata (exact field-by-field parity with
|
|
766
|
+
// submit.ts:141 — note estimatedHours parseFloat and the '' difficulty
|
|
767
|
+
// fallback, which differ from update/edit-meta's merge).
|
|
768
|
+
const submissionMetadata = (0, task_3.buildTaskMetadata)({
|
|
769
|
+
name: existingMeta?.name || '',
|
|
770
|
+
description: existingMeta?.description || '',
|
|
771
|
+
location: existingMeta?.location || '',
|
|
772
|
+
difficulty: existingMeta?.difficulty || '',
|
|
773
|
+
estHours: existingMeta?.estimatedHours ? parseFloat(existingMeta.estimatedHours) : 0,
|
|
774
|
+
submission: p.submission,
|
|
775
|
+
// The subgraph re-points task.metadata at THIS submission JSON, so a
|
|
776
|
+
// dueDate omitted here is gone for good. Appended last, only when set.
|
|
777
|
+
dueDate: existingMeta?.dueDate ? Math.floor(Number(existingMeta.dueDate)) : undefined,
|
|
778
|
+
});
|
|
779
|
+
const cid = await (0, ipfs_1.pinJson)((0, task_3.serializeTaskMetadata)(submissionMetadata), ctx.ipfs);
|
|
780
|
+
const intent = buildSubmitTask({ taskManagerAddress, taskId: parsedTaskId, submissionHash: cid, orgId });
|
|
781
|
+
intent.meta.ipfs = { cid, metadata: submissionMetadata };
|
|
782
|
+
return intent;
|
|
783
|
+
}
|
|
784
|
+
/** Port of `pop task review --action approve` — src/commands/task/review.ts. */
|
|
785
|
+
async function completeTaskIntent(ctx, p) {
|
|
786
|
+
const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
|
|
787
|
+
return buildCompleteTask({ taskManagerAddress, taskId: p.task, orgId });
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Port of `pop task review --action reject` — src/commands/task/review.ts.
|
|
791
|
+
* Pins the `{rejection}` document, then builds rejectTask. (The CLI pins
|
|
792
|
+
* after pre-flight + confirm; core has neither, so the pin is the first step.)
|
|
793
|
+
*/
|
|
794
|
+
async function rejectTaskIntent(ctx, p) {
|
|
795
|
+
const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
|
|
796
|
+
const rejectionMetadata = (0, task_3.buildTaskRejectionMetadata)(p.reason);
|
|
797
|
+
const cid = await (0, ipfs_1.pinJson)((0, task_3.serializeTaskRejectionMetadata)(rejectionMetadata), ctx.ipfs);
|
|
798
|
+
const intent = buildRejectTask({ taskManagerAddress, taskId: p.task, rejectionHash: cid, orgId });
|
|
799
|
+
intent.meta.ipfs = { cid, metadata: rejectionMetadata };
|
|
800
|
+
return intent;
|
|
801
|
+
}
|
|
802
|
+
/** Port of `pop task cancel` — src/commands/task/cancel.ts (DESTRUCTIVE; consent is host UX). */
|
|
803
|
+
async function cancelTaskIntent(ctx, p) {
|
|
804
|
+
const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
|
|
805
|
+
return buildCancelTask({ taskManagerAddress, taskId: p.task, orgId });
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Port of `pop task assign` — src/commands/task/assign.ts. --username resolves
|
|
809
|
+
* via the same inline all-chains accounts query; --assignee is validated as an
|
|
810
|
+
* address. One of the two is required.
|
|
811
|
+
*/
|
|
812
|
+
async function assignTaskIntent(ctx, p) {
|
|
813
|
+
let assignee;
|
|
814
|
+
if (p.username) {
|
|
815
|
+
const query = `{ accounts(where: { username: "${p.username}" }, first: 1) { id username } }`;
|
|
816
|
+
const results = await ctx.client.queryAllChains(query, {});
|
|
817
|
+
const account = results.map((r) => r.data?.accounts?.[0]).find(Boolean);
|
|
818
|
+
if (!account) {
|
|
819
|
+
throw new Error(`Username "${p.username}" not found. Check spelling or use --assignee with the address.`);
|
|
820
|
+
}
|
|
821
|
+
assignee = account.id;
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
assignee = (0, validation_1.requireAddress)(p.assignee, 'assignee');
|
|
825
|
+
}
|
|
826
|
+
const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
|
|
827
|
+
return buildAssignTask({ taskManagerAddress, taskId: p.task, assignee, orgId });
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Port of `pop task apply` — src/commands/task/apply.ts. Pins the
|
|
831
|
+
* `{notes, experience}` document, then builds applyForTask. The
|
|
832
|
+
* requiresApplication / duplicate-application pre-flights are host UX (the
|
|
833
|
+
* contract's NoApplicationRequired / AlreadyApplied decodes are authoritative).
|
|
834
|
+
*/
|
|
835
|
+
async function applyForTaskIntent(ctx, p) {
|
|
836
|
+
const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
|
|
837
|
+
const applicationData = (0, task_3.buildTaskApplicationMetadata)({ notes: p.notes, experience: p.experience });
|
|
838
|
+
const cid = await (0, ipfs_1.pinJson)((0, task_3.serializeTaskApplicationMetadata)(applicationData), ctx.ipfs);
|
|
839
|
+
const intent = buildApplyForTask({ taskManagerAddress, taskId: p.task, applicationHash: cid, orgId });
|
|
840
|
+
intent.meta.ipfs = { cid, metadata: applicationData };
|
|
841
|
+
return intent;
|
|
842
|
+
}
|
|
843
|
+
/** Port of `pop task approve-app` — src/commands/task/approve-application.ts. */
|
|
844
|
+
async function approveApplicationIntent(ctx, p) {
|
|
845
|
+
const applicant = (0, validation_1.requireAddress)(p.applicant, 'applicant');
|
|
846
|
+
const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
|
|
847
|
+
return buildApproveApplication({ taskManagerAddress, taskId: p.task, applicant, orgId });
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Port of `pop task update` — src/commands/task/update.ts. updateTask is a
|
|
851
|
+
* FULL OVERWRITE on-chain, so this reads the current state first (chain via
|
|
852
|
+
* the task lens for payout/bounty/deadlines — ctx.provider required; subgraph
|
|
853
|
+
* + IPFS for title/description/metadata), merges only the fields given, and
|
|
854
|
+
* re-pins metadata ONLY when name/description actually change. Refuses (INFRA)
|
|
855
|
+
* when the overwrite base is unrecoverable. A past deadline is accepted and
|
|
856
|
+
* flagged in meta.summary.deadlineIsPast.
|
|
857
|
+
*/
|
|
858
|
+
async function updateTaskIntent(ctx, p) {
|
|
859
|
+
// 0. Fail fast on input problems before any network work.
|
|
860
|
+
const changed = ['payout', 'name', 'description', 'bountyToken', 'bountyAmount', 'deadline', 'completionWindow']
|
|
861
|
+
.filter((key) => p[key] !== undefined);
|
|
862
|
+
if (changed.length === 0) {
|
|
863
|
+
throw new errors_1.CliError('Nothing to update — pass at least one field flag.', exit_codes_1.EXIT.USAGE, 'Available: --payout, --name, --description, --bounty-token, --bounty-amount, --deadline, --completion-window');
|
|
864
|
+
}
|
|
865
|
+
let deadlineIsPast = false;
|
|
866
|
+
let newAbsoluteDeadline;
|
|
867
|
+
if (p.deadline !== undefined) {
|
|
868
|
+
const parsed = parseUpdateDeadline(p.deadline);
|
|
869
|
+
newAbsoluteDeadline = parsed.value;
|
|
870
|
+
deadlineIsPast = parsed.isPast;
|
|
871
|
+
}
|
|
872
|
+
const newCompletionWindow = p.completionWindow !== undefined
|
|
873
|
+
? (0, encoding_1.parseDurationSeconds)(p.completionWindow)
|
|
874
|
+
: undefined;
|
|
875
|
+
// 1. Resolve org, gate on the v6 signature.
|
|
876
|
+
const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
|
|
877
|
+
const taskId = (0, encoding_1.parseTaskId)(p.task);
|
|
878
|
+
const features = await requireFeatures(ctx, taskManagerAddress, orgId, p.features, 'updateTask');
|
|
879
|
+
if (!features.deadlines) {
|
|
880
|
+
// Pre-v6 implementations expose a 6-field updateTask that no longer
|
|
881
|
+
// exists in the synced ABI — do NOT attempt it; degrade clearly.
|
|
882
|
+
throw new errors_1.CliError((0, version_1.featureUnavailable)('full task editing (updateTask)', 'TaskManager v6', 'Upgrade the org TaskManager beacon, or use pop task edit-meta if the org supports metadata-only edits.'), exit_codes_1.EXIT.PRECONDITION);
|
|
883
|
+
}
|
|
884
|
+
// 2. READ: current on-chain fields (authoritative via lens).
|
|
885
|
+
if (!ctx.provider) {
|
|
886
|
+
throw new errors_1.CliError('updateTask needs ctx.provider: the current payout/bounty/deadlines are read from the TaskManager lens (a full overwrite must preserve them).', exit_codes_1.EXIT.USAGE);
|
|
887
|
+
}
|
|
888
|
+
const current = await (0, task_lens_1.getTaskOnChain)(ctx.provider, taskManagerAddress, taskId);
|
|
889
|
+
if (current.status === task_lens_1.TASK_STATUS.COMPLETED || current.status === task_lens_1.TASK_STATUS.CANCELLED) {
|
|
890
|
+
throw new errors_1.CliError(`Task ${taskId} is ${(0, task_lens_1.taskStatusName)(current.status)} — terminal states are immutable on-chain.`, exit_codes_1.EXIT.PRECONDITION, 'Create a new task instead (pop task create).');
|
|
891
|
+
}
|
|
892
|
+
// 3. READ: current title/description/metadata (subgraph → IPFS fallback).
|
|
893
|
+
const metadataChanging = p.name !== undefined || p.description !== undefined;
|
|
894
|
+
let subgraphTask = null;
|
|
895
|
+
try {
|
|
896
|
+
const result = await ctx.client.query(task_1.FETCH_PROJECTS_DATA, { orgId }, ctx.chainId);
|
|
897
|
+
subgraphTask = (0, task_2.findSubgraphTask)(result.organization?.taskManager?.projects || [], taskId);
|
|
898
|
+
}
|
|
899
|
+
catch { /* handled below based on what the merge needs */ }
|
|
900
|
+
let metadata = subgraphTask?.metadata || null;
|
|
901
|
+
if (!metadata && subgraphTask?.metadataHash) {
|
|
902
|
+
try {
|
|
903
|
+
metadata = await (0, ipfs_1.fetchJson)(subgraphTask.metadataHash, ctx.ipfs);
|
|
904
|
+
}
|
|
905
|
+
catch { /* IPFS lag — handled below */ }
|
|
906
|
+
}
|
|
907
|
+
const currentName = metadata?.name ?? subgraphTask?.title ?? undefined;
|
|
908
|
+
const currentDescription = metadata?.description ?? undefined;
|
|
909
|
+
const currentMetadataHash = subgraphTask?.metadataHash ?? undefined;
|
|
910
|
+
// updateTask overwrites title + metadataHash even when only, say, payout
|
|
911
|
+
// changed — so the current values must be recoverable.
|
|
912
|
+
if (!metadataChanging && (currentName === undefined || !currentMetadataHash)) {
|
|
913
|
+
throw new errors_1.CliError(`Task ${taskId} metadata is not indexed yet (subgraph lag) — cannot preserve the current title/metadata through a full-overwrite update.`, exit_codes_1.EXIT.INFRA, 'Retry in a few seconds, or pass --name and --description explicitly.');
|
|
914
|
+
}
|
|
915
|
+
if (metadataChanging
|
|
916
|
+
&& (p.name === undefined || p.description === undefined)
|
|
917
|
+
&& (currentName === undefined || currentDescription === undefined)) {
|
|
918
|
+
throw new errors_1.CliError(`Task ${taskId} metadata is not indexed yet (subgraph lag) — cannot merge a partial metadata edit.`, exit_codes_1.EXIT.INFRA, 'Retry in a few seconds, or pass BOTH --name and --description.');
|
|
919
|
+
}
|
|
920
|
+
// 4. MERGE: given fields override, everything else preserved.
|
|
921
|
+
const finalPayout = p.payout !== undefined
|
|
922
|
+
? ethers_1.ethers.utils.parseUnits(p.payout.toString(), 18)
|
|
923
|
+
: current.payout;
|
|
924
|
+
let finalBountyToken = current.bountyToken;
|
|
925
|
+
if (p.bountyToken !== undefined) {
|
|
926
|
+
if (p.bountyToken.toLowerCase() === 'none') {
|
|
927
|
+
finalBountyToken = ethers_1.ethers.constants.AddressZero;
|
|
928
|
+
}
|
|
929
|
+
else if (ethers_1.ethers.utils.isAddress(p.bountyToken)) {
|
|
930
|
+
finalBountyToken = ethers_1.ethers.utils.getAddress(p.bountyToken);
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
throw new errors_1.CliError(`Invalid --bounty-token "${p.bountyToken}".`, exit_codes_1.EXIT.USAGE, 'Pass an ERC20 address, or "none" to clear the bounty.');
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
let finalBountyPayout = current.bountyPayout;
|
|
937
|
+
if (finalBountyToken === ethers_1.ethers.constants.AddressZero) {
|
|
938
|
+
finalBountyPayout = ethers_1.ethers.BigNumber.from(0); // zero token ⇒ zero payout (contract validation)
|
|
939
|
+
}
|
|
940
|
+
else if (p.bountyAmount !== undefined) {
|
|
941
|
+
finalBountyPayout = p.bountyAmount > 0
|
|
942
|
+
? ethers_1.ethers.utils.parseUnits(p.bountyAmount.toString(), (0, chains_1.getTokenDecimals)(finalBountyToken))
|
|
943
|
+
: ethers_1.ethers.BigNumber.from(0);
|
|
944
|
+
}
|
|
945
|
+
// (CLI-only warning when the token changed without --bounty-amount: the raw
|
|
946
|
+
// current amount is kept, which may mean a different human value.)
|
|
947
|
+
const finalDeadline = newAbsoluteDeadline !== undefined ? newAbsoluteDeadline : (current.absoluteDeadline ?? 0);
|
|
948
|
+
const finalWindow = newCompletionWindow !== undefined ? newCompletionWindow : (current.completionWindow ?? 0);
|
|
949
|
+
const finalName = p.name ?? currentName ?? '';
|
|
950
|
+
const finalDescription = p.description ?? currentDescription ?? '';
|
|
951
|
+
// Re-pin only when the metadata content actually changes.
|
|
952
|
+
let finalMetadataHash = currentMetadataHash
|
|
953
|
+
? (0, encoding_1.ipfsCidToBytes32)(currentMetadataHash)
|
|
954
|
+
: ethers_1.ethers.constants.HashZero;
|
|
955
|
+
let newCid;
|
|
956
|
+
let pinnedMetadata;
|
|
957
|
+
const metadataActuallyChanged = metadataChanging
|
|
958
|
+
&& (finalName !== currentName || finalDescription !== currentDescription);
|
|
959
|
+
if (metadataActuallyChanged) {
|
|
960
|
+
// Key order MUST match task create (frontend parity) — see metadata/task.
|
|
961
|
+
// When the current doc could not be fetched, location/difficulty/estHours
|
|
962
|
+
// reset to defaults exactly as the CLI warns they will.
|
|
963
|
+
pinnedMetadata = (0, task_3.buildTaskMetadata)({
|
|
964
|
+
name: finalName,
|
|
965
|
+
description: finalDescription,
|
|
966
|
+
location: metadata?.location || '',
|
|
967
|
+
difficulty: metadata?.difficulty || 'medium',
|
|
968
|
+
estHours: metadata?.estimatedHours || metadata?.estHours || 0,
|
|
969
|
+
submission: metadata?.submission || '',
|
|
970
|
+
// Soft due date is metadata-borne; a re-pin that drops it deletes it.
|
|
971
|
+
dueDate: metadata?.dueDate ? Math.floor(Number(metadata.dueDate)) : undefined,
|
|
972
|
+
});
|
|
973
|
+
newCid = await (0, ipfs_1.pinJson)((0, task_3.serializeTaskMetadata)(pinnedMetadata), ctx.ipfs);
|
|
974
|
+
finalMetadataHash = (0, encoding_1.ipfsCidToBytes32)(newCid);
|
|
975
|
+
}
|
|
976
|
+
const intent = buildUpdateTask({
|
|
977
|
+
taskManagerAddress,
|
|
978
|
+
taskId,
|
|
979
|
+
payoutWei: finalPayout,
|
|
980
|
+
title: finalName,
|
|
981
|
+
metadataHash: finalMetadataHash,
|
|
982
|
+
bountyToken: finalBountyToken,
|
|
983
|
+
bountyPayoutWei: finalBountyPayout,
|
|
984
|
+
absoluteDeadline: finalDeadline,
|
|
985
|
+
completionWindow: finalWindow,
|
|
986
|
+
orgId,
|
|
987
|
+
});
|
|
988
|
+
if (newCid && pinnedMetadata)
|
|
989
|
+
intent.meta.ipfs = { cid: newCid, metadata: pinnedMetadata };
|
|
990
|
+
intent.meta.summary = {
|
|
991
|
+
...intent.meta.summary,
|
|
992
|
+
fieldsChanged: changed.join(','),
|
|
993
|
+
...(deadlineIsPast ? { deadlineIsPast: true } : {}),
|
|
994
|
+
};
|
|
995
|
+
return intent;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Port of `pop task edit-meta` — src/commands/task/edit-meta.ts
|
|
999
|
+
* (updateTaskMetadata; TaskManager v5+). Reads the current name/description
|
|
1000
|
+
* (subgraph → IPFS fallback), merges, and re-pins. Returns NULL when the merge
|
|
1001
|
+
* is a no-op — the CLI short-circuits with success and never sends. The
|
|
1002
|
+
* terminal-status read uses the lens when ctx.provider is present (parity);
|
|
1003
|
+
* without one, the contract's BadStatus revert is the authority.
|
|
1004
|
+
*/
|
|
1005
|
+
async function editTaskMetadataIntent(ctx, p) {
|
|
1006
|
+
if (p.name === undefined && p.description === undefined) {
|
|
1007
|
+
throw new errors_1.CliError('Nothing to edit — pass --name and/or --description.', exit_codes_1.EXIT.USAGE);
|
|
1008
|
+
}
|
|
1009
|
+
const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
|
|
1010
|
+
const taskId = (0, encoding_1.parseTaskId)(p.task);
|
|
1011
|
+
// Feature-gate: updateTaskMetadata shipped with TaskManager v5.
|
|
1012
|
+
const features = await requireFeatures(ctx, taskManagerAddress, orgId, p.features, 'updateTaskMetadata');
|
|
1013
|
+
if (!features.editMeta) {
|
|
1014
|
+
throw new errors_1.CliError((0, version_1.featureUnavailable)('post-claim metadata editing', 'TaskManager v5', 'Upgrade the org TaskManager beacon, or recreate the task with the corrected metadata.'), exit_codes_1.EXIT.PRECONDITION);
|
|
1015
|
+
}
|
|
1016
|
+
// Authoritative status read (also proves the task exists) when a provider
|
|
1017
|
+
// is available.
|
|
1018
|
+
if (ctx.provider) {
|
|
1019
|
+
const current = await (0, task_lens_1.getTaskOnChain)(ctx.provider, taskManagerAddress, taskId);
|
|
1020
|
+
if (current.status === task_lens_1.TASK_STATUS.COMPLETED || current.status === task_lens_1.TASK_STATUS.CANCELLED) {
|
|
1021
|
+
throw new errors_1.CliError(`Task ${taskId} is ${(0, task_lens_1.taskStatusName)(current.status)} — terminal states are immutable on-chain.`, exit_codes_1.EXIT.PRECONDITION, 'Metadata of completed/cancelled tasks cannot be edited.');
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
// READ current metadata: subgraph first, IPFS pointer as fallback.
|
|
1025
|
+
let subgraphTask = null;
|
|
1026
|
+
try {
|
|
1027
|
+
const result = await ctx.client.query(task_1.FETCH_PROJECTS_DATA, { orgId }, ctx.chainId);
|
|
1028
|
+
subgraphTask = (0, task_2.findSubgraphTask)(result.organization?.taskManager?.projects || [], taskId);
|
|
1029
|
+
}
|
|
1030
|
+
catch { /* handled below */ }
|
|
1031
|
+
let metadata = subgraphTask?.metadata || null;
|
|
1032
|
+
if (!metadata && subgraphTask?.metadataHash) {
|
|
1033
|
+
try {
|
|
1034
|
+
metadata = await (0, ipfs_1.fetchJson)(subgraphTask.metadataHash, ctx.ipfs);
|
|
1035
|
+
}
|
|
1036
|
+
catch { /* handled below */ }
|
|
1037
|
+
}
|
|
1038
|
+
const currentName = metadata?.name ?? subgraphTask?.title ?? undefined;
|
|
1039
|
+
const currentDescription = metadata?.description ?? undefined;
|
|
1040
|
+
if ((p.name === undefined && currentName === undefined)
|
|
1041
|
+
|| (p.description === undefined && currentDescription === undefined)) {
|
|
1042
|
+
throw new errors_1.CliError(`Task ${taskId} metadata is not indexed yet (subgraph lag) — cannot merge a partial metadata edit.`, exit_codes_1.EXIT.INFRA, 'Retry in a few seconds, or pass BOTH --name and --description.');
|
|
1043
|
+
}
|
|
1044
|
+
// MERGE
|
|
1045
|
+
const finalName = p.name ?? currentName ?? '';
|
|
1046
|
+
const finalDescription = p.description ?? currentDescription ?? '';
|
|
1047
|
+
if (finalName === currentName && finalDescription === currentDescription) {
|
|
1048
|
+
// No-op: the CLI reports success without sending anything.
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
// Key order MUST match task create (frontend parity) — see metadata/task.
|
|
1052
|
+
const metadataJson = (0, task_3.buildTaskMetadata)({
|
|
1053
|
+
name: finalName,
|
|
1054
|
+
description: finalDescription,
|
|
1055
|
+
location: metadata?.location || '',
|
|
1056
|
+
difficulty: metadata?.difficulty || 'medium',
|
|
1057
|
+
estHours: metadata?.estimatedHours || metadata?.estHours || 0,
|
|
1058
|
+
submission: metadata?.submission || '',
|
|
1059
|
+
// Soft due date is metadata-borne; a re-pin that drops it deletes it.
|
|
1060
|
+
dueDate: metadata?.dueDate ? Math.floor(Number(metadata.dueDate)) : undefined,
|
|
1061
|
+
});
|
|
1062
|
+
const cid = await (0, ipfs_1.pinJson)((0, task_3.serializeTaskMetadata)(metadataJson), ctx.ipfs);
|
|
1063
|
+
const intent = buildUpdateTaskMetadata({ taskManagerAddress, taskId, title: finalName, metadataHash: cid, orgId });
|
|
1064
|
+
intent.meta.ipfs = { cid, metadata: metadataJson };
|
|
1065
|
+
return intent;
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Port of `pop task perms set` — src/commands/task/perms.ts:309. Resolves the
|
|
1069
|
+
* project by hex or title (PERMS_QUERY_FULL/LEGACY tiers), parses the perm
|
|
1070
|
+
* list into a mask, and builds setProjectRolePerm. The creator-hat/executor
|
|
1071
|
+
* gate stays on-chain (masks are not readable per-wearer).
|
|
1072
|
+
*/
|
|
1073
|
+
async function setProjectRolePermIntent(ctx, p) {
|
|
1074
|
+
const mask = typeof p.perms === 'number' ? p.perms : (0, perms_1.parsePermList)(p.perms);
|
|
1075
|
+
const hatId = parseHatId(p.hat);
|
|
1076
|
+
const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
|
|
1077
|
+
let pid;
|
|
1078
|
+
if (p.project.startsWith('0x') && p.project.length === 66) {
|
|
1079
|
+
pid = p.project;
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
const { data } = await ctx.client.queryWithFieldFallback((0, task_2.taskPermsTiers)(orgId), { chainId: ctx.chainId });
|
|
1083
|
+
pid = (0, task_2.resolveProjectFromList)(data.organization?.taskManager?.projects || [], p.project).pid;
|
|
1084
|
+
}
|
|
1085
|
+
return buildSetProjectRolePerm({ taskManagerAddress, projectId: pid, hatId, mask, orgId });
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Port of `pop task perms propose-global` — src/commands/task/perms.ts:400.
|
|
1089
|
+
* setConfig(ROLE_PERM) is executor-only, so the change ships as a HybridVoting
|
|
1090
|
+
* proposal whose option-0 batch targets the TaskManager. Pins the proposal
|
|
1091
|
+
* metadata {description, optionNames, createdAt} (exact key order), then
|
|
1092
|
+
* delegates to buildProposeGlobalRolePerm.
|
|
1093
|
+
*/
|
|
1094
|
+
async function proposeGlobalRolePermIntent(ctx, p) {
|
|
1095
|
+
const mask = typeof p.perms === 'number' ? p.perms : (0, perms_1.parsePermList)(p.perms);
|
|
1096
|
+
const hatId = parseHatId(p.hat);
|
|
1097
|
+
const duration = p.duration ?? 60;
|
|
1098
|
+
const { orgId, taskManagerAddress, hybridVotingAddress } = await resolveTaskManager(ctx, p.org);
|
|
1099
|
+
if (!hybridVotingAddress) {
|
|
1100
|
+
throw new errors_1.CliError('HybridVoting not deployed for this org — cannot create a governance proposal.', exit_codes_1.EXIT.PRECONDITION);
|
|
1101
|
+
}
|
|
1102
|
+
const permsLabel = (0, perms_1.formatMask)(mask);
|
|
1103
|
+
const title = `Set global task permissions for hat ${hatId.toString()} to ${permsLabel}`;
|
|
1104
|
+
// Proposal metadata — key order {description, optionNames, createdAt} is a
|
|
1105
|
+
// protocol contract (same shape every governance wrap pins).
|
|
1106
|
+
const metadata = (0, proposal_1.buildProposalMetadata)({
|
|
1107
|
+
description: `Set the GLOBAL TaskPerm mask for hat ${hatId.toString()} to ${mask} (${permsLabel}) via TaskManager.setConfig(ROLE_PERM). ${(0, perms_1.describeMask)(mask).join('; ') || 'Revokes all global task permissions for this hat.'}`,
|
|
1108
|
+
optionNames: [title, 'Keep current permissions'],
|
|
1109
|
+
});
|
|
1110
|
+
const cid = await (0, ipfs_1.pinJson)((0, proposal_1.serializeProposalMetadata)(metadata), ctx.ipfs);
|
|
1111
|
+
const intent = buildProposeGlobalRolePerm({
|
|
1112
|
+
hybridVotingAddress,
|
|
1113
|
+
taskManagerAddress,
|
|
1114
|
+
hatId,
|
|
1115
|
+
mask,
|
|
1116
|
+
durationMinutes: duration,
|
|
1117
|
+
descriptionHash: cid,
|
|
1118
|
+
orgId,
|
|
1119
|
+
});
|
|
1120
|
+
intent.meta.ipfs = { cid, metadata };
|
|
1121
|
+
return intent;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Port of `pop task folders set` — src/commands/task/folders.ts:286
|
|
1125
|
+
* (TaskManager v4+). Parses the new root, auto-fills the CAS expected root
|
|
1126
|
+
* from chain, and returns NULL when the root already matches (the CLI reports
|
|
1127
|
+
* success without sending). The stale-root retry-once loop is execution-time
|
|
1128
|
+
* behavior and stays host-side.
|
|
1129
|
+
*/
|
|
1130
|
+
async function setFoldersIntent(ctx, p) {
|
|
1131
|
+
const newRoot = parseFoldersRoot(p.newRoot);
|
|
1132
|
+
const { orgId, taskManagerAddress } = await resolveTaskManager(ctx, p.org);
|
|
1133
|
+
const features = await maybeFeatures(ctx, taskManagerAddress, orgId, p.features);
|
|
1134
|
+
if (features && !features.folders) {
|
|
1135
|
+
throw new errors_1.CliError((0, version_1.featureUnavailable)('task folders', 'TaskManager v4', 'Upgrade the org TaskManager beacon to use folder organization.'), exit_codes_1.EXIT.PRECONDITION);
|
|
1136
|
+
}
|
|
1137
|
+
let expectedRoot = p.expectedCurrentRoot;
|
|
1138
|
+
if (expectedRoot === undefined) {
|
|
1139
|
+
if (!ctx.provider) {
|
|
1140
|
+
throw new errors_1.CliError('setFolders needs the current root for its CAS guard. Provide ctx.provider (read over RPC) or params.expectedCurrentRoot.', exit_codes_1.EXIT.USAGE);
|
|
1141
|
+
}
|
|
1142
|
+
expectedRoot = await (0, task_lens_1.getFoldersRoot)(ctx.provider, taskManagerAddress);
|
|
1143
|
+
}
|
|
1144
|
+
if (expectedRoot.toLowerCase() === newRoot.toLowerCase()) {
|
|
1145
|
+
// Already up to date — the CLI reports success without sending.
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
return buildSetFolders({ taskManagerAddress, expectedCurrentRoot: expectedRoot, newRoot, orgId });
|
|
1149
|
+
}
|