Skip to content

Add multisig actions and tests - #45

Open
blasrodri wants to merge 1 commit into
XPRNetwork:masterfrom
blasrodri:feature/multisig-actions-tests
Open

blasrodri wants to merge 1 commit into
XPRNetwork:masterfrom
blasrodri:feature/multisig-actions-tests

Conversation

@blasrodri

Copy link
Copy Markdown

Summary

  • Add multisig approve, unapprove, invalidate, cancel, execute, and propose flows.
  • Support explicit signing and requested permissions, proposal hashes, and cross-account cancellation.
  • Validate action JSON and authorization values, including per-permission signer deduplication.
  • Update generated command documentation.

Verification

  • 19 tests passing.
  • TypeScript compilation passes.
  • Build passes.
  • Focused ESLint passes.

@blasrodri
blasrodri force-pushed the feature/multisig-actions-tests branch 2 times, most recently from 341855d to 32336ec Compare September 25, 2026 12:53
@blasrodri

Copy link
Copy Markdown
Author

@paulgnz can you review it please?

@paulgnz paulgnz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks Blas. Checked it out and ran it locally:

  • tsc -b builds clean.
  • 19/19 tests pass on Node 18. (The boilerplate test takes ~6s, so it needs a longer mocha timeout than the default 2s.)
  • All six actions match the live eosio.msig ABI on XPR mainnet, including the proposal_hash binary extension on approve and invalidate.
  • The requested-signer dedupe by actor@permission fixes a real bug: the old code dropped a second required permission from the same account.

Two small things before merging:

  1. msig approve link: "View Proposal" uses the approver (authorization.actor) instead of the proposer, so it points at the wrong proposal page. It was already like that before this PR, but it's a one-word fix while you're in there: args.proposer.
  2. test/commands/network.test.ts: it now asserts "chain": "proton", which depends on whatever network is saved in the local config. It fails on any machine set to proton-test. Could you stub the config or just assert on Current Network:?

FYI, not from this PR: @oclif/test crashes on load under Node 22 (Cannot read properties of undefined (reading 'filename')), so the suite only runs on Node ≤20.

Happy to approve after 1 and 2.

@blasrodri

blasrodri commented Sep 28, 2026 •

Copy link
Copy Markdown
Author

Addressed both review comments: fixed the approve proposal link to use the proposer and made the network test independent of local config. @paulgnz

@blasrodri
blasrodri force-pushed the feature/multisig-actions-tests branch from acfc4ab to 45186af Compare September 28, 2026 20:06
@paulgnz

paulgnz commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Thanks! Both fixes look right. One catch: the new assertion in test/commands/msig.test.ts reads ctx.stdout, but that test doesn't call .stdout(), so TypeScript fails to compile the file and the whole suite stops before running:

test/commands/msig.test.ts(83,16): error TS2339: Property 'stdout' does not exist on type ...

Adding .stdout() to that chain fixes it. I checked locally and all 19 tests pass:

  test
  .stub(network, 'transact', capture(transaction))
  .stdout()
  .command(['msig:approve', 'proposer', 'proposal', 'signer'])

I'll approve once that's in.

@blasrodri

blasrodri commented Sep 29, 2026 •

Copy link
Copy Markdown
Author

Added .stdout() to the msig:approve test chain before reading ctx.stdout. tsc -b, targeted lint, and git diff --check pass. The full suite could not run in this environment because the installed Node v26.7.0 triggers the known @oclif/test loader incompatibility. @paulgnz

@paulgnz paulgnz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Thanks, the .stdout() fix did it.

Verified on a fresh checkout of 6c883e1 with Node 18:

  • tsc -b builds.
  • 19/19 tests pass (mocha --require ts-node/register "test/**/*.test.ts").
  • eslint is clean on all changed files.
  • approve, cancel, exec and propose take the same arguments as on master, so existing usage doesn't break.

Two things for a follow-up, both already on master and not caused by this PR:

  1. npm test doesn't run the suite. Current mocha ignores test/mocha.opts, so the .ts files never load (ERR_UNKNOWN_FILE_EXTENSION). Moving those options into a .mocharc.json (with require: ts-node/register) should fix it.
  2. engines.node says >=14.0.0, but the code uses node: imports, which need 16+. On very new Node (26) @oclif/test also fails to load, so for now we should document Node 18/20 as the tested versions.

@squdgy

squdgy commented Sep 30, 2026

Copy link
Copy Markdown

Nice addition of functionality and tests. This should clear one item in the TODO file.

That being said, I've run this through Gemini 3.8 Flash (via Cursor) which has feedback I'm posting below. I understand that AI model output is not always on point, so please take a look and let me know what you think:


Must Be Addressed in This Branch Before Merging

These are critical correctness bugs, protocol-level consensus constraints, and regressions that directly affect the code modified in this branch. If merged as-is, they will cause transactions to fail on-chain or degrade production operations.


1. Canonical Sorting of requested Permissions in msig:propose

  • Severity: High (Protocol Rejection)
  • The Problem: The on-chain eosio.msig system contract strictly enforces canonical ordering of requested permissions:
    for ( size_t i = 1; i < requested.size(); ++i ) {
       check( requested[i-1] < requested[i], "requested permissions must be sorted and unique" );
    }
    In PR Add multisig actions and tests #45, requested is collected into a JavaScript Map and converted via [...requested.values()]. This preserves JavaScript insertion order. If a multi-action transaction lists authorizations in any non-alphabetical or non-canonical order (e.g. charlie@active then alice@active), the eosio.msig::propose transaction will be rejected by the blockchain with:
    "requested permissions must be sorted and unique".
  • Required Fix: Sort the requested array before submitting the transaction:
    const sortedRequested = [...requested.values()].sort((a, b) => {
      if (a.actor === b.actor) {
        return a.permission.localeCompare(b.permission)
      }
      return a.actor.localeCompare(b.actor)
    })

2. Fallback to Action Authorizations When getRequiredAccounts Returns Empty

  • Severity: High (On-Chain Failure)
  • The Problem: In MultisigPropose, requested accounts are discovered solely through:
    const requiredAccounts = await Promise.all(actionAuthorizations.map(async level => {
      return network.protonApi.getRequiredAccounts(parsedLevel.actor, parsedLevel.permission)
    }))
    If network.protonApi.getRequiredAccounts returns an empty array (which occurs when an account permission is controlled purely by keys rather than account weights/guardians, or if the API endpoint fails to resolve the hierarchy), requested becomes [].
    The eosio.msig contract requires:
    check( requested.size() > 0, "must have at least one requested permission" );
    The transaction will fail immediately on-chain.
  • Required Fix: If getRequiredAccounts yields no child/guardian accounts for an authorization, fallback to including the action's own { actor, permission } in requested.

3. Capture Transaction ID & Explorer Trace URL in msig:exec

  • Severity: Medium-High (Operational Blindness)
  • The Problem: msig:exec triggers the execution of the actual payload on the blockchain. The PR discards the return value of await network.transact(...) and only outputs a generic text string:
    await network.transact({ ... })
    CliUx.ux.log(`Multisig ${args.proposal} successfully executed.`)
    On XPR Network, exec can succeed at the eosio.msig level while triggering complex inline action traces. Operators and automated scripts need the actual execution transaction_id and explorer link.
  • Required Fix: Capture the transaction result and log the link, matching the pattern used in contract:set and action:
    const res = await network.transact({ ... }) as any
    CliUx.ux.log(`Multisig ${args.proposal} successfully executed.`)
    if (res?.transaction_id) {
      CliUx.ux.url('View TX', `${getExplorer()}/tx/${res.transaction_id}?tab=traces`)
    }

4. Restore Standardized Antelope Error Handling (parseDetailsError)

  • Severity: Medium (Regression)
  • The Problem: The PR removed try / catch blocks from all msig commands without adding oclif's catch(e) handler. In commands like action or account:create-funded, the CLI delegates errors to parseDetailsError(e) (src/utils/detailsError.ts), which parses Antelope RPC error structures (details[0].message), formats them with colors, and displays actionable hints (e.g. CPU/NET limits, insufficient RAM, missing authority).
    Without this, any on-chain assertion failure (e.g., proposal already exists, already marked as approved, transaction expired) dumps an unformatted stack trace or raw JSON object to stderr.
  • Required Fix: Implement catch across the msig commands:
    async catch(e: Error | any) {
      parseDetailsError(e)
    }

5. Sanitize and Validate --proposal-hash

  • Severity: Low-Medium (Input Safety)
  • The Problem: MultisigApprove accepts --proposal-hash as raw input and passes it directly to @proton/js. If a user passes an invalid hex string, incorrect byte length (not 32 bytes / 64 hex characters), or a 0x-prefixed string, the serialization crashes deep in the binary pack buffer with a cryptic exception.
  • Required Fix: Strip any leading 0x and validate that it matches ^[0-9a-fA-F]{64}$ before assembling action data.

Can Be Done in Subsequent Pull Request(s)

These are larger feature additions, UX extensions, or non-blocking enhancements that expand functionality but do not block the core correctness of this PR.


  1. New Proposal Inspection Commands (msig:get / msig:review)

    • Scope: A new command that queries the eosio.msig multi-index tables (proposal, approvals, approvals2), uncompresses/deserializes the packed transaction, displays the actions to be executed, shows current approvals vs. remaining threshold requirements, and computes the sha256 proposal hash locally.
    • Why defer: This is a net-new feature (read-only query) rather than a bug in the transaction dispatching logic.
  2. File-Based Action Payloads for msig:propose (--file / @path)

    • Scope: Allowing users to pass a path to a JSON file (e.g. proton msig:propose prop1 ./actions.json auth@active or @actions.json) instead of requiring the entire JSON payload to be passed as an in-line shell argument.
    • Why defer: Useful for developer ergonomics with large payloads, but passing stringified JSON already works and is covered by tests.
  3. Explicit --requested Override Flag in msig:propose

    • Scope: Allowing users to bypass automatic dependency calculation entirely by passing a custom comma-separated list of required signers (e.g. --requested alice@active,bob@active,carol@active).
    • Why defer: The automatic calculation covers the 90% common case; manual override can be introduced as an optional advanced flag later.
  4. Argument Signature Alignment for msig:cancel

    • Scope: Re-aligning msig:cancel to take [proposer] [proposalName] [auth] positionally (matching approve, unapprove, and exec), while maintaining the --proposer flag for backwards compatibility.
    • Why defer: The current --proposer flag works and maintains backward compatibility with older scripts. Changing CLI argument positions requires a deprecation cycle.
  5. Configurable TAPOS and Resource Headers for Proposed Transactions

    • Scope: Adding flags to msig:propose for max_cpu_usage_ms, max_net_usage_words, and custom reference block parameters.
    • Why defer: The current defaults generated by generateTransactionSettings work for standard Antelope transactions. Custom resource constraints are rarely needed except in specialized edge cases.

@blasrodri
blasrodri force-pushed the feature/multisig-actions-tests branch 2 times, most recently from 149af68 to 87cb842 Compare September 30, 2026 15:43
@blasrodri
blasrodri force-pushed the feature/multisig-actions-tests branch from 87cb842 to 8419426 Compare September 30, 2026 15:49
@blasrodri

blasrodri commented Sep 30, 2026 •

Copy link
Copy Markdown
Author

Thanks for the detailed review, @squdgy . I went through all five points and addressed them in commit 8419426:

  • msig:propose now sorts requested permissions canonically by actor and permission.
  • Empty results from getRequiredAccounts fall back to the action authorization.
  • msig:exec now prints the explorer trace URL using the returned transaction ID.
  • All msig commands now use the standard parseDetailsError handler.
  • --proposal-hash now strips an optional 0x prefix and requires exactly 64 hexadecimal characters.

I added regression coverage for sorting, fallback resolution, hash normalization and validation, formatted errors, and the exec transaction URL.

I also rebuilt the branch from master so it contains only the multisig implementation, tests, documentation, and the small test portability fixes. The deferred feature suggestions remain deferred.

Verification on Node 18.20.8:

  • 21 tests passing
  • TypeScript build passing
  • ESLint passing
  • Test TypeScript checking passing
  • git diff --check passing

The repository’s older oclif test stack still does not load under Node 26, so Node 18 is the runtime used for the full test run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants