@spectratools/assembly-cli 0.8.2 → 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.
Files changed (2) hide show
  1. package/dist/cli.js +1102 -98
  2. 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: { id: "<id>" } }
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 comment id.",
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 (1-indexed)")
5485
+ id: z3.coerce.number().int().positive().describe("Comment id (read) or thread id (write)")
5457
5486
  }),
5458
- env: env2,
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: [{ args: { id: 1 }, description: "Fetch comment #1" }],
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
- const commentCount = await client.readContract({
5464
- abi: forumAbi,
5465
- address: ABSTRACT_MAINNET_ADDRESSES.forum,
5466
- functionName: "commentCount"
5467
- });
5468
- if (c.args.id > Number(commentCount)) {
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: `Comment id ${c.args.id} does not exist (commentCount: ${commentCount})`,
5560
+ message: `Thread id ${c.args.id} does not exist (threadCount: ${threadCount})`,
5472
5561
  retryable: false
5473
5562
  });
5474
5563
  }
5475
- const commentTuple = await client.readContract({
5476
- abi: forumAbi,
5477
- address: ABSTRACT_MAINNET_ADDRESSES.forum,
5478
- functionName: "comments",
5479
- args: [BigInt(c.args.id)]
5480
- });
5481
- return c.ok(jsonSafe(decodeComment(commentTuple)));
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,6 +6012,7 @@ forum.command("stats", {
5622
6012
  });
5623
6013
 
5624
6014
  // src/commands/governance.ts
6015
+ import { TxError as TxError2 } from "@spectratools/tx-shared";
5625
6016
  import { Cli as Cli3, z as z4 } from "incur";
5626
6017
  var env3 = z4.object({
5627
6018
  ABSTRACT_RPC_URL: z4.string().optional().describe("Abstract RPC URL override")
@@ -5635,6 +6026,14 @@ var proposalStatusLabels = {
5635
6026
  4: "defeated",
5636
6027
  5: "cancelled"
5637
6028
  };
6029
+ var PROPOSAL_STATUS_PENDING = 0;
6030
+ var PROPOSAL_STATUS_ACTIVE = 1;
6031
+ var PROPOSAL_STATUS_PASSED = 2;
6032
+ var supportChoiceToValue = {
6033
+ against: 0,
6034
+ for: 1,
6035
+ abstain: 2
6036
+ };
5638
6037
  function proposalStatus(status) {
5639
6038
  const statusCode = asNum(status);
5640
6039
  return {
@@ -5749,6 +6148,23 @@ function serializeProposal(proposal) {
5749
6148
  description: proposal.description
5750
6149
  };
5751
6150
  }
6151
+ async function readProposalCount(client) {
6152
+ return await client.readContract({
6153
+ abi: governanceAbi,
6154
+ address: ABSTRACT_MAINNET_ADDRESSES.governance,
6155
+ functionName: "proposalCount"
6156
+ });
6157
+ }
6158
+ async function readProposalById(client, proposalId) {
6159
+ return decodeProposal(
6160
+ await client.readContract({
6161
+ abi: governanceAbi,
6162
+ address: ABSTRACT_MAINNET_ADDRESSES.governance,
6163
+ functionName: "proposals",
6164
+ args: [BigInt(proposalId)]
6165
+ })
6166
+ );
6167
+ }
5752
6168
  var governance = Cli3.create("governance", {
5753
6169
  description: "Inspect Assembly governance proposals, votes, and parameters."
5754
6170
  });
@@ -5806,7 +6222,10 @@ governance.command("proposals", {
5806
6222
  description: "Inspect or vote:",
5807
6223
  commands: [
5808
6224
  { command: "governance proposal", args: { id: "<id>" } },
5809
- { command: "governance vote", args: { id: "<id>" } }
6225
+ {
6226
+ command: "governance vote",
6227
+ args: { proposalId: "<id>", support: "<for|against|abstain>" }
6228
+ }
5810
6229
  ]
5811
6230
  }
5812
6231
  }
@@ -5846,103 +6265,475 @@ governance.command("proposal", {
5846
6265
  return c.ok(serializeProposal(proposal));
5847
6266
  }
5848
6267
  });
5849
- governance.command("has-voted", {
5850
- description: "Check if an address has voted on a proposal.",
6268
+ governance.command("has-voted", {
6269
+ description: "Check if an address has voted on a proposal.",
6270
+ args: z4.object({
6271
+ proposalId: z4.coerce.number().int().positive().describe("Proposal id (1-indexed)"),
6272
+ address: z4.string().describe("Voter address")
6273
+ }),
6274
+ env: env3,
6275
+ output: z4.object({
6276
+ proposalId: z4.number(),
6277
+ address: z4.string(),
6278
+ hasVoted: z4.boolean()
6279
+ }),
6280
+ examples: [
6281
+ {
6282
+ args: {
6283
+ proposalId: 1,
6284
+ address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
6285
+ },
6286
+ description: "Check whether an address already voted"
6287
+ }
6288
+ ],
6289
+ async run(c) {
6290
+ const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
6291
+ const hasVoted = await client.readContract({
6292
+ abi: governanceAbi,
6293
+ address: ABSTRACT_MAINNET_ADDRESSES.governance,
6294
+ functionName: "hasVoted",
6295
+ args: [BigInt(c.args.proposalId), c.args.address]
6296
+ });
6297
+ return c.ok({
6298
+ proposalId: c.args.proposalId,
6299
+ address: toChecksum(c.args.address),
6300
+ hasVoted
6301
+ });
6302
+ }
6303
+ });
6304
+ governance.command("params", {
6305
+ description: "Read governance threshold and timing parameters.",
6306
+ env: env3,
6307
+ output: z4.object({
6308
+ deliberationPeriod: z4.number(),
6309
+ votePeriod: z4.number(),
6310
+ quorumBps: z4.number(),
6311
+ constitutionalDeliberationPeriod: z4.number(),
6312
+ constitutionalVotePeriod: z4.number(),
6313
+ constitutionalPassBps: z4.number(),
6314
+ majorPassBps: z4.number(),
6315
+ parameterPassBps: z4.number(),
6316
+ significantPassBps: z4.number(),
6317
+ significantThresholdBps: z4.number(),
6318
+ routineThresholdBps: z4.number(),
6319
+ timelockPeriod: z4.number()
6320
+ }),
6321
+ examples: [{ description: "Inspect governance timing and pass thresholds" }],
6322
+ async run(c) {
6323
+ const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
6324
+ const getters = [
6325
+ "deliberationPeriod",
6326
+ "votePeriod",
6327
+ "quorumBps",
6328
+ "constitutionalDeliberationPeriod",
6329
+ "constitutionalVotePeriod",
6330
+ "constitutionalPassBps",
6331
+ "majorPassBps",
6332
+ "parameterPassBps",
6333
+ "significantPassBps",
6334
+ "significantThresholdBps",
6335
+ "routineThresholdBps",
6336
+ "timelockPeriod"
6337
+ ];
6338
+ const values = await client.multicall({
6339
+ allowFailure: false,
6340
+ contracts: getters.map((name) => ({
6341
+ abi: governanceAbi,
6342
+ address: ABSTRACT_MAINNET_ADDRESSES.governance,
6343
+ functionName: name
6344
+ }))
6345
+ });
6346
+ return c.ok({
6347
+ deliberationPeriod: asNum(values[0]),
6348
+ votePeriod: asNum(values[1]),
6349
+ quorumBps: asNum(values[2]),
6350
+ constitutionalDeliberationPeriod: asNum(values[3]),
6351
+ constitutionalVotePeriod: asNum(values[4]),
6352
+ constitutionalPassBps: asNum(values[5]),
6353
+ majorPassBps: asNum(values[6]),
6354
+ parameterPassBps: asNum(values[7]),
6355
+ significantPassBps: asNum(values[8]),
6356
+ significantThresholdBps: asNum(values[9]),
6357
+ routineThresholdBps: asNum(values[10]),
6358
+ timelockPeriod: asNum(values[11])
6359
+ });
6360
+ }
6361
+ });
6362
+ var txResultOutput3 = z4.union([
6363
+ z4.object({
6364
+ status: z4.enum(["success", "reverted"]),
6365
+ hash: z4.string(),
6366
+ blockNumber: z4.number(),
6367
+ gasUsed: z4.string(),
6368
+ from: z4.string(),
6369
+ to: z4.string().nullable(),
6370
+ effectiveGasPrice: z4.string().optional()
6371
+ }),
6372
+ z4.object({
6373
+ status: z4.literal("dry-run"),
6374
+ estimatedGas: z4.string(),
6375
+ simulationResult: z4.unknown()
6376
+ })
6377
+ ]);
6378
+ governance.command("vote", {
6379
+ description: "Cast a governance vote on a proposal.",
6380
+ hint: "Requires PRIVATE_KEY environment variable for signing.",
6381
+ args: z4.object({
6382
+ proposalId: z4.coerce.number().int().positive().describe("Proposal id (1-indexed)"),
6383
+ support: z4.enum(["for", "against", "abstain"]).describe("Vote support: for, against, or abstain")
6384
+ }),
6385
+ options: writeOptions,
6386
+ env: writeEnv,
6387
+ output: z4.object({
6388
+ proposalId: z4.number(),
6389
+ proposalTitle: z4.string(),
6390
+ support: z4.enum(["for", "against", "abstain"]),
6391
+ supportValue: z4.number(),
6392
+ tx: txResultOutput3
6393
+ }),
6394
+ examples: [
6395
+ {
6396
+ args: { proposalId: 1, support: "for" },
6397
+ description: "Vote in favor of proposal #1"
6398
+ },
6399
+ {
6400
+ args: { proposalId: 1, support: "abstain" },
6401
+ options: { "dry-run": true },
6402
+ description: "Simulate casting an abstain vote"
6403
+ }
6404
+ ],
6405
+ async run(c) {
6406
+ const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
6407
+ const account = resolveAccount(c.env);
6408
+ const proposalCount = await readProposalCount(client);
6409
+ if (c.args.proposalId > Number(proposalCount)) {
6410
+ return c.error({
6411
+ code: "OUT_OF_RANGE",
6412
+ message: `Proposal id ${c.args.proposalId} does not exist (proposalCount: ${proposalCount})`,
6413
+ retryable: false
6414
+ });
6415
+ }
6416
+ const proposal = await readProposalById(client, c.args.proposalId);
6417
+ const status = proposalStatus(proposal.status);
6418
+ if (status.statusCode !== PROPOSAL_STATUS_ACTIVE) {
6419
+ return c.error({
6420
+ code: "PROPOSAL_NOT_VOTING",
6421
+ message: `Proposal ${c.args.proposalId} is ${status.status} and cannot be voted right now.`,
6422
+ retryable: false
6423
+ });
6424
+ }
6425
+ const hasVoted = await client.readContract({
6426
+ abi: governanceAbi,
6427
+ address: ABSTRACT_MAINNET_ADDRESSES.governance,
6428
+ functionName: "hasVoted",
6429
+ args: [BigInt(c.args.proposalId), account.address]
6430
+ });
6431
+ if (hasVoted) {
6432
+ return c.error({
6433
+ code: "ALREADY_VOTED",
6434
+ message: `Address ${toChecksum(account.address)} has already voted on proposal ${c.args.proposalId}.`,
6435
+ retryable: false
6436
+ });
6437
+ }
6438
+ const supportValue = supportChoiceToValue[c.args.support];
6439
+ try {
6440
+ const txResult = await assemblyWriteTx({
6441
+ env: c.env,
6442
+ options: c.options,
6443
+ address: ABSTRACT_MAINNET_ADDRESSES.governance,
6444
+ abi: governanceAbi,
6445
+ functionName: "castVote",
6446
+ args: [BigInt(c.args.proposalId), supportValue]
6447
+ });
6448
+ return c.ok({
6449
+ proposalId: c.args.proposalId,
6450
+ proposalTitle: proposal.title,
6451
+ support: c.args.support,
6452
+ supportValue,
6453
+ tx: txResult
6454
+ });
6455
+ } catch (error) {
6456
+ if (error instanceof TxError2) {
6457
+ return c.error({
6458
+ code: error.code,
6459
+ message: error.message,
6460
+ retryable: error.code === "NONCE_CONFLICT"
6461
+ });
6462
+ }
6463
+ throw error;
6464
+ }
6465
+ }
6466
+ });
6467
+ governance.command("propose", {
6468
+ description: "Create a new council-originated governance proposal.",
6469
+ hint: "Requires PRIVATE_KEY environment variable for signing.",
6470
+ options: writeOptions.extend({
6471
+ title: z4.string().min(1).describe("Proposal title"),
6472
+ description: z4.string().min(1).describe("Proposal description"),
6473
+ kind: z4.coerce.number().int().nonnegative().max(255).describe("Proposal kind enum value"),
6474
+ category: z4.string().default("governance").describe("Forum category label for the proposal"),
6475
+ "risk-tier": z4.coerce.number().int().nonnegative().max(255).optional().describe("Optional max allowed intent risk tier (default: 0)"),
6476
+ amount: z4.string().optional().describe("Optional treasury amount hint (currently unsupported for intent encoding)"),
6477
+ recipient: z4.string().optional().describe("Optional treasury recipient hint (currently unsupported for intent encoding)")
6478
+ }),
6479
+ env: writeEnv,
6480
+ output: z4.object({
6481
+ proposer: z4.string(),
6482
+ category: z4.string(),
6483
+ kind: z4.number(),
6484
+ title: z4.string(),
6485
+ description: z4.string(),
6486
+ expectedProposalId: z4.number(),
6487
+ tx: txResultOutput3
6488
+ }),
6489
+ examples: [
6490
+ {
6491
+ options: {
6492
+ title: "Increase quorum requirement",
6493
+ description: "Raise quorum from 10% to 12% for governance votes.",
6494
+ kind: 3
6495
+ },
6496
+ description: "Create a governance proposal from a council member account"
6497
+ }
6498
+ ],
6499
+ async run(c) {
6500
+ const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
6501
+ const account = resolveAccount(c.env);
6502
+ if (c.options.amount && !c.options.recipient || !c.options.amount && c.options.recipient) {
6503
+ return c.error({
6504
+ code: "INVALID_PROPOSAL_OPTIONS",
6505
+ message: "Both --amount and --recipient must be provided together when setting transfer hints.",
6506
+ retryable: false
6507
+ });
6508
+ }
6509
+ if (c.options.amount || c.options.recipient) {
6510
+ return c.error({
6511
+ code: "UNSUPPORTED_TRANSFER_INTENT",
6512
+ message: "Transfer intents are not yet supported by `governance propose`. Omit --amount/--recipient and use intent-specific tooling.",
6513
+ retryable: false
6514
+ });
6515
+ }
6516
+ const isCouncilMember = await client.readContract({
6517
+ abi: councilSeatsAbi,
6518
+ address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
6519
+ functionName: "isCouncilMember",
6520
+ args: [account.address]
6521
+ });
6522
+ if (!isCouncilMember) {
6523
+ return c.error({
6524
+ code: "NOT_COUNCIL_MEMBER",
6525
+ message: `Address ${toChecksum(account.address)} is not an active council member and cannot create a council proposal.`,
6526
+ retryable: false
6527
+ });
6528
+ }
6529
+ const proposalCountBefore = await readProposalCount(client);
6530
+ const expectedProposalId = Number(proposalCountBefore) + 1;
6531
+ const proposalInput = {
6532
+ kind: c.options.kind,
6533
+ title: c.options.title,
6534
+ description: c.options.description,
6535
+ intentSteps: [],
6536
+ intentConstraints: {
6537
+ deadline: 0,
6538
+ maxAllowedRiskTier: c.options["risk-tier"] ?? 0
6539
+ },
6540
+ configUpdates: []
6541
+ };
6542
+ try {
6543
+ const txResult = await assemblyWriteTx({
6544
+ env: c.env,
6545
+ options: c.options,
6546
+ address: ABSTRACT_MAINNET_ADDRESSES.forum,
6547
+ abi: forumAbi,
6548
+ functionName: "createCouncilProposal",
6549
+ args: [c.options.category, proposalInput]
6550
+ });
6551
+ return c.ok({
6552
+ proposer: toChecksum(account.address),
6553
+ category: c.options.category,
6554
+ kind: c.options.kind,
6555
+ title: c.options.title,
6556
+ description: c.options.description,
6557
+ expectedProposalId,
6558
+ tx: txResult
6559
+ });
6560
+ } catch (error) {
6561
+ if (error instanceof TxError2) {
6562
+ return c.error({
6563
+ code: error.code,
6564
+ message: error.message,
6565
+ retryable: error.code === "NONCE_CONFLICT"
6566
+ });
6567
+ }
6568
+ throw error;
6569
+ }
6570
+ }
6571
+ });
6572
+ governance.command("queue", {
6573
+ description: "Finalize voting and queue an eligible proposal into timelock.",
6574
+ hint: "Requires PRIVATE_KEY environment variable for signing.",
5851
6575
  args: z4.object({
5852
- proposalId: z4.coerce.number().int().positive().describe("Proposal id (1-indexed)"),
5853
- address: z4.string().describe("Voter address")
6576
+ proposalId: z4.coerce.number().int().positive().describe("Proposal id (1-indexed)")
5854
6577
  }),
5855
- env: env3,
6578
+ options: writeOptions,
6579
+ env: writeEnv,
5856
6580
  output: z4.object({
5857
6581
  proposalId: z4.number(),
5858
- address: z4.string(),
5859
- hasVoted: z4.boolean()
6582
+ proposalTitle: z4.string(),
6583
+ statusBefore: z4.string(),
6584
+ tx: txResultOutput3
5860
6585
  }),
5861
6586
  examples: [
5862
6587
  {
5863
- args: {
5864
- proposalId: 1,
5865
- address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
5866
- },
5867
- description: "Check whether an address already voted"
6588
+ args: { proposalId: 1 },
6589
+ description: "Finalize voting for proposal #1 and queue if passed"
5868
6590
  }
5869
6591
  ],
5870
6592
  async run(c) {
5871
6593
  const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
5872
- const hasVoted = await client.readContract({
5873
- abi: governanceAbi,
5874
- address: ABSTRACT_MAINNET_ADDRESSES.governance,
5875
- functionName: "hasVoted",
5876
- args: [BigInt(c.args.proposalId), c.args.address]
5877
- });
5878
- return c.ok({
5879
- proposalId: c.args.proposalId,
5880
- address: toChecksum(c.args.address),
5881
- hasVoted
5882
- });
6594
+ const proposalCount = await readProposalCount(client);
6595
+ if (c.args.proposalId > Number(proposalCount)) {
6596
+ return c.error({
6597
+ code: "OUT_OF_RANGE",
6598
+ message: `Proposal id ${c.args.proposalId} does not exist (proposalCount: ${proposalCount})`,
6599
+ retryable: false
6600
+ });
6601
+ }
6602
+ const proposal = await readProposalById(client, c.args.proposalId);
6603
+ const status = proposalStatus(proposal.status);
6604
+ if (status.statusCode === PROPOSAL_STATUS_PASSED) {
6605
+ return c.error({
6606
+ code: "ALREADY_QUEUED",
6607
+ message: `Proposal ${c.args.proposalId} is already queued in timelock.`,
6608
+ retryable: false
6609
+ });
6610
+ }
6611
+ if (status.statusCode === PROPOSAL_STATUS_PENDING) {
6612
+ return c.error({
6613
+ code: "PROPOSAL_NOT_QUEUEABLE",
6614
+ message: `Proposal ${c.args.proposalId} is still in deliberation and cannot be queued yet.`,
6615
+ retryable: false
6616
+ });
6617
+ }
6618
+ if (status.statusCode !== PROPOSAL_STATUS_ACTIVE) {
6619
+ return c.error({
6620
+ code: "PROPOSAL_NOT_QUEUEABLE",
6621
+ message: `Proposal ${c.args.proposalId} is ${status.status} and cannot be queued.`,
6622
+ retryable: false
6623
+ });
6624
+ }
6625
+ const latestBlock = await client.getBlock({ blockTag: "latest" });
6626
+ if (latestBlock.timestamp < proposal.voteEndAt) {
6627
+ return c.error({
6628
+ code: "VOTING_STILL_ACTIVE",
6629
+ message: `Proposal ${c.args.proposalId} voting window is still open (ends ${relTime(proposal.voteEndAt)}).`,
6630
+ retryable: false
6631
+ });
6632
+ }
6633
+ try {
6634
+ const txResult = await assemblyWriteTx({
6635
+ env: c.env,
6636
+ options: c.options,
6637
+ address: ABSTRACT_MAINNET_ADDRESSES.governance,
6638
+ abi: governanceAbi,
6639
+ functionName: "finalizeVote",
6640
+ args: [BigInt(c.args.proposalId)]
6641
+ });
6642
+ return c.ok({
6643
+ proposalId: c.args.proposalId,
6644
+ proposalTitle: proposal.title,
6645
+ statusBefore: status.status,
6646
+ tx: txResult
6647
+ });
6648
+ } catch (error) {
6649
+ if (error instanceof TxError2) {
6650
+ return c.error({
6651
+ code: error.code,
6652
+ message: error.message,
6653
+ retryable: error.code === "NONCE_CONFLICT"
6654
+ });
6655
+ }
6656
+ throw error;
6657
+ }
5883
6658
  }
5884
6659
  });
5885
- governance.command("params", {
5886
- description: "Read governance threshold and timing parameters.",
5887
- env: env3,
6660
+ governance.command("execute", {
6661
+ description: "Execute a queued governance proposal after timelock expiry.",
6662
+ hint: "Requires PRIVATE_KEY environment variable for signing.",
6663
+ args: z4.object({
6664
+ proposalId: z4.coerce.number().int().positive().describe("Proposal id (1-indexed)")
6665
+ }),
6666
+ options: writeOptions,
6667
+ env: writeEnv,
5888
6668
  output: z4.object({
5889
- deliberationPeriod: z4.number(),
5890
- votePeriod: z4.number(),
5891
- quorumBps: z4.number(),
5892
- constitutionalDeliberationPeriod: z4.number(),
5893
- constitutionalVotePeriod: z4.number(),
5894
- constitutionalPassBps: z4.number(),
5895
- majorPassBps: z4.number(),
5896
- parameterPassBps: z4.number(),
5897
- significantPassBps: z4.number(),
5898
- significantThresholdBps: z4.number(),
5899
- routineThresholdBps: z4.number(),
5900
- timelockPeriod: z4.number()
6669
+ proposalId: z4.number(),
6670
+ proposalTitle: z4.string(),
6671
+ timelockEndsAt: timestampOutput3,
6672
+ tx: txResultOutput3
5901
6673
  }),
5902
- examples: [{ description: "Inspect governance timing and pass thresholds" }],
6674
+ examples: [
6675
+ {
6676
+ args: { proposalId: 1 },
6677
+ description: "Execute proposal #1 after timelock has expired"
6678
+ }
6679
+ ],
5903
6680
  async run(c) {
5904
6681
  const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
5905
- const getters = [
5906
- "deliberationPeriod",
5907
- "votePeriod",
5908
- "quorumBps",
5909
- "constitutionalDeliberationPeriod",
5910
- "constitutionalVotePeriod",
5911
- "constitutionalPassBps",
5912
- "majorPassBps",
5913
- "parameterPassBps",
5914
- "significantPassBps",
5915
- "significantThresholdBps",
5916
- "routineThresholdBps",
5917
- "timelockPeriod"
5918
- ];
5919
- const values = await client.multicall({
5920
- allowFailure: false,
5921
- contracts: getters.map((name) => ({
5922
- abi: governanceAbi,
6682
+ const proposalCount = await readProposalCount(client);
6683
+ if (c.args.proposalId > Number(proposalCount)) {
6684
+ return c.error({
6685
+ code: "OUT_OF_RANGE",
6686
+ message: `Proposal id ${c.args.proposalId} does not exist (proposalCount: ${proposalCount})`,
6687
+ retryable: false
6688
+ });
6689
+ }
6690
+ const proposal = await readProposalById(client, c.args.proposalId);
6691
+ const status = proposalStatus(proposal.status);
6692
+ if (status.statusCode !== PROPOSAL_STATUS_PASSED) {
6693
+ return c.error({
6694
+ code: "PROPOSAL_NOT_EXECUTABLE",
6695
+ message: `Proposal ${c.args.proposalId} is ${status.status} and cannot be executed.`,
6696
+ retryable: false
6697
+ });
6698
+ }
6699
+ const latestBlock = await client.getBlock({ blockTag: "latest" });
6700
+ if (latestBlock.timestamp < proposal.timelockEndsAt) {
6701
+ return c.error({
6702
+ code: "TIMELOCK_ACTIVE",
6703
+ message: `Proposal ${c.args.proposalId} timelock has not expired yet (ends ${relTime(proposal.timelockEndsAt)}).`,
6704
+ retryable: false
6705
+ });
6706
+ }
6707
+ try {
6708
+ const txResult = await assemblyWriteTx({
6709
+ env: c.env,
6710
+ options: c.options,
5923
6711
  address: ABSTRACT_MAINNET_ADDRESSES.governance,
5924
- functionName: name
5925
- }))
5926
- });
5927
- return c.ok({
5928
- deliberationPeriod: asNum(values[0]),
5929
- votePeriod: asNum(values[1]),
5930
- quorumBps: asNum(values[2]),
5931
- constitutionalDeliberationPeriod: asNum(values[3]),
5932
- constitutionalVotePeriod: asNum(values[4]),
5933
- constitutionalPassBps: asNum(values[5]),
5934
- majorPassBps: asNum(values[6]),
5935
- parameterPassBps: asNum(values[7]),
5936
- significantPassBps: asNum(values[8]),
5937
- significantThresholdBps: asNum(values[9]),
5938
- routineThresholdBps: asNum(values[10]),
5939
- timelockPeriod: asNum(values[11])
5940
- });
6712
+ abi: governanceAbi,
6713
+ functionName: "executeProposal",
6714
+ args: [BigInt(c.args.proposalId)]
6715
+ });
6716
+ return c.ok({
6717
+ proposalId: c.args.proposalId,
6718
+ proposalTitle: proposal.title,
6719
+ timelockEndsAt: timeValue(proposal.timelockEndsAt, c.format),
6720
+ tx: txResult
6721
+ });
6722
+ } catch (error) {
6723
+ if (error instanceof TxError2) {
6724
+ return c.error({
6725
+ code: error.code,
6726
+ message: error.message,
6727
+ retryable: error.code === "NONCE_CONFLICT"
6728
+ });
6729
+ }
6730
+ throw error;
6731
+ }
5941
6732
  }
5942
6733
  });
5943
6734
 
5944
6735
  // src/commands/members.ts
5945
- import { TxError } from "@spectratools/tx-shared";
6736
+ import { TxError as TxError3 } from "@spectratools/tx-shared";
5946
6737
  import { Cli as Cli4, z as z5 } from "incur";
5947
6738
  var DEFAULT_MEMBER_SNAPSHOT_URL = "https://www.theaiassembly.org/api/indexer/members";
5948
6739
  var REGISTERED_EVENT_SCAN_STEP = 100000n;
@@ -6435,7 +7226,7 @@ members.command("register", {
6435
7226
  } : void 0
6436
7227
  );
6437
7228
  } catch (error) {
6438
- if (error instanceof TxError && error.code === "INSUFFICIENT_FUNDS") {
7229
+ if (error instanceof TxError3 && error.code === "INSUFFICIENT_FUNDS") {
6439
7230
  return c.error({
6440
7231
  code: "INSUFFICIENT_FUNDS",
6441
7232
  message: `Insufficient funds to register. Required fee: ${eth(fee)} (${fee} wei). ${error.message}`,
@@ -6486,7 +7277,7 @@ members.command("heartbeat", {
6486
7277
  } : void 0
6487
7278
  );
6488
7279
  } catch (error) {
6489
- if (error instanceof TxError && error.code === "INSUFFICIENT_FUNDS") {
7280
+ if (error instanceof TxError3 && error.code === "INSUFFICIENT_FUNDS") {
6490
7281
  return c.error({
6491
7282
  code: "INSUFFICIENT_FUNDS",
6492
7283
  message: `Insufficient funds for heartbeat. Required fee: ${eth(fee)} (${fee} wei). ${error.message}`,
@@ -6537,7 +7328,7 @@ members.command("renew", {
6537
7328
  } : void 0
6538
7329
  );
6539
7330
  } catch (error) {
6540
- if (error instanceof TxError && error.code === "INSUFFICIENT_FUNDS") {
7331
+ if (error instanceof TxError3 && error.code === "INSUFFICIENT_FUNDS") {
6541
7332
  return c.error({
6542
7333
  code: "INSUFFICIENT_FUNDS",
6543
7334
  message: `Insufficient funds to renew. Required fee: ${eth(fee)} (${fee} wei). ${error.message}`,
@@ -6550,11 +7341,38 @@ members.command("renew", {
6550
7341
  });
6551
7342
 
6552
7343
  // src/commands/treasury.ts
7344
+ import { TxError as TxError4 } from "@spectratools/tx-shared";
6553
7345
  import { Cli as Cli5, z as z6 } from "incur";
7346
+ import { encodeAbiParameters, parseUnits, zeroAddress } from "viem";
6554
7347
  var env5 = z6.object({
6555
7348
  ABSTRACT_RPC_URL: z6.string().optional().describe("Abstract RPC URL override")
6556
7349
  });
6557
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
+ ]);
6558
7376
  var treasury = Cli5.create("treasury", {
6559
7377
  description: "Inspect treasury balances, execution status, and spend controls."
6560
7378
  });
@@ -6677,6 +7495,192 @@ treasury.command("executed", {
6677
7495
  return c.ok({ proposalId: c.args.proposalId, executed });
6678
7496
  }
6679
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
+ });
6680
7684
 
6681
7685
  // src/error-handling.ts
6682
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.8.2",
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.4.2"
36
+ "@spectratools/tx-shared": "0.5.1"
37
37
  },
38
38
  "devDependencies": {
39
39
  "typescript": "5.7.3",