Skip to content

feat(proposals): Support custom proposals#268

Open
zeljkoX wants to merge 4 commits into
mainfrom
custom-proposals
Open

feat(proposals): Support custom proposals#268
zeljkoX wants to merge 4 commits into
mainfrom
custom-proposals

Conversation

@zeljkoX
Copy link
Copy Markdown
Collaborator

@zeljkoX zeljkoX commented Jun 2, 2026

Summary by CodeRabbit

  • New Features

    • Support for arbitrary non-empty custom proposal types alongside built-in types.
    • New producer flow to create, prepare (validate binding/advice), and submit integration-built custom proposals; custom proposals still participate in signing, listing, and export/import.
  • Documentation

    • Full SDK and spec docs plus examples demonstrating custom proposal producer and cosigner workflows.
  • Tests

    • Added tests for custom proposal handling and label normalization.

@zeljkoX zeljkoX requested a review from Copilot June 2, 2026 11:35
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 76823804-b746-4480-9718-57d3539b22e7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)

Walkthrough

This pull request implements a symmetric "custom proposal producer" API across Rust and TypeScript Miden multisig SDKs, enabling integrations to create and execute arbitrary multisig proposal types with integration-owned transaction recipes while the SDK enforces binding checks and commitment validation.

Changes

Custom Proposal Producer API

Layer / File(s) Summary
Core data model
crates/miden-multisig-client/src/proposal.rs, packages/miden-multisig-client/src/types/proposal.ts, crates/miden-multisig-client/src/payload.rs, crates/miden-multisig-client/src/lib.rs
TransactionType is extended with #[non_exhaustive] Custom variant; ProposalMetadata adds CustomProposalMetadata for both Rust and TS; BUILTIN_PROPOSAL_TYPES allowlist defined to reject custom labels using reserved names.
Rust SDK Producer API
crates/miden-multisig-client/src/client/proposals.rs, crates/miden-multisig-client/src/transaction/mod.rs
New public methods propose_custom (validates label, deserializes artifact, derives commitment, creates custom proposal), prepare_custom_execution (validates binding, assembles advice), and submit_transaction (submits on-chain).
Rust supporting logic
crates/miden-multisig-client/src/error.rs, crates/miden-multisig-client/src/account.rs, crates/miden-multisig-client/src/client/helpers.rs, crates/miden-multisig-client/src/execution.rs, crates/miden-multisig-client/src/transaction/builder.rs, crates/miden-multisig-client/src/export.rs
Adds UnsupportedTransactionType error variant, handles Custom in threshold resolution and binding verification, updates builders/execution to reject custom with clear messages, adds roundtrip export test.
Server validation
crates/server/src/services/mod.rs, crates/server/src/services/push_delta_proposal.rs
Removes VALID_PROPOSAL_TYPES allowlist; replace with whitespace-trim and non-empty checks; server now accepts any non-empty proposal_type.
TypeScript types
packages/guardian-client/src/server-types.ts, packages/guardian-client/src/types.ts, packages/guardian-client/src/conversion.ts, packages/guardian-client/src/conversion.test.ts
ServerProposalType and ProposalType use (string & {}) catch-all instead of explicit 'unknown' literal; conversion preserves arbitrary labels.
TypeScript metadata codec
packages/miden-multisig-client/src/proposal/metadata.ts, packages/miden-multisig-client/src/proposal/metadata.test.ts
Codec maps unmodeled types to custom bucket while preserving rawProposalType; toGuardian emits original server label; validate accepts custom metadata as opaque.
TypeScript SDK Producer API
packages/miden-multisig-client/src/multisig.ts
New public methods proposeCustom (validates type, deserializes artifact, creates custom proposal), prepareCustomExecution (validates binding, assembles advice), submitTransaction (submits via MidenClient).
Rust example: actions
examples/demo/src/actions/proposal_management.rs
Menu-driven handlers action_create_custom_proposal and action_execute_custom_proposal gather P2ID parameters, build deterministic request, cache recipe for replay, prepare/inject advice, submit.
Rust example: state
examples/demo/src/state.rs
SessionState extended with custom_recipes HashMap to cache proposal inputs keyed by normalized proposal id.
TypeScript example: browser API
examples/_shared/multisig-browser/src/multisigApi.ts, examples/_shared/multisig-browser/package.json
CustomProposalRecipe interface; helpers buildRequestFromRecipe, createCustomP2idProposal, prepareAndSubmitCustomProposal with nonce-advancement retry logic.
TypeScript example: smoke-web UI
examples/smoke-web/src/App.tsx
App extended with custom proposal form, handlers for create/execute, conditional "Execute (custom)" button rendering, console example.
TypeScript example: smoke-web harness
examples/smoke-web/src/smokeHarness.ts
SmokeHarness extended with CreateCustomProposalInput/ExecuteCustomProposalInput, customRecipesRef map, createCustomProposal/executeCustomProposal callbacks managing recipe lifecycle.
Documentation & specifications
.gitignore, crates/miden-multisig-client/README.md, docs/MULTISIG_SDK.md, packages/miden-multisig-client/README.md, examples/smoke-web/README.md, spec/api.md, speckit/features/008-custom-proposal-producer/*
Type normalization rules, producer workflow, binding mechanics, and three new speckit documents defining requirements, data model, SDK contracts, success criteria, and error handling for custom proposals.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

enhancement

Suggested reviewers

  • tirumerla
  • onurinanc

Poem

🐰 Custom proposals hop along the SDK,
No blueprints needed, recipes in the sack,
Binding checks guard the multisig way,
Through Rust and TypeScript, come what may!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Support custom proposals' accurately describes the main change: adding support for custom proposal types across the SDK, with comprehensive implementation including new APIs, validation, and documentation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch custom-proposals

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends Guardian + the Rust/TypeScript multisig SDKs to support arbitrary (server-defined) proposal_type labels by bucketing unmodeled types as custom while preserving the original label for display, and introduces a producer-side “bring-your-own-transaction” execution-prep flow for custom proposals.

Changes:

  • Remove the server-side allowlist for metadata.proposal_type (now any non-empty string is accepted) and add regression tests.
  • Update SDK typing/metadata codecs to preserve unmodeled proposal labels as custom + rawProposalType (TS) / TransactionType::Custom + proposal_type (Rust).
  • Add producer APIs + harness/docs/examples for proposing custom artifacts and preparing execution advice (cosigner sigs + GUARDIAN ack) without the SDK building/submitting the custom transaction itself.

Reviewed changes

Copilot reviewed 37 out of 39 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
speckit/features/008-custom-proposal-producer/spec.md New feature spec for custom proposal producer API and execution-prep flow.
speckit/features/008-custom-proposal-producer/data-model.md SDK-level data model and invariants for custom artifacts/labels/binding.
speckit/features/008-custom-proposal-producer/contracts/sdk-api.md Rust/TS SDK surface + behavioral/error contract for producer APIs.
spec/api.md Document server acceptance of arbitrary proposal_type labels.
packages/miden-multisig-client/src/types/proposal.ts Define closed modeled proposal types plus custom; add rawProposalType.
packages/miden-multisig-client/src/proposal/metadata.ts Round-trip arbitrary labels as custom while preserving raw label.
packages/miden-multisig-client/src/proposal/metadata.test.ts Add tests for custom/raw-label codec behavior.
packages/miden-multisig-client/src/multisig/proposal/parser.ts Remove legacy parser implementation (now handled elsewhere).
packages/miden-multisig-client/src/multisig/proposal/execution.ts Block generic execution for custom proposals with a clearer error.
packages/miden-multisig-client/src/multisig.ts Add proposeCustom, prepareCustomExecution, and submitTransaction.
packages/miden-multisig-client/README.md Document custom proposal types + producer workflow in TS SDK.
packages/guardian-client/src/types.ts Allow arbitrary proposalType strings in the wire contract.
packages/guardian-client/src/server-types.ts Allow arbitrary proposal_type strings in server wire types.
packages/guardian-client/src/conversion.ts Preserve arbitrary proposal_type when converting to server payload.
packages/guardian-client/src/conversion.test.ts Add coverage that arbitrary proposal_type is preserved.
examples/smoke-web/src/smokeHarness.ts Add create/execute custom proposal commands and in-session recipe cache.
examples/smoke-web/src/App.tsx UI controls for proposing/executing a custom (producer) proposal.
examples/smoke-web/README.md Document smoke-web custom proposal producer flow usage.
examples/demo/src/state.rs Cache producer recipes in demo session state for custom execution.
examples/demo/src/actions/proposal_management.rs Add CLI actions to create/execute custom proposals in Rust demo.
examples/_shared/multisig-browser/src/multisigApi.ts Add shared helper APIs for custom proposal recipe + prepare/submit.
examples/_shared/multisig-browser/package.json Bump @miden-sdk/miden-sdk to 0.14.5.
docs/MULTISIG_SDK.md Document custom proposal bucket + symmetric producer API behavior.
crates/server/src/services/push_delta_proposal.rs Add server test ensuring custom proposal_type is accepted.
crates/server/src/services/mod.rs Remove proposal_type allowlist; reject only empty/whitespace labels.
crates/miden-multisig-client/src/transaction/mod.rs Add artifact deserializer helper + tests for garbage/empty bytes.
crates/miden-multisig-client/src/transaction/builder.rs Reject TransactionType::Custom in typed proposal builder path.
crates/miden-multisig-client/src/proposal.rs Add TransactionType::Custom and built-in label detection helper.
crates/miden-multisig-client/src/payload.rs Add payload helper to set custom metadata proposal_type.
crates/miden-multisig-client/src/lib.rs Re-export producer helpers (deserialize/build/salt/signature advice).
crates/miden-multisig-client/src/export.rs Ensure export/import preserves custom proposal_type label.
crates/miden-multisig-client/src/execution.rs Prevent SDK from building final tx request for custom proposals.
crates/miden-multisig-client/src/error.rs Add UnsupportedTransactionType for custom/unmodeled operations.
crates/miden-multisig-client/src/client/proposals.rs Implement propose_custom, prepare_custom_execution, submit_transaction.
crates/miden-multisig-client/src/client/helpers.rs Skip metadata reconstruction checks for opaque custom proposals.
crates/miden-multisig-client/src/account.rs Treat custom threshold as default account threshold.
crates/miden-multisig-client/README.md Document custom producer flow in Rust SDK.
.gitignore Ignore .DS_Store and *.artifact.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread spec/api.md Outdated
Comment thread packages/miden-multisig-client/src/proposal/metadata.test.ts Outdated
Comment thread packages/miden-multisig-client/src/proposal/metadata.test.ts Outdated
Comment thread packages/miden-multisig-client/src/proposal/metadata.test.ts Outdated
Comment thread packages/miden-multisig-client/src/multisig/proposal/execution.ts Outdated
Comment thread packages/miden-multisig-client/src/multisig.ts
Comment thread packages/miden-multisig-client/src/multisig.ts
Comment thread crates/miden-multisig-client/src/client/proposals.rs Dismissed
Comment thread crates/miden-multisig-client/src/export.rs Dismissed
Comment thread crates/server/src/services/push_delta_proposal.rs Dismissed
Comment thread crates/server/src/services/push_delta_proposal.rs Dismissed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/miden-multisig-client/src/types/proposal.ts (1)

92-106: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make rawProposalType required in the domain model.

This currently allows { proposalType: 'custom' } even though the original server label is what lets custom proposals round-trip back to GUARDIAN/export correctly. If backward compatibility is needed, keep that optionality in the transport/parser layer and convert immediately into a required domain shape.

💡 Proposed change
 export interface CustomProposalMetadata extends BaseProposalMetadata {
   proposalType: 'custom';
   /** Original server-defined proposal label, e.g. "b2agg" (issue `#266`). Mirrors
    * Rust `ProposalMetadata.proposal_type` so both SDKs expose the real type.
-   * Optional for backward compatibility; the parser always populates it. */
-  rawProposalType?: string;
+   * Always required in the domain model. */
+  rawProposalType: string;
 }

As per coding guidelines, "Prefer mandatory fields in domain and request-facing types over optional ones" and "Do not add backward-compatibility layers, migrations, or fallback behavior unless the task explicitly requires them".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/miden-multisig-client/src/types/proposal.ts` around lines 92 - 106,
Make rawProposalType required on the domain model by changing
CustomProposalMetadata.rawProposalType from optional to mandatory (remove the
`?`), so CustomProposalMetadata always has a server label; update any
construction/parsing code that produces domain CustomProposalMetadata (e.g., the
parser/transport layer that currently accepts `{ proposalType: 'custom' }`) to
accept the optional transport field but immediately populate and validate
rawProposalType before creating the domain CustomProposalMetadata; keep the
transport/parser layer optional if needed but ensure conversion/validation
happens where instances of ProposalMetadata (the union including
CustomProposalMetadata) are composed.
crates/server/src/services/mod.rs (1)

227-241: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist the trimmed proposal_type, not the raw input.

" p2id " and " b2agg " both pass the current check, but you write the untrimmed value back into metadata. That makes padded built-in labels slip past the reserved-name contract and creates distinct server labels for the same logical proposal type.

🐛 Proposed change
     let proposal_type = obj
         .get("proposal_type")
         .and_then(Value::as_str)
         .ok_or_else(|| {
             GuardianError::InvalidDelta("metadata.proposal_type is required".to_string())
         })?;
-    if proposal_type.trim().is_empty() {
+    let proposal_type = proposal_type.trim();
+    if proposal_type.is_empty() {
         return Err(GuardianError::InvalidDelta(
             "metadata.proposal_type must not be empty".to_string(),
         ));
     }
     obj.insert(
         "proposal_type".to_string(),
         Value::String(proposal_type.to_string()),
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/server/src/services/mod.rs` around lines 227 - 241, The code validates
proposal_type by trimming for emptiness but then writes the original untrimmed
string back; change it to persist the trimmed value: after obtaining
proposal_type (from Value::as_str), call let proposal_type =
proposal_type.trim(); use that trimmed value for the empty check and for
obj.insert (i.e., write Value::String(proposal_type.to_string())), ensuring any
leading/trailing whitespace is removed before persisting (references: the
proposal_type variable, the empty check, and the obj.insert(Value::String(...))
call).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/miden-multisig-client/src/account.rs`:
- Line 146: The test
effective_threshold_for_transaction_maps_to_expected_procedures() needs to treat
TransactionType::Custom like the account default: update the test so that when
iterating transaction types you include TransactionType::Custom and assert it
maps to the account.default threshold (or call
effective_threshold_for_transaction() and expect self.threshold()), and
add/adjust a focused unit test that covers the custom-threshold branch in
effective_threshold_for_transaction() to ensure TransactionType::Custom returns
the account default; locate references to effective_threshold_for_transaction(),
TransactionType::Custom, and
effective_threshold_for_transaction_maps_to_expected_procedures() in the test
module and modify/assert accordingly.

In `@crates/miden-multisig-client/src/client/helpers.rs`:
- Around line 197-202: The current early return when
matches!(proposal.transaction_type, TransactionType::Custom) skips validation of
transport metadata and lets malformed GUARDIAN payloads mark custom proposals
ready; instead, remove the unconditional return and validate custom proposal
metadata at the boundary in the same helper that handles other types:
parse/normalize the transport metadata into required domain types on proposal
(reconciling any optional protobuf fields), and at minimum verify the custom
proposal's required signature count against the current account threshold (use
the same account/threshold lookup logic used elsewhere) before allowing
proposal.status.is_ready() to be trusted; ensure prepare_custom_execution() is
only reachable after these checks.

In `@crates/miden-multisig-client/src/client/proposals.rs`:
- Around line 405-417: The submit_transaction path uses the SDK's cached state
and can fail if the account advanced after prepare_custom_execution; before
calling miden_client.submit_new_transaction in submit_transaction, call the
client's sync method (e.g., self.sync().await? or await
self.miden_client.sync_state()) to refresh local state, then proceed to
require_account() and submit_new_transaction; update error handling to preserve
the existing MultisigError mapping and keep the subsequent sync_state() call if
still needed.

In `@crates/miden-multisig-client/src/error.rs`:
- Around line 91-94: Add a stable error code for the new public variant
UnsupportedTransactionType and return it from MultisigError::code(): update the
enum variant metadata/implementation so it maps to a constant stable string
(e.g. "unsupported_transaction_type") and modify the MultisigError::code() match
to return Some("...") for UnsupportedTransactionType; ensure the returned code
is used consistently with other variants and documented in the same style as
existing codes so callers can rely on a stable identifier instead of parsing the
message.

In `@examples/_shared/multisig-browser/src/multisigApi.ts`:
- Around line 446-459: The current catch block treats any account nonce increase
as proof submitTransaction succeeded, which can be caused by unrelated
proposals; instead, after await multisig.syncState() verify that the specific
proposal represented by finalRequest was actually applied (e.g., look up the
transaction/proposal by its id, txHash, or unique fields in the multisig's
on-chain/ synced state via a method like multisig.getTransaction /
multisig.findTransaction / multisig.transactions and confirm it matches
finalRequest and is executed); only swallow submitError if that specific
proposal is found executed and otherwise re-throw submitError.

In `@examples/demo/src/state.rs`:
- Around line 156-163: Both cache_custom_recipe and get_custom_recipe only trim
a lowercase "0x" and miss IDs starting with "0X"; normalize the ID case before
trimming so prefixes are handled case-insensitively. Update both functions
(cache_custom_recipe and get_custom_recipe) to first convert proposal_id to
lowercase (e.g., let id = proposal_id.to_lowercase()) and then call
trim_start_matches("0x") on that normalized string before inserting or looking
up in self.custom_recipes.

In `@examples/smoke-web/src/App.tsx`:
- Around line 568-584: The custom-execute button should only be enabled or shown
when the current session actually has the producer-owned recipe required by
executeCustomProposal; update the JSX branch that checks
proposal.metadata.proposalType === 'custom' to also verify the session's recipe
availability (e.g. a boolean like sessionHasRecipe or a helper such as
recipeAvailableForProposal(proposal)) and if missing either disable the button
(disabled={!sessionReady || !sessionHasRecipe}) and change the label to indicate
"Missing recipe" or hide the custom button entirely, wiring the onClick to
handleExecuteCustomProposal(proposal.id) only when the recipe exists; keep the
non-custom branch using runAction(() => api.executeProposal({ proposalId:
proposal.id })) unchanged.

In `@examples/smoke-web/src/smokeHarness.ts`:
- Around line 374-375: customRecipesRef (a Map stored in useRef) persists across
account changes and can return recipes from a previous session; to fix, clear or
reinitialize it whenever account-scoped state is reset — add
customRecipesRef.current.clear() (or customRecipesRef.current = new Map())
inside the routines that reset account state such as createAccount, loadAccount,
and clearLocalState so executeCustomProposal({ proposalId }) cannot resolve
recipes from the prior account/session.
- Around line 1149-1159: If both input.recipe and input.proposalId are provided,
validate they match (compare trimmed input.proposalId to recipe.proposalId) and
throw a clear error if they differ; additionally verify the resolved
recipe.senderId equals the currently loaded multisig identity (currentMultisig
or its id) and throw if it does not, to prevent using a recipe from another
account; use the correct key when deleting from customRecipesRef.current (use
the validated proposalId) and only call prepareAndSubmitCustomProposal after
these checks succeed.

In `@packages/guardian-client/src/conversion.test.ts`:
- Around line 136-145: The test only validates fromServerProposalMetadata; add
the reverse assertion to ensure the camelCase→server mapping preserves custom
labels by calling toServerProposalMetadata({ proposalType: 'b2agg' }) and
asserting the returned object has proposal_type === 'b2agg'. Update the test in
conversion.test.ts to include this extra expect so regressions in
toServerProposalMetadata (the mapping code in conversion.ts) are caught.

In `@packages/miden-multisig-client/src/multisig.ts`:
- Around line 101-109: BUILTIN_PROPOSAL_TYPES is missing the reserved label
'custom', allowing proposeCustom() to accept a user-defined proposal that
conflicts with the SDK's internal bucket name; add the string 'custom' to the
BUILTIN_PROPOSAL_TYPES Set so proposeCustom() will reject that reserved label
and maintain the raw-label vs. bucket separation enforced by
CustomProposalMetadata.
- Around line 1639-1642: Reject custom proposals early in
prepareProposalExecution by checking metadata.proposalType === 'custom' and
throwing before any advice assembly or calls to this.guardian.pushDelta(...);
specifically, move or add the guard at the start of prepareProposalExecution (or
the entry point used by executeProposal/createTransactionProposalRequest) so
buildTransactionRequestFromMetadata and guardian.pushDelta are never invoked for
'custom' proposals, keeping the flow side-effect free.

In `@packages/miden-multisig-client/src/multisig/proposal/execution.ts`:
- Around line 359-363: Guard against custom proposals earlier by checking
proposal.metadata.proposalType === 'custom' before any side-effecting calls like
appendGuardianAckAdvice() or guardian.pushDelta; move or add the validation in
the exported workflows (the functions that call appendGuardianAckAdvice()) so
they throw the same Error for custom proposals immediately and never call
appendGuardianAckAdvice(), ensuring guardian.pushDelta is not invoked for
unsupported custom types.

---

Outside diff comments:
In `@crates/server/src/services/mod.rs`:
- Around line 227-241: The code validates proposal_type by trimming for
emptiness but then writes the original untrimmed string back; change it to
persist the trimmed value: after obtaining proposal_type (from Value::as_str),
call let proposal_type = proposal_type.trim(); use that trimmed value for the
empty check and for obj.insert (i.e., write
Value::String(proposal_type.to_string())), ensuring any leading/trailing
whitespace is removed before persisting (references: the proposal_type variable,
the empty check, and the obj.insert(Value::String(...)) call).

In `@packages/miden-multisig-client/src/types/proposal.ts`:
- Around line 92-106: Make rawProposalType required on the domain model by
changing CustomProposalMetadata.rawProposalType from optional to mandatory
(remove the `?`), so CustomProposalMetadata always has a server label; update
any construction/parsing code that produces domain CustomProposalMetadata (e.g.,
the parser/transport layer that currently accepts `{ proposalType: 'custom' }`)
to accept the optional transport field but immediately populate and validate
rawProposalType before creating the domain CustomProposalMetadata; keep the
transport/parser layer optional if needed but ensure conversion/validation
happens where instances of ProposalMetadata (the union including
CustomProposalMetadata) are composed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9702e687-6894-4988-91f2-763238c3fee0

📥 Commits

Reviewing files that changed from the base of the PR and between d5fbbcf and 8add1b4.

⛔ Files ignored due to path filters (1)
  • examples/_shared/multisig-browser/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (38)
  • .gitignore
  • crates/miden-multisig-client/README.md
  • crates/miden-multisig-client/src/account.rs
  • crates/miden-multisig-client/src/client/helpers.rs
  • crates/miden-multisig-client/src/client/proposals.rs
  • crates/miden-multisig-client/src/error.rs
  • crates/miden-multisig-client/src/execution.rs
  • crates/miden-multisig-client/src/export.rs
  • crates/miden-multisig-client/src/lib.rs
  • crates/miden-multisig-client/src/payload.rs
  • crates/miden-multisig-client/src/proposal.rs
  • crates/miden-multisig-client/src/transaction/builder.rs
  • crates/miden-multisig-client/src/transaction/mod.rs
  • crates/server/src/services/mod.rs
  • crates/server/src/services/push_delta_proposal.rs
  • docs/MULTISIG_SDK.md
  • examples/_shared/multisig-browser/package.json
  • examples/_shared/multisig-browser/src/multisigApi.ts
  • examples/demo/src/actions/proposal_management.rs
  • examples/demo/src/state.rs
  • examples/smoke-web/README.md
  • examples/smoke-web/src/App.tsx
  • examples/smoke-web/src/smokeHarness.ts
  • packages/guardian-client/src/conversion.test.ts
  • packages/guardian-client/src/conversion.ts
  • packages/guardian-client/src/server-types.ts
  • packages/guardian-client/src/types.ts
  • packages/miden-multisig-client/README.md
  • packages/miden-multisig-client/src/multisig.ts
  • packages/miden-multisig-client/src/multisig/proposal/execution.ts
  • packages/miden-multisig-client/src/multisig/proposal/parser.ts
  • packages/miden-multisig-client/src/proposal/metadata.test.ts
  • packages/miden-multisig-client/src/proposal/metadata.ts
  • packages/miden-multisig-client/src/types/proposal.ts
  • spec/api.md
  • speckit/features/008-custom-proposal-producer/contracts/sdk-api.md
  • speckit/features/008-custom-proposal-producer/data-model.md
  • speckit/features/008-custom-proposal-producer/spec.md
💤 Files with no reviewable changes (1)
  • packages/miden-multisig-client/src/multisig/proposal/parser.ts

Comment thread crates/miden-multisig-client/src/account.rs
Comment thread crates/miden-multisig-client/src/client/helpers.rs
Comment thread crates/miden-multisig-client/src/client/proposals.rs
Comment thread crates/miden-multisig-client/src/error.rs
Comment thread examples/_shared/multisig-browser/src/multisigApi.ts Outdated
Comment thread examples/smoke-web/src/smokeHarness.ts
Comment thread packages/guardian-client/src/conversion.test.ts
Comment thread packages/miden-multisig-client/src/multisig.ts
Comment thread packages/miden-multisig-client/src/multisig.ts
Comment thread packages/miden-multisig-client/src/multisig/proposal/execution.ts Outdated
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Jun 2, 2026

Codecov Report

❌ Patch coverage is 49.72376% with 182 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.04%. Comparing base (d5fbbcf) to head (e03ae1c).

❌ Your patch status has failed because the patch coverage (49.72%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #268      +/-   ##
==========================================
- Coverage   76.40%   76.04%   -0.36%     
==========================================
  Files         136      136              
  Lines       24910    25265     +355     
==========================================
+ Hits        19032    19214     +182     
- Misses       5878     6051     +173     

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d5fbbcf...e03ae1c. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@zeljkoX zeljkoX linked an issue Jun 2, 2026 that may be closed by this pull request
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 37 out of 39 changed files in this pull request and generated 3 comments.

Comment on lines +1032 to +1035
if (!/^[a-z0-9_]+$/.test(label)) {
throw new Error(
`proposalType '${label}' must be lowercase snake_case ([a-z0-9_]): no spaces, hyphens, or other characters`,
);
Comment on lines +251 to +254
return Err(MultisigError::InvalidConfig(format!(
"proposal_type '{}' must be lowercase snake_case ([a-z0-9_]): no spaces, hyphens, or other characters",
proposal_type
)));
Comment thread speckit/features/008-custom-proposal-producer/spec.md
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
speckit/features/008-custom-proposal-producer/spec.md (1)

102-102: ⚡ Quick win

Clarify the FR cross-reference for canonical serialization.

FR-020 references “canonical serialization from FR-018,” but FR-018 currently defines return behavior, not serialization guidance. Please either point to the correct FR or add the canonical-serialization definition where referenced.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@speckit/features/008-custom-proposal-producer/spec.md` at line 102, FR-020's
cross-reference is incorrect: it points to FR-018 which does not define
canonical serialization. Update the spec by either (A) changing the reference in
FR-020 from "FR-018" to the correct FR that contains the canonical-serialization
definition (replace "FR-018" with the proper FR identifier), or (B) add a short
canonical-serialization paragraph into FR-018 (or an adjacent FR) that defines
the recommended canonical serialization used for persistence/replay; ensure the
text mentions "canonical serialization" and is referenced verbatim by FR-020 so
the cross-reference is accurate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@speckit/features/008-custom-proposal-producer/spec.md`:
- Line 97: The normative requirement FR-015 in
speckit/features/008-custom-proposal-producer/spec.md contains a duplicated word
("serialized serialized"); edit the FR-015 sentence to remove the extra
"serialized" so it reads "The serialized transaction request MUST NOT be stored
anywhere..." and keep the rest of the clause intact (references to create and
execute-prep behavior and FR-010 should be unchanged).

---

Nitpick comments:
In `@speckit/features/008-custom-proposal-producer/spec.md`:
- Line 102: FR-020's cross-reference is incorrect: it points to FR-018 which
does not define canonical serialization. Update the spec by either (A) changing
the reference in FR-020 from "FR-018" to the correct FR that contains the
canonical-serialization definition (replace "FR-018" with the proper FR
identifier), or (B) add a short canonical-serialization paragraph into FR-018
(or an adjacent FR) that defines the recommended canonical serialization used
for persistence/replay; ensure the text mentions "canonical serialization" and
is referenced verbatim by FR-020 so the cross-reference is accurate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d87729e-145a-454a-910c-31c146d25b6c

📥 Commits

Reviewing files that changed from the base of the PR and between 8add1b4 and e03ae1c.

📒 Files selected for processing (26)
  • crates/miden-multisig-client/README.md
  • crates/miden-multisig-client/src/account.rs
  • crates/miden-multisig-client/src/client/helpers.rs
  • crates/miden-multisig-client/src/client/proposals.rs
  • crates/miden-multisig-client/src/error.rs
  • crates/miden-multisig-client/src/proposal.rs
  • crates/miden-multisig-client/src/transaction/mod.rs
  • crates/server/src/services/mod.rs
  • docs/MULTISIG_SDK.md
  • examples/_shared/multisig-browser/src/multisigApi.ts
  • examples/demo/src/actions/proposal_management.rs
  • examples/demo/src/state.rs
  • examples/smoke-web/README.md
  • examples/smoke-web/src/App.tsx
  • examples/smoke-web/src/smokeHarness.ts
  • packages/guardian-client/src/conversion.test.ts
  • packages/miden-multisig-client/README.md
  • packages/miden-multisig-client/src/multisig.ts
  • packages/miden-multisig-client/src/multisig/proposal/execution.ts
  • packages/miden-multisig-client/src/proposal/metadata.test.ts
  • packages/miden-multisig-client/src/proposal/metadata.ts
  • packages/miden-multisig-client/src/types/proposal.ts
  • spec/api.md
  • speckit/features/008-custom-proposal-producer/contracts/sdk-api.md
  • speckit/features/008-custom-proposal-producer/data-model.md
  • speckit/features/008-custom-proposal-producer/spec.md
💤 Files with no reviewable changes (1)
  • packages/miden-multisig-client/src/multisig/proposal/execution.ts
✅ Files skipped from review due to trivial changes (6)
  • examples/smoke-web/README.md
  • crates/miden-multisig-client/README.md
  • packages/miden-multisig-client/README.md
  • docs/MULTISIG_SDK.md
  • speckit/features/008-custom-proposal-producer/contracts/sdk-api.md
  • speckit/features/008-custom-proposal-producer/data-model.md
🚧 Files skipped from review as they are similar to previous changes (12)
  • packages/guardian-client/src/conversion.test.ts
  • crates/miden-multisig-client/src/account.rs
  • crates/miden-multisig-client/src/transaction/mod.rs
  • examples/demo/src/state.rs
  • packages/miden-multisig-client/src/proposal/metadata.ts
  • packages/miden-multisig-client/src/proposal/metadata.test.ts
  • examples/demo/src/actions/proposal_management.rs
  • packages/miden-multisig-client/src/types/proposal.ts
  • crates/miden-multisig-client/src/client/helpers.rs
  • examples/smoke-web/src/smokeHarness.ts
  • examples/smoke-web/src/App.tsx
  • crates/miden-multisig-client/src/proposal.rs

- **FR-012**: Documentation MUST explain the producer flow, the producer's responsibility to reproduce the exact transaction at execution, and that the SDK cannot interpret a custom transaction — so cosigners must review the raw transaction summary, not the label or description.
- **FR-013**: The end-to-end producer flow (create → sign → execute) for a custom label MUST be demonstrable through at least one example harness.
- **FR-014**: For v1, the SDK MUST require a full producer-supplied transaction (in serializable form) as the creation input and MUST derive the canonical transaction summary itself; it MUST NOT accept a pre-built transaction summary as a creation input. This keeps the SDK's executability check at creation and the binding re-verification at execution intact. (Accepting a pre-built summary is explicitly out of scope for v1.)
- **FR-015**: The serialized serialized transaction request MUST NOT be stored anywhere by the SDK or server — not embedded in proposal metadata, not persisted server-side. The integration owns its transaction recipe and supplies the transaction request bytes at create (to derive the summary) and at execute-prep (for the binding check). This keeps the server a minimal coordinator (FR-010) and means custom execution is performed by a party holding the recipe.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix duplicated wording in normative requirement.

Line 97 says “serialized serialized transaction request,” which reads as an editorial error in a MUST requirement. Please remove the duplicate word.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@speckit/features/008-custom-proposal-producer/spec.md` at line 97, The
normative requirement FR-015 in
speckit/features/008-custom-proposal-producer/spec.md contains a duplicated word
("serialized serialized"); edit the FR-015 sentence to remove the extra
"serialized" so it reads "The serialized transaction request MUST NOT be stored
anywhere..." and keep the rest of the clause intact (references to create and
execute-prep behavior and FR-010 should be unchanged).

@Dominik1999
Copy link
Copy Markdown
Collaborator

Can we get this merged soon and get a release? We need it to test the wallet flow for v015 which we want to release next week.

Is it possible to get a review from another real person? Atm it is just AI reviewing, right?

@zeljkoX zeljkoX requested a review from haseebrabbani June 4, 2026 20:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support custom or unknown proposal types in guardian

5 participants