@arkade-os/swap 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -303,8 +303,7 @@ import {
303
303
  awaitOnchainFill,
304
304
  claimOnchainFill,
305
305
  addAssetSwap,
306
- preimageForRfqSecrets,
307
- rfqSecretsToRecord,
306
+ swapSecretsToRecord,
308
307
  } from "@arkade-os/swap";
309
308
 
310
309
  const swap = await requestOnchainSend(wallet, arkServerUrl, httpTransport(solverUrl), {
@@ -323,7 +322,7 @@ await addAssetSwap(repository, {
323
322
  swapPkScript: hex.encode(swap.swapPkScript),
324
323
  htlcPkScriptHex: hex.encode(swap.htlc.pkScript),
325
324
  htlcLocktime: swap.htlc.refundLocktime,
326
- ...rfqSecretsToRecord(swap.secrets),
325
+ ...swapSecretsToRecord(swap.secrets),
327
326
  });
328
327
  await wallet.send({ address: swap.address, amount: swap.fundAmount });
329
328
 
@@ -333,7 +332,7 @@ const utxo = await awaitOnchainFill(chain, swap.htlc, minConfirmations);
333
332
  await claimOnchainFill(chain, {
334
333
  htlc: swap.htlc,
335
334
  utxo,
336
- preimage: await preimageForRfqSecrets(wallet, swap.secrets),
335
+ preimage: swap.secrets.preimage,
337
336
  payoutPkScript,
338
337
  feeRateSatVb,
339
338
  sign,
@@ -355,11 +354,9 @@ Crash recovery is record-driven, not chain-driven: `classifyOnchainHtlc` re-deri
355
354
  state (unfunded / awaiting confirmations / claimable / refundable / claimed-with-P / swept) from
356
355
  `ChainSource` plus the stored outpoint — without the stored record a spent HTLC is
357
356
  indistinguishable from an unfunded one, which is why persisting before funding is mandatory. The
358
- `AssetSwap` record carries the onchain fields (`paymentHash`, `signingDescriptor`,
359
- `preimageHex` for caller-supplied P, `fallbackSecrets`, `htlcPkScriptHex`, `htlcLocktime`,
360
- `l1Txid`) and the statuses `awaiting_fill / claimable / claimed / refunded_l1`.
361
- `fallbackSecrets` is versioned and discriminated: `{ version: 1, type: "stored",
362
- senderPrivateKeyHex, preimageHex? }`.
357
+ `AssetSwap` record carries the onchain fields (`paymentHash`, `signingDescriptor`, `preimageHex`
358
+ for a P that cannot be re-derived, `htlcPkScriptHex`, `htlcLocktime`, `l1Txid`) and the statuses
359
+ `awaiting_fill / claimable / claimed / refunded_l1`.
363
360
 
364
361
  **On-board corridors are covered.** `requestLightningReceive` (`lightning:BTC -> arkade:BTC`) and
365
362
  `requestOnchainReceive` (`onchain:BTC -> arkade:BTC`) mirror the send-side flows: quote → derive
@@ -421,29 +418,59 @@ Lightning HTLC lapses, and **the payer is refunded** — the trader loses the in
421
418
  funds it was holding. Which is why staying online to claim is an obligation and not a preference:
422
419
  covclaimd cannot claim this covenant today, so the claim packet's offline path does not yet run.
423
420
 
424
- ## RFQ secrets are derived, not stored
421
+ ## Swap secrets come from the wallet, not from this package
425
422
 
426
- The two secrets an RFQ swap needs the VHTLC `sender` key and, for an onchain send, the preimage
427
- are functions of the wallet seed plus one HD-allocated descriptor. The record keeps the descriptor,
428
- which is public, so a copied browser profile or a device backup yields nothing spendable.
423
+ This package holds no key logic at all. It names the leg it is building and the SDK answers:
424
+
425
+ ```ts
426
+ // a leg we fund — all it needs is the key that refunds it
427
+ const { pubkey: refundPubkey, descriptor: refundDescriptor } = await provisionRefundKey(wallet);
428
+ // a leg we claim — the key that receives it, and the P that unlocks it
429
+ const { pubkey, descriptor, preimage, paymentHash, mustPersistPreimage } =
430
+ await provisionClaimSecret(wallet);
431
+ ```
432
+
433
+ Where the key comes from is the wallet's decision, invisible here: an HD wallet allocates a fresh
434
+ descriptor per swap, a static wallet answers with its one `tr(pubkey)`. The record keeps the
435
+ descriptor, which is public, and `contractSigner(wallet, descriptor)` recovers the signer.
436
+
437
+ What each swap stores, and what is recoverable:
438
+
439
+ | Wallet answers with | Spending key | Preimage (when the leg needs one) | Secret at rest |
440
+ | ------------------- | --------------------- | --------------------------------- | ----------------- |
441
+ | fresh HD descriptor | re-derives from seed | derives deterministically | none |
442
+ | static `tr(pubkey)` | the wallet's identity | random, stored on the record | the preimage only |
443
+
444
+ The preimage split follows the **descriptor's shape**, not the wallet's type: an HD child
445
+ descriptor is unique to its swap, so `sha256(sign_det(...))` is safe; a static descriptor is the
446
+ same key for every swap, so a derived preimage would repeat across swaps — one solver learning its
447
+ own preimage would learn every other swap's — and a per-swap random preimage is stored instead.
448
+ `mustPersistPreimage` says which you got. A stored preimage is the one secret at rest in the
449
+ design, and it is never a private key.
429
450
 
430
451
  ```ts
431
452
  const swap = await requestOnchainSend(/* … */);
432
- swap.secrets; // { derivable: true, signingDescriptor } — persist it, it holds no secret
433
- await saveSwap({ ...record, ...rfqSecretsToRecord(swap.secrets) });
434
-
435
- // Later, from the seed plus that descriptor. Guard the lookup: offer-corridor
436
- // records (and records that lost their secrets fields) carry no secrets at
437
- // all, and a `!` here would crash the whole recovery loop on the first one.
438
- const secrets = rfqSecretsOfRecord(record);
439
- if (secrets) {
440
- const preimage = await preimageForRfqSecrets(wallet, secrets);
453
+ // `swapSecretsToRecord` stores the public descriptor always, and `preimageHex`
454
+ // only when the wallet said it cannot re-derive P.
455
+ await saveSwap({ ...record, ...swapSecretsToRecord(swap.secrets) });
456
+
457
+ // Later, from the seed plus that descriptor. Only ask for a preimage the
458
+ // corridor gave us one for: a lightning send's P belongs to the payee, so
459
+ // this throws on those records rather than inventing something the chain will
460
+ // never match. `LIGHTNING_SEND_PAIR` is exported from this package.
461
+ if (record.signingDescriptor && record.pair !== LIGHTNING_SEND_PAIR) {
462
+ const preimage = await contractPreimage(
463
+ wallet,
464
+ record.signingDescriptor,
465
+ record.preimageHex ? hex.decode(record.preimageHex) : undefined,
466
+ );
441
467
  }
442
468
 
443
469
  // For a refund, take the composition instead of the guard: it turns all three
444
- // ways a wallet can fail to produce the sender key — no secrets on the record,
445
- // an unreadable fallback arm, a descriptor from another seed into one typed
446
- // `RefundNotLocallyPossibleError` carrying which. Wire `refundArkade` to this.
470
+ // ways a wallet can fail to produce the sender key — the record names no
471
+ // descriptor, the descriptor is another seed's, the wallet holds the key but
472
+ // cannot sign into one typed `RefundNotLocallyPossibleError` carrying which,
473
+ // and lets a signer outage stay retryable. Wire `refundArkade` to this.
447
474
  const sender = await senderIdentityForSwapRecord(wallet, record);
448
475
  ```
449
476
 
@@ -454,17 +481,15 @@ terminal: the lockup stays funded and watched, a solver claim still ends the swa
454
481
  `pending`. The manager reports the same state when nothing is wired to act (`enableAutoActions:
455
482
  false`, or no callbacks) and the window has passed.
456
483
 
457
- `derivable: false` is the fallback for wallets that cannot allocate (static / `auto` / custom
458
- signers). It carries the raw `senderPrivateKey` and, for onchain sends, `preimage`;
459
- `rfqSecretsToRecord` stores them under `AssetSwap.fallbackSecrets` as a complete versioned
460
- record. The discriminant is a type-level fact, so a consumer written against the derivable arm
461
- alone will not compile against the fallback. A caller-supplied preimage on an HD wallet keeps
462
- `signingDescriptor` for the sender key and stores only `preimageHex` as secret material.
484
+ A caller-supplied preimage keeps `signingDescriptor` for the sender key and stores only
485
+ `preimageHex` as secret material.
463
486
 
464
- Each swap **allocates** its own descriptor rather than peeking at the current one: two swaps sharing
465
- a descriptor derive the _identical_ preimage, so one solver learning its own preimage would learn the
466
- other swap's. On restore, `adoptSwapDescriptor` moves the wallet's watermark past a restored record's
467
- index so it cannot be handed out twice.
487
+ On an HD wallet each swap **allocates** its own descriptor rather than peeking at the current one:
488
+ two swaps sharing a descriptor derive the _identical_ preimage, so one solver learning its own
489
+ preimage would learn the other swap's. (Static wallets share their one descriptor by design that
490
+ is why their preimages are stored per swap, never derived.) On restore, `adoptContractDescriptor`
491
+ (from `@arkade-os/sdk`) moves the wallet's watermark past a restored record's index so it cannot be
492
+ handed out twice; a static descriptor names no index and adopts as a no-op.
468
493
 
469
494
  The derivation is `sha256(signSchnorrDeterministic(sha256("Arkade-RFQ-Preimage-v1" ‖ xonly(32) ‖
470
495
  u32le(0))))`, mirroring NArk's Boltz scheme (`SwapsManagementService.cs:128-160`) with an
@@ -480,24 +505,40 @@ index from the wallet's receive stream, and a swap index never becomes a funded
480
505
  so it looks _unused_ to a seed-only `restore()` gap scan. Many consecutive swap allocations between
481
506
  two funded receive indices can therefore exceed the scan's `gapLimit` (default 20) and stop it
482
507
  before later-funded addresses are found. Keep the swap repository in backups (restore then adopts
483
- each record's descriptor via `adoptSwapDescriptor`), or raise `gapLimit` on seed-only restores
508
+ each record's descriptor via `adoptContractDescriptor`), or raise `gapLimit` on seed-only restores
484
509
  after heavy swap use.
485
510
 
486
511
  ## Breaking changes on this branch (pre-release migration notes)
487
512
 
488
513
  The package is pre-release; these notes replace a changelog for consumers tracking the branch.
489
514
 
515
+ - **`secrets.ts` is gone; key provisioning moved into `@arkade-os/sdk`.** This package no longer
516
+ derives, mints, or names keys. It asks the SDK for what the leg needs — `provisionRefundKey(wallet)`
517
+ for a leg it funds, `provisionClaimSecret(wallet, { preimage? })` for one it claims — and
518
+ recovers with `contractSigner(wallet, descriptor)` / `contractPreimage(wallet, descriptor,
519
+ stored?)`. The returned `ProvisionedKey` / `ProvisionedClaimSecret` replace `SwapSecrets`, and
520
+ `descriptor` replaces `signingDescriptor` on them. Removed from this package with no
521
+ replacement here: `deriveSwapSecrets`, `randomSwapSecrets`, `senderPubkeyForRfqSecrets`,
522
+ `preimageForRfqSecrets`, `senderIdentityForRfqSecrets`, `isPerSwapDescriptor`, `derivePreimage`,
523
+ `buildPreimageMessage`, `RFQ_PREIMAGE_TAG`, `isDeterministicSigner`, `adoptSwapDescriptor` (now
524
+ `adoptContractDescriptor` in the SDK), `SwapSecrets` / `DerivedSwapSecrets` /
525
+ `StoredSwapSecrets`, and `rfqSecretsToRecord` / `rfqSecretsOfRecord` — persist a provisioned
526
+ secret with **`swapSecretsToRecord`** from `store` instead, and read P back with
527
+ `contractPreimage`. `RefundNotLocallyPossibleError` and `senderIdentityForSwapRecord` stay here
528
+ (now in `refundBlocked.ts`): they are swap lifecycle, not key provisioning.
529
+ - **No swap record can carry a private key.** `AssetSwap.fallbackSecrets` and the
530
+ `AssetSwapFallbackSecrets` types are deleted rather than kept readable, and `preimageHex` — set
531
+ only when the wallet reports `mustPersistPreimage` — is the record's one secret field. A record
532
+ written by 0.0.1–0.0.3 carries no `signingDescriptor`, so `senderIdentityForSwapRecord` refuses
533
+ it with `no-secrets` rather than silently mis-signing; those versions shipped before any
534
+ consumer, which is the window for doing this without a secret migration.
490
535
  - **`requestLightningSend` / `requestOnchainSend` return `secrets`, not top-level raw key material.**
491
536
  `senderPrivateKey` is gone from both return types; caller-owned onchain preimages live inside
492
537
  `secrets` and must be persisted with the record. `pushRefundWithoutReceiver` /
493
538
  `refundIfUnresolved` take `sender: Identity` instead of `senderPrivateKey: Uint8Array` — build
494
539
  it from the record with `senderIdentityForSwapRecord`, which is what keeps a wallet that cannot
495
- sign reporting `RefundNotLocallyPossibleError` rather than a `TypeError` at the push site;
496
- `senderIdentityForRfqSecrets` is for callers that already hold resolved secrets. `AssetSwap`
497
- gains `signingDescriptor?`,
498
- `preimageHex?`, and complete stored-arm `fallbackSecrets?`. Landed while the package is
499
- unpublished and consumer-free, which is the whole window for doing it: after a consumer ships,
500
- the same change becomes a secret migration across every deployed wallet.
540
+ sign reporting `RefundNotLocallyPossibleError` rather than a `TypeError` at the push site.
541
+ `AssetSwap` gains `signingDescriptor?` and `preimageHex?`.
501
542
  - **Every derived address changed, in both corridors.** The lightning-send lockup moved from the
502
543
  3-leaf program-artifact VHTLC to the 8-leaf `VHTLC.ScriptV2` (non-interactive claim and refund
503
544
  leaves), and the L1 HTLC's claim leaf gained a `SIZE 32 EQUALVERIFY` preimage-length guard. Both
@@ -516,11 +557,11 @@ scanned? })` — the server key is required because a spend is classified by reb
516
557
  indistinguishable from a fill. Leaves have no such failure mode.
517
558
  - **A spend that cannot be classified is no longer restored as `fulfilled`.** It leaves the funding
518
559
  txid unanswered so a later scan decides it. Records are never written on a guess.
519
- - **`AssetSwap` gained the secret-bearing `signingDescriptor?` / `fallbackSecrets?` fields**, and
520
- `preimageHex` narrowed from "the claim preimage P" to "caller-supplied P only". The repository
521
- version stays `1` — the package is unreleased, so there is no stored record to migrate — but a
522
- field-mapped backend must persist the record whole: silently dropping `fallbackSecrets` on write
523
- loses the stored arm's claim and refund keys.
560
+ - **`AssetSwap` gained `signingDescriptor?`**, and `preimageHex` now means "P that cannot be
561
+ re-derived" caller-supplied, or minted for a static descriptor. The repository version stays
562
+ `1` — the package is unreleased, so there is no stored record to migrate — but a field-mapped
563
+ backend must persist the record whole: silently dropping `preimageHex` leaves a static swap
564
+ permanently unclaimable.
524
565
  - **A write that gates something irreversible throws; one that follows it does not.**
525
566
  `addAssetSwap` and `updateAssetSwap` throw on a failed read or write — nothing irreversible may
526
567
  happen until the record is durable, which is why `cancelOffer` writes its `cancelling` marker