molly-cli 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/dist/commands/address.d.ts +2 -0
- package/dist/commands/address.js +24 -0
- package/dist/commands/campaign.d.ts +2 -0
- package/dist/commands/campaign.js +253 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +36 -0
- package/dist/commands/factory.d.ts +2 -0
- package/dist/commands/factory.js +273 -0
- package/dist/commands/identity.d.ts +2 -0
- package/dist/commands/identity.js +114 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +24 -0
- package/dist/lib/client.d.ts +18 -0
- package/dist/lib/client.js +60 -0
- package/dist/lib/config-manager.d.ts +12 -0
- package/dist/lib/config-manager.js +55 -0
- package/dist/lib/contracts.d.ts +6 -0
- package/dist/lib/contracts.js +6 -0
- package/dist/lib/errors.d.ts +13 -0
- package/dist/lib/errors.js +13 -0
- package/dist/lib/output.d.ts +6 -0
- package/dist/lib/output.js +69 -0
- package/dist/lib/tx.d.ts +16 -0
- package/dist/lib/tx.js +29 -0
- package/dist/lib/types.d.ts +15 -0
- package/dist/lib/types.js +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createAccount } from "genlayer-js";
|
|
2
|
+
import { resolveConfig } from "../lib/config-manager.js";
|
|
3
|
+
import { output, outputError } from "../lib/output.js";
|
|
4
|
+
import { EXIT_AUTH_ERROR } from "../lib/errors.js";
|
|
5
|
+
export function registerAddress(program) {
|
|
6
|
+
program
|
|
7
|
+
.command("address")
|
|
8
|
+
.description("Print wallet address")
|
|
9
|
+
.action((_opts, cmd) => {
|
|
10
|
+
const globals = cmd.optsWithGlobals();
|
|
11
|
+
const pretty = globals.pretty;
|
|
12
|
+
try {
|
|
13
|
+
const config = resolveConfig({ privateKey: globals.privateKey });
|
|
14
|
+
if (!config.privateKey) {
|
|
15
|
+
outputError("No wallet configured. Run 'molly init' first or set PRIVATE_KEY env var.", EXIT_AUTH_ERROR, pretty);
|
|
16
|
+
}
|
|
17
|
+
const account = createAccount(config.privateKey);
|
|
18
|
+
output({ address: account.address }, pretty);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
outputError(err, EXIT_AUTH_ERROR, pretty);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { resolveConfig } from "../lib/config-manager.js";
|
|
2
|
+
import { buildClient, buildReadClient } from "../lib/client.js";
|
|
3
|
+
import { writeAndWait, readContract } from "../lib/tx.js";
|
|
4
|
+
import { output, outputError } from "../lib/output.js";
|
|
5
|
+
import { EXIT_TX_ERROR, EXIT_NETWORK_ERROR, EXIT_INPUT_ERROR } from "../lib/errors.js";
|
|
6
|
+
function getAddress(cmd) {
|
|
7
|
+
const globals = cmd.optsWithGlobals();
|
|
8
|
+
const address = globals.address;
|
|
9
|
+
if (!address) {
|
|
10
|
+
outputError("Campaign address is required. Use: molly campaign --address <addr> <command>", EXIT_INPUT_ERROR, globals.pretty);
|
|
11
|
+
}
|
|
12
|
+
return address;
|
|
13
|
+
}
|
|
14
|
+
export function registerCampaign(program) {
|
|
15
|
+
const campaign = program
|
|
16
|
+
.command("campaign")
|
|
17
|
+
.description("Interact with a deployed campaign")
|
|
18
|
+
.requiredOption("--address <addr>", "Campaign contract address");
|
|
19
|
+
// ── Read commands ──────────────────────────────────────────────────
|
|
20
|
+
campaign
|
|
21
|
+
.command("metadata")
|
|
22
|
+
.description("Get campaign metadata")
|
|
23
|
+
.action(async (_opts, cmd) => {
|
|
24
|
+
const globals = cmd.optsWithGlobals();
|
|
25
|
+
const pretty = globals.pretty;
|
|
26
|
+
const address = getAddress(cmd);
|
|
27
|
+
try {
|
|
28
|
+
const config = resolveConfig({ network: globals.network });
|
|
29
|
+
const { client } = buildReadClient(config);
|
|
30
|
+
const data = await readContract(client, address, "get_campaign_metadata", []);
|
|
31
|
+
output({ ok: true, data }, pretty);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
outputError(err, EXIT_NETWORK_ERROR, pretty);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
campaign
|
|
38
|
+
.command("info")
|
|
39
|
+
.description("Get full campaign info including submissions")
|
|
40
|
+
.action(async (_opts, cmd) => {
|
|
41
|
+
const globals = cmd.optsWithGlobals();
|
|
42
|
+
const pretty = globals.pretty;
|
|
43
|
+
const address = getAddress(cmd);
|
|
44
|
+
try {
|
|
45
|
+
const config = resolveConfig({ network: globals.network });
|
|
46
|
+
const { client } = buildReadClient(config);
|
|
47
|
+
const data = await readContract(client, address, "get_campaign_info", []);
|
|
48
|
+
output({ ok: true, data }, pretty);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
outputError(err, EXIT_NETWORK_ERROR, pretty);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
campaign
|
|
55
|
+
.command("scoreboard")
|
|
56
|
+
.description("Get scores per period")
|
|
57
|
+
.action(async (_opts, cmd) => {
|
|
58
|
+
const globals = cmd.optsWithGlobals();
|
|
59
|
+
const pretty = globals.pretty;
|
|
60
|
+
const address = getAddress(cmd);
|
|
61
|
+
try {
|
|
62
|
+
const config = resolveConfig({ network: globals.network });
|
|
63
|
+
const { client } = buildReadClient(config);
|
|
64
|
+
const data = await readContract(client, address, "get_scoreboard", []);
|
|
65
|
+
output({ ok: true, data }, pretty);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
outputError(err, EXIT_NETWORK_ERROR, pretty);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
campaign
|
|
72
|
+
.command("submissions <mission-id>")
|
|
73
|
+
.description("List submissions for a mission")
|
|
74
|
+
.action(async (missionId, _opts, cmd) => {
|
|
75
|
+
const globals = cmd.optsWithGlobals();
|
|
76
|
+
const pretty = globals.pretty;
|
|
77
|
+
const address = getAddress(cmd);
|
|
78
|
+
try {
|
|
79
|
+
const config = resolveConfig({ network: globals.network });
|
|
80
|
+
const { client } = buildReadClient(config);
|
|
81
|
+
const data = await readContract(client, address, "get_submissions", [missionId]);
|
|
82
|
+
output({ ok: true, data }, pretty);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
outputError(err, EXIT_NETWORK_ERROR, pretty);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
campaign
|
|
89
|
+
.command("distribution <period>")
|
|
90
|
+
.description("Get period distribution")
|
|
91
|
+
.action(async (period, _opts, cmd) => {
|
|
92
|
+
const globals = cmd.optsWithGlobals();
|
|
93
|
+
const pretty = globals.pretty;
|
|
94
|
+
const address = getAddress(cmd);
|
|
95
|
+
try {
|
|
96
|
+
const config = resolveConfig({ network: globals.network });
|
|
97
|
+
const { client } = buildReadClient(config);
|
|
98
|
+
const data = await readContract(client, address, "get_period_distribution", [parseInt(period, 10)]);
|
|
99
|
+
output({ ok: true, data }, pretty);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
outputError(err, EXIT_NETWORK_ERROR, pretty);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// ── Write commands ─────────────────────────────────────────────────
|
|
106
|
+
campaign
|
|
107
|
+
.command("submit <mission-id> <post-url>")
|
|
108
|
+
.description("Submit a post to a campaign mission")
|
|
109
|
+
.option("--referrer <code>", "Referrer code", "")
|
|
110
|
+
.action(async (missionId, postUrl, opts, cmd) => {
|
|
111
|
+
const globals = cmd.optsWithGlobals();
|
|
112
|
+
const pretty = globals.pretty;
|
|
113
|
+
const address = getAddress(cmd);
|
|
114
|
+
try {
|
|
115
|
+
const config = resolveConfig({ privateKey: globals.privateKey, network: globals.network });
|
|
116
|
+
const { client } = buildClient(config);
|
|
117
|
+
const { hash } = await writeAndWait(client, address, "submit_post", [missionId, postUrl, opts.referrer || ""], { timeout: globals.timeout });
|
|
118
|
+
output({ ok: true, hash, missionId, postUrl }, pretty);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
outputError(err, EXIT_TX_ERROR, pretty);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
campaign
|
|
125
|
+
.command("resubmit <mission-id> <post-url>")
|
|
126
|
+
.description("Resubmit a post for re-evaluation")
|
|
127
|
+
.action(async (missionId, postUrl, _opts, cmd) => {
|
|
128
|
+
const globals = cmd.optsWithGlobals();
|
|
129
|
+
const pretty = globals.pretty;
|
|
130
|
+
const address = getAddress(cmd);
|
|
131
|
+
try {
|
|
132
|
+
const config = resolveConfig({ privateKey: globals.privateKey, network: globals.network });
|
|
133
|
+
const { client } = buildClient(config);
|
|
134
|
+
const { hash } = await writeAndWait(client, address, "resubmit_post", [missionId, postUrl], { timeout: globals.timeout });
|
|
135
|
+
output({ ok: true, hash, missionId, postUrl }, pretty);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
outputError(err, EXIT_TX_ERROR, pretty);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
campaign
|
|
142
|
+
.command("retry <mission-id> <post-url>")
|
|
143
|
+
.description("Retry a failed submission")
|
|
144
|
+
.action(async (missionId, postUrl, _opts, cmd) => {
|
|
145
|
+
const globals = cmd.optsWithGlobals();
|
|
146
|
+
const pretty = globals.pretty;
|
|
147
|
+
const address = getAddress(cmd);
|
|
148
|
+
try {
|
|
149
|
+
const config = resolveConfig({ privateKey: globals.privateKey, network: globals.network });
|
|
150
|
+
const { client } = buildClient(config);
|
|
151
|
+
const { hash } = await writeAndWait(client, address, "retry_submission", [missionId, postUrl], { timeout: globals.timeout });
|
|
152
|
+
output({ ok: true, hash, missionId, postUrl }, pretty);
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
outputError(err, EXIT_TX_ERROR, pretty);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
campaign
|
|
159
|
+
.command("bridge-distribution <period>")
|
|
160
|
+
.description("Bridge a period distribution to EVM")
|
|
161
|
+
.action(async (period, _opts, cmd) => {
|
|
162
|
+
const globals = cmd.optsWithGlobals();
|
|
163
|
+
const pretty = globals.pretty;
|
|
164
|
+
const address = getAddress(cmd);
|
|
165
|
+
try {
|
|
166
|
+
const config = resolveConfig({ privateKey: globals.privateKey, network: globals.network });
|
|
167
|
+
const { client } = buildClient(config);
|
|
168
|
+
const { hash } = await writeAndWait(client, address, "bridge_period_distribution", [parseInt(period, 10)], { timeout: globals.timeout });
|
|
169
|
+
output({ ok: true, hash, period: parseInt(period, 10) }, pretty);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
outputError(err, EXIT_TX_ERROR, pretty);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
campaign
|
|
176
|
+
.command("challenge <post-id>")
|
|
177
|
+
.description("Challenge a deleted post")
|
|
178
|
+
.action(async (postId, _opts, cmd) => {
|
|
179
|
+
const globals = cmd.optsWithGlobals();
|
|
180
|
+
const pretty = globals.pretty;
|
|
181
|
+
const address = getAddress(cmd);
|
|
182
|
+
try {
|
|
183
|
+
const config = resolveConfig({ privateKey: globals.privateKey, network: globals.network });
|
|
184
|
+
const { client } = buildClient(config);
|
|
185
|
+
const { hash } = await writeAndWait(client, address, "challenge_deleted_post", [postId], { timeout: globals.timeout });
|
|
186
|
+
output({ ok: true, hash, postId }, pretty);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
outputError(err, EXIT_TX_ERROR, pretty);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
campaign
|
|
193
|
+
.command("disqualify <post-id>")
|
|
194
|
+
.description("Disqualify a submission (creator only)")
|
|
195
|
+
.action(async (postId, _opts, cmd) => {
|
|
196
|
+
const globals = cmd.optsWithGlobals();
|
|
197
|
+
const pretty = globals.pretty;
|
|
198
|
+
const address = getAddress(cmd);
|
|
199
|
+
try {
|
|
200
|
+
const config = resolveConfig({ privateKey: globals.privateKey, network: globals.network });
|
|
201
|
+
const { client } = buildClient(config);
|
|
202
|
+
const { hash } = await writeAndWait(client, address, "disqualify_submission", [postId], { timeout: globals.timeout });
|
|
203
|
+
output({ ok: true, hash, postId }, pretty);
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
outputError(err, EXIT_TX_ERROR, pretty);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
campaign
|
|
210
|
+
.command("update-targeting")
|
|
211
|
+
.description("Update campaign targeting (creator only)")
|
|
212
|
+
.option("--whitelisted <addrs...>", "Whitelisted submitter addresses")
|
|
213
|
+
.option("--verified-only <bool>", "Require verified users")
|
|
214
|
+
.option("--min-followers <n>", "Minimum followers", parseInt)
|
|
215
|
+
.option("--max-followers <n>", "Maximum followers", parseInt)
|
|
216
|
+
.action(async (opts, cmd) => {
|
|
217
|
+
const globals = cmd.optsWithGlobals();
|
|
218
|
+
const pretty = globals.pretty;
|
|
219
|
+
const address = getAddress(cmd);
|
|
220
|
+
try {
|
|
221
|
+
const config = resolveConfig({ privateKey: globals.privateKey, network: globals.network });
|
|
222
|
+
const { client } = buildClient(config);
|
|
223
|
+
const args = [
|
|
224
|
+
opts.whitelisted || null,
|
|
225
|
+
opts.verifiedOnly !== undefined ? opts.verifiedOnly === "true" : null,
|
|
226
|
+
opts.minFollowers ?? null,
|
|
227
|
+
opts.maxFollowers ?? null,
|
|
228
|
+
];
|
|
229
|
+
const { hash } = await writeAndWait(client, address, "update_campaign_targeting", args, { timeout: globals.timeout });
|
|
230
|
+
output({ ok: true, hash }, pretty);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
outputError(err, EXIT_TX_ERROR, pretty);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
campaign
|
|
237
|
+
.command("update-duration <periods>")
|
|
238
|
+
.description("Update campaign duration in periods (creator only)")
|
|
239
|
+
.action(async (periods, _opts, cmd) => {
|
|
240
|
+
const globals = cmd.optsWithGlobals();
|
|
241
|
+
const pretty = globals.pretty;
|
|
242
|
+
const address = getAddress(cmd);
|
|
243
|
+
try {
|
|
244
|
+
const config = resolveConfig({ privateKey: globals.privateKey, network: globals.network });
|
|
245
|
+
const { client } = buildClient(config);
|
|
246
|
+
const { hash } = await writeAndWait(client, address, "update_campaign_duration_periods", [parseInt(periods, 10)], { timeout: globals.timeout });
|
|
247
|
+
output({ ok: true, hash, periods: parseInt(periods, 10) }, pretty);
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
outputError(err, EXIT_TX_ERROR, pretty);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { updateConfig, getConfigPath, resolveConfig } from "../lib/config-manager.js";
|
|
2
|
+
import { output, outputError } from "../lib/output.js";
|
|
3
|
+
import { EXIT_INPUT_ERROR } from "../lib/errors.js";
|
|
4
|
+
export function registerConfig(program) {
|
|
5
|
+
const configCmd = program
|
|
6
|
+
.command("config")
|
|
7
|
+
.description("Show or update configuration");
|
|
8
|
+
configCmd
|
|
9
|
+
.command("show", { isDefault: true })
|
|
10
|
+
.description("Show current resolved configuration")
|
|
11
|
+
.action((_opts, cmd) => {
|
|
12
|
+
const pretty = cmd.optsWithGlobals().pretty;
|
|
13
|
+
const resolved = resolveConfig({});
|
|
14
|
+
// Mask private key for display
|
|
15
|
+
const display = {
|
|
16
|
+
...resolved,
|
|
17
|
+
privateKey: resolved.privateKey
|
|
18
|
+
? resolved.privateKey.slice(0, 6) + "..." + resolved.privateKey.slice(-4)
|
|
19
|
+
: "(not set)",
|
|
20
|
+
configPath: getConfigPath(),
|
|
21
|
+
};
|
|
22
|
+
output(display, pretty);
|
|
23
|
+
});
|
|
24
|
+
configCmd
|
|
25
|
+
.command("set <key> <value>")
|
|
26
|
+
.description("Update a config value (network, factoryAddress, identityAddress, privateKey)")
|
|
27
|
+
.action((key, value, _opts, cmd) => {
|
|
28
|
+
const pretty = cmd.optsWithGlobals().pretty;
|
|
29
|
+
const validKeys = ["network", "factoryAddress", "identityAddress", "privateKey"];
|
|
30
|
+
if (!validKeys.includes(key)) {
|
|
31
|
+
outputError(`Invalid config key: ${key}. Valid keys: ${validKeys.join(", ")}`, EXIT_INPUT_ERROR, pretty);
|
|
32
|
+
}
|
|
33
|
+
updateConfig(key, value);
|
|
34
|
+
output({ ok: true, key, value: key === "privateKey" ? "***" : value }, pretty);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { resolveConfig } from "../lib/config-manager.js";
|
|
2
|
+
import { buildClient, buildReadClient } from "../lib/client.js";
|
|
3
|
+
import { writeAndWait, readContract } from "../lib/tx.js";
|
|
4
|
+
import { output, outputError } from "../lib/output.js";
|
|
5
|
+
import { EXIT_TX_ERROR, EXIT_NETWORK_ERROR, EXIT_INPUT_ERROR } from "../lib/errors.js";
|
|
6
|
+
// Default campaign parameter values (from scripts/create-campaign.ts:50-76)
|
|
7
|
+
const CAMPAIGN_DEFAULTS = {
|
|
8
|
+
knowledge_base: "",
|
|
9
|
+
rules: "1. Posts must be original content\n2. No spam or duplicate submissions\n3. Content must be relevant to the campaign goal",
|
|
10
|
+
style: "",
|
|
11
|
+
campaign_duration_periods: 4,
|
|
12
|
+
period_length_days: 7,
|
|
13
|
+
missions: {
|
|
14
|
+
main: {
|
|
15
|
+
title: "Main Mission",
|
|
16
|
+
description: "Create engaging social media content that promotes the campaign goal",
|
|
17
|
+
rules: "Posts must be original and relevant to the campaign theme",
|
|
18
|
+
active: true,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
distribution_contract_chain_id: 0,
|
|
22
|
+
distribution_contract_address: "0x0000000000000000000000000000000000000000",
|
|
23
|
+
only_verified_users: false,
|
|
24
|
+
minimum_followers: 0,
|
|
25
|
+
maximum_followers: 0,
|
|
26
|
+
whitelisted_submitters: [],
|
|
27
|
+
alpha: 50,
|
|
28
|
+
beta: 50,
|
|
29
|
+
gate_weights: [1, 1, 1],
|
|
30
|
+
metric_weights: [1, 1, 1, 1],
|
|
31
|
+
allow_old_posts: true,
|
|
32
|
+
max_submissions_per_participant: 0,
|
|
33
|
+
target_chain_rpc_url: "",
|
|
34
|
+
disqualification_enabled: false,
|
|
35
|
+
num_shards: 3,
|
|
36
|
+
};
|
|
37
|
+
function generateId(title) {
|
|
38
|
+
const slug = title
|
|
39
|
+
.toLowerCase()
|
|
40
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
41
|
+
.replace(/(^-|-$)/g, "");
|
|
42
|
+
const timestamp = Date.now().toString(36);
|
|
43
|
+
return `${slug}-${timestamp}`;
|
|
44
|
+
}
|
|
45
|
+
function getStartTime() {
|
|
46
|
+
const now = new Date();
|
|
47
|
+
now.setHours(now.getHours() + 1);
|
|
48
|
+
now.setMinutes(0, 0, 0);
|
|
49
|
+
return now.toISOString();
|
|
50
|
+
}
|
|
51
|
+
export function registerFactory(program) {
|
|
52
|
+
const factory = program
|
|
53
|
+
.command("factory")
|
|
54
|
+
.description("Manage campaigns via CampaignFactory");
|
|
55
|
+
factory
|
|
56
|
+
.command("create")
|
|
57
|
+
.description("Create a new campaign")
|
|
58
|
+
.option("--params-json <json>", "Full campaign params as JSON")
|
|
59
|
+
.option("--id <id>", "Campaign ID")
|
|
60
|
+
.option("--title <title>", "Campaign title")
|
|
61
|
+
.option("--goal <goal>", "Campaign goal")
|
|
62
|
+
.option("--knowledge-base <text>", "Knowledge base")
|
|
63
|
+
.option("--rules <text>", "Campaign rules")
|
|
64
|
+
.option("--style <text>", "Campaign style")
|
|
65
|
+
.option("--start <iso>", "Start datetime ISO")
|
|
66
|
+
.option("--duration-periods <n>", "Number of periods", parseInt)
|
|
67
|
+
.option("--period-days <n>", "Days per period", parseInt)
|
|
68
|
+
.option("--missions-json <json>", "Missions as JSON object")
|
|
69
|
+
.option("--dist-chain-id <n>", "Distribution contract chain ID", parseInt)
|
|
70
|
+
.option("--dist-address <addr>", "Distribution contract address")
|
|
71
|
+
.option("--verified-only", "Require verified users")
|
|
72
|
+
.option("--min-followers <n>", "Minimum followers", parseInt)
|
|
73
|
+
.option("--max-followers <n>", "Maximum followers", parseInt)
|
|
74
|
+
.option("--whitelisted <addrs...>", "Whitelisted submitter addresses")
|
|
75
|
+
.option("--alpha <n>", "Alpha parameter", parseInt)
|
|
76
|
+
.option("--beta <n>", "Beta parameter", parseInt)
|
|
77
|
+
.option("--gate-weights <weights...>", "Gate weights (space-separated)")
|
|
78
|
+
.option("--metric-weights <weights...>", "Metric weights (space-separated)")
|
|
79
|
+
.option("--no-old-posts", "Disallow old posts")
|
|
80
|
+
.option("--max-submissions <n>", "Max submissions per participant", parseInt)
|
|
81
|
+
.option("--target-rpc <url>", "Target chain RPC URL")
|
|
82
|
+
.option("--enable-disqualification", "Enable disqualification")
|
|
83
|
+
.option("--num-shards <n>", "Number of shards", parseInt)
|
|
84
|
+
.action(async (opts, cmd) => {
|
|
85
|
+
const globals = cmd.optsWithGlobals();
|
|
86
|
+
const pretty = globals.pretty;
|
|
87
|
+
try {
|
|
88
|
+
const config = resolveConfig({ privateKey: globals.privateKey, network: globals.network });
|
|
89
|
+
let params;
|
|
90
|
+
if (opts.paramsJson) {
|
|
91
|
+
try {
|
|
92
|
+
params = JSON.parse(opts.paramsJson);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
outputError("Invalid --params-json", EXIT_INPUT_ERROR, pretty);
|
|
96
|
+
return; // unreachable but helps TS
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
if (!opts.title || !opts.goal) {
|
|
101
|
+
outputError("--title and --goal are required (or use --params-json)", EXIT_INPUT_ERROR, pretty);
|
|
102
|
+
}
|
|
103
|
+
params = {};
|
|
104
|
+
if (opts.title)
|
|
105
|
+
params.title = opts.title;
|
|
106
|
+
if (opts.goal)
|
|
107
|
+
params.goal = opts.goal;
|
|
108
|
+
if (opts.id)
|
|
109
|
+
params.id = opts.id;
|
|
110
|
+
if (opts.knowledgeBase)
|
|
111
|
+
params.knowledge_base = opts.knowledgeBase;
|
|
112
|
+
if (opts.rules)
|
|
113
|
+
params.rules = opts.rules;
|
|
114
|
+
if (opts.style)
|
|
115
|
+
params.style = opts.style;
|
|
116
|
+
if (opts.start)
|
|
117
|
+
params.start_datetime_iso = opts.start;
|
|
118
|
+
if (opts.durationPeriods)
|
|
119
|
+
params.campaign_duration_periods = opts.durationPeriods;
|
|
120
|
+
if (opts.periodDays)
|
|
121
|
+
params.period_length_days = opts.periodDays;
|
|
122
|
+
if (opts.missionsJson) {
|
|
123
|
+
try {
|
|
124
|
+
params.missions = JSON.parse(opts.missionsJson);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
outputError("Invalid --missions-json", EXIT_INPUT_ERROR, pretty);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (opts.distChainId)
|
|
131
|
+
params.distribution_contract_chain_id = opts.distChainId;
|
|
132
|
+
if (opts.distAddress)
|
|
133
|
+
params.distribution_contract_address = opts.distAddress;
|
|
134
|
+
if (opts.verifiedOnly)
|
|
135
|
+
params.only_verified_users = true;
|
|
136
|
+
if (opts.minFollowers)
|
|
137
|
+
params.minimum_followers = opts.minFollowers;
|
|
138
|
+
if (opts.maxFollowers)
|
|
139
|
+
params.maximum_followers = opts.maxFollowers;
|
|
140
|
+
if (opts.whitelisted)
|
|
141
|
+
params.whitelisted_submitters = opts.whitelisted;
|
|
142
|
+
if (opts.alpha)
|
|
143
|
+
params.alpha = opts.alpha;
|
|
144
|
+
if (opts.beta)
|
|
145
|
+
params.beta = opts.beta;
|
|
146
|
+
if (opts.gateWeights)
|
|
147
|
+
params.gate_weights = opts.gateWeights.map(Number);
|
|
148
|
+
if (opts.metricWeights)
|
|
149
|
+
params.metric_weights = opts.metricWeights.map(Number);
|
|
150
|
+
if (opts.oldPosts === false)
|
|
151
|
+
params.allow_old_posts = false;
|
|
152
|
+
if (opts.maxSubmissions)
|
|
153
|
+
params.max_submissions_per_participant = opts.maxSubmissions;
|
|
154
|
+
if (opts.targetRpc)
|
|
155
|
+
params.target_chain_rpc_url = opts.targetRpc;
|
|
156
|
+
if (opts.enableDisqualification)
|
|
157
|
+
params.disqualification_enabled = true;
|
|
158
|
+
if (opts.numShards)
|
|
159
|
+
params.num_shards = opts.numShards;
|
|
160
|
+
}
|
|
161
|
+
// Merge with defaults
|
|
162
|
+
const p = params;
|
|
163
|
+
const campaign = {
|
|
164
|
+
id: p.id || generateId(p.title),
|
|
165
|
+
title: p.title,
|
|
166
|
+
goal: p.goal,
|
|
167
|
+
knowledge_base: p.knowledge_base ?? CAMPAIGN_DEFAULTS.knowledge_base,
|
|
168
|
+
rules: p.rules ?? CAMPAIGN_DEFAULTS.rules,
|
|
169
|
+
style: p.style ?? CAMPAIGN_DEFAULTS.style,
|
|
170
|
+
start_datetime_iso: p.start_datetime_iso ?? getStartTime(),
|
|
171
|
+
campaign_duration_periods: p.campaign_duration_periods ?? CAMPAIGN_DEFAULTS.campaign_duration_periods,
|
|
172
|
+
period_length_days: p.period_length_days ?? CAMPAIGN_DEFAULTS.period_length_days,
|
|
173
|
+
missions: p.missions ?? CAMPAIGN_DEFAULTS.missions,
|
|
174
|
+
distribution_contract_chain_id: p.distribution_contract_chain_id ?? CAMPAIGN_DEFAULTS.distribution_contract_chain_id,
|
|
175
|
+
distribution_contract_address: p.distribution_contract_address ?? CAMPAIGN_DEFAULTS.distribution_contract_address,
|
|
176
|
+
only_verified_users: p.only_verified_users ?? CAMPAIGN_DEFAULTS.only_verified_users,
|
|
177
|
+
minimum_followers: p.minimum_followers ?? CAMPAIGN_DEFAULTS.minimum_followers,
|
|
178
|
+
maximum_followers: p.maximum_followers ?? CAMPAIGN_DEFAULTS.maximum_followers,
|
|
179
|
+
whitelisted_submitters: p.whitelisted_submitters ?? CAMPAIGN_DEFAULTS.whitelisted_submitters,
|
|
180
|
+
alpha: p.alpha ?? CAMPAIGN_DEFAULTS.alpha,
|
|
181
|
+
beta: p.beta ?? CAMPAIGN_DEFAULTS.beta,
|
|
182
|
+
gate_weights: p.gate_weights ?? CAMPAIGN_DEFAULTS.gate_weights,
|
|
183
|
+
metric_weights: p.metric_weights ?? CAMPAIGN_DEFAULTS.metric_weights,
|
|
184
|
+
allow_old_posts: p.allow_old_posts ?? CAMPAIGN_DEFAULTS.allow_old_posts,
|
|
185
|
+
max_submissions_per_participant: p.max_submissions_per_participant ?? CAMPAIGN_DEFAULTS.max_submissions_per_participant,
|
|
186
|
+
target_chain_rpc_url: p.target_chain_rpc_url ?? CAMPAIGN_DEFAULTS.target_chain_rpc_url,
|
|
187
|
+
disqualification_enabled: p.disqualification_enabled ?? CAMPAIGN_DEFAULTS.disqualification_enabled,
|
|
188
|
+
num_shards: p.num_shards ?? CAMPAIGN_DEFAULTS.num_shards,
|
|
189
|
+
};
|
|
190
|
+
const { client } = buildClient(config);
|
|
191
|
+
// create_campaign args match CampaignFactory.__init__.py:73-99
|
|
192
|
+
const { hash } = await writeAndWait(client, config.factoryAddress, "create_campaign", [
|
|
193
|
+
campaign.id,
|
|
194
|
+
campaign.title,
|
|
195
|
+
campaign.goal,
|
|
196
|
+
campaign.knowledge_base,
|
|
197
|
+
campaign.rules,
|
|
198
|
+
campaign.style,
|
|
199
|
+
campaign.start_datetime_iso,
|
|
200
|
+
campaign.campaign_duration_periods,
|
|
201
|
+
campaign.period_length_days,
|
|
202
|
+
campaign.missions,
|
|
203
|
+
campaign.distribution_contract_chain_id,
|
|
204
|
+
campaign.distribution_contract_address,
|
|
205
|
+
campaign.only_verified_users,
|
|
206
|
+
campaign.minimum_followers,
|
|
207
|
+
campaign.maximum_followers,
|
|
208
|
+
campaign.whitelisted_submitters,
|
|
209
|
+
campaign.alpha,
|
|
210
|
+
campaign.beta,
|
|
211
|
+
campaign.gate_weights,
|
|
212
|
+
campaign.metric_weights,
|
|
213
|
+
campaign.allow_old_posts,
|
|
214
|
+
campaign.max_submissions_per_participant,
|
|
215
|
+
campaign.target_chain_rpc_url,
|
|
216
|
+
campaign.disqualification_enabled,
|
|
217
|
+
campaign.num_shards,
|
|
218
|
+
], { timeout: globals.timeout });
|
|
219
|
+
output({ ok: true, hash, campaignId: campaign.id }, pretty);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
outputError(err, EXIT_TX_ERROR, pretty);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
factory
|
|
226
|
+
.command("get <campaign-id>")
|
|
227
|
+
.description("Get campaign address by ID")
|
|
228
|
+
.action(async (campaignId, _opts, cmd) => {
|
|
229
|
+
const globals = cmd.optsWithGlobals();
|
|
230
|
+
const pretty = globals.pretty;
|
|
231
|
+
try {
|
|
232
|
+
const config = resolveConfig({ network: globals.network });
|
|
233
|
+
const { client } = buildReadClient(config);
|
|
234
|
+
const address = await readContract(client, config.factoryAddress, "get_contract_by_id", [campaignId]);
|
|
235
|
+
output({ ok: true, campaignId, address }, pretty);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
outputError(err, EXIT_NETWORK_ERROR, pretty);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
factory
|
|
242
|
+
.command("list-all")
|
|
243
|
+
.description("List all campaigns (IDs and addresses)")
|
|
244
|
+
.action(async (_opts, cmd) => {
|
|
245
|
+
const globals = cmd.optsWithGlobals();
|
|
246
|
+
const pretty = globals.pretty;
|
|
247
|
+
try {
|
|
248
|
+
const config = resolveConfig({ network: globals.network });
|
|
249
|
+
const { client } = buildReadClient(config);
|
|
250
|
+
const campaigns = await readContract(client, config.factoryAddress, "get_all_campaigns", []);
|
|
251
|
+
output({ ok: true, campaigns }, pretty);
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
outputError(err, EXIT_NETWORK_ERROR, pretty);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
factory
|
|
258
|
+
.command("list <ids...>")
|
|
259
|
+
.description("Get multiple campaign addresses by IDs")
|
|
260
|
+
.action(async (ids, _opts, cmd) => {
|
|
261
|
+
const globals = cmd.optsWithGlobals();
|
|
262
|
+
const pretty = globals.pretty;
|
|
263
|
+
try {
|
|
264
|
+
const config = resolveConfig({ network: globals.network });
|
|
265
|
+
const { client } = buildReadClient(config);
|
|
266
|
+
const addresses = await readContract(client, config.factoryAddress, "get_contracts_by_ids", [ids]);
|
|
267
|
+
output({ ok: true, ids, addresses }, pretty);
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
outputError(err, EXIT_NETWORK_ERROR, pretty);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { resolveConfig } from "../lib/config-manager.js";
|
|
2
|
+
import { buildClient, buildReadClient } from "../lib/client.js";
|
|
3
|
+
import { writeAndWait, readContract } from "../lib/tx.js";
|
|
4
|
+
import { output, outputError } from "../lib/output.js";
|
|
5
|
+
import { EXIT_TX_ERROR, EXIT_NETWORK_ERROR } from "../lib/errors.js";
|
|
6
|
+
export function registerIdentity(program) {
|
|
7
|
+
const identity = program
|
|
8
|
+
.command("identity")
|
|
9
|
+
.description("Manage MoltBook identity");
|
|
10
|
+
identity
|
|
11
|
+
.command("link-start <username>")
|
|
12
|
+
.description("Initiate address link — returns a verification token")
|
|
13
|
+
.action(async (username, _opts, cmd) => {
|
|
14
|
+
const globals = cmd.optsWithGlobals();
|
|
15
|
+
const pretty = globals.pretty;
|
|
16
|
+
try {
|
|
17
|
+
const config = resolveConfig({ privateKey: globals.privateKey, network: globals.network });
|
|
18
|
+
const { client, account } = buildClient(config);
|
|
19
|
+
const { hash } = await writeAndWait(client, config.identityAddress, "initiate_address_link", [username], { timeout: globals.timeout });
|
|
20
|
+
// Read the token back
|
|
21
|
+
const tokenInfo = await readContract(client, config.identityAddress, "get_token", [account.address]);
|
|
22
|
+
output({ ok: true, hash, address: account.address, token: tokenInfo }, pretty);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
outputError(err, EXIT_TX_ERROR, pretty);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
identity
|
|
29
|
+
.command("link-complete <username>")
|
|
30
|
+
.description("Complete address link (token must be in your MoltBook profile)")
|
|
31
|
+
.action(async (username, _opts, cmd) => {
|
|
32
|
+
const globals = cmd.optsWithGlobals();
|
|
33
|
+
const pretty = globals.pretty;
|
|
34
|
+
try {
|
|
35
|
+
const config = resolveConfig({ privateKey: globals.privateKey, network: globals.network });
|
|
36
|
+
const { client } = buildClient(config);
|
|
37
|
+
const { hash } = await writeAndWait(client, config.identityAddress, "complete_address_link", [username], { timeout: globals.timeout });
|
|
38
|
+
output({ ok: true, hash }, pretty);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
outputError(err, EXIT_TX_ERROR, pretty);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
identity
|
|
45
|
+
.command("get-wallet <username>")
|
|
46
|
+
.description("Look up wallet address by MoltBook username")
|
|
47
|
+
.action(async (username, _opts, cmd) => {
|
|
48
|
+
const globals = cmd.optsWithGlobals();
|
|
49
|
+
const pretty = globals.pretty;
|
|
50
|
+
try {
|
|
51
|
+
const config = resolveConfig({ network: globals.network });
|
|
52
|
+
const { client } = buildReadClient(config);
|
|
53
|
+
const wallet = await readContract(client, config.identityAddress, "get_wallet", [username]);
|
|
54
|
+
output({ ok: true, username, wallet }, pretty);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
outputError(err, EXIT_NETWORK_ERROR, pretty);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
identity
|
|
61
|
+
.command("get-username <address>")
|
|
62
|
+
.description("Look up MoltBook username by wallet address")
|
|
63
|
+
.action(async (address, _opts, cmd) => {
|
|
64
|
+
const globals = cmd.optsWithGlobals();
|
|
65
|
+
const pretty = globals.pretty;
|
|
66
|
+
try {
|
|
67
|
+
const config = resolveConfig({ network: globals.network });
|
|
68
|
+
const { client } = buildReadClient(config);
|
|
69
|
+
const username = await readContract(client, config.identityAddress, "get_username", [address]);
|
|
70
|
+
output({ ok: true, address, username }, pretty);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
outputError(err, EXIT_NETWORK_ERROR, pretty);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
identity
|
|
77
|
+
.command("get-token [address]")
|
|
78
|
+
.description("Check pending verification token for an address")
|
|
79
|
+
.action(async (address, _opts, cmd) => {
|
|
80
|
+
const globals = cmd.optsWithGlobals();
|
|
81
|
+
const pretty = globals.pretty;
|
|
82
|
+
try {
|
|
83
|
+
const config = resolveConfig({ privateKey: globals.privateKey, network: globals.network });
|
|
84
|
+
let targetAddress = address;
|
|
85
|
+
if (!targetAddress) {
|
|
86
|
+
// Need a wallet to derive own address
|
|
87
|
+
const { account } = buildClient(config);
|
|
88
|
+
targetAddress = account.address;
|
|
89
|
+
}
|
|
90
|
+
const { client } = buildReadClient(config);
|
|
91
|
+
const token = await readContract(client, config.identityAddress, "get_token", [targetAddress]);
|
|
92
|
+
output({ ok: true, address: targetAddress, token }, pretty);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
outputError(err, EXIT_NETWORK_ERROR, pretty);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
identity
|
|
99
|
+
.command("bridge <username> <chain-id>")
|
|
100
|
+
.description("Bridge identity to an EVM chain")
|
|
101
|
+
.action(async (username, chainId, _opts, cmd) => {
|
|
102
|
+
const globals = cmd.optsWithGlobals();
|
|
103
|
+
const pretty = globals.pretty;
|
|
104
|
+
try {
|
|
105
|
+
const config = resolveConfig({ privateKey: globals.privateKey, network: globals.network });
|
|
106
|
+
const { client } = buildClient(config);
|
|
107
|
+
const { hash } = await writeAndWait(client, config.identityAddress, "bridge_user_to_chain", [username, parseInt(chainId, 10)], { timeout: globals.timeout });
|
|
108
|
+
output({ ok: true, hash, username, chainId: parseInt(chainId, 10) }, pretty);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
outputError(err, EXIT_TX_ERROR, pretty);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createAccount } from "genlayer-js";
|
|
2
|
+
import { randomBytes } from "crypto";
|
|
3
|
+
import { readConfig, writeConfig, getConfigPath } from "../lib/config-manager.js";
|
|
4
|
+
import { output, outputError } from "../lib/output.js";
|
|
5
|
+
import { EXIT_AUTH_ERROR } from "../lib/errors.js";
|
|
6
|
+
export function registerInit(program) {
|
|
7
|
+
program
|
|
8
|
+
.command("init")
|
|
9
|
+
.description("Generate a new wallet and save config")
|
|
10
|
+
.action(async (_opts, cmd) => {
|
|
11
|
+
const pretty = cmd.optsWithGlobals().pretty;
|
|
12
|
+
try {
|
|
13
|
+
const existing = readConfig();
|
|
14
|
+
if (existing.privateKey) {
|
|
15
|
+
const account = createAccount(existing.privateKey);
|
|
16
|
+
output({
|
|
17
|
+
ok: true,
|
|
18
|
+
message: "Wallet already exists",
|
|
19
|
+
address: account.address,
|
|
20
|
+
configPath: getConfigPath(),
|
|
21
|
+
}, pretty);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const privateKey = `0x${randomBytes(32).toString("hex")}`;
|
|
25
|
+
const account = createAccount(privateKey);
|
|
26
|
+
writeConfig({ ...existing, privateKey });
|
|
27
|
+
output({
|
|
28
|
+
ok: true,
|
|
29
|
+
address: account.address,
|
|
30
|
+
configPath: getConfigPath(),
|
|
31
|
+
}, pretty);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
outputError(err, EXIT_AUTH_ERROR, pretty);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { registerInit } from "./commands/init.js";
|
|
4
|
+
import { registerConfig } from "./commands/config.js";
|
|
5
|
+
import { registerAddress } from "./commands/address.js";
|
|
6
|
+
import { registerIdentity } from "./commands/identity.js";
|
|
7
|
+
import { registerFactory } from "./commands/factory.js";
|
|
8
|
+
import { registerCampaign } from "./commands/campaign.js";
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name("molly")
|
|
12
|
+
.description("CLI for AI bots to interact with GenLayer via Moltrally")
|
|
13
|
+
.version("0.1.0")
|
|
14
|
+
.option("--network <url>", "GenLayer RPC URL")
|
|
15
|
+
.option("--private-key <key>", "Private key (overrides config)")
|
|
16
|
+
.option("--pretty", "Pretty-print JSON output")
|
|
17
|
+
.option("--timeout <ms>", "Transaction timeout in milliseconds", parseInt);
|
|
18
|
+
registerInit(program);
|
|
19
|
+
registerConfig(program);
|
|
20
|
+
registerAddress(program);
|
|
21
|
+
registerIdentity(program);
|
|
22
|
+
registerFactory(program);
|
|
23
|
+
registerCampaign(program);
|
|
24
|
+
program.parseAsync(process.argv);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createAccount } from "genlayer-js";
|
|
2
|
+
import type { GenLayerClient, GenLayerChain } from "genlayer-js/types";
|
|
3
|
+
import type { ResolvedConfig } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Build a read-only client (no account needed).
|
|
6
|
+
* Use this for readContract calls that don't require a wallet.
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildReadClient(config: ResolvedConfig): {
|
|
9
|
+
client: GenLayerClient<GenLayerChain>;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Build a full client with account for write operations.
|
|
13
|
+
* Throws a clear error if no private key is configured.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildClient(config: ResolvedConfig): {
|
|
16
|
+
client: GenLayerClient<GenLayerChain>;
|
|
17
|
+
account: ReturnType<typeof createAccount>;
|
|
18
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createAccount, createClient } from "genlayer-js";
|
|
2
|
+
import { localnet, studionet } from "genlayer-js/chains";
|
|
3
|
+
/**
|
|
4
|
+
* Select the base chain config based on the network URL.
|
|
5
|
+
* localnet (127.0.0.1 / localhost) uses chain ID 61127.
|
|
6
|
+
* studionet (studio.genlayer.com) uses chain ID 61999.
|
|
7
|
+
*/
|
|
8
|
+
function resolveBaseChain(networkUrl) {
|
|
9
|
+
try {
|
|
10
|
+
const url = new URL(networkUrl);
|
|
11
|
+
if (url.hostname === "127.0.0.1" || url.hostname === "localhost") {
|
|
12
|
+
return localnet;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// fall through to default
|
|
17
|
+
}
|
|
18
|
+
return studionet;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build a read-only client (no account needed).
|
|
22
|
+
* Use this for readContract calls that don't require a wallet.
|
|
23
|
+
*/
|
|
24
|
+
export function buildReadClient(config) {
|
|
25
|
+
const baseChain = resolveBaseChain(config.network);
|
|
26
|
+
const chain = {
|
|
27
|
+
...baseChain,
|
|
28
|
+
rpcUrls: {
|
|
29
|
+
default: { http: [config.network] },
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
const client = createClient({
|
|
33
|
+
chain,
|
|
34
|
+
endpoint: config.network,
|
|
35
|
+
});
|
|
36
|
+
return { client };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build a full client with account for write operations.
|
|
40
|
+
* Throws a clear error if no private key is configured.
|
|
41
|
+
*/
|
|
42
|
+
export function buildClient(config) {
|
|
43
|
+
if (!config.privateKey) {
|
|
44
|
+
throw new Error("No wallet configured. Run 'molly init' or set PRIVATE_KEY env var.");
|
|
45
|
+
}
|
|
46
|
+
const account = createAccount(config.privateKey);
|
|
47
|
+
const baseChain = resolveBaseChain(config.network);
|
|
48
|
+
const chain = {
|
|
49
|
+
...baseChain,
|
|
50
|
+
rpcUrls: {
|
|
51
|
+
default: { http: [config.network] },
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const client = createClient({
|
|
55
|
+
chain,
|
|
56
|
+
account,
|
|
57
|
+
endpoint: config.network,
|
|
58
|
+
});
|
|
59
|
+
return { client, account };
|
|
60
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MollyConfig, ResolvedConfig } from "./types.js";
|
|
2
|
+
export declare function getConfigPath(): string;
|
|
3
|
+
export declare function readConfig(): MollyConfig;
|
|
4
|
+
export declare function writeConfig(config: MollyConfig): void;
|
|
5
|
+
export declare function updateConfig(key: string, value: string): void;
|
|
6
|
+
/**
|
|
7
|
+
* Resolution order: CLI flag > env var > config file > default
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveConfig(overrides: {
|
|
10
|
+
privateKey?: string;
|
|
11
|
+
network?: string;
|
|
12
|
+
}): ResolvedConfig;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { studionet } from "genlayer-js/chains";
|
|
5
|
+
import { DEFAULT_FACTORY_ADDRESS, DEFAULT_IDENTITY_ADDRESS, } from "./contracts.js";
|
|
6
|
+
const CONFIG_DIR = path.join(os.homedir(), ".molly");
|
|
7
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
8
|
+
/** Default network URL from the studionet chain config */
|
|
9
|
+
const DEFAULT_NETWORK = studionet.rpcUrls.default.http[0];
|
|
10
|
+
export function getConfigPath() {
|
|
11
|
+
return CONFIG_PATH;
|
|
12
|
+
}
|
|
13
|
+
export function readConfig() {
|
|
14
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function writeConfig(config) {
|
|
25
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
26
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
29
|
+
}
|
|
30
|
+
export function updateConfig(key, value) {
|
|
31
|
+
const config = readConfig();
|
|
32
|
+
config[key] = value;
|
|
33
|
+
writeConfig(config);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolution order: CLI flag > env var > config file > default
|
|
37
|
+
*/
|
|
38
|
+
export function resolveConfig(overrides) {
|
|
39
|
+
const file = readConfig();
|
|
40
|
+
const privateKey = overrides.privateKey ||
|
|
41
|
+
process.env.PRIVATE_KEY ||
|
|
42
|
+
file.privateKey ||
|
|
43
|
+
"";
|
|
44
|
+
const network = overrides.network ||
|
|
45
|
+
process.env.MOLLY_NETWORK ||
|
|
46
|
+
file.network ||
|
|
47
|
+
DEFAULT_NETWORK;
|
|
48
|
+
const factoryAddress = process.env.MOLLY_FACTORY_ADDRESS ||
|
|
49
|
+
file.factoryAddress ||
|
|
50
|
+
DEFAULT_FACTORY_ADDRESS;
|
|
51
|
+
const identityAddress = process.env.MOLLY_IDENTITY_ADDRESS ||
|
|
52
|
+
file.identityAddress ||
|
|
53
|
+
DEFAULT_IDENTITY_ADDRESS;
|
|
54
|
+
return { privateKey, network, factoryAddress, identityAddress };
|
|
55
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default contract addresses for studionet (studio.genlayer.com).
|
|
3
|
+
* Override via ~/.molly/config.json or CLI flags.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_FACTORY_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
6
|
+
export declare const DEFAULT_IDENTITY_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default contract addresses for studionet (studio.genlayer.com).
|
|
3
|
+
* Override via ~/.molly/config.json or CLI flags.
|
|
4
|
+
*/
|
|
5
|
+
export const DEFAULT_FACTORY_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
6
|
+
export const DEFAULT_IDENTITY_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exit codes:
|
|
3
|
+
* 0 = success
|
|
4
|
+
* 1 = transaction / contract error
|
|
5
|
+
* 2 = auth / wallet error
|
|
6
|
+
* 3 = network error
|
|
7
|
+
* 4 = invalid input
|
|
8
|
+
*/
|
|
9
|
+
export declare const EXIT_SUCCESS = 0;
|
|
10
|
+
export declare const EXIT_TX_ERROR = 1;
|
|
11
|
+
export declare const EXIT_AUTH_ERROR = 2;
|
|
12
|
+
export declare const EXIT_NETWORK_ERROR = 3;
|
|
13
|
+
export declare const EXIT_INPUT_ERROR = 4;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exit codes:
|
|
3
|
+
* 0 = success
|
|
4
|
+
* 1 = transaction / contract error
|
|
5
|
+
* 2 = auth / wallet error
|
|
6
|
+
* 3 = network error
|
|
7
|
+
* 4 = invalid input
|
|
8
|
+
*/
|
|
9
|
+
export const EXIT_SUCCESS = 0;
|
|
10
|
+
export const EXIT_TX_ERROR = 1;
|
|
11
|
+
export const EXIT_AUTH_ERROR = 2;
|
|
12
|
+
export const EXIT_NETWORK_ERROR = 3;
|
|
13
|
+
export const EXIT_INPUT_ERROR = 4;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured output for bot consumption.
|
|
3
|
+
* All data goes to stdout as JSON. Errors go to stderr as JSON.
|
|
4
|
+
*/
|
|
5
|
+
export declare function output(data: unknown, pretty?: boolean): void;
|
|
6
|
+
export declare function outputError(error: unknown, exitCode: number, pretty?: boolean): never;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured output for bot consumption.
|
|
3
|
+
* All data goes to stdout as JSON. Errors go to stderr as JSON.
|
|
4
|
+
*/
|
|
5
|
+
export function output(data, pretty) {
|
|
6
|
+
if (pretty) {
|
|
7
|
+
console.log(JSON.stringify(data, replacer, 2));
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
console.log(JSON.stringify(data, replacer));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function outputError(error, exitCode, pretty) {
|
|
14
|
+
const payload = { ok: false };
|
|
15
|
+
if (error instanceof Error) {
|
|
16
|
+
payload.error = error.message;
|
|
17
|
+
}
|
|
18
|
+
else if (typeof error === "string") {
|
|
19
|
+
payload.error = error;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
payload.error = String(error);
|
|
23
|
+
}
|
|
24
|
+
if (pretty) {
|
|
25
|
+
process.stderr.write(JSON.stringify(payload, null, 2) + "\n");
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
process.stderr.write(JSON.stringify(payload) + "\n");
|
|
29
|
+
}
|
|
30
|
+
process.exit(exitCode);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Detect CalldataAddress byte objects: { bytes: { 0: n, 1: n, ... } }
|
|
34
|
+
* and convert to hex string "0x..."
|
|
35
|
+
*/
|
|
36
|
+
function isCalldataAddress(value) {
|
|
37
|
+
if (typeof value !== "object" || value === null)
|
|
38
|
+
return false;
|
|
39
|
+
const obj = value;
|
|
40
|
+
if (!("bytes" in obj) || typeof obj.bytes !== "object" || obj.bytes === null)
|
|
41
|
+
return false;
|
|
42
|
+
const bytes = obj.bytes;
|
|
43
|
+
return "0" in bytes && typeof bytes["0"] === "number";
|
|
44
|
+
}
|
|
45
|
+
function bytesObjectToHex(bytes) {
|
|
46
|
+
const keys = Object.keys(bytes).map(Number).sort((a, b) => a - b);
|
|
47
|
+
const hex = keys.map((k) => bytes[String(k)].toString(16).padStart(2, "0")).join("");
|
|
48
|
+
return "0x" + hex;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* JSON replacer that converts Map instances to plain objects,
|
|
52
|
+
* BigInt to string, and CalldataAddress byte objects to hex strings.
|
|
53
|
+
*/
|
|
54
|
+
function replacer(_key, value) {
|
|
55
|
+
if (value instanceof Map) {
|
|
56
|
+
const obj = {};
|
|
57
|
+
for (const [k, v] of value) {
|
|
58
|
+
obj[String(k)] = v;
|
|
59
|
+
}
|
|
60
|
+
return obj;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === "bigint") {
|
|
63
|
+
return value.toString();
|
|
64
|
+
}
|
|
65
|
+
if (isCalldataAddress(value)) {
|
|
66
|
+
return bytesObjectToHex(value.bytes);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
package/dist/lib/tx.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { GenLayerClient, GenLayerChain, GenLayerTransaction, CalldataEncodable } from "genlayer-js/types";
|
|
2
|
+
/**
|
|
3
|
+
* Write to a contract and wait for the tx to be accepted.
|
|
4
|
+
* Pattern from scripts/create-campaign.ts:175-215
|
|
5
|
+
*/
|
|
6
|
+
export declare function writeAndWait(client: GenLayerClient<GenLayerChain>, address: string, functionName: string, args: CalldataEncodable[], opts?: {
|
|
7
|
+
leaderOnly?: boolean;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
hash: string;
|
|
11
|
+
receipt: GenLayerTransaction;
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* Read from a contract (view call).
|
|
15
|
+
*/
|
|
16
|
+
export declare function readContract(client: GenLayerClient<GenLayerChain>, address: string, functionName: string, args: CalldataEncodable[]): Promise<CalldataEncodable>;
|
package/dist/lib/tx.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write to a contract and wait for the tx to be accepted.
|
|
3
|
+
* Pattern from scripts/create-campaign.ts:175-215
|
|
4
|
+
*/
|
|
5
|
+
export async function writeAndWait(client, address, functionName, args, opts) {
|
|
6
|
+
const hash = await client.writeContract({
|
|
7
|
+
address: address,
|
|
8
|
+
functionName,
|
|
9
|
+
args,
|
|
10
|
+
value: 0n,
|
|
11
|
+
leaderOnly: opts?.leaderOnly ?? true,
|
|
12
|
+
});
|
|
13
|
+
const receipt = await client.waitForTransactionReceipt({
|
|
14
|
+
hash,
|
|
15
|
+
status: "ACCEPTED",
|
|
16
|
+
retries: opts?.timeout ? Math.ceil(opts.timeout / 2000) : 60,
|
|
17
|
+
});
|
|
18
|
+
return { hash, receipt };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read from a contract (view call).
|
|
22
|
+
*/
|
|
23
|
+
export async function readContract(client, address, functionName, args) {
|
|
24
|
+
return client.readContract({
|
|
25
|
+
address: address,
|
|
26
|
+
functionName,
|
|
27
|
+
args,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface MollyConfig {
|
|
2
|
+
privateKey?: string;
|
|
3
|
+
network?: string;
|
|
4
|
+
factoryAddress?: string;
|
|
5
|
+
identityAddress?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ResolvedConfig {
|
|
8
|
+
privateKey: string;
|
|
9
|
+
network: string;
|
|
10
|
+
factoryAddress: string;
|
|
11
|
+
identityAddress: string;
|
|
12
|
+
}
|
|
13
|
+
export interface OutputOptions {
|
|
14
|
+
pretty?: boolean;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "molly-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for AI bots to interact with the GenLayer blockchain via Moltrally",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"molly": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/genlayerlabs/moltrally.git",
|
|
17
|
+
"directory": "cli"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/genlayerlabs/moltrally/tree/main/cli",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/genlayerlabs/moltrally/issues"
|
|
22
|
+
},
|
|
23
|
+
"author": "GenLayer Labs",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"commander": "^13.1.0",
|
|
29
|
+
"genlayer-js": "^0.18.9"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.10.2",
|
|
33
|
+
"tsx": "^4.19.2",
|
|
34
|
+
"typescript": "^5.7.2"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"keywords": [
|
|
40
|
+
"genlayer",
|
|
41
|
+
"moltrally",
|
|
42
|
+
"moltbook",
|
|
43
|
+
"cli",
|
|
44
|
+
"ai",
|
|
45
|
+
"bot"
|
|
46
|
+
],
|
|
47
|
+
"license": "MIT"
|
|
48
|
+
}
|