Branch: web-wallet/withdraw, taken from seedelf-web-wallet after #254 (chunk 9) merged. It ends with a PR back into seedelf-web-wallet.
Withdraw takes money out of the Seedelf balance: to any address (the CLI’s sweep), or by removing a Seedelf and freeing the ADA locked with it (the CLI’s remove). Both are Seedelf script spends, so the machinery is there:
ScriptSpend in seedelf-core does the owned inputs, proofs, one-time key, giveme.my collateral, draft → Ogmios → finalize, and budgets by purpose and index. ScriptSpend::mint already takes a negative amount for a burn.background/script-spend.ts is the whole build and Send flow.What’s new: change that goes to an address instead of back into the contract (Max, and a removal’s ADA), the destination checks, the ADA Handle lookup, and two screens.
git checkout web-wallet/withdraw && git pull
git fetch origin && git merge origin/seedelf-web-wallet # if it moved since
git log --oneline HEAD..origin/main # merge main first if this lists anything
seedelf-cli/src/commands/sweep.rs and remove.rs: still building inline.seedelf-core/src/build.rs: ScriptSpend, transfer, select_script_inputs, change_outputs.seedelf-web-wallet/wasm/src/lib.rs: transfer_spend, check_spendable, draft_of, finishing.extension/src/background/{script-spend,transfer}.ts and extension/src/ui/screens/Transfer.tsx.| Decision | Decided | Notes |
|---|---|---|
| Where a removed Seedelf’s ADA goes | The user picks: the Cardano account (0/0, the default) or the Seedelf balance, each with a note on what it links |
Returning it to whatever paid for the mint links nothing new. An account-paid Seedelf’s ADA put into the Seedelf balance ties the name to that new UTxO, and to whatever it’s later spent with. The Seedelf balance is right for a stealth-minted Seedelf. |
| Withdraw destinations | Any key address on this network, or an ADA Handle. Script addresses are refused, as in the CLI (the output carries no datum). A warning when it’s your own Cardano account | Withdrawing to your own account re-links the money (privacy.md Exit). The handle is resolved through Koios, which then sees it. |
| Max | Max sends every Seedelf UTxO that fits one transaction, up to 20 (the CLI’s MAXIMUM_WALLET_UTXOS), with all their tokens, and the review says what’s left |
The form notes that spending everything at once ties those UTxOs together (co-spending). |
| What an amount sends | An ADA amount (the AdaInput rules) plus optional tokens, each with an amount, as in transfer |
The destination output needs its address minimum, about 1 ₳, more with tokens. |
seedelf-core/src/build.rs)ScriptSpend::change_to(addr): what’s left goes to a key address instead of back into the contract, through change_outputs (tokens 20 to an output). That is Max, and a removal’s ADA.is_payable_address(addr, network_flag): Shelley, on this network, and no script in the payment or the delegation part (the CLI’s is_not_a_script and is_on_correct_network).sweep(chain, available, to, lovelace, tokens, change_owner, signer), with sweep_from for the CLI’s --utxo. It pays to a fixed output, refused below the address minimum, and the change goes back into the contract. Selection is transfer’s: the UTxOs holding the tokens first.sweep_all(chain, inputs, to, change_owner, signer): spends exactly inputs, with no fixed output; everything less the fee goes to to.remove(chain, seedelf_utxo, change_owner, signer): spends the Seedelf’s UTxO and burns its token (mint(name, -1, create_mint_redeemer(""))). The rest goes back into the contract unless .change_to(addr). It refuses a UTxO without exactly one Seedelf token.sweep and remove run()s become thin. They keep their argument checks and the ADA Handle lookup.
tests/cli/remove.rs mocks Ogmios with mount_evaluate(2) (two “spend” budgets). A ScriptSpend burn needs a spend budget and the policy’s, so it switches to mount_evaluate_mint(1). That is the same mock fix util mint needed. The assertions stay.withdraw module in mint_test.rs):
draftWithdraw / finishWithdraw: the request is network, params, the spendable utxos, to (a bech32 address), lovelace (null for Max), and tokens. The result is the transfer summary’s shape, plus to, max and left (how many UTxOs Max couldn’t take).
draftRemove / finishRemove: the request is network, params, the Seedelf’s utxo, and to (a bech32 address, or null for the Seedelf balance).
CardanoAccount.isOwnAddress(address): whether an address carries the account’s stake key, as every address a normal wallet shows does. It drives the own-account warning. Since the privacy review (§2.17) it also matches the account’s payment keys (the first 20 of each chain, and those the last balance reading found), so an enterprise address under them is caught too.signScriptSpend is reused unchanged.withdraw.ts: WithdrawService with send (sweep) and remove, each a build plus submit on script-spend.ts. There’s a session key each, and PendingTx.kind gains "withdraw" and "remove".$handle.
asset_nft_address for the ADA Handle policy (f0ff48bb…, the same on preprod), trying the plain name, then CIP-68 (000de140 + name), as the CLI does.assetNftAddress.withdraw-resolve: { to } → { address, handle?, own }, for the form’s feedback.withdraw-build: { to, lovelace | null, tokens } → WithdrawSummary.withdraw-submit.remove-build: { name, to: "account" | "seedelf" } → RemoveSummary.remove-submit.$handle), resolved as it’s typed: “Sends to addr_test1…”, or the handle’s address.AdaInput with Max, as in move-in); optional token amounts, which are hidden under Max (“every token goes too”).sweep, remove), and seedelf-wasm native tests for the address and Seedelf checks, Max’s cap, and isOwnAddress.record-withdraw.mjs (an amount and Max to an address) and record-remove.mjs (the synthetic owned Seedelf web-wallet). Both use the 12-word phrase’s synthetic UTxOs as additionalUtxo, on real preprod Ogmios.seedelf-platform/CLAUDE.md: sweep and remove are extracted, and every CLI script spend is on ScriptSpend.MAX_TX_BUDGET still guards it.is_payable_address refuses any script part.--address).