@spectratools/assembly-cli 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +636 -33
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -54,7 +54,7 @@ Current `assembly-cli` write commands require `PRIVATE_KEY`, but tx-shared suppo
54
54
 
55
55
  - private key (`PRIVATE_KEY`)
56
56
  - keystore (`--keystore` + `--password` or `KEYSTORE_PASSWORD`)
57
- - Privy (`PRIVY_APP_ID`, `PRIVY_WALLET_ID`, `PRIVY_AUTHORIZATION_KEY`; implementation tracked in [#117](https://github.com/spectra-the-bot/spectra-tools/issues/117))
57
+ - Privy (`--privy` and env `PRIVY_APP_ID`, `PRIVY_WALLET_ID`, `PRIVY_AUTHORIZATION_KEY`; optional `--privy-api-url` / `PRIVY_API_URL`)
58
58
 
59
59
  ## Command Group Intent Summary
60
60
 
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,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 txResultOutput2 = z4.union([
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: txResultOutput2
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 TxError) {
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: txResultOutput2
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 TxError) {
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: txResultOutput2
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 TxError) {
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: txResultOutput2
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 TxError) {
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 TxError2 } from "@spectratools/tx-shared";
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 TxError2 && error.code === "INSUFFICIENT_FUNDS") {
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 TxError2 && error.code === "INSUFFICIENT_FUNDS") {
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 TxError2 && error.code === "INSUFFICIENT_FUNDS") {
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.9.0",
3
+ "version": "0.10.1",
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.3"
36
+ "@spectratools/tx-shared": "0.5.2"
37
37
  },
38
38
  "devDependencies": {
39
39
  "typescript": "5.7.3",