Skip to content

Apps: wallet.createOffer, so an app's offers are Sage's own - #899

Open
awizardxch wants to merge 1 commit into
xch-dev:mainfrom
awizardxch:feat/bridge-create-offer
Open

awizardxch wants to merge 1 commit into
xch-dev:mainfrom
awizardxch:feat/bridge-create-offer

Conversation

@awizardxch

Copy link
Copy Markdown
Contributor

Problem

An app running inside Sage that trades through offers has no way to ask Sage for an offer. It can only build the offer itself and ask Sage to sign the coin spends (wallet.signCoinSpends). Sage never records the result, so the offer:

  • is missing from Sage's Offers page,
  • does not reserve its coins, so Sage can spend them elsewhere and break the offer without noticing, and
  • cannot be cancelled from Sage.

A website connected over WalletConnect does not have this problem, because chia_createOffer calls make_offer and the offer is imported. The app bridge has no matching method.

Change

This adds a new user bridge method, wallet.createOffer. It is the bridge twin of chia_createOffer.

  • Params:

    • offerAssets and requestAssets take { assetId?, hiddenPuzzleHash?, amount }. An empty or omitted assetId means XCH. Otherwise it is a CAT asset id, NFT id or option id, as make_offer accepts. amount is in mojos, as a number or an integer string.
    • fee (optional, mojos).
    • expiresAtSecond (optional).
    • Unknown fields are rejected.
  • Validation: before any approval is shown, every amount must be a positive integer number of mojos and the fee must be an integer. An empty offer is refused. Malformed requests fail without a prompt.

  • Approval: approval is required on every call and can never be auto-approved. The new createOffer card in the bridge-approval system app shows:

    • what is offered and what is requested, with the wallet's token name/ticker and precision where it has a token record, plus the raw amount and asset id;
    • the fee in XCH;
    • the expiry, if one was given.

    The approval is bound to the active wallet (binds_approval_to_wallet).

  • Execution: calls make_offer with auto_import: true (no receive address, automatic coin selection). The offer is listed on the Offers page, its coins are reserved, and it can be cancelled there. Returns { offer, id }, as chia_createOffer does.

  • Capability: the method is gated by the existing wallet.sign_coin_spends. Making an offer means Sage signs spends of its own coins after a per-request approval, which is what that capability already grants.

    I did not add a new capability key for a compatibility reason. An unknown capability key fails manifest parsing on older Sage versions. An app that declared a new wallet.create_offer capability could therefore not install on any Sage released before this change, even though it would fall back fine without the method. If you would prefer a dedicated capability anyway, it is a small change, and I'm happy to make it.

  • SDK: sage-app-sdk gains wallet.createOffer(input) and the WalletCreateOfferParams / WalletCreateOfferResult types (generated).

  • Docs: docs/generated/user-bridge-methods.md regenerated with pnpm generate:app-docs.

Unit tests in create_offer.rs cover:

  • the mapping to an imported MakeOffer;
  • fee default 0;
  • invalid amounts and fees rejected;
  • the empty offer rejected;
  • unknown fields rejected.

How to test

  1. cargo test -p sage-apps create_offer

  2. cargo clippy --workspace --all-features --all-targets, cargo fmt --all -- --check

  3. pnpm generate:types && pnpm build:packages && node builtin-apps/src/system/build.mjs bridge-approval (type-checks the approval card against the generated types), then pnpm lint and pnpm prettier:check.

  4. In a dev build, from any installed app that has wallet.sign_coin_spends, call:

    const sage = await getSageClient();
    await sage.wallet.createOffer({
      offerAssets: [{ assetId: '', amount: 1000 }],
      requestAssets: [{ assetId: '<a CAT asset id>', amount: 1 }],
      fee: 0,
    });
    • The approval card lists the legs and the fee.
    • Reject returns User denied the request.
    • Approve returns { offer, id }. The offer then appears on the Offers page with its coins reserved, and Cancel works there.
  5. Call it with {}. It fails with Failed to decode wallet.createOffer params ... and no approval is shown.

🤖 Generated with Claude Code

An app that trades through offers could only build an offer itself and ask
Sage to sign its coin spends, so Sage never recorded it: the offer was missing
from the Offers page, its coins were not reserved, and it could not be
cancelled from Sage. WalletConnect sites already get this with
chia_createOffer.

wallet.createOffer is the bridge twin of chia_createOffer: offerAssets and
requestAssets (assetId empty or omitted for XCH, else a CAT asset id, NFT id
or option id; optional hiddenPuzzleHash; amount in mojos), an optional fee
and an optional expiresAtSecond. It always asks for approval: the approval
card lists what is offered and requested (with the wallet's token names and
precision where it knows them, and the raw amounts and asset ids), the fee
and the expiry. On approval it calls make_offer with auto_import, so the
offer is listed, its coins reserved and it can be cancelled in Sage, and
returns { offer, id } as chia_createOffer does. Amounts are validated as
positive integer mojos before the approval is shown.

It is gated by wallet.sign_coin_spends, since making an offer is Sage
signing spends of its own coins after a per-request approval, and binds the
approval to the active wallet. The SDK gains wallet.createOffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant