@spectratools/assembly-cli 0.9.0 → 0.10.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/cli.js +636 -33
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -5242,11 +5242,40 @@ council.command("withdraw-refund", {
|
|
|
5242
5242
|
});
|
|
5243
5243
|
|
|
5244
5244
|
// src/commands/forum.ts
|
|
5245
|
+
import { TxError } from "@spectratools/tx-shared";
|
|
5245
5246
|
import { Cli as Cli2, z as z3 } from "incur";
|
|
5246
5247
|
var env2 = z3.object({
|
|
5247
5248
|
ABSTRACT_RPC_URL: z3.string().optional().describe("Abstract RPC URL override")
|
|
5248
5249
|
});
|
|
5250
|
+
var commentEnv = env2.extend({
|
|
5251
|
+
PRIVATE_KEY: z3.string().optional().describe("Private key (required only when posting a comment via --body)")
|
|
5252
|
+
});
|
|
5249
5253
|
var timestampOutput2 = z3.union([z3.number(), z3.string()]);
|
|
5254
|
+
var txResultOutput2 = z3.union([
|
|
5255
|
+
z3.object({
|
|
5256
|
+
status: z3.literal("success"),
|
|
5257
|
+
hash: z3.string(),
|
|
5258
|
+
blockNumber: z3.number(),
|
|
5259
|
+
gasUsed: z3.string(),
|
|
5260
|
+
from: z3.string(),
|
|
5261
|
+
to: z3.string().nullable(),
|
|
5262
|
+
effectiveGasPrice: z3.string().optional()
|
|
5263
|
+
}),
|
|
5264
|
+
z3.object({
|
|
5265
|
+
status: z3.literal("reverted"),
|
|
5266
|
+
hash: z3.string(),
|
|
5267
|
+
blockNumber: z3.number(),
|
|
5268
|
+
gasUsed: z3.string(),
|
|
5269
|
+
from: z3.string(),
|
|
5270
|
+
to: z3.string().nullable(),
|
|
5271
|
+
effectiveGasPrice: z3.string().optional()
|
|
5272
|
+
}),
|
|
5273
|
+
z3.object({
|
|
5274
|
+
status: z3.literal("dry-run"),
|
|
5275
|
+
estimatedGas: z3.string(),
|
|
5276
|
+
simulationResult: z3.unknown()
|
|
5277
|
+
})
|
|
5278
|
+
]);
|
|
5250
5279
|
function decodeThread(value) {
|
|
5251
5280
|
const [id, kind, author, createdAt, category, title, body, proposalId, petitionId] = value;
|
|
5252
5281
|
return {
|
|
@@ -5356,7 +5385,7 @@ forum.command("threads", {
|
|
|
5356
5385
|
description: "Inspect or comment:",
|
|
5357
5386
|
commands: [
|
|
5358
5387
|
{ command: "forum thread", args: { id: "<id>" } },
|
|
5359
|
-
{ command: "forum post-comment", args: {
|
|
5388
|
+
{ command: "forum post-comment", args: { threadId: "<id>" } }
|
|
5360
5389
|
]
|
|
5361
5390
|
}
|
|
5362
5391
|
}
|
|
@@ -5451,34 +5480,395 @@ forum.command("comments", {
|
|
|
5451
5480
|
}
|
|
5452
5481
|
});
|
|
5453
5482
|
forum.command("comment", {
|
|
5454
|
-
description: "Get one comment by
|
|
5483
|
+
description: "Get one comment by id, or post to a thread when --body is provided.",
|
|
5455
5484
|
args: z3.object({
|
|
5456
|
-
id: z3.coerce.number().int().positive().describe("Comment id (
|
|
5485
|
+
id: z3.coerce.number().int().positive().describe("Comment id (read) or thread id (write)")
|
|
5457
5486
|
}),
|
|
5458
|
-
|
|
5487
|
+
options: writeOptions.extend({
|
|
5488
|
+
body: z3.string().min(1).optional().describe("Comment body (write mode)"),
|
|
5489
|
+
"parent-id": z3.coerce.number().int().nonnegative().default(0).describe("Optional parent comment id for threaded replies (write mode)")
|
|
5490
|
+
}),
|
|
5491
|
+
env: commentEnv,
|
|
5459
5492
|
output: z3.record(z3.string(), z3.unknown()),
|
|
5460
|
-
examples: [
|
|
5493
|
+
examples: [
|
|
5494
|
+
{ args: { id: 1 }, description: "Fetch comment #1" },
|
|
5495
|
+
{
|
|
5496
|
+
args: { id: 1 },
|
|
5497
|
+
options: { body: "I support this proposal." },
|
|
5498
|
+
description: "Post a new comment on thread #1"
|
|
5499
|
+
}
|
|
5500
|
+
],
|
|
5461
5501
|
async run(c) {
|
|
5462
5502
|
const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5503
|
+
if (!c.options.body) {
|
|
5504
|
+
const commentCount2 = await client.readContract({
|
|
5505
|
+
abi: forumAbi,
|
|
5506
|
+
address: ABSTRACT_MAINNET_ADDRESSES.forum,
|
|
5507
|
+
functionName: "commentCount"
|
|
5508
|
+
});
|
|
5509
|
+
if (c.args.id > Number(commentCount2)) {
|
|
5510
|
+
return c.error({
|
|
5511
|
+
code: "OUT_OF_RANGE",
|
|
5512
|
+
message: `Comment id ${c.args.id} does not exist (commentCount: ${commentCount2})`,
|
|
5513
|
+
retryable: false
|
|
5514
|
+
});
|
|
5515
|
+
}
|
|
5516
|
+
const commentTuple = await client.readContract({
|
|
5517
|
+
abi: forumAbi,
|
|
5518
|
+
address: ABSTRACT_MAINNET_ADDRESSES.forum,
|
|
5519
|
+
functionName: "comments",
|
|
5520
|
+
args: [BigInt(c.args.id)]
|
|
5521
|
+
});
|
|
5522
|
+
return c.ok(jsonSafe(decodeComment(commentTuple)));
|
|
5523
|
+
}
|
|
5524
|
+
if (!c.env.PRIVATE_KEY) {
|
|
5525
|
+
return c.error({
|
|
5526
|
+
code: "MISSING_PRIVATE_KEY",
|
|
5527
|
+
message: "PRIVATE_KEY is required when posting a comment.",
|
|
5528
|
+
retryable: false
|
|
5529
|
+
});
|
|
5530
|
+
}
|
|
5531
|
+
const account = resolveAccount({ PRIVATE_KEY: c.env.PRIVATE_KEY });
|
|
5532
|
+
const [activeMember, threadCount, commentCount] = await Promise.all([
|
|
5533
|
+
client.readContract({
|
|
5534
|
+
abi: registryAbi,
|
|
5535
|
+
address: ABSTRACT_MAINNET_ADDRESSES.registry,
|
|
5536
|
+
functionName: "isActive",
|
|
5537
|
+
args: [account.address]
|
|
5538
|
+
}),
|
|
5539
|
+
client.readContract({
|
|
5540
|
+
abi: forumAbi,
|
|
5541
|
+
address: ABSTRACT_MAINNET_ADDRESSES.forum,
|
|
5542
|
+
functionName: "threadCount"
|
|
5543
|
+
}),
|
|
5544
|
+
client.readContract({
|
|
5545
|
+
abi: forumAbi,
|
|
5546
|
+
address: ABSTRACT_MAINNET_ADDRESSES.forum,
|
|
5547
|
+
functionName: "commentCount"
|
|
5548
|
+
})
|
|
5549
|
+
]);
|
|
5550
|
+
if (!activeMember) {
|
|
5551
|
+
return c.error({
|
|
5552
|
+
code: "NOT_ACTIVE_MEMBER",
|
|
5553
|
+
message: `Address ${toChecksum(account.address)} is not an active Assembly member.`,
|
|
5554
|
+
retryable: false
|
|
5555
|
+
});
|
|
5556
|
+
}
|
|
5557
|
+
if (c.args.id > Number(threadCount)) {
|
|
5469
5558
|
return c.error({
|
|
5470
5559
|
code: "OUT_OF_RANGE",
|
|
5471
|
-
message: `
|
|
5560
|
+
message: `Thread id ${c.args.id} does not exist (threadCount: ${threadCount})`,
|
|
5472
5561
|
retryable: false
|
|
5473
5562
|
});
|
|
5474
5563
|
}
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5564
|
+
if (c.options["parent-id"] > Number(commentCount)) {
|
|
5565
|
+
return c.error({
|
|
5566
|
+
code: "OUT_OF_RANGE",
|
|
5567
|
+
message: `Parent comment id ${c.options["parent-id"]} does not exist (commentCount: ${commentCount})`,
|
|
5568
|
+
retryable: false
|
|
5569
|
+
});
|
|
5570
|
+
}
|
|
5571
|
+
const expectedCommentId = Number(commentCount) + 1;
|
|
5572
|
+
try {
|
|
5573
|
+
const txResult = await assemblyWriteTx({
|
|
5574
|
+
env: {
|
|
5575
|
+
PRIVATE_KEY: c.env.PRIVATE_KEY,
|
|
5576
|
+
ABSTRACT_RPC_URL: c.env.ABSTRACT_RPC_URL
|
|
5577
|
+
},
|
|
5578
|
+
options: c.options,
|
|
5579
|
+
address: ABSTRACT_MAINNET_ADDRESSES.forum,
|
|
5580
|
+
abi: forumAbi,
|
|
5581
|
+
functionName: "postComment",
|
|
5582
|
+
args: [BigInt(c.args.id), BigInt(c.options["parent-id"]), c.options.body]
|
|
5583
|
+
});
|
|
5584
|
+
return c.ok({
|
|
5585
|
+
author: toChecksum(account.address),
|
|
5586
|
+
threadId: c.args.id,
|
|
5587
|
+
parentId: c.options["parent-id"],
|
|
5588
|
+
expectedCommentId,
|
|
5589
|
+
tx: txResult
|
|
5590
|
+
});
|
|
5591
|
+
} catch (error) {
|
|
5592
|
+
if (error instanceof TxError) {
|
|
5593
|
+
return c.error({
|
|
5594
|
+
code: error.code,
|
|
5595
|
+
message: error.message,
|
|
5596
|
+
retryable: error.code === "NONCE_CONFLICT"
|
|
5597
|
+
});
|
|
5598
|
+
}
|
|
5599
|
+
throw error;
|
|
5600
|
+
}
|
|
5601
|
+
}
|
|
5602
|
+
});
|
|
5603
|
+
forum.command("post", {
|
|
5604
|
+
description: "Create a new discussion thread in the forum.",
|
|
5605
|
+
hint: "Requires PRIVATE_KEY environment variable for signing.",
|
|
5606
|
+
options: writeOptions.extend({
|
|
5607
|
+
category: z3.string().min(1).describe("Thread category label (e.g., general, governance)"),
|
|
5608
|
+
title: z3.string().min(1).describe("Thread title"),
|
|
5609
|
+
body: z3.string().min(1).describe("Thread body")
|
|
5610
|
+
}),
|
|
5611
|
+
env: writeEnv,
|
|
5612
|
+
output: z3.object({
|
|
5613
|
+
author: z3.string(),
|
|
5614
|
+
category: z3.string(),
|
|
5615
|
+
title: z3.string(),
|
|
5616
|
+
expectedThreadId: z3.number(),
|
|
5617
|
+
tx: txResultOutput2
|
|
5618
|
+
}),
|
|
5619
|
+
examples: [
|
|
5620
|
+
{
|
|
5621
|
+
options: {
|
|
5622
|
+
category: "general",
|
|
5623
|
+
title: "Roadmap discussion",
|
|
5624
|
+
body: "Should we prioritize treasury automation in Q2?"
|
|
5625
|
+
},
|
|
5626
|
+
description: "Post a new discussion thread"
|
|
5627
|
+
}
|
|
5628
|
+
],
|
|
5629
|
+
async run(c) {
|
|
5630
|
+
const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
|
|
5631
|
+
const account = resolveAccount(c.env);
|
|
5632
|
+
const [activeMember, threadCountBefore] = await Promise.all([
|
|
5633
|
+
client.readContract({
|
|
5634
|
+
abi: registryAbi,
|
|
5635
|
+
address: ABSTRACT_MAINNET_ADDRESSES.registry,
|
|
5636
|
+
functionName: "isActive",
|
|
5637
|
+
args: [account.address]
|
|
5638
|
+
}),
|
|
5639
|
+
client.readContract({
|
|
5640
|
+
abi: forumAbi,
|
|
5641
|
+
address: ABSTRACT_MAINNET_ADDRESSES.forum,
|
|
5642
|
+
functionName: "threadCount"
|
|
5643
|
+
})
|
|
5644
|
+
]);
|
|
5645
|
+
if (!activeMember) {
|
|
5646
|
+
return c.error({
|
|
5647
|
+
code: "NOT_ACTIVE_MEMBER",
|
|
5648
|
+
message: `Address ${toChecksum(account.address)} is not an active Assembly member.`,
|
|
5649
|
+
retryable: false
|
|
5650
|
+
});
|
|
5651
|
+
}
|
|
5652
|
+
const expectedThreadId = Number(threadCountBefore) + 1;
|
|
5653
|
+
try {
|
|
5654
|
+
const txResult = await assemblyWriteTx({
|
|
5655
|
+
env: c.env,
|
|
5656
|
+
options: c.options,
|
|
5657
|
+
address: ABSTRACT_MAINNET_ADDRESSES.forum,
|
|
5658
|
+
abi: forumAbi,
|
|
5659
|
+
functionName: "postDiscussionThread",
|
|
5660
|
+
args: [c.options.category, c.options.title, c.options.body]
|
|
5661
|
+
});
|
|
5662
|
+
return c.ok({
|
|
5663
|
+
author: toChecksum(account.address),
|
|
5664
|
+
category: c.options.category,
|
|
5665
|
+
title: c.options.title,
|
|
5666
|
+
expectedThreadId,
|
|
5667
|
+
tx: txResult
|
|
5668
|
+
});
|
|
5669
|
+
} catch (error) {
|
|
5670
|
+
if (error instanceof TxError) {
|
|
5671
|
+
return c.error({
|
|
5672
|
+
code: error.code,
|
|
5673
|
+
message: error.message,
|
|
5674
|
+
retryable: error.code === "NONCE_CONFLICT"
|
|
5675
|
+
});
|
|
5676
|
+
}
|
|
5677
|
+
throw error;
|
|
5678
|
+
}
|
|
5679
|
+
}
|
|
5680
|
+
});
|
|
5681
|
+
forum.command("post-comment", {
|
|
5682
|
+
description: "Post a comment to a forum thread.",
|
|
5683
|
+
hint: "Requires PRIVATE_KEY environment variable for signing.",
|
|
5684
|
+
args: z3.object({
|
|
5685
|
+
threadId: z3.coerce.number().int().positive().describe("Thread id to comment on")
|
|
5686
|
+
}),
|
|
5687
|
+
options: writeOptions.extend({
|
|
5688
|
+
body: z3.string().min(1).describe("Comment body"),
|
|
5689
|
+
"parent-id": z3.coerce.number().int().nonnegative().default(0).describe("Optional parent comment id for threaded replies")
|
|
5690
|
+
}),
|
|
5691
|
+
env: writeEnv,
|
|
5692
|
+
output: z3.object({
|
|
5693
|
+
author: z3.string(),
|
|
5694
|
+
threadId: z3.number(),
|
|
5695
|
+
parentId: z3.number(),
|
|
5696
|
+
expectedCommentId: z3.number(),
|
|
5697
|
+
tx: txResultOutput2
|
|
5698
|
+
}),
|
|
5699
|
+
examples: [
|
|
5700
|
+
{
|
|
5701
|
+
args: { threadId: 1 },
|
|
5702
|
+
options: {
|
|
5703
|
+
body: "Appreciate the update \u2014 support from me."
|
|
5704
|
+
},
|
|
5705
|
+
description: "Post a comment on thread #1"
|
|
5706
|
+
}
|
|
5707
|
+
],
|
|
5708
|
+
async run(c) {
|
|
5709
|
+
const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
|
|
5710
|
+
const account = resolveAccount(c.env);
|
|
5711
|
+
const [activeMember, threadCount, commentCount] = await Promise.all([
|
|
5712
|
+
client.readContract({
|
|
5713
|
+
abi: registryAbi,
|
|
5714
|
+
address: ABSTRACT_MAINNET_ADDRESSES.registry,
|
|
5715
|
+
functionName: "isActive",
|
|
5716
|
+
args: [account.address]
|
|
5717
|
+
}),
|
|
5718
|
+
client.readContract({
|
|
5719
|
+
abi: forumAbi,
|
|
5720
|
+
address: ABSTRACT_MAINNET_ADDRESSES.forum,
|
|
5721
|
+
functionName: "threadCount"
|
|
5722
|
+
}),
|
|
5723
|
+
client.readContract({
|
|
5724
|
+
abi: forumAbi,
|
|
5725
|
+
address: ABSTRACT_MAINNET_ADDRESSES.forum,
|
|
5726
|
+
functionName: "commentCount"
|
|
5727
|
+
})
|
|
5728
|
+
]);
|
|
5729
|
+
if (!activeMember) {
|
|
5730
|
+
return c.error({
|
|
5731
|
+
code: "NOT_ACTIVE_MEMBER",
|
|
5732
|
+
message: `Address ${toChecksum(account.address)} is not an active Assembly member.`,
|
|
5733
|
+
retryable: false
|
|
5734
|
+
});
|
|
5735
|
+
}
|
|
5736
|
+
if (c.args.threadId > Number(threadCount)) {
|
|
5737
|
+
return c.error({
|
|
5738
|
+
code: "OUT_OF_RANGE",
|
|
5739
|
+
message: `Thread id ${c.args.threadId} does not exist (threadCount: ${threadCount})`,
|
|
5740
|
+
retryable: false
|
|
5741
|
+
});
|
|
5742
|
+
}
|
|
5743
|
+
if (c.options["parent-id"] > Number(commentCount)) {
|
|
5744
|
+
return c.error({
|
|
5745
|
+
code: "OUT_OF_RANGE",
|
|
5746
|
+
message: `Parent comment id ${c.options["parent-id"]} does not exist (commentCount: ${commentCount})`,
|
|
5747
|
+
retryable: false
|
|
5748
|
+
});
|
|
5749
|
+
}
|
|
5750
|
+
const expectedCommentId = Number(commentCount) + 1;
|
|
5751
|
+
try {
|
|
5752
|
+
const txResult = await assemblyWriteTx({
|
|
5753
|
+
env: c.env,
|
|
5754
|
+
options: c.options,
|
|
5755
|
+
address: ABSTRACT_MAINNET_ADDRESSES.forum,
|
|
5756
|
+
abi: forumAbi,
|
|
5757
|
+
functionName: "postComment",
|
|
5758
|
+
args: [BigInt(c.args.threadId), BigInt(c.options["parent-id"]), c.options.body]
|
|
5759
|
+
});
|
|
5760
|
+
return c.ok({
|
|
5761
|
+
author: toChecksum(account.address),
|
|
5762
|
+
threadId: c.args.threadId,
|
|
5763
|
+
parentId: c.options["parent-id"],
|
|
5764
|
+
expectedCommentId,
|
|
5765
|
+
tx: txResult
|
|
5766
|
+
});
|
|
5767
|
+
} catch (error) {
|
|
5768
|
+
if (error instanceof TxError) {
|
|
5769
|
+
return c.error({
|
|
5770
|
+
code: error.code,
|
|
5771
|
+
message: error.message,
|
|
5772
|
+
retryable: error.code === "NONCE_CONFLICT"
|
|
5773
|
+
});
|
|
5774
|
+
}
|
|
5775
|
+
throw error;
|
|
5776
|
+
}
|
|
5777
|
+
}
|
|
5778
|
+
});
|
|
5779
|
+
forum.command("sign-petition", {
|
|
5780
|
+
description: "Sign an existing petition as an active member.",
|
|
5781
|
+
hint: "Requires PRIVATE_KEY environment variable for signing.",
|
|
5782
|
+
args: z3.object({
|
|
5783
|
+
petitionId: z3.coerce.number().int().positive().describe("Petition id (1-indexed)")
|
|
5784
|
+
}),
|
|
5785
|
+
options: writeOptions,
|
|
5786
|
+
env: writeEnv,
|
|
5787
|
+
output: z3.object({
|
|
5788
|
+
signer: z3.string(),
|
|
5789
|
+
petitionId: z3.number(),
|
|
5790
|
+
expectedSignatures: z3.number(),
|
|
5791
|
+
tx: txResultOutput2
|
|
5792
|
+
}),
|
|
5793
|
+
examples: [{ args: { petitionId: 1 }, description: "Sign petition #1" }],
|
|
5794
|
+
async run(c) {
|
|
5795
|
+
const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
|
|
5796
|
+
const account = resolveAccount(c.env);
|
|
5797
|
+
const [activeMember, petitionCount] = await Promise.all([
|
|
5798
|
+
client.readContract({
|
|
5799
|
+
abi: registryAbi,
|
|
5800
|
+
address: ABSTRACT_MAINNET_ADDRESSES.registry,
|
|
5801
|
+
functionName: "isActive",
|
|
5802
|
+
args: [account.address]
|
|
5803
|
+
}),
|
|
5804
|
+
client.readContract({
|
|
5805
|
+
abi: forumAbi,
|
|
5806
|
+
address: ABSTRACT_MAINNET_ADDRESSES.forum,
|
|
5807
|
+
functionName: "petitionCount"
|
|
5808
|
+
})
|
|
5809
|
+
]);
|
|
5810
|
+
if (!activeMember) {
|
|
5811
|
+
return c.error({
|
|
5812
|
+
code: "NOT_ACTIVE_MEMBER",
|
|
5813
|
+
message: `Address ${toChecksum(account.address)} is not an active Assembly member.`,
|
|
5814
|
+
retryable: false
|
|
5815
|
+
});
|
|
5816
|
+
}
|
|
5817
|
+
if (c.args.petitionId > Number(petitionCount)) {
|
|
5818
|
+
return c.error({
|
|
5819
|
+
code: "OUT_OF_RANGE",
|
|
5820
|
+
message: `Petition id ${c.args.petitionId} does not exist (petitionCount: ${petitionCount})`,
|
|
5821
|
+
retryable: false
|
|
5822
|
+
});
|
|
5823
|
+
}
|
|
5824
|
+
const [alreadySigned, petitionTuple] = await Promise.all([
|
|
5825
|
+
client.readContract({
|
|
5826
|
+
abi: forumAbi,
|
|
5827
|
+
address: ABSTRACT_MAINNET_ADDRESSES.forum,
|
|
5828
|
+
functionName: "hasSignedPetition",
|
|
5829
|
+
args: [BigInt(c.args.petitionId), account.address]
|
|
5830
|
+
}),
|
|
5831
|
+
client.readContract({
|
|
5832
|
+
abi: forumAbi,
|
|
5833
|
+
address: ABSTRACT_MAINNET_ADDRESSES.forum,
|
|
5834
|
+
functionName: "petitions",
|
|
5835
|
+
args: [BigInt(c.args.petitionId)]
|
|
5836
|
+
})
|
|
5837
|
+
]);
|
|
5838
|
+
if (alreadySigned) {
|
|
5839
|
+
return c.error({
|
|
5840
|
+
code: "ALREADY_SIGNED",
|
|
5841
|
+
message: `Address ${toChecksum(account.address)} has already signed petition #${c.args.petitionId}.`,
|
|
5842
|
+
retryable: false
|
|
5843
|
+
});
|
|
5844
|
+
}
|
|
5845
|
+
const petition = decodePetition(petitionTuple);
|
|
5846
|
+
const expectedSignatures = petition.signatures + 1;
|
|
5847
|
+
try {
|
|
5848
|
+
const txResult = await assemblyWriteTx({
|
|
5849
|
+
env: c.env,
|
|
5850
|
+
options: c.options,
|
|
5851
|
+
address: ABSTRACT_MAINNET_ADDRESSES.forum,
|
|
5852
|
+
abi: forumAbi,
|
|
5853
|
+
functionName: "signPetition",
|
|
5854
|
+
args: [BigInt(c.args.petitionId)]
|
|
5855
|
+
});
|
|
5856
|
+
return c.ok({
|
|
5857
|
+
signer: toChecksum(account.address),
|
|
5858
|
+
petitionId: c.args.petitionId,
|
|
5859
|
+
expectedSignatures,
|
|
5860
|
+
tx: txResult
|
|
5861
|
+
});
|
|
5862
|
+
} catch (error) {
|
|
5863
|
+
if (error instanceof TxError) {
|
|
5864
|
+
return c.error({
|
|
5865
|
+
code: error.code,
|
|
5866
|
+
message: error.message,
|
|
5867
|
+
retryable: error.code === "NONCE_CONFLICT"
|
|
5868
|
+
});
|
|
5869
|
+
}
|
|
5870
|
+
throw error;
|
|
5871
|
+
}
|
|
5482
5872
|
}
|
|
5483
5873
|
});
|
|
5484
5874
|
forum.command("petitions", {
|
|
@@ -5622,7 +6012,7 @@ forum.command("stats", {
|
|
|
5622
6012
|
});
|
|
5623
6013
|
|
|
5624
6014
|
// src/commands/governance.ts
|
|
5625
|
-
import { TxError } from "@spectratools/tx-shared";
|
|
6015
|
+
import { TxError as TxError2 } from "@spectratools/tx-shared";
|
|
5626
6016
|
import { Cli as Cli3, z as z4 } from "incur";
|
|
5627
6017
|
var env3 = z4.object({
|
|
5628
6018
|
ABSTRACT_RPC_URL: z4.string().optional().describe("Abstract RPC URL override")
|
|
@@ -5969,7 +6359,7 @@ governance.command("params", {
|
|
|
5969
6359
|
});
|
|
5970
6360
|
}
|
|
5971
6361
|
});
|
|
5972
|
-
var
|
|
6362
|
+
var txResultOutput3 = z4.union([
|
|
5973
6363
|
z4.object({
|
|
5974
6364
|
status: z4.enum(["success", "reverted"]),
|
|
5975
6365
|
hash: z4.string(),
|
|
@@ -5999,7 +6389,7 @@ governance.command("vote", {
|
|
|
5999
6389
|
proposalTitle: z4.string(),
|
|
6000
6390
|
support: z4.enum(["for", "against", "abstain"]),
|
|
6001
6391
|
supportValue: z4.number(),
|
|
6002
|
-
tx:
|
|
6392
|
+
tx: txResultOutput3
|
|
6003
6393
|
}),
|
|
6004
6394
|
examples: [
|
|
6005
6395
|
{
|
|
@@ -6063,7 +6453,7 @@ governance.command("vote", {
|
|
|
6063
6453
|
tx: txResult
|
|
6064
6454
|
});
|
|
6065
6455
|
} catch (error) {
|
|
6066
|
-
if (error instanceof
|
|
6456
|
+
if (error instanceof TxError2) {
|
|
6067
6457
|
return c.error({
|
|
6068
6458
|
code: error.code,
|
|
6069
6459
|
message: error.message,
|
|
@@ -6094,7 +6484,7 @@ governance.command("propose", {
|
|
|
6094
6484
|
title: z4.string(),
|
|
6095
6485
|
description: z4.string(),
|
|
6096
6486
|
expectedProposalId: z4.number(),
|
|
6097
|
-
tx:
|
|
6487
|
+
tx: txResultOutput3
|
|
6098
6488
|
}),
|
|
6099
6489
|
examples: [
|
|
6100
6490
|
{
|
|
@@ -6168,7 +6558,7 @@ governance.command("propose", {
|
|
|
6168
6558
|
tx: txResult
|
|
6169
6559
|
});
|
|
6170
6560
|
} catch (error) {
|
|
6171
|
-
if (error instanceof
|
|
6561
|
+
if (error instanceof TxError2) {
|
|
6172
6562
|
return c.error({
|
|
6173
6563
|
code: error.code,
|
|
6174
6564
|
message: error.message,
|
|
@@ -6191,7 +6581,7 @@ governance.command("queue", {
|
|
|
6191
6581
|
proposalId: z4.number(),
|
|
6192
6582
|
proposalTitle: z4.string(),
|
|
6193
6583
|
statusBefore: z4.string(),
|
|
6194
|
-
tx:
|
|
6584
|
+
tx: txResultOutput3
|
|
6195
6585
|
}),
|
|
6196
6586
|
examples: [
|
|
6197
6587
|
{
|
|
@@ -6256,7 +6646,7 @@ governance.command("queue", {
|
|
|
6256
6646
|
tx: txResult
|
|
6257
6647
|
});
|
|
6258
6648
|
} catch (error) {
|
|
6259
|
-
if (error instanceof
|
|
6649
|
+
if (error instanceof TxError2) {
|
|
6260
6650
|
return c.error({
|
|
6261
6651
|
code: error.code,
|
|
6262
6652
|
message: error.message,
|
|
@@ -6279,7 +6669,7 @@ governance.command("execute", {
|
|
|
6279
6669
|
proposalId: z4.number(),
|
|
6280
6670
|
proposalTitle: z4.string(),
|
|
6281
6671
|
timelockEndsAt: timestampOutput3,
|
|
6282
|
-
tx:
|
|
6672
|
+
tx: txResultOutput3
|
|
6283
6673
|
}),
|
|
6284
6674
|
examples: [
|
|
6285
6675
|
{
|
|
@@ -6330,7 +6720,7 @@ governance.command("execute", {
|
|
|
6330
6720
|
tx: txResult
|
|
6331
6721
|
});
|
|
6332
6722
|
} catch (error) {
|
|
6333
|
-
if (error instanceof
|
|
6723
|
+
if (error instanceof TxError2) {
|
|
6334
6724
|
return c.error({
|
|
6335
6725
|
code: error.code,
|
|
6336
6726
|
message: error.message,
|
|
@@ -6343,7 +6733,7 @@ governance.command("execute", {
|
|
|
6343
6733
|
});
|
|
6344
6734
|
|
|
6345
6735
|
// src/commands/members.ts
|
|
6346
|
-
import { TxError as
|
|
6736
|
+
import { TxError as TxError3 } from "@spectratools/tx-shared";
|
|
6347
6737
|
import { Cli as Cli4, z as z5 } from "incur";
|
|
6348
6738
|
var DEFAULT_MEMBER_SNAPSHOT_URL = "https://www.theaiassembly.org/api/indexer/members";
|
|
6349
6739
|
var REGISTERED_EVENT_SCAN_STEP = 100000n;
|
|
@@ -6836,7 +7226,7 @@ members.command("register", {
|
|
|
6836
7226
|
} : void 0
|
|
6837
7227
|
);
|
|
6838
7228
|
} catch (error) {
|
|
6839
|
-
if (error instanceof
|
|
7229
|
+
if (error instanceof TxError3 && error.code === "INSUFFICIENT_FUNDS") {
|
|
6840
7230
|
return c.error({
|
|
6841
7231
|
code: "INSUFFICIENT_FUNDS",
|
|
6842
7232
|
message: `Insufficient funds to register. Required fee: ${eth(fee)} (${fee} wei). ${error.message}`,
|
|
@@ -6887,7 +7277,7 @@ members.command("heartbeat", {
|
|
|
6887
7277
|
} : void 0
|
|
6888
7278
|
);
|
|
6889
7279
|
} catch (error) {
|
|
6890
|
-
if (error instanceof
|
|
7280
|
+
if (error instanceof TxError3 && error.code === "INSUFFICIENT_FUNDS") {
|
|
6891
7281
|
return c.error({
|
|
6892
7282
|
code: "INSUFFICIENT_FUNDS",
|
|
6893
7283
|
message: `Insufficient funds for heartbeat. Required fee: ${eth(fee)} (${fee} wei). ${error.message}`,
|
|
@@ -6938,7 +7328,7 @@ members.command("renew", {
|
|
|
6938
7328
|
} : void 0
|
|
6939
7329
|
);
|
|
6940
7330
|
} catch (error) {
|
|
6941
|
-
if (error instanceof
|
|
7331
|
+
if (error instanceof TxError3 && error.code === "INSUFFICIENT_FUNDS") {
|
|
6942
7332
|
return c.error({
|
|
6943
7333
|
code: "INSUFFICIENT_FUNDS",
|
|
6944
7334
|
message: `Insufficient funds to renew. Required fee: ${eth(fee)} (${fee} wei). ${error.message}`,
|
|
@@ -6951,11 +7341,38 @@ members.command("renew", {
|
|
|
6951
7341
|
});
|
|
6952
7342
|
|
|
6953
7343
|
// src/commands/treasury.ts
|
|
7344
|
+
import { TxError as TxError4 } from "@spectratools/tx-shared";
|
|
6954
7345
|
import { Cli as Cli5, z as z6 } from "incur";
|
|
7346
|
+
import { encodeAbiParameters, parseUnits, zeroAddress } from "viem";
|
|
6955
7347
|
var env5 = z6.object({
|
|
6956
7348
|
ABSTRACT_RPC_URL: z6.string().optional().describe("Abstract RPC URL override")
|
|
6957
7349
|
});
|
|
6958
7350
|
var timestampOutput5 = z6.union([z6.number(), z6.string()]);
|
|
7351
|
+
var txResultOutput4 = z6.union([
|
|
7352
|
+
z6.object({
|
|
7353
|
+
status: z6.literal("success"),
|
|
7354
|
+
hash: z6.string(),
|
|
7355
|
+
blockNumber: z6.number(),
|
|
7356
|
+
gasUsed: z6.string(),
|
|
7357
|
+
from: z6.string(),
|
|
7358
|
+
to: z6.string().nullable(),
|
|
7359
|
+
effectiveGasPrice: z6.string().optional()
|
|
7360
|
+
}),
|
|
7361
|
+
z6.object({
|
|
7362
|
+
status: z6.literal("reverted"),
|
|
7363
|
+
hash: z6.string(),
|
|
7364
|
+
blockNumber: z6.number(),
|
|
7365
|
+
gasUsed: z6.string(),
|
|
7366
|
+
from: z6.string(),
|
|
7367
|
+
to: z6.string().nullable(),
|
|
7368
|
+
effectiveGasPrice: z6.string().optional()
|
|
7369
|
+
}),
|
|
7370
|
+
z6.object({
|
|
7371
|
+
status: z6.literal("dry-run"),
|
|
7372
|
+
estimatedGas: z6.string(),
|
|
7373
|
+
simulationResult: z6.unknown()
|
|
7374
|
+
})
|
|
7375
|
+
]);
|
|
6959
7376
|
var treasury = Cli5.create("treasury", {
|
|
6960
7377
|
description: "Inspect treasury balances, execution status, and spend controls."
|
|
6961
7378
|
});
|
|
@@ -7078,6 +7495,192 @@ treasury.command("executed", {
|
|
|
7078
7495
|
return c.ok({ proposalId: c.args.proposalId, executed });
|
|
7079
7496
|
}
|
|
7080
7497
|
});
|
|
7498
|
+
treasury.command("propose-spend", {
|
|
7499
|
+
description: "Create a council proposal that spends treasury funds via TreasuryTransferIntentModule.",
|
|
7500
|
+
hint: "Requires PRIVATE_KEY environment variable for signing.",
|
|
7501
|
+
options: writeOptions.extend({
|
|
7502
|
+
token: z6.string().describe("Token address to spend (use 0x0000000000000000000000000000000000000000 for ETH)"),
|
|
7503
|
+
recipient: z6.string().describe("Recipient address"),
|
|
7504
|
+
amount: z6.string().describe("Token amount as decimal string (human units)"),
|
|
7505
|
+
decimals: z6.coerce.number().int().min(0).max(36).default(18).describe("Token decimals used to parse --amount (default: 18)"),
|
|
7506
|
+
title: z6.string().min(1).describe("Proposal title"),
|
|
7507
|
+
description: z6.string().min(1).describe("Proposal description"),
|
|
7508
|
+
category: z6.string().default("treasury").describe("Forum category label for this proposal"),
|
|
7509
|
+
"risk-tier": z6.coerce.number().int().min(0).max(3).default(3).describe("Max allowed risk tier in intent constraints (0-3, default: 3)")
|
|
7510
|
+
}),
|
|
7511
|
+
env: writeEnv,
|
|
7512
|
+
output: z6.object({
|
|
7513
|
+
proposer: z6.string(),
|
|
7514
|
+
category: z6.string(),
|
|
7515
|
+
token: z6.string(),
|
|
7516
|
+
recipient: z6.string(),
|
|
7517
|
+
amount: z6.string(),
|
|
7518
|
+
amountWei: z6.string(),
|
|
7519
|
+
expectedProposalId: z6.number(),
|
|
7520
|
+
expectedThreadId: z6.number(),
|
|
7521
|
+
tx: txResultOutput4
|
|
7522
|
+
}),
|
|
7523
|
+
examples: [
|
|
7524
|
+
{
|
|
7525
|
+
options: {
|
|
7526
|
+
token: "0x0000000000000000000000000000000000000000",
|
|
7527
|
+
recipient: "0x00000000000000000000000000000000000000b0",
|
|
7528
|
+
amount: "0.5",
|
|
7529
|
+
title: "Fund grants round",
|
|
7530
|
+
description: "Allocate 0.5 ETH from treasury to the grants multisig."
|
|
7531
|
+
},
|
|
7532
|
+
description: "Propose a treasury spend transfer"
|
|
7533
|
+
}
|
|
7534
|
+
],
|
|
7535
|
+
async run(c) {
|
|
7536
|
+
const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
|
|
7537
|
+
const account = resolveAccount(c.env);
|
|
7538
|
+
const token = toChecksum(c.options.token);
|
|
7539
|
+
const recipient = toChecksum(c.options.recipient);
|
|
7540
|
+
let amountWei;
|
|
7541
|
+
try {
|
|
7542
|
+
amountWei = parseUnits(c.options.amount, c.options.decimals);
|
|
7543
|
+
} catch {
|
|
7544
|
+
return c.error({
|
|
7545
|
+
code: "INVALID_AMOUNT",
|
|
7546
|
+
message: `Invalid amount "${c.options.amount}" for decimals=${c.options.decimals}.`,
|
|
7547
|
+
retryable: false
|
|
7548
|
+
});
|
|
7549
|
+
}
|
|
7550
|
+
if (amountWei <= 0n) {
|
|
7551
|
+
return c.error({
|
|
7552
|
+
code: "INVALID_AMOUNT",
|
|
7553
|
+
message: "--amount must be greater than zero.",
|
|
7554
|
+
retryable: false
|
|
7555
|
+
});
|
|
7556
|
+
}
|
|
7557
|
+
const [activeMember, isCouncilMember, transferModule, proposalCount, threadCount, whitelisted] = await Promise.all([
|
|
7558
|
+
client.readContract({
|
|
7559
|
+
abi: registryAbi,
|
|
7560
|
+
address: ABSTRACT_MAINNET_ADDRESSES.registry,
|
|
7561
|
+
functionName: "isActive",
|
|
7562
|
+
args: [account.address]
|
|
7563
|
+
}),
|
|
7564
|
+
client.readContract({
|
|
7565
|
+
abi: councilSeatsAbi,
|
|
7566
|
+
address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
|
|
7567
|
+
functionName: "isCouncilMember",
|
|
7568
|
+
args: [account.address]
|
|
7569
|
+
}),
|
|
7570
|
+
client.readContract({
|
|
7571
|
+
abi: treasuryAbi,
|
|
7572
|
+
address: ABSTRACT_MAINNET_ADDRESSES.treasury,
|
|
7573
|
+
functionName: "treasuryTransferModule"
|
|
7574
|
+
}),
|
|
7575
|
+
client.readContract({
|
|
7576
|
+
abi: governanceAbi,
|
|
7577
|
+
address: ABSTRACT_MAINNET_ADDRESSES.governance,
|
|
7578
|
+
functionName: "proposalCount"
|
|
7579
|
+
}),
|
|
7580
|
+
client.readContract({
|
|
7581
|
+
abi: forumAbi,
|
|
7582
|
+
address: ABSTRACT_MAINNET_ADDRESSES.forum,
|
|
7583
|
+
functionName: "threadCount"
|
|
7584
|
+
}),
|
|
7585
|
+
token === zeroAddress ? Promise.resolve(true) : client.readContract({
|
|
7586
|
+
abi: treasuryAbi,
|
|
7587
|
+
address: ABSTRACT_MAINNET_ADDRESSES.treasury,
|
|
7588
|
+
functionName: "isAssetWhitelisted",
|
|
7589
|
+
args: [token]
|
|
7590
|
+
})
|
|
7591
|
+
]);
|
|
7592
|
+
if (!activeMember) {
|
|
7593
|
+
return c.error({
|
|
7594
|
+
code: "NOT_ACTIVE_MEMBER",
|
|
7595
|
+
message: `Address ${toChecksum(account.address)} is not an active Assembly member.`,
|
|
7596
|
+
retryable: false
|
|
7597
|
+
});
|
|
7598
|
+
}
|
|
7599
|
+
if (!isCouncilMember) {
|
|
7600
|
+
return c.error({
|
|
7601
|
+
code: "NOT_COUNCIL_MEMBER",
|
|
7602
|
+
message: `Address ${toChecksum(account.address)} is not an active council member and cannot create treasury spend proposals.`,
|
|
7603
|
+
retryable: false
|
|
7604
|
+
});
|
|
7605
|
+
}
|
|
7606
|
+
if (!whitelisted) {
|
|
7607
|
+
return c.error({
|
|
7608
|
+
code: "ASSET_NOT_WHITELISTED",
|
|
7609
|
+
message: `Token ${token} is not treasury-whitelisted for spending.`,
|
|
7610
|
+
retryable: false
|
|
7611
|
+
});
|
|
7612
|
+
}
|
|
7613
|
+
const transferModuleAllowed = await client.readContract({
|
|
7614
|
+
abi: treasuryAbi,
|
|
7615
|
+
address: ABSTRACT_MAINNET_ADDRESSES.treasury,
|
|
7616
|
+
functionName: "isIntentModuleAllowed",
|
|
7617
|
+
args: [transferModule]
|
|
7618
|
+
});
|
|
7619
|
+
if (!transferModuleAllowed) {
|
|
7620
|
+
return c.error({
|
|
7621
|
+
code: "INTENT_MODULE_DISABLED",
|
|
7622
|
+
message: `Treasury transfer intent module ${toChecksum(transferModule)} is currently disabled.`,
|
|
7623
|
+
retryable: false
|
|
7624
|
+
});
|
|
7625
|
+
}
|
|
7626
|
+
const moduleData = encodeAbiParameters(
|
|
7627
|
+
[
|
|
7628
|
+
{ name: "asset", type: "address" },
|
|
7629
|
+
{ name: "recipient", type: "address" },
|
|
7630
|
+
{ name: "amount", type: "uint256" }
|
|
7631
|
+
],
|
|
7632
|
+
[token, recipient, amountWei]
|
|
7633
|
+
);
|
|
7634
|
+
const proposalInput = {
|
|
7635
|
+
kind: 2,
|
|
7636
|
+
title: c.options.title,
|
|
7637
|
+
description: c.options.description,
|
|
7638
|
+
intentSteps: [
|
|
7639
|
+
{
|
|
7640
|
+
module: transferModule,
|
|
7641
|
+
moduleData
|
|
7642
|
+
}
|
|
7643
|
+
],
|
|
7644
|
+
intentConstraints: {
|
|
7645
|
+
deadline: 0,
|
|
7646
|
+
maxAllowedRiskTier: c.options["risk-tier"]
|
|
7647
|
+
},
|
|
7648
|
+
configUpdates: []
|
|
7649
|
+
};
|
|
7650
|
+
const expectedProposalId = Number(proposalCount) + 1;
|
|
7651
|
+
const expectedThreadId = Number(threadCount) + 1;
|
|
7652
|
+
try {
|
|
7653
|
+
const txResult = await assemblyWriteTx({
|
|
7654
|
+
env: c.env,
|
|
7655
|
+
options: c.options,
|
|
7656
|
+
address: ABSTRACT_MAINNET_ADDRESSES.forum,
|
|
7657
|
+
abi: forumAbi,
|
|
7658
|
+
functionName: "createCouncilProposal",
|
|
7659
|
+
args: [c.options.category, proposalInput]
|
|
7660
|
+
});
|
|
7661
|
+
return c.ok({
|
|
7662
|
+
proposer: toChecksum(account.address),
|
|
7663
|
+
category: c.options.category,
|
|
7664
|
+
token,
|
|
7665
|
+
recipient,
|
|
7666
|
+
amount: c.options.amount,
|
|
7667
|
+
amountWei: amountWei.toString(),
|
|
7668
|
+
expectedProposalId,
|
|
7669
|
+
expectedThreadId,
|
|
7670
|
+
tx: txResult
|
|
7671
|
+
});
|
|
7672
|
+
} catch (error) {
|
|
7673
|
+
if (error instanceof TxError4) {
|
|
7674
|
+
return c.error({
|
|
7675
|
+
code: error.code,
|
|
7676
|
+
message: error.message,
|
|
7677
|
+
retryable: error.code === "NONCE_CONFLICT"
|
|
7678
|
+
});
|
|
7679
|
+
}
|
|
7680
|
+
throw error;
|
|
7681
|
+
}
|
|
7682
|
+
}
|
|
7683
|
+
});
|
|
7081
7684
|
|
|
7082
7685
|
// src/error-handling.ts
|
|
7083
7686
|
import { AsyncLocalStorage } from "async_hooks";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spectratools/assembly-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "CLI for Assembly governance on Abstract (members, council, forum, proposals, and treasury).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"ox": "^0.14.0",
|
|
34
34
|
"viem": "^2.47.0",
|
|
35
35
|
"@spectratools/cli-shared": "0.1.1",
|
|
36
|
-
"@spectratools/tx-shared": "0.
|
|
36
|
+
"@spectratools/tx-shared": "0.5.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"typescript": "5.7.3",
|