Skip to content

swap-roundtrip: CLI soak + demo playbook + missing swap-CLI commands (reject / cancel / wait) #437

Description

@vrogojin

Goal

Add a fourth manual-test soak + demo-playbook pair covering the swap module so the same trio of artifacts that already exist for transfers, accounting, and full-recovery also exists for swap. Close out the small set of CLI gaps that block a clean, deterministic soak.

Target scenario (the soak's "ALL GREEN" path):

SETUP        alice faucet 100 UCT,  bob faucet 100 ETH
PROPOSE      alice → @bob: offer 50 UCT, want 5 ETH
ACCEPT       bob accepts + deposits 5 ETH into escrow
DEPOSIT      alice deposits 50 UCT into escrow
COMPLETE     escrow pays out: alice receives 5 ETH, bob receives 50 UCT
NET          alice  -50 UCT  +5 ETH      bob  +50 UCT  -5 ETH

This issue is purely planning — implementation comes after. The issue is structured so an implementer can pick it up cold and ship in one sitting.


1. What we already have

The trio for an existing module is consistently:

Module Soak script Walkthrough md Demo playbook
transfer manual-test-roundtrip-391.sh docs/DEMO-PLAYBOOK-PAYMENT-ROUNDTRIP.md
accounting manual-test-accounting-roundtrip.sh docs/DEMO-PLAYBOOK-ACCOUNTING-ROUNDTRIP.md
recovery manual-test-full-recovery.sh manual-test-full-recovery.md docs/DEMO-PLAYBOOK.md
swap MISSING MISSING MISSING

CLI surface today (sphere swap … → legacy bridge):

  • sphere swap propose --to @<r> --offer <amount> <coin> --want <amount> <coin> [--escrow ...] [--timeout ...] [--message ...]
  • sphere swap list [--all] [--role proposer|acceptor] [--progress <state>]
  • sphere swap accept <id> [--deposit] [--no-wait]
  • sphere swap deposit <id>
  • sphere swap status <id> [--query-escrow]
  • sphere swap ping <addr>

SDK surface (already wired in modules/swap/SwapModule.ts):

  • proposeSwap(deal, options?)SwapProposalResult
  • acceptSwap(swapId)void
  • rejectSwap(swapId, reason?)void
  • deposit(swapId)TransferResult
  • cancelSwap(swapId)void
  • verifyPayout(swapId)boolean
  • getSwapStatus(swapId, options?)SwapRef
  • getSwaps(filter?)SwapRef[]
  • Events: swap:proposed, swap:proposal_received, swap:accepted, swap:rejected, swap:announced, swap:awaiting_counter, swap:depositing, swap:deposit_sent, swap:deposit_confirmed, swap:deposits_covered, swap:concluding, swap:payout_received, swap:completed, swap:cancelled, swap:failed, swap:bounce_received, swap:deposit_returned

Faucet supports per-coin amounts: sphere faucet 100 UCT, sphere faucet 100 ETH — required by the scenario.


2. CLI gaps that block the soak

The SDK methods exist; the CLI just doesn't expose them yet. Implement these three commands as thin wrappers over the existing SDK methods.

2.1 sphere swap reject <id> [--reason "<text>"]

Why: the canonical "soak section B" for any soak is the negative path — proposer offers, acceptor rejects, no funds move. Today the only way to test that is swap accept followed by swap cancel (which doesn't exist either). The SDK already has rejectSwap(swapId, reason?).

Behavior:

  • Acceptor-only command. Errors if invoked by the proposer side.
  • Transitions the local swap record to cancelled with reason: 'rejected'.
  • Sends rejection DM to the proposer; proposer's swap also transitions to cancelled on DM receipt (already wired in SwapModule.handleIncomingDM).
  • --reason is propagated into the rejection DM (max 256 chars; truncate + warn beyond).

2.2 sphere swap cancel <id>

Why: the proposer needs an out between "I sent a proposal" and "deposits are locked at escrow." The SDK already has cancelSwap(swapId) which handles the state-machine details (refund-before-announce vs request-escrow-cancel-after-announce). The CLI just has to expose it.

Behavior:

  • Available to either party at any non-terminal state.
  • Before escrow announce: pure-local transition to cancelled (no on-chain action).
  • After announce, before concluding: sends cancel DM to escrow; escrow returns deposits. Wait for swap:deposit_returned (or timeout per --timeout, default 60s) before exiting.
  • After concluding: rejects with a clear "cannot cancel — escrow is executing payouts" message.

2.3 sphere swap wait <id> [--state <name>] [--timeout <seconds>] [--exit-on-failure]

Why: today every soak has to spin a polling loop around sphere swap status with sleeps, which is brittle. The SDK already emits the right events; the CLI just needs to surface a blocking-wait primitive so soaks/scripts can write:

sphere swap propose --to @bob --offer 50 UCT --want 5 ETH | tee propose.log
SWAP_ID=$(jq -r .swap_id propose.log)
sphere swap wait $SWAP_ID --state completed --timeout 300 --exit-on-failure

Behavior:

  • Subscribes to swap:* events for the given <id>.
  • Default --state is completed; values: any of the swap states above.
  • Polls getSwapStatus() once at start to short-circuit if already in target state.
  • Exits 0 on reaching target state. Exits non-zero on terminal-but-wrong state (e.g., cancelled / failed) iff --exit-on-failure set; otherwise prints the terminal state and exits 0.
  • Honors --timeout (clamped to [5, 86400]). Exits 124 on timeout.
  • --json emits one JSON line per state transition while waiting; default emits human-readable [hh:mm:ss] swap <prefix> → <state>.

Optional, lower-priority

  • sphere swap accept --no-wait should already work; the soak should verify that. If it currently blocks, file a follow-up.
  • Daemon event support for swap:completed etc. — verify sphere daemon start --event 'swap:completed' --action 'log:./events.log' actually subscribes to the swap event bus (the daemon's event-routing already supports any <module>:<event>, but a runtime test is cheap to write and may surface a regression).

3. New artifacts

3.1 manual-test-swap-roundtrip.sh

Follows the manual-test-accounting-roundtrip.sh pattern. Single peer (no peer2/daemons needed — the soak's job is to exercise the swap protocol, not cross-device sync).

Sections:

Section 1   Create alice + bob (testnet)
Section 2   Faucet alice 100 UCT;  faucet bob 100 ETH; capture baselines
Section 3   Alice proposes:  sphere swap propose --to @bob --offer 50 UCT --want 5 ETH
Section 4   Bob lists incoming proposals:  sphere swap list --role acceptor --progress proposed
            (asserts the proposal landed and captures $SWAP_ID)
Section 5   Bob accepts + deposits:  sphere swap accept $SWAP_ID --deposit
Section 6   Alice deposits:  sphere swap deposit $SWAP_ID
Section 7   Wait for completion:  sphere swap wait $SWAP_ID --state completed --timeout 300 --exit-on-failure
            (BOTH parties run wait in parallel; both must exit 0)
Section 8   Verify net deltas (integer-only smallest-unit):
              alice net UCT  = -50 × 10^18
              alice net ETH  = +5  × 10^18
              bob   net UCT  = +50 × 10^18
              bob   net ETH  = -5  × 10^18
Section 9   Verify final state:  sphere swap status $SWAP_ID  → progress: completed
Section 10  Cross-hop / poison-pill scan:
              grep -c "SERIALIZATION_ERROR\|VERIFICATION_FAILED\|DUPLICATE_BUNDLE_MEMBERSHIP" \
                $SNAP/*.log  → must be 0
Section 11  ALL GREEN banner

Optional Scenario B (negative-path) — same script, after Section 11:

Section 12  Alice proposes:  sphere swap propose --to @bob --offer 5 UCT --want 0.1 ETH
            (smaller stake — leftover faucet balance after Scenario A)
Section 13  Bob rejects:  sphere swap reject <id> --reason "demo: declining"
Section 14  Wait + assert both sides see `cancelled` with reason 'rejected'
Section 15  Assert no balance change between Section 11 final balances and post-reject balances

Optional Scenario C (cancel-before-deposit):

Section 16  Alice proposes; bob does NOT accept
Section 17  Alice cancels:  sphere swap cancel <id>
Section 18  Assert local state moved to `cancelled`; no DM races; no balance change

Soak conventions to follow (already used by other soaks):

  • set -euo pipefail + trap teardown EXIT INT TERM
  • SWAP_TEST_DIR env var for workspace (default /tmp/swap-roundtrip-$$)
  • KEEP=1 keeps the workspace
  • SUFFIX env override for nametags
  • All payments in smallest units; UI-unit assertions only via the CLI's --json parser
  • Per-step tee $SNAP/<step>.log so the final pass/fail scan has logs to grep
  • ASSERT OK (<label>): <message> for green; ASSERT FAIL (...) aborts via exit 1
  • Final ALL GREEN — swap round-trip succeeded banner on success

Wall-time budget: 4–8 min on a healthy testnet (one propose + two deposits + escrow finalize is roughly the cost of two send round-trips).

3.2 docs/DEMO-PLAYBOOK-SWAP-ROUNDTRIP.md

Follows the DEMO-PLAYBOOK-ACCOUNTING-ROUNDTRIP.md pattern (single peer, presenter-narrative). Sections:

At a glance
  | Phase  | What you show                                          | Time |
  | §0     | Prereqs & setup                                        | 2 m  |
  | §1     | Create alice + bob                                     | 2 m  |
  | §2     | Faucet alice 100 UCT + bob 100 ETH                     | 2 m  |
  | §3     | Alice proposes: 50 UCT for 5 ETH                       | 3 m  |
  | §4     | Bob sees the proposal in his swap list                 | 1 m  |
  | §5     | Bob accepts + deposits 5 ETH                           | 2 m  |
  | §6     | Alice deposits 50 UCT                                  | 2 m  |
  | §7     | Watch both sides receive their payouts                 | 3 m  |
  | §8     | Show final balances + swap status `completed`          | 1 m  |
  | Wrap   | Q&A                                                    | 2 m  |
  |        | **Total**                                              | ~20m |

(then per-section presenter narrative with talk tracks for what to show on screen)

Include presenter cheat-sheet table at the bottom mirroring the other playbooks:

When you want to… Run
Propose a swap sphere swap propose --to @<r> --offer <a> <coin> --want <a> <coin>
List inbound proposals sphere swap list --role acceptor --progress proposed
Accept + deposit in one shot sphere swap accept <id> --deposit
Just accept (deposit later) sphere swap accept <id>
Reject a proposal sphere swap reject <id> [--reason "…"]
Deposit your side sphere swap deposit <id>
Cancel your own proposal sphere swap cancel <id>
Block until terminal state sphere swap wait <id> --state completed [--timeout <s>]
Show swap detail sphere swap status <id>
Live escrow query sphere swap status <id> --query-escrow

Include the standard "Common live-demo failure modes" section — at minimum:

  • Escrow service unreachable (sphere swap ping <escrow-addr> to diagnose).
  • Either party missing the deposit coin (faucet a top-up; restart from Section 3 with a smaller deal).
  • Long escrow finalize → swap wait times out — show how to extend --timeout.
  • Alice and Bob accidentally swap roles in the deal direction — explain --offer = what YOU give up.

3.3 Optional: manual-test-swap-roundtrip.md

Only if §D-style assertion-heavy QA walkthrough is desired. The pattern for accounting/transfer skips this and lets the .sh script be the assertion document — recommend the same for swap.


4. Implementation checklist

[ ] sphere swap reject <id> [--reason "<text>"]
    [ ] CLI command + flag parsing
    [ ] thin wrapper around sphere.swap.rejectSwap(id, reason)
    [ ] --json output: { swap_id, prev_state, new_state, reason }
    [ ] --help block
    [ ] unit test against a mocked SwapModule
    [ ] integration test on testnet (cheap — no funds move)

[ ] sphere swap cancel <id> [--timeout <seconds>]
    [ ] CLI command + flag parsing
    [ ] state-aware wrapper:
        - if state < announced: just call cancelSwap()
        - if announced <= state < concluding: cancelSwap() + wait for swap:deposit_returned or swap:cancelled
        - if state >= concluding: refuse with clear error
    [ ] --json output: { swap_id, prev_state, new_state, deposits_returned: bool }
    [ ] --help block
    [ ] unit tests covering all three state branches
    [ ] integration test on testnet (one happy-path cancel-before-announce, one cancel-after-announce)

[ ] sphere swap wait <id> [--state <name>] [--timeout <seconds>] [--exit-on-failure]
    [ ] CLI command + flag parsing
    [ ] event subscription via Sphere event bus (filter by swap_id)
    [ ] short-circuit poll at startup
    [ ] terminal-state exit semantics (exit code 0 / 1 / 124)
    [ ] --json streaming output (one line per transition)
    [ ] --help block
    [ ] unit tests with a stubbed event bus
    [ ] integration test: spawn propose → wait → cancel; verify wait exits with the right code

[ ] manual-test-swap-roundtrip.sh (Scenario A only first; B & C optional)
    [ ] script outline above
    [ ] integer-only delta assertions
    [ ] poison-pill scan
    [ ] teardown trap
    [ ] env-var contract (SWAP_TEST_DIR, KEEP, SUFFIX)

[ ] docs/DEMO-PLAYBOOK-SWAP-ROUNDTRIP.md
    [ ] presenter narrative
    [ ] presenter cheat sheet
    [ ] failure-mode section

[ ] CI / soak harness wiring
    [ ] Add the new soak to whatever nightly runner exists (mirror what
        manual-test-accounting-roundtrip.sh gets)
    [ ] No CI cost unless an escrow service is provisioned for CI; if not,
        the soak is operator-run only (same posture as the others)

5. Acceptance criteria

  1. sphere swap reject <id> exits 0 with both local + remote sides transitioned to cancelled (reason='rejected').
  2. sphere swap cancel <id> works correctly across all three state-branch cases above; deposits returned (if any) before exit on the announced-but-not-concluding branch.
  3. sphere swap wait <id> --state completed --timeout 300 --exit-on-failure exits 0 on completed, exits 1 on cancelled / failed, exits 124 on timeout.
  4. manual-test-swap-roundtrip.sh exits 0 against real testnet with ALL GREEN — swap round-trip succeeded on the happy path; integer-only delta assertions for both UCT and ETH match expectation.
  5. docs/DEMO-PLAYBOOK-SWAP-ROUNDTRIP.md is a self-contained presenter narrative for a ~20 min live demo. Cheat sheet covers every command used in the script.
  6. No regressions in the existing three soaks (manual-test-roundtrip-391.sh, manual-test-accounting-roundtrip.sh, manual-test-full-recovery.sh) — re-run them after the new CLI commands land.

6. Out of scope (deliberately)

  • NFT swaps — v1 swap is coin-only by SDK design (see docs/SWAP-ARCHITECTURE.md header). Future protocol revision.
  • Multi-asset swap deals — single coin per party today; multi-asset reserved for future.
  • Escrow-service-side changes — this issue is purely client-side. The escrow service at /home/vrogojin/escrow-service is out of scope.
  • Cross-device sync of swap state — the existing manual-test-full-recovery.sh already covers Profile-backed state recovery; swap records ride the same storage layer and will recover incidentally. A dedicated cross-device swap soak is a separate follow-up.
  • Multi-hop / atomic-swap chains — single-hop only.

7. References

  • Existing soaks: manual-test-roundtrip-391.sh, manual-test-accounting-roundtrip.sh, manual-test-full-recovery.sh
  • Existing playbooks: docs/DEMO-PLAYBOOK.md, docs/DEMO-PLAYBOOK-PAYMENT-ROUNDTRIP.md, docs/DEMO-PLAYBOOK-ACCOUNTING-ROUNDTRIP.md
  • Swap docs: docs/SWAP-ARCHITECTURE.md, docs/SWAP-SPEC.md, docs/SWAP-PROTOCOL-V2.md
  • Swap module: modules/swap/SwapModule.ts, modules/swap/types.ts
  • Existing CLI swap commands: src/legacy/legacy-cli.ts (search for 'swap-*' keys)
  • Swap event names (for the daemon wiring + the wait command): the 17 swap:* events listed in §1 above.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions