Branch: web-wallet/transfer. It already exists: this plan is its first commit. It ends with a PR back into seedelf-web-wallet.
A transfer pays someone’s Seedelf from the Seedelf balance. It’s the CLI’s transfer, and the second Seedelf script spend after the stealth mint. Most of the machinery exists already:
ScriptSpend does the owned inputs, proofs, one-time key, giveme.my collateral, draft → Ogmios → finalize, and budgets by purpose and index.signScriptSpend checks giveme.my’s signature and signs at Send.This chunk is mostly a new kind of output (the recipient’s re-randomized register), a lookup, and a screen.
git checkout web-wallet/transfer && 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/transfer.rs: the CLI, still building inline.seedelf-core/src/build.rs: ScriptSpend, select_script_inputs, and mint / mint_from, which are the template.seedelf-core/src/utxos.rs: find_seedelf_datum and find_seedelf_utxo.seedelf-web-wallet/wasm/src/lib.rs: draft_mint, finish_mint, sign_script_spend.extension/src/background/mint.ts (buildStealth, submit) and extension/src/ui/screens/{CreateSeedelf,MoveIn}.tsx.transfer does todayseedelf-cli/src/commands/transfer.rs (about 500 lines) will become a thin run() around a builder.
--seedelfs (token names, hex), --lovelaces per recipient, optional --asset per recipient, and optional --utxos to spend.wallet_minimum_lovelace_with_assets(tokens), about 1.46 ADA with no tokens.find_seedelf_datum(name, policy, every contract UTxO), the datum of the UTxO holding that Seedelf token, found in the same whole-contract scan the balance uses.utxos::select(params, owned, total lovelace, total tokens).register.rerandomize() (this refuses points outside the prime-order subgroup), at the contract with the lovelace and tokens.extract_budgets), which is exactly what Budgets::from_ogmios fixes.ScriptSpend::finalize replaces all of it.Confirmed with the user on 2026-09-24: every suggestion stands except paying your own Seedelf, which is allowed with a warning.
| Decision | Decided | Notes |
|---|---|---|
| How the recipient is given | Paste the full token name (5eed0e1f…, 64 hex characters). Show the tag it reads as, and whether it was found on chain. |
Tags aren’t unique: anyone can mint “alice”. A tag search, like the CLI’s util find, could come later, but it must always show the full name to pick. |
| Recipients per transfer | One | The CLI allows several. One keeps the form and review simple, and a second transfer is cheap. |
| What’s sent | An ADA amount (the move-in AdaInput rules: 6 decimals, the supply cap, “more than you have”), plus optional tokens, each with an amount |
Unlike move-in, a token doesn’t move in full: people pay parts of a balance. |
| Paying your own Seedelf | Allowed, with a warning (the plan suggested refusing) | It moves money in a circle and costs a fee, but it’s the user’s call. The worker can tell (the recipient’s register isOwned), so the summary says toSelf and the form and review warn. |
| Max | No Max in v1 | “Everything” is what withdraw (chunk 10) is for. |
| Privacy nudges | The round-amount nudge from move-in, plus one line: sending right after moving in is easy to match by timing | See privacy.md Known links. |
seedelf-core/src/build.rs)build::transfer(chain, available, payments, change_owner, signer) -> Result<ScriptSpend>, next to mint and mint_from, with a transfer_from variant for the CLI’s explicit --utxo. A payment is { register (the recipient's, as found on chain), lovelace, tokens }.
wallet_minimum_lovelace_with_assets(tokens).register.rerandomize()? and nothing else. Never write the recipient’s register as found: re-randomizing is what makes the payment unlinkable, and it’s also the torsion check.ScriptSpend::output(output, &tokens).select_script_inputs sorts pure ADA first. For a transfer, UTxOs holding a token being sent must come first.
ScriptSpend::remainder now reports “the UTxOs spent don’t hold the tokens being sent” as a hard error. It must count as NotEnough (SEEDELF_SHORT), or selection stops instead of adding the UTxO that holds the token.select_script_inputs the tokens needed, and sort the UTxOs holding them first. That’s the “mandatory” idea from move_in.transfer run() becomes thin. It looks up each recipient, then calls build::transfer, proves, drafts, evaluates, finalizes, and has giveme.my witness it. Keep its CLI shape, several recipients included: transfer takes a slice.
seedelf-cli/tests/cli/transfer.rs must stay green. Its mount_evaluate(n) labels every budget spend, which is right for a transfer.seedelf-core/tests/, a transfer module in mint_test.rs or a new file that reuses its decoder and ledger_minimum_fee):
"00"×48, and a non-torsion-free point), too little ADA for the recipient’s minimum, and tokens the balance doesn’t hold.draftTransfer(key, requestJson) and finishTransfer(key, requestJson), the same two-step shape as draftMint / finishMint, with the same one-time key seed.
network, the epoch_params row, the owned spendable utxos, recipient, lovelace, tokens: [{ policyId, assetName, quantity }], then seed and evaluation for the finish.recipient is the recipient’s UTxO row from Koios.Register (extract_bytes_with_logging) with valid points.toSelf).mint_spend: every input owned, none holding a Seedelf.signScriptSpend is reused unchanged.MintService.buildStealth and its Send path are the flow every Seedelf spend needs:
signScriptSpend → submit → pendingbackground/script-spend.ts (functions, or a base class) that takes the WASM draft and finish calls, and use it from mint and from a new transfer.ts. Chunk 10’s sweep and remove then just plug in.credential_utxos for the whole contract (the query the balance already makes), then pick the UTxO holding (seedelfPolicyId, name) locally.
asset_utxos and the like): that tells Koios exactly who is being paid. Note it in privacy.md.5eed0e1f."transfer-build": { to, lovelace, tokens } → TransferSummary: the recipient’s name and tag, the amount, the tokens, the fee parts, the change, the inputs."transfer-submit": { txHash } → PendingTx.PendingTx.kind gains "transfer".AdaInput).formatQuantity / decimals, as in move-in’s token list).transfer especially); seedelf-wasm native tests for the recipient checks, including paying yourself (allowed, flagged) and a recipient UTxO with the wrong token or no register.record-transfer.mjs like record-mint.mjs.
additionalUtxo) to a real preprod Seedelf. TAK1 (5eed0e1f54414b31009dda2589…) is owned by that same public phrase, so either pay another phrase’s Seedelf, or point the recorder at any other live Seedelf (policy_asset_list for the policy lists them).transfer, and the shared service flow.seedelf-platform/CLAUDE.md: transfer is extracted.rerandomize refuses non-prime-order points. Keep an explicit test for it, and never skip it.select_script_inputs and MAX_TX_BUDGET already guard this.