Skip to content

Manage every asset and make, take and cancel offers from a vault - #1017

Open
Rigidity wants to merge 10 commits into
rigidity/vault-recoveryfrom
rigidity/vault-assets-offers
Open

Rigidity wants to merge 10 commits into
rigidity/vault-recoveryfrom
rigidity/vault-assets-offers

Conversation

@Rigidity

@Rigidity Rigidity commented Nov 15, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #953 and #955. Part of #945. Stacked on #1016.

Summary

A vault can now hold and manage every asset Sage supports, and make, take and cancel offers.

Assets (#953)

  • Working from a vault:
    • CAT issuance
    • DID create, normalize and transfer
    • NFT transfer, URI updates, and DID assign and unassign
    • option mint, transfer, exercise and cancel
    • clawback v2: sending, clawing back, claiming after expiry, and being finalized to the vault
  • NFTs minted with the vault as minter (bulk_mint_nfts without did_id): the vault spend creates the intermediate launchers itself, so the existing minter detection finds the vault.
  • CATs the vault can issue more of (issue_cat with multi_issuance, then the new issue_more_cat) use EverythingWithSingletonTail. The vault pipeline adds the TAIL's extra-delta messages automatically. This is ported from the SDK's test-only vault_tail_messages.
  • Clear signing for DIDs and options: the SDK can't parse vault-owned DIDs and options, so Sage checks them itself.
    • Each must be locked to this vault's own address and run exactly the delegated puzzle the vault approved.
    • The child is parsed the way sync does, to show where it goes or that it's melted.
    • Those messages are then left out of the spend the SDK parses, so the SDK checks the rest.
    • The confirmation dialog lists these DIDs and options. It no longer shows the 1 mojo per NFT launcher as XCH leaving the vault.
  • Clawbacks are spent through whichever side the vault is on. The vault asserts the time bound itself, since clear signing only trusts the vault's own conditions for that.

Offers (#955)

  • Always pre-split: a vault transaction pays each offered XCH or CAT into a P2ConditionsOrSingleton coin (FixedPuzzleMember) locked to fixed conditions. Their memos are (vault address, conditions). The offer spends those coins through the fixed path with no vault signature, so the vault stays usable while the offer is open.
    • make_offer pre-splits a vault's offers even when pre_split_fee isn't set, so WalletConnect's chia_createOffer works.
  • Each pre-split coin asserts every requested payment, plus the expiry when there is one.
  • Ephemeral key: each coin also needs an AGG_SIG_ME from a random key generated for that offer and dropped after signing. It's never stored. Without it, anyone could take the offer from the fixed conditions the memos reveal on chain, not just people it was shared with.
  • Cancel and cancel-all take the coins back through the vault, which asserts each coin's ID for clear signing.
  • Taking goes through the normal vault pipeline.
  • Status: an offer is completed when it's taken through the fixed path, and cancelled when the vault takes the coins back.
  • Locked balance: pre-split coins are locked through the existing offer record, and become spendable again once the offer ends.
  • Rekey and recovery warning: the rekey and start-recovery dialogs warn about open offers, which those don't cancel.

Decisions

  • Direct offers (spending the vault itself) are refused with VaultDirectOfferNotSupported.
  • Offering NFTs or options from a vault is refused with VaultOfferAssetNotSupported, until sync can track singletons in pre-split coins.
  • No migration for locked balance: I chose not to add one that would permanently exclude pre-split coins from coin selection.

Deferred

  • There's no UI for issuing more of a vault's CAT; only the API exists.
  • NFTs and options can't be offered from a vault.
  • Reclaiming an expired option's underlying through its clawback path isn't handled for vault creators.
  • Vault-owned XCHandles NFTs aren't tested.
  • There's no negative test for a DID or option locked elsewhere that's still sent a message.

Testing

  • sage-wallet: 13 new simulator tests covering each action above from a vault, and offers from vault to BLS, BLS to vault and vault to vault.
    • Cancelling returns the coins to the vault.
    • NFTs minted by a vault show the vault as minter.
    • The test signer now clear-signs every vault transaction.
  • sage-rpc: 2 new tests. They cover locked balance and offer status across a restart, and make_offer pre-splitting without pre_split_fee.
  • Sandbox, on the top of the stack:
    • Made an offer of 2 TXCH for a CAT from a minted vault. The 2 TXCH left the selectable balance.
    • Bob took it. The vault received the CAT and the offer showed as completed.
    • The rekey dialog warned about the open offer before it was taken.
  • Flakiness: test_vault_receives_clawbacks failed in two of three full workspace runs. Alice's second clawback send sometimes ran before her change from the first had synced ("no spendable coins"). Both clawbacks now go in one transaction. Three full sage-wallet runs passed after that.
  • Checks: strict clippy and the sage-wallet and sage-rpc tests passed on this branch alone. Since then it gained the offer pre-split default and the clawback test fix, which were tested at the top of the stack. The full set passed at the top of the stack: prettier, lint, tsc -b, unit tests, fmt, clippy, workspace tests, machete, the sqlx check, and a bindings regen with no changes.

Screenshots

Make Offer from a vault The vault's offer, taken
Make offer Offers
Rekeying with an open offer
Rekey warning

@Rigidity
Rigidity added this pull request to stack #970 November 15, 2026 05:50
@Rigidity Rigidity changed the title rigidity/vault assets offers Manage every asset and make, take and cancel offers from a vault Nov 15, 2026
…Ts and issue CATs

A vault's DIDs and options are clear signed by Sage, since the SDK doesn't parse
them: each must be locked to the vault's own address and run the delegated
puzzle the vault approved. Clawbacks are spent through whichever side the vault
is, and the vault asserts that the path is open. A vault can mint NFTs as their
minter, by creating their intermediate launchers itself, and issue CATs with a
TAIL that only it can issue more of.
A vault's offers pay each offered asset into a coin locked to the offer's fixed
conditions, which the offer spends without the vault's signature, so the vault
stays usable while the offer is open. Each coin asserts the requested payments
and needs a signature from a random key that's discarded once the offer is
signed, so the offer can't be taken from what the vault's transaction reveals on
chain. Cancelling takes the coins back through the vault, which asserts each
coin's id. Offers that would spend the vault directly are refused.
…ear signing

A DID's child is parsed from its spend, like sync parses it, rather than taken
from its delegated conditions. Minting NFTs from the vault or its DIDs takes a
mojo for each launcher, which is no longer shown as XCH leaving the vault.
…s to the API

issue_cat can issue a CAT that only the vault can issue more of, with the new
issue_more_cat. bulk_mint_nfts can leave out the DID for a vault, which mints as
itself. Making and taking offers sign through the vault, and confirmations list
the DIDs and options a vault spends.
@Rigidity
Rigidity force-pushed the rigidity/vault-assets-offers branch from 62476b9 to 227852b Compare November 15, 2026 06:00

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.

Manage NFTs, DIDs, options, CAT issuance, and clawbacks from a vault

1 participant