feat(utxo): on-demand backend-side UTXO derivation + delete-my-account + receive precheck#37
Merged
Merged
Conversation
…eceive precheck Replace the 100-key signup pre-pool with a single encrypted delegation root stored on the user's pay_accounts row. The backend derives merchant UTXO public keys on demand by walking the privacy channel's utxo_balances view, so the receive-side is no longer bounded by the batch the user generated at signup. - pay_accounts.encrypted_delegation_key (migration 0008). receive_utxos table dropped along with its enum + repository. - core/crypto/utxo-derivation.ts: server-side per-index P-256 keypair derivation from a 32-byte root (HKDF + SHA-256 + P-256). - core/service/utxo/utxo-balance.ts: chain walk, free-index finder, and receive-side precheck (validateReceiveDestinations). Walk terminates on 3 consecutive -1 (never-created) returns from the chain. - POST /account/delegation-key (one-shot encrypted-at-rest handover via encrypt-sk.ts + SERVICE_AUTH_SECRET). - DELETE /account/me (hard delete; transactions FK cascades). - GET /account/:walletPublicKey/public (public profile lookup; replaces the old /utxo/receive/:wallet/available endpoint). - pay/instant/prepare derives on demand; pay/instant/execute runs the precheck before submitting; merchantUtxoIndexes replaces the DB row ids. - transactions/balance switches from SUM(transactions) to chain-derived walk. - 6 unit tests covering the chain walk + precheck.
…nore) The user's global ~/.gitignore.global excludes *.sql, so the new migration file was silently dropped from the previous commit. _journal.json points at it, so the integration suite's runMigrations() crashes with NotFound.
AquiGorka
added a commit
to Moonlight-Protocol/local-dev
that referenced
this pull request
Jun 2, 2026
…ation-key pay-platform's /utxo/receive endpoint was dropped in Moonlight-Protocol/pay-platform#37; merchants now register a single 32-byte UTXO root and pay-platform derives receive UTXOs on demand from it. The pos-instant e2e test was still POSTing 5 pre-generated public keys to the gone endpoint and failing at Step 2. Replace the 5-key generation + /utxo/receive POST with one random 32-byte root POSTed to /account/delegation-key. Same backend contract as the production signup flow; the test bypasses the wallet UI, not the contract.
The deposit + 5-temp-CREATE + 5-SPEND + 5-merchant-CREATE pattern made provider-platform's fee classifier compute a NEGATIVE bundle fee (totalInflows = deposit = netStroops, totalOutflows = 2 * netStroops), so the executor's totalFee was -netStroops and MoonlightOperation.create rejected it with "Amount too low". Switch to browser-wallet's deposit pattern (lib/client/deposit.ts + browser-wallet/src/background/handlers/private/deposit.ts): deposit(opex, netStroops + BUNDLE_FEE) + N merchant CREATEs summing to netStroops The on-chain SAC transfer to the channel sends netStroops + BUNDLE_FEE so inflows/outflows align with the bundle's deposit op. Fee classifier sees inflows = deposit, outflows = merchant creates, fee = BUNDLE_FEE (positive). BUNDLE_FEE = 500_000 stroops (0.05 XLM), matches lifecycle DEPOSIT_FEE. Also hardcode feePct = 0 — merchant-fee concept is being reworked separately. Removes the now-unused deriveP256Keypair + buildPkcs8P256 helpers.
Drop fee_pct column from pay_accounts (migration 0009), remove feePct from /account/opex body + response, /pay/instant/prepare opex sub-object, and instant-execute amount accounting (replaces feeStroops /netStroops with the customer-paid amountStroops). The bundle's fee is the fixed BUNDLE_FEE the executor pays the PP, not a merchant cut. Sign the deposit op with the OpEx Ed25519 key (browser-wallet pattern) so the chain accepts the bundle.
Hardcoded expirationLedger = 999_999_999 was too far ahead of the chain's current ledger sequence (~3M on local), causing the channel contract to reject the bundle: HostError: Error(Auth, InvalidInput) "signature expiration is too late" Match lifecycle's lib/client/deposit.ts pattern — fetch latestLedger from the RPC server and use `latestLedger + 1000` as the deposit op expiration.
provider-platform's mempool → executor → verifier pipeline is async (5 s executor tick + chain confirm + 10 s verifier tick), so the bundle status doesn't reach COMPLETED until ~10-20 s after the POST returns. Returning to the caller before then yielded "payment completed" status but the merchant's chain-derived balance was still 0. Poll GET /providers/:pp/entity/bundles/:id every 5 s up to 120 s (matches local-dev's lib/client/bundle.ts:waitForBundle pattern). Only record the IN transaction and return COMPLETED once the bundle actually settles. FAILED / EXPIRED bundles surface as errors. Also drops the stale feeStroops / netStroops / feePct references left over from the merchant-fee removal — the transaction row uses the amountStroops directly (feeStroops defaults to 0).
AquiGorka
added a commit
to Moonlight-Protocol/local-dev
that referenced
this pull request
Jun 4, 2026
## Summary Extend the full-flow playwright spec with a Step 13b that exercises `DELETE /api/v1/account/me` from the UI and asserts the `pay_accounts` row is hard-deleted via a direct DB query (not API). ## What changed in this repo **DB helpers** — `playwright/helpers/db.ts` - `payAccountExists(walletPublicKey)` — `EXISTS (SELECT 1 ...)`. - `fetchEncryptedDelegationKey(walletPublicKey)` — returns the stored ciphertext, `null` if the column isn't set, `undefined` if the row is gone. Used to assert the encrypted root was persisted at signup. **Spec** — `playwright/tests/full-flow.spec.ts` - New Step 13b after the existing Step 13: assert the row exists + has an encrypted delegation key, click the Delete-account button in moonlight-pay, accept the browser confirm dialog, wait for the `/login` redirect, then assert the `pay_accounts` row is gone. ## Companion PRs - pay-platform: Moonlight-Protocol/pay-platform#37 - moonlight-pay: Moonlight-Protocol/moonlight-pay#35 ## Test plan - [x] `./test.sh playwright` — 16/16 green in 6.5 min on this branch (when run with `PAY_PLATFORM_PATH` / `MOONLIGHT_PAY_PATH` pointing at the companion-PR branches).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Flip the moonlight-pay receive-UTXO model from a 100-key signup pre-pool to an encrypted delegation root stored on the user's
pay_accountsrow. Pay-platform derives merchant UTXO public keys on demand by walking the privacy channel'sutxo_balancesview, so the receive-side is no longer bounded by a one-shot batch.What changed in this repo
Schema / migration
pay_accounts.encrypted_delegation_key(text, nullable) — added inmigration/0008_drop_receive_utxos_add_delegation_key.sql.receive_utxostable + its enum + its repository: dropped.Crypto + on-chain helpers
src/core/crypto/utxo-derivation.ts— server-side per-index P-256 keypair derivation from a 32-byte root (HKDF + SHA-256 + P-256), mirror of the moonlight-pay client's one-shotcomputeUtxoRoot.src/core/channel-client/index.ts— cachedPrivacyChannelfactory.src/core/service/utxo/utxo-balance.ts—fetchUtxoBalances,walkFundedBalances,findFreeUtxoIndexes,validateReceiveDestinations. Walk terminates on 3 consecutive-1(never-created) returns. The receive-side precheck (validateReceiveDestinations) re-queries on-chain at execute time and substitutes free indexes if the proposed set was funded between prepare and execute.src/core/service/utxo/utxo-balance_test.ts— 6 unit tests, including the precheck non-zero-balance and spent-UTXO rejection cases.API
POST /api/v1/account/delegation-key— one-shot handover; encrypts via the existingencrypt-sk.ts(AES-256-GCM + PBKDF2 fromSERVICE_AUTH_SECRET).DELETE /api/v1/account/me— hard delete; the existingtransactions→pay_accountsFK cascades.GET /api/v1/account/:walletPublicKey/public— public merchant profile lookup. Replaces the old/utxo/receive/:wallet/availableendpoint (the POS only needs merchant name / onboarded flag, not a UTXO list).pay/instant/preparederives the next free indexes on demand.pay/instant/executerunsvalidateReceiveDestinationsbefore submitting the bundle; the request body now passesmerchantUtxoIndexesinstead of DB row IDs.transactions/balanceswitches fromSUM(transactions)to chain-derived walk./utxo/*router is removed.Version
deno.json: 0.5.18 → 0.5.19.Companion PRs
Test plan
deno fmt --checkdeno lintdeno check src/main.tsdeno task test:unit(27/27)./test.sh playwright16/16 green (includes new Step 13b verifying DELETE /me removes the row)