Seedelf-Wallet

Chunk 34 · Lovejoin token pools

1.6.0’s feature (the owner, 2026-10-13). Lovejoin PR #163 adds a token-mixing variant of the contracts: a launch pools one native token, and every box in it holds exactly box_lovelace ADA plus Q of (policy, name).

We launch no pools. Seedelf users launch them from their private balance: they choose the token and pay the ADA for the launch and the reference scripts. They also carry their own bootstrap and movement problems.

Why it matters: today a token in a one-time account, or one coming back into a Seedelf, is never mixed, so under some circumstances it can be tied to its owner. With token pools, someone buying NIGHT or a stable token can mix it and go back into the mixer, the way ADA already does.

Status: 🚧 planned, nothing built. One feature branch, web-wallet/token-pools, from main after #307. The plan went through two rounds of the owner’s questions on 2026-10-13. Two of its designs, D1 and D2, are still under discussion, and the steps that depend on them say so.

Read first:

The owner’s calls this builds on (2026-10-13)

  1. The contracts are more or less frozen: “nothing is really wrong with them explicitly”.
    • The fee contract doesn’t matter to the wallet. Its private flow never uses fee shards, so the contract’s N ≥ 3 junk-input issue needs no work here.
  2. 4 ₳ a box in the pools the wallet launches, “but we may need to come back to this one with a provable test showing what exactly it has to be for this flow”.
    • The owner chose 3 ₳ first. Then the review found that a 3 ₳ box comes back as about 2.70 ₳ plus Q, which can’t pay for a partial token send on its own (about 3.39 ₳), so the send would co-spend another UTxO.
    • Step S0d is that test, and its answer pins the value.
  3. Reference scripts at the holder, mix_box and mix_logic only. About 23.3 ₳ that never comes back, plus the fee:
    • mix_logic’s reference, 15.82 ₳;
    • mix_box’s, 3.63 ₳;
    • the reference UTxO, about 1.84 ₳;
    • the stake deposit, 2.00 ₳.

    fee_contract is not published.

  4. Pool spam needs careful discussion: “a rich bad actor … creating an overwhelming amount of bs pools just to slow down the sync”.
    • The direction: listed tokens only, “as we can do it by volume of trading”.
    • Also: “the pool discovery page might be an easy way to filter nonsense or spam pools”. The earliest pool per listed box value “will probably work here”.
    • Still open: “how many pools can exist for that single token”. And “pool creation will be on the user and not the wallet this time”. See D1.
  5. Bootstrap needs a review for every pool, ADA too: “even 30 does not really cut it”.
    • The direction: Bootstrap a pool, at any time. “A user can just bootstrap a pool on their own knowing they won’t come back automatically unlike just using the pool.” See D2.
  6. Mainnet’s check is preprod’s: “test on preprod as that is free, then make sure it works. That way mainnet would be good to go.” No real-money test; the full preprod rehearsal gates launching on mainnet.
  7. Backward compatibility with the existing ADA pool comes first. “New ada pools will probably not be created at this moment.”

Backward compatibility with the classic ADA pool

Nothing a 1.5.x user has, or relies on, moves. Each point is a test or a gate, not a hope.

  1. The classic pool stays as it is. Mainnet’s c145c10f… and preprod’s 67ffe4ed… stay bundled, as Pool::classic, which is today’s Protocol::of. They keep the same hashes, reference UTxOs and 10 ₳. Every builder’s output for them stays byte-identical. lovejoin_mainnet_test.rs, withdraw_zero_test.rs, lovejoin_test.rs and the WebAssembly mainnet tests pass unedited.
  2. The classic pool is the only ADA pool. An ADA pool on the new code is never shown, read or used, “at this moment”. Wanting one later is a decision for then, not code for now.
  3. A return or a mix with no tokens plans exactly as 1.5.0 does. A regression test runs 1.5.0’s planner cases through the new planner and compares the plans byte for byte: the deposits, the fan-outs, the costs on the review. Token boxes only ever add to a plan.
  4. Every record written before still reads, and means the same. A record without a pool is the classic pool’s. That covers:
    • the sealed lovejoin.<network> Schedule (due times, notMixed, unsure, Withdrawing), and chain records;
    • lovejoinMaybe, SESSION_LOVEJOIN_PUBLIC and …SENDING;
    • spent.ts’s reservations, and the session and auto records.

    The upgrade test takes a 1.5.0 vault mid-flight (a chain in flight, a withdraw in its write-ahead slot, boxes due) and checks that it all finishes after the update as it would have before.

  5. Classic boxes keep coming back on time across the update: the same delays, one box a run, the same quiet times. Nothing redraws a due time already drawn.
  6. The data layer keeps 1.5.x working.
    • lovejoin/pool and /since keep their shape and data, and Kupo’s patterns stay.
    • New routes go beside them.
    • The API is deployed before the wallet that reads them is uploaded.
  7. The classic pool’s screens read as today. A user who never touches a token pool sees the same words and numbers. The “10 ₳” strings take the pool’s value, and English stays byte-identical for the classic pool, chunk 19’s rule. The Settings switch keeps its meaning, and Activity’s Into Lovejoin and Lovejoin mix stay.
  8. A restore finds classic boxes exactly as before. Token pools add to the scan, never replace any of it.

What PR #163 changes

The datum and the box value

The one-shot mint and the parameter chain

What it doesn’t check, and what it costs

The bytecode

How it fits together today

The owner scalar

One pool, hardcoded, at:

The flows

The data layer

Choices made here (the owner can overturn any)

1. Pools as data, in two contract versions (seedelf-core/src/lovejoin/)

A Pool holds:

The classic pool stays bundled, as its own trust anchor.

BoxValue::cbor() ports Lovejoin’s crypto/ref/src/value.rs, replaces denom_value_bytes, and is tested on Lovejoin’s 64 box_value parity vectors.

Templates

2. Trusting a pool: verify_pool

Cheap checks first, offline before network:

  1. Shape.
    • The output is at 288fea77…, with no stake credential and no reference script.
    • It holds exactly one P.lovejoin and nothing but ADA besides.
    • Its inline datum decodes strictly as 8 fields within range.
  2. Derived hashes. M, L and F are derived from P, and must equal the datum’s. Every address and reward account is built from the derived hashes, never from the datum.
  3. A genuine one-shot: hash(apply(one_shot_mint, seed)) == P. The script makes the NFT one of a kind, so the seed alone proves it, whoever supplied the seed:
    • the bundled list;
    • the data layer;
    • or the inputs of the transaction that made the UTxO, which takes one tx_info, on the Koios path only.
  4. Not an ADA pool on the new code (backward compatibility, 2).

Verified and usable aren’t the same thing.

An escape hatch for a pool that turns unreturnable: bring a box back with the fee paid from the public account, saying that it ties the box to the account. The withdraw builder has no fee input today (lovejoin.rs:795-833).

3. Finding pools and reading them

The answer waits on D1. Whatever it decides:

4. Builders generalized (core + WebAssembly)

ScriptSpend

Deposit

Mix

Withdraw

Script refs

Large integers

5. Starting a pool

One transaction, straight from the private balance. It holds:

It’s about 6.6 KB, and atomic: no half-launched pool, no stranded deposit.

How it’s put together

Checks before the review

The review

Mainnet

6. Choosing pools

Weighed against the review’s traps.

7. Returns through token pools (sessions.ts, wasm/src/lovejoin.rs)

8. Records

A record with no pool is the classic pool’s. Each of these gains a pool:

These stay network-wide: one box a run, the spread, the quiet times, and the inTurn names.

Elsewhere, every place learns every pool:

Two new Activity kinds: Started a Lovejoin pool, and Into Lovejoin · NIGHT.

9. Screens

The pools page is discovery, and the spam filter (the owner, 2026-10-13).

The Lovejoin page

How a mix is shown needs a review as a whole (the owner, 2026-10-14: “how to display a current lovejoin mix just in general needs a review … Mixing from public does not show on the mixes so there is no way to track it”). What the page does today:

Every other screen — Home, Settings, UTxOs, Activity, ClaimAll, SiteSessions, the swap review — shows its figures per pool.

Strings

Docs

10. The data layer (mainnet; deployed before the wallet’s upload)

Its shape waits on D1. Either way:

Open for discussion

D1 · Pool spam and sync

What spam costs an attacker

What it does, with no defence

The defences, every one the same for everyone:

  Defence What it costs
A Listed tokens only: the vetted tokens/list.json decides whose pools exist by default An unlisted token’s pool stays invisible unless you ask for it, and asking shows interest
B One pool per denomination: the earliest genuine pool for each (token, Q, box_lovelace). A squat is harmless: a pool has no owner or admin Still spammable across many Qs
C Rank and cap: at most k pools per token, ranked by distinct deposits, and 75 credentials in all (one request) An attacker buys activity, at about 0.18 ₳ a deposit transaction
D A curated list each release (npm run lovejoin-pools; the owner reviews the diff, as for tokens and DReps) A new pool waits for a release
E The data layer ranks; the wallet re-verifies; on Koios, the bundled list Server work

The owner’s direction (2026-10-13): A, listing by trading volume, with the pools page as the filter.

Still open: how many pools a listed token may have. A launch needs no token in hand, so anyone can launch any number of genuine NIGHT pools, each with a different Q.

Either way: npm run lovejoin-pools writes the bundled list at release. It’s the Koios baseline, and the data layer can serve the same list faster.

D2 · Bootstrap and the crowd, for every pool

The review’s point: the floor counts boxes, not owners.

The ideas:

The owner’s direction (2026-10-13): d, as Bootstrap a pool.

Still open:

The steps

One branch, and each step ends with its tests green.

Step What Waits on
S0 Free spikes: (a) uplc 1.1.23 against the 1.1.24 artifacts, reproducing the hashes and running last-run.json’s token Mix and Owner (a failure means a coordinated Pallas/uplc bump); (b) a synthetic token Mix and Owner evaluated by mainnet’s Ogmios; (c) a preprod launch probe (a foreign inline mint, cert 0, reference outputs at the holder, giveme.my signing); (d) the box ADA test below —
S1 Asks of PR #163: commit the unapplied token blueprint; a bootstrap default of 4 ₳ (or S0d’s figure); reference scripts at the holder, not at a key’s base address the owner (outward-facing)
34a Core: Pool, BoxValue, the templates (from the PR head 791c59a, behind one constant), derive, verify_pool, the generalized ScriptSpend and builders; the classic path byte-identical S0a
34a′ Records: their shapes, migrating 1.5.0’s, and reading every verified pool. Gated on backward compatibility 3–5 and 8: the plan-equality test and the mid-flight 1.5.0 vault 34a
34b The launch builder and its WebAssembly S0c
34c The pool registry and the data layer D1
34d The tile’s flows per pool (private, public, seed, again); Bootstrap a pool; the withdraw and its escape hatch; Activity 34a′; the gate for mixing is a placeholder until D2
34e The planner, and returns through token pools D2
34f Screens, strings, es and ja, the docs, and the policy’s Changes 34d, 34e
Re-pin The merge commit’s templates; S0a and S0b again the merge
34g The preprod rehearsal; the cross-area release review; npm run tokens, dreps and lovejoin-pools; deploy 34c; then package and upload; the handoff note all

S0d, the box ADA test the owner asked for. A Rust test that proves the figure rather than estimating it.

The highest risks, taken first: S0a, the evaluator; then 34a′, about 9k lines of TypeScript state that live ADA users depend on.

Verification

Every step

The classic pool’s regression tests pass unedited, every step. On preprod, one vault runs a classic mix and return before the update and after it.

Rust tests

Extension tests

Preprod, for real (tADA, free):

  1. Launch a pool at 4 ₳ (or S0d’s figure) with a test token: one the owner mints with cardano-cli, or LJTEST.
  2. Bootstrap it.
  3. A private mix and a public one.
  4. A box coming back by itself into a Seedelf.
  5. A return holding the token.
  6. A restore finding the token boxes.
  7. Discovery of PR #163’s own pool, c5852bb7…, expected to be not usable: 2 ₳, with its scripts at a key address.

Mainnet: only the free checks of §5. Launching on mainnet waits until every preprod step above has worked.

Still open

  1. D1: who sets each listed token’s box value, and by what rule.
  2. D2: what replaces the 30-box floor as the gate for mixing.
  3. Seed or Bootstrap: keep today’s seed beside Bootstrap, or let Bootstrap replace it.
  4. S1: what to ask of PR #163 before it merges (the owner’s repo, so the owner’s call).
  5. Cert 0 or cert 7 for L’s registration: S0c’s probe decides, unless the owner prefers Lovejoin’s tested cert 7.
  6. How a mix is shown (the owner, 2026-10-14): a mix from the public account can’t be followed once it’s sent. See The Lovejoin page.