Skip to content

feat(mobile): multisig proposal, approval, and execution flow - #552

Merged
Miracle656 merged 2 commits into
Miracle656:mainfrom
Elizabethxxx:feat/mobile-multisig-approval
Jul 30, 2026
Merged

feat(mobile): multisig proposal, approval, and execution flow#552
Miracle656 merged 2 commits into
Miracle656:mainfrom
Elizabethxxx:feat/mobile-multisig-approval

Conversation

@Elizabethxxx

Copy link
Copy Markdown
Contributor

Turns the read-only multisig view into a working coordination tool: an owner raises a transfer, owners approve it, and the approval that reaches the threshold executes it.

closes #484

What is here

  • frontend/mobile/lib/multisig.ts — chain access plus the rules the screen applies before touching it.
  • frontend/mobile/app/multisig.tsx — connect a wallet, view owners/threshold/balance, propose, approve, execute.
  • 24 tests over the proposal and approval rules.

Acceptance: a proposal can be approved by the required signers and executed

Propose → approve → approve (2-of-3) ends with the transfer made and the proposal showing Executed.

There is no separate Execute button, and that is deliberate. contracts/multisig-wallet has no execute entry point: sign_transaction performs the transfer inside the invocation that reaches the threshold. So the deciding approval is the execution. The screen names it accordingly — the button reads Approve and execute and warns that approving sends the funds immediately — rather than offering a button the contract cannot back.

Three fixes carried over from the web port

The web wallet's lib/multisig.ts was the reference. Three things did not survive review:

  1. propose_transaction takes caller as its first argument and calls caller.require_auth(). The web version omits it, so every proposal it builds is rejected by the contract. It is passed here.
  2. The owner is the source account rather than a separate fee payer. They must authorise the call regardless, and a source-account invoker needs no second signature — which also drops the fallback that minted a throwaway keypair from Friendbot to pay fees.
  3. Amounts convert through integer string arithmetic instead of parseFloat(x) * 10_000_000, which misrounds ordinary values at stroop precision (0.7 lands a stroop short). This is money; there is a test pinning it.

Failing fast instead of after a fee

Approval preconditions are checked locally, so a rejection is immediate and specific instead of arriving as a contract panic after a round trip and a fee: the signer is an owner, has not already approved, the proposal is not already executed, and — for the approval that will execute — the wallet actually holds enough XLM to cover the transfer. A non-deciding approval is still allowed when the balance is short, since funds can arrive before the transfer is attempted.

Scope and configuration

Deployment stays on the desktop wizard; this screen connects to an existing wallet. The contract address is stored under the same veil_multisig_contract key the web wallet uses. Network comes from EXPO_PUBLIC_SOROBAN_RPC_URL and EXPO_PUBLIC_NETWORK_PASSPHRASE, defaulting to Soroban testnet.

The signing key field is prefilled from this device's stored signer but stays editable, because approving on behalf of another owner means pasting a different key — the same affordance the web PendingQueue offers.

The RPC wrappers are thin passes over the Stellar SDK and are exercised against a real contract rather than mocked; the tests cover the decisions made before submission.

Checks

npm run typecheck and npm test pass in frontend/mobile.

Adds /multisig, which turns the read-only multisig view into a working
coordination tool: an owner raises a transfer, owners approve it, and
the approval that reaches the threshold executes it.

There is no separate Execute action because the contract has no execute
entry point -- contracts/multisig-wallet performs the transfer inside
the sign_transaction invocation that reaches the threshold. The screen
names that approval for what it is ("Approve and execute") and warns
that it sends the funds immediately, rather than offering a button the
contract cannot back.

Three fixes carried over from the web port:

- propose_transaction takes caller as its first argument and calls
  caller.require_auth(). The web version omits it, so every proposal it
  builds is rejected. It is passed here.
- The owner is the source account rather than a separate fee payer, so
  prepareTransaction resolves require_auth against the source account
  and no second signature is needed. This also drops the fallback that
  minted a throwaway keypair from Friendbot.
- Amounts convert through integer string arithmetic instead of
  parseFloat(x) * 10_000_000, which misrounds ordinary values such as
  0.7 at stroop precision.

Approval preconditions -- owner membership, duplicate approvals, and
whether the wallet can actually cover a transfer that is about to
execute -- are checked locally, so a rejection is immediate and
specific instead of arriving as a contract panic after a fee.

Deployment stays on the desktop wizard; the contract address is read
from the veil_multisig_contract key the web wallet already uses.
@Elizabethxxx
Elizabethxxx requested a review from Miracle656 as a code owner July 28, 2026 20:19
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

@Elizabethxxx is attempting to deploy a commit to the miracle656's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Jul 28, 2026

Copy link
Copy Markdown

@Elizabethxxx Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

app/multisig.tsx replaces the route stub from Miracle656#507; README keeps both sections.

Also repoint lib/multisig.ts at lib/network.ts. It captured
EXPO_PUBLIC_NETWORK_PASSPHRASE and EXPO_PUBLIC_SOROBAN_RPC_URL into module
constants at load time, so the runtime testnet/mainnet switch added in Miracle656#526
would not have reached it — proposals would keep going to whichever network the
bundle started on. Resolved per call instead.

tsc clean; jest 16 suites / 290 tests; expo lint clean.
@Miracle656

Copy link
Copy Markdown
Owner

Merging. This one actually submits — real SorobanRpc.Server, simulation checked with isSimulationError, sendTransaction followed by polling on GetTransactionStatus until it leaves NOT_FOUND. After a run of PRs in this batch that returned fabricated hashes for money-moving actions, it's worth saying explicitly that this doesn't.

Two things I want to note as done right, because I nearly "fixed" both before reading closely:

  • The signer-secret field prefills from the device key and only falls back to manual entry, with the comment explaining why ("this device's key is the common case, but approving on behalf of another owner means pasting a different one"). That's the correct trade-off for multisig specifically, where a co-owner's key genuinely isn't on this device — and it's secureTextEntry.
  • MULTISIG_CONTRACT_STORAGE_KEY is documented as shared with the web wallet, so a proposal created on one shows up on the other.

215 lines of tests over a 422-line module, suite now at 16 files / 290 tests.

One change I made: lib/multisig.ts captured the network into module constants at load:

const NETWORK_PASSPHRASE = process.env['EXPO_PUBLIC_NETWORK_PASSPHRASE']?.trim() || Networks.TESTNET;
const RPC_URL = process.env['EXPO_PUBLIC_SOROBAN_RPC_URL']?.trim() || ...

#526 landed a runtime testnet/mainnet switch after you opened this, so those constants would have gone stale the moment a user switched — proposals would keep going to whichever network the bundle started on, silently. Now resolved per call through getNetwork(). Worth being aware of generally: module-level process.env reads are frozen at bundle load, and there's now a live network setting that can move underneath them.

Verified: tsc --noEmit clean, jest 16 suites / 290 tests, expo lint clean, CI green.

#484's acceptance — a proposal can be approved by required signers and executed — is structurally complete. Actually exercising it needs a deployed multisig contract and two signers, so it's worth a testnet run before relying on it.

@Miracle656
Miracle656 merged commit 0d4738f into Miracle656:main Jul 30, 2026
10 of 13 checks passed
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.

56. Multisig approval flow

3 participants