@spectratools/assembly-cli 0.8.1 → 0.9.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 +406 -5
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -5622,6 +5622,7 @@ forum.command("stats", {
5622
5622
  });
5623
5623
 
5624
5624
  // src/commands/governance.ts
5625
+ import { TxError } from "@spectratools/tx-shared";
5625
5626
  import { Cli as Cli3, z as z4 } from "incur";
5626
5627
  var env3 = z4.object({
5627
5628
  ABSTRACT_RPC_URL: z4.string().optional().describe("Abstract RPC URL override")
@@ -5635,6 +5636,14 @@ var proposalStatusLabels = {
5635
5636
  4: "defeated",
5636
5637
  5: "cancelled"
5637
5638
  };
5639
+ var PROPOSAL_STATUS_PENDING = 0;
5640
+ var PROPOSAL_STATUS_ACTIVE = 1;
5641
+ var PROPOSAL_STATUS_PASSED = 2;
5642
+ var supportChoiceToValue = {
5643
+ against: 0,
5644
+ for: 1,
5645
+ abstain: 2
5646
+ };
5638
5647
  function proposalStatus(status) {
5639
5648
  const statusCode = asNum(status);
5640
5649
  return {
@@ -5749,6 +5758,23 @@ function serializeProposal(proposal) {
5749
5758
  description: proposal.description
5750
5759
  };
5751
5760
  }
5761
+ async function readProposalCount(client) {
5762
+ return await client.readContract({
5763
+ abi: governanceAbi,
5764
+ address: ABSTRACT_MAINNET_ADDRESSES.governance,
5765
+ functionName: "proposalCount"
5766
+ });
5767
+ }
5768
+ async function readProposalById(client, proposalId) {
5769
+ return decodeProposal(
5770
+ await client.readContract({
5771
+ abi: governanceAbi,
5772
+ address: ABSTRACT_MAINNET_ADDRESSES.governance,
5773
+ functionName: "proposals",
5774
+ args: [BigInt(proposalId)]
5775
+ })
5776
+ );
5777
+ }
5752
5778
  var governance = Cli3.create("governance", {
5753
5779
  description: "Inspect Assembly governance proposals, votes, and parameters."
5754
5780
  });
@@ -5806,7 +5832,10 @@ governance.command("proposals", {
5806
5832
  description: "Inspect or vote:",
5807
5833
  commands: [
5808
5834
  { command: "governance proposal", args: { id: "<id>" } },
5809
- { command: "governance vote", args: { id: "<id>" } }
5835
+ {
5836
+ command: "governance vote",
5837
+ args: { proposalId: "<id>", support: "<for|against|abstain>" }
5838
+ }
5810
5839
  ]
5811
5840
  }
5812
5841
  }
@@ -5940,9 +5969,381 @@ governance.command("params", {
5940
5969
  });
5941
5970
  }
5942
5971
  });
5972
+ var txResultOutput2 = z4.union([
5973
+ z4.object({
5974
+ status: z4.enum(["success", "reverted"]),
5975
+ hash: z4.string(),
5976
+ blockNumber: z4.number(),
5977
+ gasUsed: z4.string(),
5978
+ from: z4.string(),
5979
+ to: z4.string().nullable(),
5980
+ effectiveGasPrice: z4.string().optional()
5981
+ }),
5982
+ z4.object({
5983
+ status: z4.literal("dry-run"),
5984
+ estimatedGas: z4.string(),
5985
+ simulationResult: z4.unknown()
5986
+ })
5987
+ ]);
5988
+ governance.command("vote", {
5989
+ description: "Cast a governance vote on a proposal.",
5990
+ hint: "Requires PRIVATE_KEY environment variable for signing.",
5991
+ args: z4.object({
5992
+ proposalId: z4.coerce.number().int().positive().describe("Proposal id (1-indexed)"),
5993
+ support: z4.enum(["for", "against", "abstain"]).describe("Vote support: for, against, or abstain")
5994
+ }),
5995
+ options: writeOptions,
5996
+ env: writeEnv,
5997
+ output: z4.object({
5998
+ proposalId: z4.number(),
5999
+ proposalTitle: z4.string(),
6000
+ support: z4.enum(["for", "against", "abstain"]),
6001
+ supportValue: z4.number(),
6002
+ tx: txResultOutput2
6003
+ }),
6004
+ examples: [
6005
+ {
6006
+ args: { proposalId: 1, support: "for" },
6007
+ description: "Vote in favor of proposal #1"
6008
+ },
6009
+ {
6010
+ args: { proposalId: 1, support: "abstain" },
6011
+ options: { "dry-run": true },
6012
+ description: "Simulate casting an abstain vote"
6013
+ }
6014
+ ],
6015
+ async run(c) {
6016
+ const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
6017
+ const account = resolveAccount(c.env);
6018
+ const proposalCount = await readProposalCount(client);
6019
+ if (c.args.proposalId > Number(proposalCount)) {
6020
+ return c.error({
6021
+ code: "OUT_OF_RANGE",
6022
+ message: `Proposal id ${c.args.proposalId} does not exist (proposalCount: ${proposalCount})`,
6023
+ retryable: false
6024
+ });
6025
+ }
6026
+ const proposal = await readProposalById(client, c.args.proposalId);
6027
+ const status = proposalStatus(proposal.status);
6028
+ if (status.statusCode !== PROPOSAL_STATUS_ACTIVE) {
6029
+ return c.error({
6030
+ code: "PROPOSAL_NOT_VOTING",
6031
+ message: `Proposal ${c.args.proposalId} is ${status.status} and cannot be voted right now.`,
6032
+ retryable: false
6033
+ });
6034
+ }
6035
+ const hasVoted = await client.readContract({
6036
+ abi: governanceAbi,
6037
+ address: ABSTRACT_MAINNET_ADDRESSES.governance,
6038
+ functionName: "hasVoted",
6039
+ args: [BigInt(c.args.proposalId), account.address]
6040
+ });
6041
+ if (hasVoted) {
6042
+ return c.error({
6043
+ code: "ALREADY_VOTED",
6044
+ message: `Address ${toChecksum(account.address)} has already voted on proposal ${c.args.proposalId}.`,
6045
+ retryable: false
6046
+ });
6047
+ }
6048
+ const supportValue = supportChoiceToValue[c.args.support];
6049
+ try {
6050
+ const txResult = await assemblyWriteTx({
6051
+ env: c.env,
6052
+ options: c.options,
6053
+ address: ABSTRACT_MAINNET_ADDRESSES.governance,
6054
+ abi: governanceAbi,
6055
+ functionName: "castVote",
6056
+ args: [BigInt(c.args.proposalId), supportValue]
6057
+ });
6058
+ return c.ok({
6059
+ proposalId: c.args.proposalId,
6060
+ proposalTitle: proposal.title,
6061
+ support: c.args.support,
6062
+ supportValue,
6063
+ tx: txResult
6064
+ });
6065
+ } catch (error) {
6066
+ if (error instanceof TxError) {
6067
+ return c.error({
6068
+ code: error.code,
6069
+ message: error.message,
6070
+ retryable: error.code === "NONCE_CONFLICT"
6071
+ });
6072
+ }
6073
+ throw error;
6074
+ }
6075
+ }
6076
+ });
6077
+ governance.command("propose", {
6078
+ description: "Create a new council-originated governance proposal.",
6079
+ hint: "Requires PRIVATE_KEY environment variable for signing.",
6080
+ options: writeOptions.extend({
6081
+ title: z4.string().min(1).describe("Proposal title"),
6082
+ description: z4.string().min(1).describe("Proposal description"),
6083
+ kind: z4.coerce.number().int().nonnegative().max(255).describe("Proposal kind enum value"),
6084
+ category: z4.string().default("governance").describe("Forum category label for the proposal"),
6085
+ "risk-tier": z4.coerce.number().int().nonnegative().max(255).optional().describe("Optional max allowed intent risk tier (default: 0)"),
6086
+ amount: z4.string().optional().describe("Optional treasury amount hint (currently unsupported for intent encoding)"),
6087
+ recipient: z4.string().optional().describe("Optional treasury recipient hint (currently unsupported for intent encoding)")
6088
+ }),
6089
+ env: writeEnv,
6090
+ output: z4.object({
6091
+ proposer: z4.string(),
6092
+ category: z4.string(),
6093
+ kind: z4.number(),
6094
+ title: z4.string(),
6095
+ description: z4.string(),
6096
+ expectedProposalId: z4.number(),
6097
+ tx: txResultOutput2
6098
+ }),
6099
+ examples: [
6100
+ {
6101
+ options: {
6102
+ title: "Increase quorum requirement",
6103
+ description: "Raise quorum from 10% to 12% for governance votes.",
6104
+ kind: 3
6105
+ },
6106
+ description: "Create a governance proposal from a council member account"
6107
+ }
6108
+ ],
6109
+ async run(c) {
6110
+ const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
6111
+ const account = resolveAccount(c.env);
6112
+ if (c.options.amount && !c.options.recipient || !c.options.amount && c.options.recipient) {
6113
+ return c.error({
6114
+ code: "INVALID_PROPOSAL_OPTIONS",
6115
+ message: "Both --amount and --recipient must be provided together when setting transfer hints.",
6116
+ retryable: false
6117
+ });
6118
+ }
6119
+ if (c.options.amount || c.options.recipient) {
6120
+ return c.error({
6121
+ code: "UNSUPPORTED_TRANSFER_INTENT",
6122
+ message: "Transfer intents are not yet supported by `governance propose`. Omit --amount/--recipient and use intent-specific tooling.",
6123
+ retryable: false
6124
+ });
6125
+ }
6126
+ const isCouncilMember = await client.readContract({
6127
+ abi: councilSeatsAbi,
6128
+ address: ABSTRACT_MAINNET_ADDRESSES.councilSeats,
6129
+ functionName: "isCouncilMember",
6130
+ args: [account.address]
6131
+ });
6132
+ if (!isCouncilMember) {
6133
+ return c.error({
6134
+ code: "NOT_COUNCIL_MEMBER",
6135
+ message: `Address ${toChecksum(account.address)} is not an active council member and cannot create a council proposal.`,
6136
+ retryable: false
6137
+ });
6138
+ }
6139
+ const proposalCountBefore = await readProposalCount(client);
6140
+ const expectedProposalId = Number(proposalCountBefore) + 1;
6141
+ const proposalInput = {
6142
+ kind: c.options.kind,
6143
+ title: c.options.title,
6144
+ description: c.options.description,
6145
+ intentSteps: [],
6146
+ intentConstraints: {
6147
+ deadline: 0,
6148
+ maxAllowedRiskTier: c.options["risk-tier"] ?? 0
6149
+ },
6150
+ configUpdates: []
6151
+ };
6152
+ try {
6153
+ const txResult = await assemblyWriteTx({
6154
+ env: c.env,
6155
+ options: c.options,
6156
+ address: ABSTRACT_MAINNET_ADDRESSES.forum,
6157
+ abi: forumAbi,
6158
+ functionName: "createCouncilProposal",
6159
+ args: [c.options.category, proposalInput]
6160
+ });
6161
+ return c.ok({
6162
+ proposer: toChecksum(account.address),
6163
+ category: c.options.category,
6164
+ kind: c.options.kind,
6165
+ title: c.options.title,
6166
+ description: c.options.description,
6167
+ expectedProposalId,
6168
+ tx: txResult
6169
+ });
6170
+ } catch (error) {
6171
+ if (error instanceof TxError) {
6172
+ return c.error({
6173
+ code: error.code,
6174
+ message: error.message,
6175
+ retryable: error.code === "NONCE_CONFLICT"
6176
+ });
6177
+ }
6178
+ throw error;
6179
+ }
6180
+ }
6181
+ });
6182
+ governance.command("queue", {
6183
+ description: "Finalize voting and queue an eligible proposal into timelock.",
6184
+ hint: "Requires PRIVATE_KEY environment variable for signing.",
6185
+ args: z4.object({
6186
+ proposalId: z4.coerce.number().int().positive().describe("Proposal id (1-indexed)")
6187
+ }),
6188
+ options: writeOptions,
6189
+ env: writeEnv,
6190
+ output: z4.object({
6191
+ proposalId: z4.number(),
6192
+ proposalTitle: z4.string(),
6193
+ statusBefore: z4.string(),
6194
+ tx: txResultOutput2
6195
+ }),
6196
+ examples: [
6197
+ {
6198
+ args: { proposalId: 1 },
6199
+ description: "Finalize voting for proposal #1 and queue if passed"
6200
+ }
6201
+ ],
6202
+ async run(c) {
6203
+ const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
6204
+ const proposalCount = await readProposalCount(client);
6205
+ if (c.args.proposalId > Number(proposalCount)) {
6206
+ return c.error({
6207
+ code: "OUT_OF_RANGE",
6208
+ message: `Proposal id ${c.args.proposalId} does not exist (proposalCount: ${proposalCount})`,
6209
+ retryable: false
6210
+ });
6211
+ }
6212
+ const proposal = await readProposalById(client, c.args.proposalId);
6213
+ const status = proposalStatus(proposal.status);
6214
+ if (status.statusCode === PROPOSAL_STATUS_PASSED) {
6215
+ return c.error({
6216
+ code: "ALREADY_QUEUED",
6217
+ message: `Proposal ${c.args.proposalId} is already queued in timelock.`,
6218
+ retryable: false
6219
+ });
6220
+ }
6221
+ if (status.statusCode === PROPOSAL_STATUS_PENDING) {
6222
+ return c.error({
6223
+ code: "PROPOSAL_NOT_QUEUEABLE",
6224
+ message: `Proposal ${c.args.proposalId} is still in deliberation and cannot be queued yet.`,
6225
+ retryable: false
6226
+ });
6227
+ }
6228
+ if (status.statusCode !== PROPOSAL_STATUS_ACTIVE) {
6229
+ return c.error({
6230
+ code: "PROPOSAL_NOT_QUEUEABLE",
6231
+ message: `Proposal ${c.args.proposalId} is ${status.status} and cannot be queued.`,
6232
+ retryable: false
6233
+ });
6234
+ }
6235
+ const latestBlock = await client.getBlock({ blockTag: "latest" });
6236
+ if (latestBlock.timestamp < proposal.voteEndAt) {
6237
+ return c.error({
6238
+ code: "VOTING_STILL_ACTIVE",
6239
+ message: `Proposal ${c.args.proposalId} voting window is still open (ends ${relTime(proposal.voteEndAt)}).`,
6240
+ retryable: false
6241
+ });
6242
+ }
6243
+ try {
6244
+ const txResult = await assemblyWriteTx({
6245
+ env: c.env,
6246
+ options: c.options,
6247
+ address: ABSTRACT_MAINNET_ADDRESSES.governance,
6248
+ abi: governanceAbi,
6249
+ functionName: "finalizeVote",
6250
+ args: [BigInt(c.args.proposalId)]
6251
+ });
6252
+ return c.ok({
6253
+ proposalId: c.args.proposalId,
6254
+ proposalTitle: proposal.title,
6255
+ statusBefore: status.status,
6256
+ tx: txResult
6257
+ });
6258
+ } catch (error) {
6259
+ if (error instanceof TxError) {
6260
+ return c.error({
6261
+ code: error.code,
6262
+ message: error.message,
6263
+ retryable: error.code === "NONCE_CONFLICT"
6264
+ });
6265
+ }
6266
+ throw error;
6267
+ }
6268
+ }
6269
+ });
6270
+ governance.command("execute", {
6271
+ description: "Execute a queued governance proposal after timelock expiry.",
6272
+ hint: "Requires PRIVATE_KEY environment variable for signing.",
6273
+ args: z4.object({
6274
+ proposalId: z4.coerce.number().int().positive().describe("Proposal id (1-indexed)")
6275
+ }),
6276
+ options: writeOptions,
6277
+ env: writeEnv,
6278
+ output: z4.object({
6279
+ proposalId: z4.number(),
6280
+ proposalTitle: z4.string(),
6281
+ timelockEndsAt: timestampOutput3,
6282
+ tx: txResultOutput2
6283
+ }),
6284
+ examples: [
6285
+ {
6286
+ args: { proposalId: 1 },
6287
+ description: "Execute proposal #1 after timelock has expired"
6288
+ }
6289
+ ],
6290
+ async run(c) {
6291
+ const client = createAssemblyPublicClient(c.env.ABSTRACT_RPC_URL);
6292
+ const proposalCount = await readProposalCount(client);
6293
+ if (c.args.proposalId > Number(proposalCount)) {
6294
+ return c.error({
6295
+ code: "OUT_OF_RANGE",
6296
+ message: `Proposal id ${c.args.proposalId} does not exist (proposalCount: ${proposalCount})`,
6297
+ retryable: false
6298
+ });
6299
+ }
6300
+ const proposal = await readProposalById(client, c.args.proposalId);
6301
+ const status = proposalStatus(proposal.status);
6302
+ if (status.statusCode !== PROPOSAL_STATUS_PASSED) {
6303
+ return c.error({
6304
+ code: "PROPOSAL_NOT_EXECUTABLE",
6305
+ message: `Proposal ${c.args.proposalId} is ${status.status} and cannot be executed.`,
6306
+ retryable: false
6307
+ });
6308
+ }
6309
+ const latestBlock = await client.getBlock({ blockTag: "latest" });
6310
+ if (latestBlock.timestamp < proposal.timelockEndsAt) {
6311
+ return c.error({
6312
+ code: "TIMELOCK_ACTIVE",
6313
+ message: `Proposal ${c.args.proposalId} timelock has not expired yet (ends ${relTime(proposal.timelockEndsAt)}).`,
6314
+ retryable: false
6315
+ });
6316
+ }
6317
+ try {
6318
+ const txResult = await assemblyWriteTx({
6319
+ env: c.env,
6320
+ options: c.options,
6321
+ address: ABSTRACT_MAINNET_ADDRESSES.governance,
6322
+ abi: governanceAbi,
6323
+ functionName: "executeProposal",
6324
+ args: [BigInt(c.args.proposalId)]
6325
+ });
6326
+ return c.ok({
6327
+ proposalId: c.args.proposalId,
6328
+ proposalTitle: proposal.title,
6329
+ timelockEndsAt: timeValue(proposal.timelockEndsAt, c.format),
6330
+ tx: txResult
6331
+ });
6332
+ } catch (error) {
6333
+ if (error instanceof TxError) {
6334
+ return c.error({
6335
+ code: error.code,
6336
+ message: error.message,
6337
+ retryable: error.code === "NONCE_CONFLICT"
6338
+ });
6339
+ }
6340
+ throw error;
6341
+ }
6342
+ }
6343
+ });
5943
6344
 
5944
6345
  // src/commands/members.ts
5945
- import { TxError } from "@spectratools/tx-shared";
6346
+ import { TxError as TxError2 } from "@spectratools/tx-shared";
5946
6347
  import { Cli as Cli4, z as z5 } from "incur";
5947
6348
  var DEFAULT_MEMBER_SNAPSHOT_URL = "https://www.theaiassembly.org/api/indexer/members";
5948
6349
  var REGISTERED_EVENT_SCAN_STEP = 100000n;
@@ -6435,7 +6836,7 @@ members.command("register", {
6435
6836
  } : void 0
6436
6837
  );
6437
6838
  } catch (error) {
6438
- if (error instanceof TxError && error.code === "INSUFFICIENT_FUNDS") {
6839
+ if (error instanceof TxError2 && error.code === "INSUFFICIENT_FUNDS") {
6439
6840
  return c.error({
6440
6841
  code: "INSUFFICIENT_FUNDS",
6441
6842
  message: `Insufficient funds to register. Required fee: ${eth(fee)} (${fee} wei). ${error.message}`,
@@ -6486,7 +6887,7 @@ members.command("heartbeat", {
6486
6887
  } : void 0
6487
6888
  );
6488
6889
  } catch (error) {
6489
- if (error instanceof TxError && error.code === "INSUFFICIENT_FUNDS") {
6890
+ if (error instanceof TxError2 && error.code === "INSUFFICIENT_FUNDS") {
6490
6891
  return c.error({
6491
6892
  code: "INSUFFICIENT_FUNDS",
6492
6893
  message: `Insufficient funds for heartbeat. Required fee: ${eth(fee)} (${fee} wei). ${error.message}`,
@@ -6537,7 +6938,7 @@ members.command("renew", {
6537
6938
  } : void 0
6538
6939
  );
6539
6940
  } catch (error) {
6540
- if (error instanceof TxError && error.code === "INSUFFICIENT_FUNDS") {
6941
+ if (error instanceof TxError2 && error.code === "INSUFFICIENT_FUNDS") {
6541
6942
  return c.error({
6542
6943
  code: "INSUFFICIENT_FUNDS",
6543
6944
  message: `Insufficient funds to renew. Required fee: ${eth(fee)} (${fee} wei). ${error.message}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectratools/assembly-cli",
3
- "version": "0.8.1",
3
+ "version": "0.9.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.1"
36
+ "@spectratools/tx-shared": "0.4.3"
37
37
  },
38
38
  "devDependencies": {
39
39
  "typescript": "5.7.3",