Skip to content

feat: spending policy, contract state snapshots, multi-sig execution, wallet security audit - #529

Merged
Just-Bamford merged 10 commits into
Sorokit:mainfrom
Johnalex-hub:feat/sdk-security-and-state-workflows
Aug 29, 2026
Merged

feat: spending policy, contract state snapshots, multi-sig execution, wallet security audit#529
Just-Bamford merged 10 commits into
Sorokit:mainfrom
Johnalex-hub:feat/sdk-security-and-state-workflows

Conversation

@Johnalex-hub

Copy link
Copy Markdown
Contributor

Adds four independent SDK features. Each was developed on its own branch and merged here; detail is in the individual commit messages.

  • Spending policy engine (src/transaction/spendingPolicy.ts) — per-transaction/daily/monthly limits per asset, destination restrictions, multi-approver thresholds. Capacity is reserved at evaluation time so concurrent requests cannot bypass a shared ceiling.
  • Contract state history (src/soroban/contractStateHistory.ts) — versioned snapshots with deterministic fingerprints, pinContractState, integrity checks, comparison. Uncaptured ledgers report an explicit error rather than empty state.
  • Multi-sig contract execution (src/soroban/multiSigExecution.ts) — N-of-M workflow over the canonical transaction hash; signatures verified with Keypair.verify, no new signing format. Invalid and duplicate signatures never advance the threshold.
  • Wallet security audit (src/wallet/securityAudit.ts) — structured report exposing every scoring factor. Unknown vulnerability data is penalized, never treated as safe.

All four are exported from their module barrel and src/index.ts.

Verification: 158 new tests, all passing. tsc --noEmit reports 22 errors — identical to the pre-existing count on main, none in this code. Lint clean, build succeeds. The remaining suite failures (freighter, lobstr, logger, priceSubscriptions, scheduler, soroban) are pre-existing on main and untouched here.

Closes #520
Closes #521
Closes #522
Closes #524

…trols

Applications had no way to enforce spending policy before a transaction was
signed or submitted. This adds SpendingPolicyEngine, which evaluates a request
against configured limits and returns a structured decision.

Supports per-transaction, daily and monthly limits configured per asset via
setSpendingLimit(asset, amount, period), destination allow/deny restrictions,
and approval thresholds requiring N distinct approvers.

Concurrency: evaluate() reserves capacity at decision time rather than at
submission time. Authorized and pending-approval records both count toward
cumulative windows, so two concurrent requests cannot each independently fit
under one ceiling. rejectRequest() and markFailed() release the reservation;
markCompleted() retains it.

Amounts are compared as scaled BigInts at stroop precision so values beyond the
IEEE-754 safe integer range keep full precision. Daily and monthly windows are
computed in UTC.

Duplicate approvals from the same identity are rejected so a single approver
cannot satisfy a multi-approver requirement alone.

The engine records only decisions made through this SDK instance; it does not
observe on-chain activity, so limits constrain the application rather than the
account itself. Documented on the class.
…ueries

Reproducing a past contract state required capturing it by hand. This adds
ContractStateHistory, a registry of SDK-managed snapshots keyed by contract and
ledger sequence.

Each snapshot carries contract id, ledger reference, capture timestamp and a
deterministic state fingerprint. The fingerprint is an FNV-1a digest over a
canonical JSON encoding that sorts object keys at every depth, so structurally
equal states always fingerprint identically regardless of key insertion order.
It detects drift and makes comparison cheap; it is not collision-resistant and
is documented as unsuitable as a security boundary.

pinContractState(contractId, version) pins a contract to a captured ledger.
Pinning or querying a ledger that was never captured fails with an explicit
CONTRACT_READ_FAILED rather than silently resolving the nearest version or
returning empty state, so callers can distinguish "unavailable" from "no state".

Scope is documented on the class: this stores what was captured through it and
cannot reconstruct arbitrary historical ledger state, which lives outside RPC
retention windows. compareSnapshots reports added, removed and changed entries
and rejects comparisons across different contracts.

Captured state is copied and frozen so later mutation of the caller's object
cannot invalidate a stored snapshot.

Complements the existing label-keyed contractSnapshot.ts and stateSnapshots.ts,
which capture live state over RPC and carry no version or integrity metadata.
N-of-M contract authorization required callers to coordinate signature
collection and assemble the final envelope by hand. This adds
MultiSigContractExecution, which separates preparation, signing-request
creation, signature collection, validation and submission.

The canonical signing payload is the transaction hash for the request's network
passphrase — the same payload the Stellar protocol signs — so no independent
signing format is introduced. Signatures are verified with Keypair.verify
before being accepted, matching the verification approach already used in
submitTransaction.ts. Because the payload is network-bound, a request cannot be
replayed against a different network or a modified transaction body.

Signatures that fail verification are rejected and contribute no weight, so an
invalid signature can never advance the threshold. Duplicate submissions from
the same signer are rejected, preventing one signer from satisfying a
multi-signer threshold alone. Thresholds accrue by signer weight rather than
signer count, and a threshold exceeding total declared weight is rejected at
creation time as unreachable.

Expired requests can neither collect signatures nor execute, even when the
threshold was met before expiry. Executing marks the request executed so the
same authorization cannot be assembled twice.

execute() returns the assembled signed XDR for submitTransaction(); the
workflow never signs or submits on the caller's behalf.
Applications had no consistent way to judge whether a wallet adapter supports
expected security properties or whether a connection carries known risk. This
adds auditWalletSecurity, which returns a structured report rather than a single
opaque number.

The report exposes every contributing factor with its severity, confidence and
exact score penalty, so callers can act on the underlying evidence. The 0-100
score is derived deterministically from a fixed severity-to-penalty table:
identical inputs always produce an identical score, and the score always equals
the base of 100 plus the sum of the factor deltas.

Assessment covers declared adapter capabilities, adapter availability,
authentication state, connection origin (HTTPS, localhost HTTP, plain HTTP and
unparseable origins are distinguished), and vulnerability data from a
caller-supplied source.

Unknown is never treated as safe. With no configured source, or a source whose
knownWallets does not cover this wallet, vulnerability status is reported as
unknown, penalized, and vulnerabilityDataAvailable is false. An unknown adapter
version causes version-scoped advisories to be treated as applicable rather than
excluded. Undated and stale sources are penalized. A clean result states
explicitly that absence of a report is not proof of safety.

The score's limits are documented on the function: it reflects only what was
observable at audit time, capabilities are self-declared by the adapter and are
not verified, and the number is not a safety guarantee.
…nd-state-workflows

# Conflicts:
#	src/index.ts
…y-and-state-workflows

# Conflicts:
#	src/index.ts
#	src/soroban/index.ts
@drips-wave

drips-wave Bot commented Aug 28, 2026

Copy link
Copy Markdown

@Johnalex-hub 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

…workflows

Resolve barrel export conflicts in src/index.ts and src/transaction/index.ts.

Both sides were purely additive re-export blocks appended to the same
region of each barrel, so the resolution keeps both:

- HEAD: spending policy, contract state history, multi-sig execution,
  wallet security audit exports.
- upstream/main (Sorokit#528): fee forecasting, dependency graph, portfolio
  aggregation, SDK diagnostics exports.

No symbol collisions between the two sets. Verified: tsc error count
unchanged from the pre-merge baseline (22, all pre-existing), and all
310 tests across both sides' new suites pass.
@Just-Bamford
Just-Bamford merged commit 8717588 into Sorokit:main Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants