From f986b14290d677b23c2bbe5ba0a0f760dcdd1979 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 14 Apr 2026 11:58:16 +0200 Subject: [PATCH 1/6] staticaddr: add restore and reconciliation hooks --- staticaddr/address/manager.go | 140 +++++++++++++++++++++++++++-- staticaddr/address/manager_test.go | 104 +++++++++++++++++++++ staticaddr/deposit/manager.go | 32 +++++-- test/walletkit_mock.go | 19 ++++ 4 files changed, 279 insertions(+), 16 deletions(-) diff --git a/staticaddr/address/manager.go b/staticaddr/address/manager.go index e96d362bf..035e335a8 100644 --- a/staticaddr/address/manager.go +++ b/staticaddr/address/manager.go @@ -3,7 +3,9 @@ package address import ( "bytes" "context" + "errors" "fmt" + "strings" "sync" "sync/atomic" @@ -21,6 +23,12 @@ import ( "github.com/lightningnetwork/lnd/lnwallet" ) +var ( + // ErrNoStaticAddress is returned when no static address parameters are + // present in the store. + ErrNoStaticAddress = errors.New("no static address parameters found") +) + // ManagerConfig holds the configuration for the address manager. type ManagerConfig struct { // AddressClient is the client that communicates with the loop server @@ -199,6 +207,108 @@ func (m *Manager) NewAddress(ctx context.Context) (*btcutil.AddressTaproot, return nil, 0, err } + _, err = m.importAddressTapscript(ctx, staticAddress) + if err != nil { + return nil, 0, err + } + + address, err := m.GetTaprootAddress( + clientPubKey.PubKey, serverPubKey, int64(serverParams.Expiry), + ) + if err != nil { + return nil, 0, err + } + + return address, int64(serverParams.Expiry), nil +} + +// RestoreAddress recreates a static address record locally and makes sure the +// corresponding tapscript is imported into lnd. If the same address already +// exists locally, the call is idempotent. +func (m *Manager) RestoreAddress(ctx context.Context, + addrParams *Parameters) (*btcutil.AddressTaproot, bool, error) { + + if addrParams == nil { + return nil, false, fmt.Errorf("missing static address parameters") + } + + staticAddress, err := script.NewStaticAddress( + input.MuSig2Version100RC2, int64(addrParams.Expiry), + addrParams.ClientPubkey, addrParams.ServerPubkey, + ) + if err != nil { + return nil, false, err + } + + pkScript, err := staticAddress.StaticAddressScript() + if err != nil { + return nil, false, err + } + + if len(addrParams.PkScript) != 0 && + !bytes.Equal(addrParams.PkScript, pkScript) { + + return nil, false, fmt.Errorf("static address pk script mismatch") + } + + addrParams.PkScript = pkScript + if addrParams.InitiationHeight <= 0 { + addrParams.InitiationHeight = m.currentHeight.Load() + } + + m.Lock() + existing, err := m.cfg.Store.GetAllStaticAddresses(ctx) + if err != nil { + m.Unlock() + + return nil, false, err + } + + changed := false + switch { + case len(existing) == 0: + err = m.cfg.Store.CreateStaticAddress(ctx, addrParams) + if err != nil { + m.Unlock() + + return nil, false, err + } + changed = true + + case len(existing) > 1: + m.Unlock() + + return nil, false, fmt.Errorf("more than one static address found") + + case !sameAddressParameters(existing[0], addrParams): + m.Unlock() + + return nil, false, fmt.Errorf("existing static address differs from " + + "backup") + } + m.Unlock() + + imported, err := m.importAddressTapscript(ctx, staticAddress) + if err != nil { + return nil, false, err + } + + changed = changed || imported + + addr, err := m.GetTaprootAddress( + addrParams.ClientPubkey, addrParams.ServerPubkey, + int64(addrParams.Expiry), + ) + if err != nil { + return nil, false, err + } + + return addr, changed, nil +} + +func (m *Manager) importAddressTapscript(ctx context.Context, + staticAddress *script.StaticAddress) (bool, error) { + // Import the static address tapscript into our lnd wallet, so we can // track unspent outputs of it. tapScript := input.TapscriptFullTree( @@ -206,20 +316,34 @@ func (m *Manager) NewAddress(ctx context.Context) (*btcutil.AddressTaproot, ) addr, err := m.cfg.WalletKit.ImportTaprootScript(ctx, tapScript) if err != nil { - return nil, 0, err + // Restoring into an lnd instance that already imported the script is + // expected. Treat the duplicate import as success. + if strings.Contains(err.Error(), "already exists") { + log.Infof("Static address tapscript already imported") + return false, nil + } + + return false, err } log.Infof("Imported static address taproot script to lnd wallet: %v", addr) - address, err := m.GetTaprootAddress( - clientPubKey.PubKey, serverPubKey, int64(serverParams.Expiry), - ) - if err != nil { - return nil, 0, err + return true, nil +} + +func sameAddressParameters(a, b *Parameters) bool { + if a == nil || b == nil { + return false } - return address, int64(serverParams.Expiry), nil + return a.ClientPubkey.IsEqual(b.ClientPubkey) && + a.ServerPubkey.IsEqual(b.ServerPubkey) && + a.Expiry == b.Expiry && + bytes.Equal(a.PkScript, b.PkScript) && + a.KeyLocator == b.KeyLocator && + a.ProtocolVersion == b.ProtocolVersion && + a.InitiationHeight == b.InitiationHeight } // GetTaprootAddress returns a taproot address for the given client and server @@ -297,7 +421,7 @@ func (m *Manager) GetStaticAddressParameters(ctx context.Context) (*Parameters, } if len(params) == 0 { - return nil, fmt.Errorf("no static address parameters found") + return nil, ErrNoStaticAddress } return params[0], nil diff --git a/staticaddr/address/manager_test.go b/staticaddr/address/manager_test.go index c23f246cd..a96e47735 100644 --- a/staticaddr/address/manager_test.go +++ b/staticaddr/address/manager_test.go @@ -128,6 +128,110 @@ func TestManager(t *testing.T) { require.EqualValues(t, defaultExpiry, expiry) } +// TestRestoreAddress verifies that restoring an address recreates the same +// static address locally without requiring a server call. +func TestRestoreAddress(t *testing.T) { + ctxb := t.Context() + + testContext := NewAddressManagerTestContext(t) + + keyDesc, err := testContext.mockLnd.WalletKit.DeriveKey( + ctxb, &keychain.KeyLocator{ + Family: keychain.KeyFamily(swap.StaticAddressKeyFamily), + Index: 7, + }, + ) + require.NoError(t, err) + + staticAddress, err := script.NewStaticAddress( + input.MuSig2Version100RC2, int64(defaultExpiry), + keyDesc.PubKey, defaultServerPubkey, + ) + require.NoError(t, err) + + pkScript, err := staticAddress.StaticAddressScript() + require.NoError(t, err) + + addressParams := &Parameters{ + ClientPubkey: keyDesc.PubKey, + ServerPubkey: defaultServerPubkey, + Expiry: defaultExpiry, + PkScript: pkScript, + KeyLocator: keyDesc.KeyLocator, + ProtocolVersion: 0, + InitiationHeight: 123, + } + + taprootAddress, restored, err := testContext.manager.RestoreAddress( + ctxb, addressParams, + ) + require.NoError(t, err) + require.True(t, restored) + + expectedAddress, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(staticAddress.TaprootKey), + testContext.manager.cfg.ChainParams, + ) + require.NoError(t, err) + require.Equal(t, expectedAddress.String(), taprootAddress.String()) + + storedParams, err := testContext.manager.GetStaticAddressParameters(ctxb) + require.NoError(t, err) + require.True(t, sameAddressParameters(storedParams, addressParams)) + + taprootAddress, restored, err = testContext.manager.RestoreAddress( + ctxb, addressParams, + ) + require.NoError(t, err) + require.False(t, restored) + require.Equal(t, expectedAddress.String(), taprootAddress.String()) +} + +// TestRestoreAddressRejectsDifferentInitiationHeight verifies that a restore +// request with the same address material but a different initiation height is +// rejected instead of being treated as idempotent. +func TestRestoreAddressRejectsDifferentInitiationHeight(t *testing.T) { + ctxb := t.Context() + + testContext := NewAddressManagerTestContext(t) + + keyDesc, err := testContext.mockLnd.WalletKit.DeriveKey( + ctxb, &keychain.KeyLocator{ + Family: keychain.KeyFamily(swap.StaticAddressKeyFamily), + Index: 7, + }, + ) + require.NoError(t, err) + + staticAddress, err := script.NewStaticAddress( + input.MuSig2Version100RC2, int64(defaultExpiry), + keyDesc.PubKey, defaultServerPubkey, + ) + require.NoError(t, err) + + pkScript, err := staticAddress.StaticAddressScript() + require.NoError(t, err) + + addressParams := &Parameters{ + ClientPubkey: keyDesc.PubKey, + ServerPubkey: defaultServerPubkey, + Expiry: defaultExpiry, + PkScript: pkScript, + KeyLocator: keyDesc.KeyLocator, + ProtocolVersion: 0, + InitiationHeight: 123, + } + + _, _, err = testContext.manager.RestoreAddress(ctxb, addressParams) + require.NoError(t, err) + + differentHeight := *addressParams + differentHeight.InitiationHeight = 456 + + _, _, err = testContext.manager.RestoreAddress(ctxb, &differentHeight) + require.ErrorContains(t, err, "existing static address differs from backup") +} + // GenerateExpectedTaprootAddress generates the expected taproot address that // the predefined parameters are supposed to generate. func GenerateExpectedTaprootAddress(t *ManagerTestContext) ( diff --git a/staticaddr/deposit/manager.go b/staticaddr/deposit/manager.go index af8820302..9376bfea3 100644 --- a/staticaddr/deposit/manager.go +++ b/staticaddr/deposit/manager.go @@ -64,6 +64,10 @@ type Manager struct { // mu guards access to the activeDeposits map. mu sync.Mutex + // reconcileMu serializes deposit recovery and reconciliation so restore + // requests can't race the background polling loop. + reconcileMu sync.Mutex + // activeDeposits contains all the active static address outputs. activeDeposits map[wire.OutPoint]*FSM @@ -108,7 +112,7 @@ func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error { // Reconcile immediately on startup so deposits are available // before the first ticker fires. - err = m.reconcileDeposits(ctx) + _, err = m.ReconcileDeposits(ctx) if err != nil { log.Errorf("unable to reconcile deposits: %v", err) } @@ -162,6 +166,9 @@ func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error { // recoverDeposits recovers static address parameters, previous deposits and // state machines from the database and starts the deposit notifier. func (m *Manager) recoverDeposits(ctx context.Context) error { + m.reconcileMu.Lock() + defer m.reconcileMu.Unlock() + log.Infof("Recovering static address parameters and deposits...") // Recover deposits. @@ -218,7 +225,7 @@ func (m *Manager) pollDeposits(ctx context.Context) { for { select { case <-ticker.C: - err := m.reconcileDeposits(ctx) + _, err := m.ReconcileDeposits(ctx) if err != nil { log.Errorf("unable to reconcile "+ "deposits: %v", err) @@ -235,38 +242,47 @@ func (m *Manager) pollDeposits(ctx context.Context) { // wallet and matches it against the deposits in our memory that we've seen so // far. It picks the newly identified deposits and starts a state machine per // deposit to track its progress. -func (m *Manager) reconcileDeposits(ctx context.Context) error { +func (m *Manager) reconcileDeposits(ctx context.Context) (int, error) { log.Tracef("Reconciling new deposits...") utxos, err := m.cfg.AddressManager.ListUnspent( ctx, MinConfs, MaxConfs, ) if err != nil { - return fmt.Errorf("unable to list new deposits: %w", err) + return 0, fmt.Errorf("unable to list new deposits: %w", err) } newDeposits := m.filterNewDeposits(utxos) if len(newDeposits) == 0 { log.Tracef("No new deposits...") - return nil + return 0, nil } for _, utxo := range newDeposits { deposit, err := m.createNewDeposit(ctx, utxo) if err != nil { - return fmt.Errorf("unable to retain new deposit: %w", + return 0, fmt.Errorf("unable to retain new deposit: %w", err) } log.Debugf("Received deposit: %v", deposit) err = m.startDepositFsm(ctx, deposit) if err != nil { - return fmt.Errorf("unable to start new deposit FSM: %w", + return 0, fmt.Errorf("unable to start new deposit FSM: %w", err) } } - return nil + return len(newDeposits), nil +} + +// ReconcileDeposits triggers a best-effort reconciliation pass and returns the +// number of newly discovered deposits. +func (m *Manager) ReconcileDeposits(ctx context.Context) (int, error) { + m.reconcileMu.Lock() + defer m.reconcileMu.Unlock() + + return m.reconcileDeposits(ctx) } // createNewDeposit transforms the wallet utxo into a deposit struct and stores diff --git a/test/walletkit_mock.go b/test/walletkit_mock.go index ee42fa162..b80e9314b 100644 --- a/test/walletkit_mock.go +++ b/test/walletkit_mock.go @@ -9,6 +9,7 @@ import ( "time" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg" @@ -33,6 +34,8 @@ type mockWalletKit struct { lnd *LndMockServices keyIndex int32 + importedTaprootScripts map[string]struct{} + feeEstimateLock sync.Mutex feeEstimates map[int32]chainfee.SatPerKWeight minRelayFee chainfee.SatPerKWeight @@ -338,5 +341,21 @@ func (m *mockWalletKit) ImportPublicKey(ctx context.Context, func (m *mockWalletKit) ImportTaprootScript(ctx context.Context, tapscript *waddrmgr.Tapscript) (btcutil.Address, error) { + taprootKey, err := tapscript.TaprootKey() + if err != nil { + return nil, err + } + + if m.importedTaprootScripts == nil { + m.importedTaprootScripts = make(map[string]struct{}) + } + + key := string(schnorr.SerializePubKey(taprootKey)) + if _, ok := m.importedTaprootScripts[key]; ok { + return nil, fmt.Errorf("taproot script already exists") + } + + m.importedTaprootScripts[key] = struct{}{} + return nil, nil } From 487a7561be89607ca6c89fdfd891e3d1fab217f6 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 14 Apr 2026 12:02:56 +0200 Subject: [PATCH 2/6] recovery: add backup and restore package Introduce a dedicated recovery package for encrypted local backups of static-address state and raw l402 token files. The package owns the backup file format, seed-derived encryption, static-address key re-derivation with gap fallback, token file restore, and best-effort deposit reconciliation orchestration. It also includes package-level documentation and focused tests for the file helpers. --- recovery/README.md | 297 +++++++++ recovery/service.go | 1048 +++++++++++++++++++++++++++++++ recovery/service_test.go | 1280 ++++++++++++++++++++++++++++++++++++++ swap/keychain.go | 14 +- 4 files changed, 2636 insertions(+), 3 deletions(-) create mode 100644 recovery/README.md create mode 100644 recovery/service.go create mode 100644 recovery/service_test.go diff --git a/recovery/README.md b/recovery/README.md new file mode 100644 index 000000000..5aaea1a15 --- /dev/null +++ b/recovery/README.md @@ -0,0 +1,297 @@ +# Recovery Package + +This package implements local recovery for Loop's static-address and L402 +state. + +## Goal + +Recovery is generation-based. A generation is anchored by: + +- one paid L402 token +- one immutable static-address generation root tied to that L402 + +Today that generation root still materializes locally as one legacy static +address. The backup format now also stores the stable generation metadata that +future multi-address `main`/`change` issuance will recover by scanning from, +without rewriting the backup file. + +The recovery flow is designed to let a fresh or repaired Loop instance rebuild +that generation after local disk loss, data-directory replacement, or partial +corruption. + +The current PR intentionally uses a single immutable backup per L402 +generation. Once written, that backup file is never updated in place. + +## Backup Model + +The daemon writes at most one encrypted backup file for each paid L402 token +ID: + +`/L402_backup__.enc` + +In the normal layout this resolves inside the active network-specific Loop data +directory, for example: + +`~/.loop/mainnet/L402_backup_1776159001000000000_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef.enc` + +There is no canonical mutable "latest backup" file anymore. + +If `loop recover` is called without `--backup_file`, Loop scans the active +network directory, validates all immutable backups it can decrypt, and restores +the backup with the latest timestamp in its filename. Legacy timestamp-less +backup filenames remain readable and fall back to the encrypted payload's L402 +creation time for ordering. + +## What Is Backed Up + +Each encrypted backup stores: + +- a backup format version +- the Loop network +- the paid L402 token ID +- the paid L402 token creation time +- the raw paid `l402.token` file +- immutable static-address generation metadata +- the static-address protocol version +- the generation server pubkey +- the static-address expiry +- the `main` key family +- the `change` key locator +- the `change` key family +- the `change` key index +- the generation start height +- the restore lookahead +- the current legacy static-address materialization +- the static-address client pubkey +- the static-address client key locator +- the static-address `pkScript` +- the derived taproot address string +- the static-address initiation height + +The L402 file is preserved as a raw blob so restore remains compatible with the +current Aperture token-store format. + +Deposit FSM state is not serialized into the backup. Deposits are rediscovered +after restore through the normal reconciliation path. + +## Why The Backup Stores Both Root And Legacy Address Data + +The immutable generation metadata is the forward-compatible root for future +multi-address recovery. The legacy single-address materialization is still kept +because the current branch restores the V0 one-address model directly. + +That means the backup already contains the stable metadata that a future +multi-address PR will need, while today's restore path can still recreate the +existing legacy static address exactly. + +## Encryption Model + +The file is encrypted with `secretbox` using a symmetric key derived from lnd +via `Signer.DeriveSharedKey`. + +The derivation uses: + +- a fixed NUMS public key +- the static-address main key family +- key index `0` + +This ties backup decryption to the same lnd seed that controls the static +address keys without introducing a user-managed recovery password in this +phase. + +## When Backups Are Written + +The backup is only written once a complete recoverable generation exists. +Today that means both of the following must already exist locally: + +- a paid current `l402.token` +- a local legacy static address bound to that token + +Pending tokens are not backed up. + +If the immutable backup file for the current paid token ID already exists, +backup creation is a no-op. + +## Startup Behavior + +Startup is responsible for materializing the current generation before the +backup is written. + +On startup `loopd` now: + +1. creates the recovery service +2. if the install is fresh and immutable backup files already exist locally, + restores the latest backup by filename timestamp +3. otherwise, asks the static-address manager for the current static address +4. if the address does not exist yet, fetches the paid L402, derives the + client key, requests the legacy static address from the server, stores it, + and imports the tapscript into lnd +5. writes the immutable backup for the resulting paid-L402/static-address + generation + +This gives the branch the "one backup per L402" property without later backup +refreshes. + +### Existing Users + +For existing users that already have a paid L402 and a legacy static address, +the first startup with the upgraded client backfills the missing immutable +backup for the active generation. + +### Fresh Installs + +For fresh installations, startup first checks whether immutable backups already +exist in the active Loop data directory. + +If they do, Loop restores the latest backup by filename timestamp instead of +creating a new paid L402 generation. + +If they do not, startup materializes the initial paid L402 plus legacy static- +address generation so the backup can be written immediately. + +The `loop static new` command is therefore no longer the only creation point. +It now returns the current static address and only falls back to on-demand +creation if startup initialization did not complete earlier. + +## Restore Flow + +`loop recover --backup_file ` restores a specific immutable backup. If +`--backup_file` is omitted, Loop restores the most recent valid immutable +backup in the active network directory. + +Current restore performs the following steps: + +1. derive the local encryption key from lnd +2. read and decrypt the backup file +3. validate the backup version, network, and L402 token ID +4. restore the paid `l402.token` file if it is not already present with the + same contents +5. if legacy static-address metadata is present, reconstruct the client pubkey +6. recreate the local legacy static-address record and re-import its tapscript + into lnd +7. trigger best-effort deposit reconciliation + +Client-key reconstruction uses the following strategy: + +- first try the exact backed-up key locator +- if that fails, derive keys backwards with a gap of 20 in the static-address + main key family +- when the backup contains the client pubkey, require that the derived key + matches it before accepting the address reconstruction + +The multi-address scan-and-rebuild flow is intentionally not activated in this +branch yet. This branch only makes sure the immutable backup already contains +the metadata that future flow will need. + +## Future Multi-Address Generation + +The planned multi-address model uses only two client-side key families: + +- `main` addresses for externally visible static-address deposits +- `change` addresses for outputs that return value back into the static-address + address space + +The future `static_addresses` table remains a table of concrete derived +addresses. Each row represents one address child and stores: + +- the client pubkey +- the server pubkey +- the client key family +- the client key index +- the resulting `pkScript` +- the protocol version +- the generation initiation height + +The immutable backup does not store every row. Instead it stores the generation +root metadata that allows those rows to be rediscovered by scanning. + +For each future `main` or `change` address: + +1. the client chooses the appropriate key family +2. the client derives the next pubkey from lnd for that family +3. the client combines that pubkey with the L402-bound server pubkey using the + static-address MuSig2 construction for the backed-up protocol version +4. the taproot tweak commits to the static-address timeout leaf +5. the resulting taproot output key yields the final P2TR `pkScript` +6. the concrete child row is stored locally in `static_addresses` + +Because the backup is immutable, future restore must regenerate candidate +`main` and `change` children from the backed-up branch metadata, rescan from +the backed-up start height, and rebuild local table rows from what is found on +chain. It must not depend on a mutable "last issued child index" snapshot. + +## Server Proof For Multi-Address Inputs + +For a future static swap or withdrawal that spends multi-address inputs, the +server-side proof model is: + +1. the paid L402 authenticates the request and identifies the generation +2. the L402 selects the fixed generation server pubkey and the fixed + protocol/expiry parameters +3. for each input, the client sends the concrete client pubkey that was used to + construct that input's address +4. the server recomputes the timeout leaf for the backed-up protocol version + and expiry +5. the server recomputes the MuSig2 aggregate key from the concrete client + pubkey for that input, the server pubkey bound to the L402 generation, and + the taproot tweak implied by the timeout leaf +6. the server derives the expected taproot output key and the expected P2TR + `pkScript` +7. the server compares that derived `pkScript` with the prevout `pkScript` of + the input being authorized + +If they match, the input belongs to that L402 generation because the output +commits to the generation's server key and the concrete client pubkey used for +that input. + +This proof is about generation membership, not about proving a particular child +index to the server. The immutable backup therefore only needs the stable +generation root metadata, while exact row discovery remains a client-side +wallet-and-chain scan problem. + +## Operational Limits + +Current restore still restores the legacy one-address model only. + +Some practical consequences follow from that: + +- restoring an older immutable backup is best done into a fresh Loop data + directory +- only one legacy static address can be recreated directly by the current + restore code +- historical deposit state is rebuilt best-effort from reconciliation, not by + replaying every stored deposit transition + +## Why The Backup Is Immutable + +The multi-address work needs recovery to be based on stable root material, not +on mutable local cursor snapshots. + +Using one immutable backup per L402 forces that discipline now: + +- the backup must describe a recoverable generation root +- restore must be able to rediscover state from deterministic wallet- and + chain-derived scanning +- later address issuance must not depend on backup files being rewritten + +That is the key design constraint for the next PRs. + +## Package Boundaries + +This package owns: + +- backup payload definition +- backup encryption and decryption +- immutable backup-file discovery and selection +- paid L402 token-file backup and restore +- legacy static-address key re-derivation and restore orchestration +- immutable generation metadata for future multi-address restore +- post-restore deposit reconciliation orchestration + +This package does not own: + +- CLI command handling +- gRPC transport +- the static-address server protocol +- the future multi-address scanning implementation +- `loopd` startup wiring diff --git a/recovery/service.go b/recovery/service.go new file mode 100644 index 000000000..a5b97cca8 --- /dev/null +++ b/recovery/service.go @@ -0,0 +1,1048 @@ +package recovery + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "strconv" + "strings" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/lightninglabs/aperture/l402" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/staticaddr/address" + staticaddrscript "github.com/lightninglabs/loop/staticaddr/script" + staticaddrversion "github.com/lightninglabs/loop/staticaddr/version" + "github.com/lightninglabs/loop/swap" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lncfg" + "github.com/lightningnetwork/lnd/lntypes" + "golang.org/x/crypto/nacl/secretbox" +) + +const ( + backupVersion = 2 + + backupBaseName = "L402_backup" + + backupFileExt = ".enc" + + backupKeyGap = 20 + + // defaultMultiAddressLookahead is the scan window that future + // main/change restore flows should use when reconstructing addresses + // from immutable generation metadata. + defaultMultiAddressLookahead = 20 + + // defaultChangeKeyIndex is the starting child index for the future + // change-output branch. It is backed up explicitly so later + // multi-address restore does not need to infer it from protocol + // defaults. + defaultChangeKeyIndex = 0 + + paidTokenFileName = "l402.token" + + pendingTokenFileName = "l402.token.pending" +) + +var backupKeyLocator = keychain.KeyLocator{ + Family: keychain.KeyFamily(swap.StaticAddressKeyFamily), + Index: 0, +} + +var backupMagic = []byte("loopbak1") + +// StaticAddressManager is the subset of static-address behavior required for +// creating and restoring recovery backups. +type StaticAddressManager interface { + GetStaticAddressParameters(context.Context) (*address.Parameters, error) + GetTaprootAddress(clientPubkey, serverPubkey *btcec.PublicKey, + expiry int64) (*btcutil.AddressTaproot, error) + RestoreAddress(context.Context, + *address.Parameters) (*btcutil.AddressTaproot, bool, error) +} + +// DepositManager is the subset of deposit-manager behavior required to +// reconcile deposits after restore. +type DepositManager interface { + ReconcileDeposits(context.Context) (int, error) +} + +// RecoverResult describes the outcome of a restore attempt. +type RecoverResult struct { + BackupFile string + StaticAddress string + RestoredStaticAddress bool + RestoredL402 bool + NumDepositsFound int + DepositReconciliationError string +} + +// Service coordinates creation and restoration of encrypted local recovery +// backups for Loop static-address and L402 state. +type Service struct { + dataDir string + network string + signer lndclient.SignerClient + walletKit lndclient.WalletKitClient + staticAddressManager StaticAddressManager + depositManager DepositManager +} + +type backupPayload struct { + Version uint32 `json:"version"` + Network string `json:"network"` + L402TokenID string `json:"l402_token_id"` + L402TokenCreatedAt int64 `json:"l402_token_created_at"` + AddressGeneration *staticAddressGenerationBackup `json:"address_generation,omitempty"` + StaticAddress *staticAddressBackup `json:"static_address,omitempty"` + TokenFiles []*l402TokenFileEntry `json:"token_files,omitempty"` +} + +// staticAddressGenerationBackup is the immutable generation root that future +// multi-address recovery will scan from. It is intentionally stable across +// main/change child issuance and does not track mutable address cursors. +type staticAddressGenerationBackup struct { + ProtocolVersion uint32 `json:"protocol_version"` + ServerPubKey []byte `json:"server_pubkey"` + Expiry uint32 `json:"expiry"` + MainKeyFamily int32 `json:"main_key_family"` + ChangeKeyFamily int32 `json:"change_key_family"` + ChangeKeyIndex uint32 `json:"change_key_index"` + StartHeight int32 `json:"start_height,omitempty"` + Lookahead uint32 `json:"lookahead,omitempty"` +} + +// staticAddressBackup contains the legacy single-address materialization that +// the current branch can still restore directly. +type staticAddressBackup struct { + ProtocolVersion uint32 `json:"protocol_version"` + ClientPubKey []byte `json:"client_pubkey,omitempty"` + ServerPubKey []byte `json:"server_pubkey"` + Expiry uint32 `json:"expiry"` + ClientKeyFamily int32 `json:"client_key_family"` + ClientKeyIndex uint32 `json:"client_key_index"` + PkScript []byte `json:"pk_script,omitempty"` + Address string `json:"address,omitempty"` + InitiationHeight int32 `json:"initiation_height,omitempty"` +} + +type l402TokenFileEntry struct { + Name string `json:"name"` + Data []byte `json:"data"` +} + +type currentTokenState struct { + TokenID string + TokenCreatedAt int64 + TokenFiles []*l402TokenFileEntry +} + +type tokenRestoreResult struct { + restored bool + writtenPaths []string +} + +type backupFileDetails struct { + tokenID string + titleTimestamp int64 +} + +// NewService constructs a recovery service for a specific loop network data +// directory. +func NewService(dataDir, network string, signer lndclient.SignerClient, + walletKit lndclient.WalletKitClient, + staticAddressManager StaticAddressManager, + depositManager DepositManager) *Service { + + return &Service{ + dataDir: dataDir, + network: network, + signer: signer, + walletKit: walletKit, + staticAddressManager: staticAddressManager, + depositManager: depositManager, + } +} + +// WriteBackup writes an encrypted backup file for the current paid-L402 / +// static-address generation. It returns an empty path when there is no +// complete recoverable generation yet, or when the current L402 already has an +// immutable backup on disk. +func (s *Service) WriteBackup(ctx context.Context) (string, error) { + payload, hasState, err := s.buildPayload(ctx) + if err != nil || !hasState { + return "", err + } + + if backupFile, err := findBackupFileForToken( + s.dataDir, payload.L402TokenID, + ); err != nil { + return "", err + } else if backupFile != "" { + return "", nil + } + + fileName := backupFilePath( + s.dataDir, payload.L402TokenID, payload.L402TokenCreatedAt, + ) + + plaintext, err := json.Marshal(payload) + if err != nil { + return "", err + } + + key, err := s.deriveEncryptionKey(ctx) + if err != nil { + return "", err + } + + encrypted, err := encryptBackupPayload(key, plaintext) + if err != nil { + return "", err + } + + err = writeFileAtomically(fileName, encrypted) + if err != nil { + return "", err + } + + return fileName, nil +} + +// RestoreLatestOnFreshInstall restores the most recent local backup only when +// loopd has no local token files or static-address state yet. It returns the +// restore result together with a boolean indicating whether a restore was +// actually performed. +func (s *Service) RestoreLatestOnFreshInstall(ctx context.Context) ( + *RecoverResult, bool, error) { + + freshInstall, err := s.isFreshInstall(ctx) + if err != nil { + return nil, false, err + } + if !freshInstall { + return nil, false, nil + } + + result, err := s.Restore(ctx, "") + switch { + case err == nil: + return result, true, nil + + case errors.Is(err, os.ErrNotExist): + return nil, false, nil + + default: + return nil, false, err + } +} + +// Restore restores the local static-address and L402 state from an encrypted +// backup file. If backupFile is empty, the most recent immutable generation +// backup in the active network directory is used. +func (s *Service) Restore(ctx context.Context, backupFile string) ( + *RecoverResult, error) { + + key, err := s.deriveEncryptionKey(ctx) + if err != nil { + return nil, err + } + + fileName, err := s.resolveBackupFile(key, backupFile) + if err != nil { + return nil, err + } + + encrypted, err := os.ReadFile(fileName) + if err != nil { + return nil, err + } + + plaintext, err := decryptBackupPayload(key, encrypted) + if err != nil { + return nil, err + } + + var payload backupPayload + err = json.Unmarshal(plaintext, &payload) + if err != nil { + return nil, err + } + + err = payload.validateNetwork(s.network) + if err != nil { + return nil, err + } + + result := &RecoverResult{ + BackupFile: fileName, + } + + var restoreParams *address.Parameters + if payload.StaticAddress != nil { + restoreParams, err = s.prepareStaticAddressRestore( + ctx, payload.StaticAddress, + ) + if err != nil { + return nil, err + } + } + + tokenRestore, err := s.restoreTokenFiles(payload.TokenFiles) + if err != nil { + return nil, err + } + result.RestoredL402 = tokenRestore.restored + + if restoreParams != nil { + addr, restored, err := s.restorePreparedStaticAddress( + ctx, restoreParams, + ) + if err != nil { + rollbackErr := cleanupRestoredTokenFiles( + tokenRestore.writtenPaths, + ) + if rollbackErr != nil { + return nil, fmt.Errorf("unable to restore static "+ + "address: %w (also failed to roll back "+ + "restored token files: %v)", err, + rollbackErr) + } + + return nil, err + } + + result.StaticAddress = addr + result.RestoredStaticAddress = restored + } + + if payload.StaticAddress != nil && s.depositManager != nil { + numDeposits, err := s.depositManager.ReconcileDeposits(ctx) + if err != nil { + result.DepositReconciliationError = err.Error() + } else { + result.NumDepositsFound = numDeposits + } + } + + return result, nil +} + +func (p *backupPayload) validateNetwork(currentNetwork string) error { + switch { + case p.Version != backupVersion: + return fmt.Errorf("unsupported backup version %d", p.Version) + + case p.Network == "": + return fmt.Errorf("backup file is missing a network") + + case p.L402TokenID == "": + return fmt.Errorf("backup file is missing an L402 token ID") + + case p.Network != currentNetwork: + return fmt.Errorf("backup file network %s does not match "+ + "daemon network %s", p.Network, currentNetwork) + } + + return nil +} + +func (s *Service) buildPayload(ctx context.Context) (*backupPayload, bool, + error) { + + tokenState, err := s.currentPaidToken() + if err != nil { + return nil, false, err + } + if tokenState == nil || s.staticAddressManager == nil { + return nil, false, nil + } + + payload := &backupPayload{ + Version: backupVersion, + Network: s.network, + L402TokenID: tokenState.TokenID, + L402TokenCreatedAt: tokenState.TokenCreatedAt, + TokenFiles: tokenState.TokenFiles, + } + + addrParams, err := s.staticAddressManager.GetStaticAddressParameters(ctx) + switch { + case err == nil: + taprootAddress, err := s.staticAddressManager.GetTaprootAddress( + addrParams.ClientPubkey, addrParams.ServerPubkey, + int64(addrParams.Expiry), + ) + if err != nil { + return nil, false, err + } + + payload.AddressGeneration = &staticAddressGenerationBackup{ + ProtocolVersion: uint32(addrParams.ProtocolVersion), + ServerPubKey: addrParams.ServerPubkey.SerializeCompressed(), + Expiry: addrParams.Expiry, + MainKeyFamily: int32(addrParams.KeyLocator.Family), + ChangeKeyFamily: swap.StaticAddressChangeKeyFamily, + ChangeKeyIndex: defaultChangeKeyIndex, + StartHeight: addrParams.InitiationHeight, + Lookahead: defaultMultiAddressLookahead, + } + + payload.StaticAddress = &staticAddressBackup{ + ProtocolVersion: uint32(addrParams.ProtocolVersion), + ClientPubKey: addrParams.ClientPubkey.SerializeCompressed(), + ServerPubKey: addrParams.ServerPubkey.SerializeCompressed(), + Expiry: addrParams.Expiry, + ClientKeyFamily: int32(addrParams.KeyLocator.Family), + ClientKeyIndex: addrParams.KeyLocator.Index, + PkScript: slices.Clone(addrParams.PkScript), + Address: taprootAddress.String(), + InitiationHeight: addrParams.InitiationHeight, + } + + case errors.Is(err, address.ErrNoStaticAddress): + // The current L402 does not have a complete static-address + // generation yet, so there is nothing immutable to back up. + return nil, false, nil + + default: + return nil, false, err + } + + hasState := payload.AddressGeneration != nil && + len(payload.TokenFiles) > 0 + + return payload, hasState, nil +} + +func (s *Service) currentPaidToken() (*currentTokenState, error) { + tokenStore, err := l402.NewFileStore(s.dataDir) + if err != nil { + return nil, err + } + + token, err := tokenStore.CurrentToken() + switch { + case err == nil: + + case errors.Is(err, l402.ErrNoToken): + return nil, nil + + default: + return nil, err + } + + // Only fully paid tokens define an immutable recoverable generation. + if token.Preimage == (lntypes.Preimage{}) { + return nil, nil + } + + tokenID, err := decodeTokenID(token) + if err != nil { + return nil, err + } + + path := filepath.Join(s.dataDir, paidTokenFileName) + data, err := os.ReadFile(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, nil + } + + return nil, err + } + + return ¤tTokenState{ + TokenID: tokenID, + TokenCreatedAt: token.TimeCreated.UnixNano(), + TokenFiles: []*l402TokenFileEntry{{ + Name: paidTokenFileName, + Data: data, + }}, + }, nil +} + +func decodeTokenID(token *l402.Token) (string, error) { + identifier, err := l402.DecodeIdentifier( + bytes.NewReader(token.BaseMacaroon().Id()), + ) + if err != nil { + return "", err + } + + return identifier.TokenID.String(), nil +} + +func (s *Service) readTokenFiles() ([]*l402TokenFileEntry, error) { + path := filepath.Join(s.dataDir, paidTokenFileName) + data, err := os.ReadFile(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, nil + } + + return nil, err + } + + return []*l402TokenFileEntry{{ + Name: paidTokenFileName, + Data: data, + }}, nil +} + +func (s *Service) resolveBackupFile(key [32]byte, backupFile string) (string, + error) { + + if backupFile != "" { + return lncfg.CleanAndExpandPath(backupFile), nil + } + + return latestBackupFilePath(s.dataDir, key, s.network) +} + +type backupSelection struct { + fileName string + tokenID string + sortTimestamp int64 +} + +func latestBackupFilePath(dataDir string, key [32]byte, + network string) (string, error) { + + dirEntries, err := os.ReadDir(dataDir) + if err != nil { + return "", err + } + + var ( + latestSelection *backupSelection + firstErr error + ) + + for _, entry := range dirEntries { + if entry.IsDir() { + continue + } + + details, ok := parseBackupFileName(entry.Name()) + if !ok { + continue + } + + path := filepath.Join(dataDir, entry.Name()) + payload, err := readBackupPayload(key, path) + if err != nil { + if firstErr == nil { + firstErr = err + } + continue + } + err = payload.validateNetwork(network) + if err != nil { + if firstErr == nil { + firstErr = err + } + continue + } + if payload.L402TokenID != details.tokenID { + if firstErr == nil { + firstErr = fmt.Errorf("backup file %s token id "+ + "mismatch", path) + } + continue + } + + sortTimestamp := details.titleTimestamp + if sortTimestamp == 0 { + sortTimestamp = payload.L402TokenCreatedAt + } + + selection := &backupSelection{ + fileName: path, + tokenID: details.tokenID, + sortTimestamp: sortTimestamp, + } + + if latestSelection == nil || + selection.sortTimestamp > latestSelection.sortTimestamp || + (selection.sortTimestamp == latestSelection.sortTimestamp && + selection.tokenID > latestSelection.tokenID) { + + latestSelection = selection + } + } + + if latestSelection != nil { + return latestSelection.fileName, nil + } + if firstErr != nil { + return "", firstErr + } + + return "", os.ErrNotExist +} + +func backupFilePath(dataDir, tokenID string, tokenCreatedAt int64) string { + return filepath.Join(dataDir, backupFileName(tokenID, tokenCreatedAt)) +} + +func backupFileName(tokenID string, tokenCreatedAt int64) string { + if tokenCreatedAt <= 0 { + return fmt.Sprintf("%s_%s%s", backupBaseName, tokenID, backupFileExt) + } + + return fmt.Sprintf( + "%s_%019d_%s%s", backupBaseName, tokenCreatedAt, tokenID, + backupFileExt, + ) +} + +func backupFileTokenID(name string) (string, bool) { + details, ok := parseBackupFileName(name) + if !ok { + return "", false + } + + return details.tokenID, true +} + +func parseBackupFileName(name string) (*backupFileDetails, bool) { + if !strings.HasPrefix(name, backupBaseName+"_") || + !strings.HasSuffix(name, backupFileExt) { + + return nil, false + } + + remainder := strings.TrimSuffix( + strings.TrimPrefix(name, backupBaseName+"_"), backupFileExt, + ) + + parts := strings.SplitN(remainder, "_", 2) + + var ( + tokenID string + titleTimestamp int64 + ) + switch len(parts) { + case 1: + tokenID = parts[0] + + case 2: + parsedTimestamp, err := strconv.ParseInt(parts[0], 10, 64) + if err != nil { + return nil, false + } + + titleTimestamp = parsedTimestamp + tokenID = parts[1] + + default: + return nil, false + } + + _, err := l402.MakeIDFromString(tokenID) + if err != nil { + return nil, false + } + + return &backupFileDetails{ + tokenID: tokenID, + titleTimestamp: titleTimestamp, + }, true +} + +func findBackupFileForToken(dataDir, tokenID string) (string, error) { + dirEntries, err := os.ReadDir(dataDir) + if err != nil { + return "", err + } + + for _, entry := range dirEntries { + if entry.IsDir() { + continue + } + + parsedTokenID, ok := backupFileTokenID(entry.Name()) + if ok && parsedTokenID == tokenID { + return filepath.Join(dataDir, entry.Name()), nil + } + } + + return "", nil +} + +func readBackupPlaintext(key [32]byte, path string) ([]byte, error) { + ciphertext, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + return decryptBackupPayload(key, ciphertext) +} + +func readBackupPayload(key [32]byte, path string) (*backupPayload, error) { + plaintext, err := readBackupPlaintext(key, path) + if err != nil { + return nil, err + } + + var payload backupPayload + err = json.Unmarshal(plaintext, &payload) + if err != nil { + return nil, err + } + + return &payload, nil +} + +func (s *Service) prepareStaticAddressRestore(ctx context.Context, + backup *staticAddressBackup) (*address.Parameters, error) { + + if s.staticAddressManager == nil { + return nil, fmt.Errorf("static address restore is unavailable") + } + + if !staticaddrversion.AddressProtocolVersion( + backup.ProtocolVersion, + ).Valid() { + + return nil, fmt.Errorf("invalid static address protocol version %d", + backup.ProtocolVersion) + } + + serverPubKey, err := btcec.ParsePubKey(backup.ServerPubKey) + if err != nil { + return nil, err + } + + clientPubKey, locator, err := s.resolveClientKey( + ctx, backup, serverPubKey, + ) + if err != nil { + return nil, err + } + + return &address.Parameters{ + ClientPubkey: clientPubKey, + ServerPubkey: serverPubKey, + Expiry: backup.Expiry, + PkScript: slices.Clone(backup.PkScript), + KeyLocator: locator, + ProtocolVersion: staticaddrversion.AddressProtocolVersion( + backup.ProtocolVersion, + ), + InitiationHeight: backup.InitiationHeight, + }, nil +} + +func (s *Service) restoreTokenFiles( + backupFiles []*l402TokenFileEntry) (*tokenRestoreResult, error) { + + if len(backupFiles) == 0 { + return &tokenRestoreResult{}, nil + } + + existingFiles, err := s.readTokenFiles() + if err != nil { + return nil, err + } + + existingByName := make(map[string][]byte, len(existingFiles)) + for _, file := range existingFiles { + existingByName[file.Name] = file.Data + } + + backupByName := make(map[string][]byte, len(backupFiles)) + for _, file := range backupFiles { + if !isTokenFileName(file.Name) { + return nil, fmt.Errorf("unexpected token file name %q", + file.Name) + } + + backupByName[file.Name] = file.Data + } + + for name := range existingByName { + if _, ok := backupByName[name]; ok { + continue + } + + return nil, fmt.Errorf("token store already contains "+ + "unexpected file %q", name) + } + + result := &tokenRestoreResult{} + for name, data := range backupByName { + path := filepath.Join(s.dataDir, name) + if current, ok := existingByName[name]; ok { + if !bytes.Equal(current, data) { + return nil, fmt.Errorf("token file %q already exists "+ + "with different contents", name) + } + + continue + } + + err := writeFileAtomically(path, data) + if err != nil { + return nil, err + } + result.restored = true + result.writtenPaths = append(result.writtenPaths, path) + } + + return result, nil +} + +func (s *Service) restorePreparedStaticAddress(ctx context.Context, + params *address.Parameters) (string, bool, error) { + + addr, restored, err := s.staticAddressManager.RestoreAddress( + ctx, params, + ) + if err != nil { + return "", false, err + } + + return addr.String(), restored, nil +} + +func cleanupRestoredTokenFiles(paths []string) error { + if len(paths) == 0 { + return nil + } + + var cleanupErrs []error + for _, path := range paths { + err := os.Remove(path) + if err != nil && !errors.Is(err, os.ErrNotExist) { + cleanupErrs = append( + cleanupErrs, fmt.Errorf("remove %s: %w", path, err), + ) + } + } + + return errors.Join(cleanupErrs...) +} + +func (s *Service) resolveClientKey(ctx context.Context, + backup *staticAddressBackup, serverPubKey *btcec.PublicKey) ( + *btcec.PublicKey, keychain.KeyLocator, error) { + + var expectedClientPubKey *btcec.PublicKey + if len(backup.ClientPubKey) > 0 { + parsedClientPubKey, err := btcec.ParsePubKey( + backup.ClientPubKey, + ) + if err != nil { + return nil, keychain.KeyLocator{}, err + } + expectedClientPubKey = parsedClientPubKey + } + + locator := keychain.KeyLocator{ + Family: keychain.KeyFamily(backup.ClientKeyFamily), + Index: backup.ClientKeyIndex, + } + + hasExactLocator := locator.Family != 0 || locator.Index != 0 + if hasExactLocator { + exactKey, err := s.walletKit.DeriveKey(ctx, &locator) + if err == nil { + match, err := s.staticAddressMatches( + exactKey.PubKey, expectedClientPubKey, serverPubKey, + backup, + ) + if err == nil && match { + return exactKey.PubKey, locator, nil + } + } + } + + startIndex := max(0, int(backup.ClientKeyIndex)-backupKeyGap) + endIndex := int(backup.ClientKeyIndex) + backupKeyGap + for idx := startIndex; idx <= endIndex; idx++ { + candidateLocator := keychain.KeyLocator{ + Family: keychain.KeyFamily(backup.ClientKeyFamily), + Index: uint32(idx), + } + if hasExactLocator && candidateLocator == locator { + continue + } + + candidateKey, err := s.walletKit.DeriveKey( + ctx, &candidateLocator, + ) + if err != nil { + continue + } + + match, err := s.staticAddressMatches( + candidateKey.PubKey, expectedClientPubKey, serverPubKey, + backup, + ) + if err != nil { + return nil, keychain.KeyLocator{}, err + } + if match { + return candidateKey.PubKey, candidateLocator, nil + } + } + + return nil, keychain.KeyLocator{}, fmt.Errorf("unable to derive " + + "static address client key from backup") +} + +func (s *Service) staticAddressMatches(clientPubKey, + expectedClientPubKey, serverPubKey *btcec.PublicKey, + backup *staticAddressBackup) (bool, error) { + + if expectedClientPubKey != nil && + !clientPubKey.IsEqual(expectedClientPubKey) { + + return false, nil + } + + staticAddress, err := staticaddrscript.NewStaticAddress( + input.MuSig2Version100RC2, int64(backup.Expiry), clientPubKey, + serverPubKey, + ) + if err != nil { + return false, err + } + + if len(backup.PkScript) != 0 { + pkScript, err := staticAddress.StaticAddressScript() + if err != nil { + return false, err + } + + if bytes.Equal(pkScript, backup.PkScript) { + return true, nil + } + } + + if backup.Address != "" { + address, err := s.staticAddressManager.GetTaprootAddress( + clientPubKey, serverPubKey, int64(backup.Expiry), + ) + if err != nil { + return false, err + } + + if address.String() == backup.Address { + return true, nil + } + } + + return false, nil +} + +func (s *Service) deriveEncryptionKey(ctx context.Context) ([32]byte, error) { + return s.signer.DeriveSharedKey( + ctx, lndclient.SharedKeyNUMS, &backupKeyLocator, + ) +} + +func encryptBackupPayload(key [32]byte, plaintext []byte) ([]byte, error) { + var nonce [24]byte + _, err := rand.Read(nonce[:]) + if err != nil { + return nil, err + } + + cipherText := secretbox.Seal(nil, plaintext, &nonce, &key) + encoded := make([]byte, 0, len(backupMagic)+len(nonce)+len(cipherText)) + encoded = append(encoded, backupMagic...) + encoded = append(encoded, nonce[:]...) + encoded = append(encoded, cipherText...) + + return encoded, nil +} + +func decryptBackupPayload(key [32]byte, ciphertext []byte) ([]byte, error) { + if len(ciphertext) < len(backupMagic)+24 { + return nil, fmt.Errorf("backup file is too short") + } + if !bytes.Equal(ciphertext[:len(backupMagic)], backupMagic) { + return nil, fmt.Errorf("backup file has an unknown format") + } + + var nonce [24]byte + copy(nonce[:], ciphertext[len(backupMagic):len(backupMagic)+24]) + + plaintext, ok := secretbox.Open( + nil, ciphertext[len(backupMagic)+24:], &nonce, &key, + ) + if !ok { + return nil, fmt.Errorf("unable to decrypt backup file") + } + + return plaintext, nil +} + +func writeFileAtomically(path string, data []byte) error { + tempPath := path + ".tmp" + + err := os.WriteFile(tempPath, data, 0600) + if err != nil { + return err + } + + return os.Rename(tempPath, path) +} + +func isTokenFileName(name string) bool { + return filepath.Base(name) == name && name == paidTokenFileName +} + +func (s *Service) isFreshInstall(ctx context.Context) (bool, error) { + hasTokenFiles, err := hasAnyLocalTokenFiles(s.dataDir) + if err != nil || hasTokenFiles { + return !hasTokenFiles && err == nil, err + } + + if s.staticAddressManager == nil { + return true, nil + } + + _, err = s.staticAddressManager.GetStaticAddressParameters(ctx) + switch { + case err == nil: + return false, nil + + case errors.Is(err, address.ErrNoStaticAddress): + return true, nil + + default: + return false, err + } +} + +func hasAnyLocalTokenFiles(dataDir string) (bool, error) { + for _, name := range []string{paidTokenFileName, pendingTokenFileName} { + path := filepath.Join(dataDir, name) + _, err := os.Stat(path) + switch { + case err == nil: + return true, nil + + case errors.Is(err, os.ErrNotExist): + continue + + default: + return false, err + } + } + + return false, nil +} diff --git a/recovery/service_test.go b/recovery/service_test.go new file mode 100644 index 000000000..2119af9d1 --- /dev/null +++ b/recovery/service_test.go @@ -0,0 +1,1280 @@ +package recovery + +import ( + "bytes" + "context" + "encoding/binary" + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "testing" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/lightninglabs/aperture/l402" + "github.com/lightninglabs/loop/staticaddr/address" + staticaddrscript "github.com/lightninglabs/loop/staticaddr/script" + staticaddrversion "github.com/lightninglabs/loop/staticaddr/version" + "github.com/lightninglabs/loop/swap" + testutils "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" + "gopkg.in/macaroon.v2" +) + +func TestEncryptDecryptBackupPayload(t *testing.T) { + t.Parallel() + + var key [32]byte + copy(key[:], []byte("0123456789abcdefghijklmnopqrstuv")) + + plaintext := []byte("loop recovery backup payload") + + encrypted, err := encryptBackupPayload(key, plaintext) + require.NoError(t, err) + require.NotEqual(t, plaintext, encrypted) + + decrypted, err := decryptBackupPayload(key, encrypted) + require.NoError(t, err) + require.Equal(t, plaintext, decrypted) +} + +func TestWriteBackupReturnsEmptyWithoutState(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + lnd := testutils.NewMockLnd() + + svc := NewService( + dir, "testnet", lnd.Signer, lnd.WalletKit, nil, nil, + ) + + backupFile, err := svc.WriteBackup(context.Background()) + require.NoError(t, err) + require.Empty(t, backupFile) + require.Empty(t, listBackupFiles(t, dir)) +} + +func TestWriteBackupReturnsEmptyWithTokenOnly(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + lnd := testutils.NewMockLnd() + + writePaidToken( + t, dir, 1, time.Date(2026, time.April, 14, 9, 30, 1, 0, time.UTC), + ) + + svc := NewService( + dir, "testnet", lnd.Signer, lnd.WalletKit, nil, nil, + ) + + backupFile, err := svc.WriteBackup(context.Background()) + require.NoError(t, err) + require.Empty(t, backupFile) + require.Empty(t, listBackupFiles(t, dir)) +} + +func TestWriteBackupReturnsEmptyWithPendingToken(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + lnd := testutils.NewMockLnd() + addrParams := makeStaticAddressParams( + t, lnd, 7, defaultRecoveryServerPubkey, 144, 321, + ) + + writePendingToken( + t, dir, 1, time.Date(2026, time.April, 14, 9, 30, 1, 0, time.UTC), + ) + + svc := NewService( + dir, "testnet", lnd.Signer, lnd.WalletKit, + &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + params: addrParams, + }, nil, + ) + + backupFile, err := svc.WriteBackup(context.Background()) + require.NoError(t, err) + require.Empty(t, backupFile) + require.Empty(t, listBackupFiles(t, dir)) +} + +func TestWriteBackupIncludesStaticAddressAndPaidToken(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + lnd := testutils.NewMockLnd() + + addrParams := makeStaticAddressParams( + t, lnd, 7, defaultRecoveryServerPubkey, 144, 321, + ) + staticMgr := &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + params: addrParams, + } + + tokenCreatedAt := time.Date( + 2026, time.April, 14, 9, 30, 1, 123, time.UTC, + ) + tokenID := writePaidToken(t, dir, 1, tokenCreatedAt) + + svc := NewService( + dir, "testnet", lnd.Signer, lnd.WalletKit, staticMgr, nil, + ) + + backupFile, err := svc.WriteBackup(context.Background()) + require.NoError(t, err) + require.Equal( + t, backupFilePath(dir, tokenID, tokenCreatedAt.UnixNano()), + backupFile, + ) + + key, err := svc.deriveEncryptionKey(context.Background()) + require.NoError(t, err) + + payload, err := readBackupPayload(key, backupFile) + require.NoError(t, err) + + require.EqualValues(t, backupVersion, payload.Version) + require.Equal(t, "testnet", payload.Network) + require.Equal(t, tokenID, payload.L402TokenID) + require.Equal(t, tokenCreatedAt.UnixNano(), payload.L402TokenCreatedAt) + require.NotNil(t, payload.AddressGeneration) + require.EqualValues( + t, addrParams.ProtocolVersion, + payload.AddressGeneration.ProtocolVersion, + ) + require.Equal( + t, addrParams.ServerPubkey.SerializeCompressed(), + payload.AddressGeneration.ServerPubKey, + ) + require.Equal( + t, addrParams.Expiry, payload.AddressGeneration.Expiry, + ) + require.Equal( + t, int32(addrParams.KeyLocator.Family), + payload.AddressGeneration.MainKeyFamily, + ) + require.Equal( + t, swap.StaticAddressChangeKeyFamily, + payload.AddressGeneration.ChangeKeyFamily, + ) + require.Equal( + t, uint32(defaultChangeKeyIndex), + payload.AddressGeneration.ChangeKeyIndex, + ) + require.Equal( + t, addrParams.InitiationHeight, + payload.AddressGeneration.StartHeight, + ) + require.Equal( + t, uint32(defaultMultiAddressLookahead), + payload.AddressGeneration.Lookahead, + ) + require.NotNil(t, payload.StaticAddress) + require.EqualValues( + t, addrParams.ProtocolVersion, payload.StaticAddress.ProtocolVersion, + ) + require.Equal( + t, addrParams.ClientPubkey.SerializeCompressed(), + payload.StaticAddress.ClientPubKey, + ) + require.Equal( + t, addrParams.ServerPubkey.SerializeCompressed(), + payload.StaticAddress.ServerPubKey, + ) + require.Equal(t, addrParams.Expiry, payload.StaticAddress.Expiry) + require.Equal( + t, int32(addrParams.KeyLocator.Family), + payload.StaticAddress.ClientKeyFamily, + ) + require.Equal( + t, addrParams.KeyLocator.Index, + payload.StaticAddress.ClientKeyIndex, + ) + require.Equal(t, addrParams.PkScript, payload.StaticAddress.PkScript) + require.Equal( + t, addrParams.InitiationHeight, + payload.StaticAddress.InitiationHeight, + ) + require.Len(t, payload.TokenFiles, 1) + require.Equal(t, paidTokenFileName, payload.TokenFiles[0].Name) + require.NotEmpty(t, payload.TokenFiles[0].Data) +} + +func TestAddressGenerationReconstructsLegacyStaticAddress(t *testing.T) { + t.Parallel() + + ctx := context.Background() + dir := t.TempDir() + lnd := testutils.NewMockLnd() + + addrParams := makeStaticAddressParams( + t, lnd, 7, defaultRecoveryServerPubkey, 144, 321, + ) + staticMgr := &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + params: addrParams, + } + + writePaidToken( + t, dir, 1, time.Date(2026, time.April, 14, 9, 30, 1, 123, time.UTC), + ) + + svc := NewService( + dir, "testnet", lnd.Signer, lnd.WalletKit, staticMgr, nil, + ) + + backupFile, err := svc.WriteBackup(ctx) + require.NoError(t, err) + + key, err := svc.deriveEncryptionKey(ctx) + require.NoError(t, err) + + payload, err := readBackupPayload(key, backupFile) + require.NoError(t, err) + require.NotNil(t, payload.AddressGeneration) + require.NotNil(t, payload.StaticAddress) + + serverPubKey, err := btcec.ParsePubKey( + payload.AddressGeneration.ServerPubKey, + ) + require.NoError(t, err) + + clientKeyDesc, err := lnd.WalletKit.DeriveKey( + ctx, &keychain.KeyLocator{ + Family: keychain.KeyFamily( + payload.AddressGeneration.MainKeyFamily, + ), + Index: payload.StaticAddress.ClientKeyIndex, + }, + ) + require.NoError(t, err) + + reconstructed, err := staticaddrscript.NewStaticAddress( + input.MuSig2Version100RC2, + int64(payload.AddressGeneration.Expiry), clientKeyDesc.PubKey, + serverPubKey, + ) + require.NoError(t, err) + + pkScript, err := reconstructed.StaticAddressScript() + require.NoError(t, err) + require.Equal(t, payload.StaticAddress.PkScript, pkScript) + + reconstructedAddr, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(reconstructed.TaprootKey), + lnd.ChainParams, + ) + require.NoError(t, err) + require.Equal(t, payload.StaticAddress.Address, reconstructedAddr.String()) +} + +func TestAddressGenerationReconstructsChangeStaticAddress(t *testing.T) { + t.Parallel() + + ctx := context.Background() + dir := t.TempDir() + lnd := testutils.NewMockLnd() + + addrParams := makeStaticAddressParams( + t, lnd, 7, defaultRecoveryServerPubkey, 144, 321, + ) + staticMgr := &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + params: addrParams, + } + + expectedChangeKey, err := lnd.WalletKit.DeriveKey( + ctx, &keychain.KeyLocator{ + Family: keychain.KeyFamily(swap.StaticAddressChangeKeyFamily), + Index: defaultChangeKeyIndex, + }, + ) + require.NoError(t, err) + + expectedChangeStaticAddr, err := staticaddrscript.NewStaticAddress( + input.MuSig2Version100RC2, + int64(addrParams.Expiry), expectedChangeKey.PubKey, + addrParams.ServerPubkey, + ) + require.NoError(t, err) + + expectedChangePkScript, err := expectedChangeStaticAddr.StaticAddressScript() + require.NoError(t, err) + + expectedChangeAddr, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(expectedChangeStaticAddr.TaprootKey), + lnd.ChainParams, + ) + require.NoError(t, err) + + writePaidToken( + t, dir, 1, time.Date(2026, time.April, 14, 9, 30, 1, 123, time.UTC), + ) + + svc := NewService( + dir, "testnet", lnd.Signer, lnd.WalletKit, staticMgr, nil, + ) + + backupFile, err := svc.WriteBackup(ctx) + require.NoError(t, err) + + key, err := svc.deriveEncryptionKey(ctx) + require.NoError(t, err) + + payload, err := readBackupPayload(key, backupFile) + require.NoError(t, err) + require.NotNil(t, payload.AddressGeneration) + require.NotNil(t, payload.StaticAddress) + + serverPubKey, err := btcec.ParsePubKey( + payload.AddressGeneration.ServerPubKey, + ) + require.NoError(t, err) + + changeKeyDesc, err := lnd.WalletKit.DeriveKey( + ctx, &keychain.KeyLocator{ + Family: keychain.KeyFamily( + payload.AddressGeneration.ChangeKeyFamily, + ), + Index: payload.AddressGeneration.ChangeKeyIndex, + }, + ) + require.NoError(t, err) + + reconstructed, err := staticaddrscript.NewStaticAddress( + input.MuSig2Version100RC2, + int64(payload.AddressGeneration.Expiry), changeKeyDesc.PubKey, + serverPubKey, + ) + require.NoError(t, err) + + pkScript, err := reconstructed.StaticAddressScript() + require.NoError(t, err) + require.Equal(t, expectedChangePkScript, pkScript) + + reconstructedAddr, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(reconstructed.TaprootKey), + lnd.ChainParams, + ) + require.NoError(t, err) + require.Equal(t, expectedChangeAddr.String(), reconstructedAddr.String()) + require.NotEqual(t, payload.StaticAddress.Address, reconstructedAddr.String()) +} + +func TestWriteBackupIsImmutablePerL402(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + lnd := testutils.NewMockLnd() + addrParams := makeStaticAddressParams( + t, lnd, 7, defaultRecoveryServerPubkey, 144, 321, + ) + staticMgr := &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + params: addrParams, + } + + tokenID := writePaidToken( + t, dir, 2, time.Date(2026, time.April, 14, 9, 30, 1, 0, time.UTC), + ) + + svc := NewService( + dir, "testnet", lnd.Signer, lnd.WalletKit, staticMgr, nil, + ) + + firstBackup, err := svc.WriteBackup(context.Background()) + require.NoError(t, err) + require.Equal( + t, + backupFilePath( + dir, tokenID, + time.Date(2026, time.April, 14, 9, 30, 1, 0, time.UTC). + UnixNano(), + ), + firstBackup, + ) + + secondBackup, err := svc.WriteBackup(context.Background()) + require.NoError(t, err) + require.Empty(t, secondBackup) + require.Equal(t, []string{firstBackup}, listBackupFiles(t, dir)) +} + +func TestWriteBackupSkipsLegacyFileForSameToken(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + lnd := testutils.NewMockLnd() + addrParams := makeStaticAddressParams( + t, lnd, 7, defaultRecoveryServerPubkey, 144, 321, + ) + staticMgr := &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + params: addrParams, + } + + tokenCreatedAt := time.Date( + 2026, time.April, 14, 9, 30, 1, 0, time.UTC, + ) + tokenID := writePaidToken(t, dir, 2, tokenCreatedAt) + + svc := NewService( + dir, "testnet", lnd.Signer, lnd.WalletKit, staticMgr, nil, + ) + + legacyPath := filepath.Join( + dir, fmt.Sprintf("%s_%s%s", backupBaseName, tokenID, backupFileExt), + ) + err := os.WriteFile(legacyPath, []byte("legacy"), 0600) + require.NoError(t, err) + + backupFile, err := svc.WriteBackup(context.Background()) + require.NoError(t, err) + require.Empty(t, backupFile) + require.Equal(t, []string{legacyPath}, listBackupFiles(t, dir)) +} + +func TestRestoreLatestBackupPrefersNewestGeneration(t *testing.T) { + t.Parallel() + + ctx := context.Background() + lnd := testutils.NewMockLnd() + backupDir := t.TempDir() + restoreDir := t.TempDir() + + addrParams := makeStaticAddressParams( + t, lnd, 7, defaultRecoveryServerPubkey, 144, 321, + ) + staticMgr := &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + params: addrParams, + } + sourceSvc := NewService( + backupDir, "testnet", lnd.Signer, lnd.WalletKit, staticMgr, nil, + ) + + writePaidToken( + t, backupDir, 0x20, + time.Date(2026, time.April, 14, 9, 30, 1, 0, time.UTC), + ) + firstBackup, err := sourceSvc.WriteBackup(ctx) + require.NoError(t, err) + + writePaidToken( + t, backupDir, 0x10, + time.Date(2026, time.April, 14, 9, 31, 1, 0, time.UTC), + ) + secondBackup, err := sourceSvc.WriteBackup(ctx) + require.NoError(t, err) + + copyFile(t, firstBackup, filepath.Join(restoreDir, filepath.Base(firstBackup))) + copyFile( + t, secondBackup, filepath.Join(restoreDir, filepath.Base(secondBackup)), + ) + + destStaticMgr := &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + } + destSvc := NewService( + restoreDir, "testnet", lnd.Signer, lnd.WalletKit, + destStaticMgr, nil, + ) + + result, err := destSvc.Restore(ctx, "") + require.NoError(t, err) + require.Equal(t, filepath.Join( + restoreDir, filepath.Base(secondBackup), + ), result.BackupFile) + require.True(t, result.RestoredL402) + require.True(t, result.RestoredStaticAddress) +} + +func TestRestoreLatestOnFreshInstallUsesLatestTimestampInTitle(t *testing.T) { + t.Parallel() + + ctx := context.Background() + lnd := testutils.NewMockLnd() + backupDir := t.TempDir() + + addrParams := makeStaticAddressParams( + t, lnd, 7, defaultRecoveryServerPubkey, 144, 321, + ) + staticMgr := &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + params: addrParams, + } + sourceSvc := NewService( + backupDir, "testnet", lnd.Signer, lnd.WalletKit, staticMgr, nil, + ) + + firstCreatedAt := time.Date( + 2026, time.April, 14, 9, 30, 1, 0, time.UTC, + ) + writePaidToken(t, backupDir, 0x20, firstCreatedAt) + firstBackup, err := sourceSvc.WriteBackup(ctx) + require.NoError(t, err) + + secondCreatedAt := time.Date( + 2026, time.April, 14, 9, 31, 1, 0, time.UTC, + ) + writePaidToken(t, backupDir, 0x10, secondCreatedAt) + secondBackup, err := sourceSvc.WriteBackup(ctx) + require.NoError(t, err) + + err = os.Remove(filepath.Join(backupDir, paidTokenFileName)) + require.NoError(t, err) + + destStaticMgr := &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + } + destSvc := NewService( + backupDir, "testnet", lnd.Signer, lnd.WalletKit, + destStaticMgr, nil, + ) + + result, restored, err := destSvc.RestoreLatestOnFreshInstall(ctx) + require.NoError(t, err) + require.True(t, restored) + require.Equal(t, secondBackup, result.BackupFile) + + // Keep both variables referenced to make the intended ordering explicit. + require.NotEqual(t, firstBackup, secondBackup) +} + +func TestRestoreStaticAddressAndPaidToken(t *testing.T) { + t.Parallel() + + ctx := context.Background() + lnd := testutils.NewMockLnd() + backupDir := t.TempDir() + restoreDir := t.TempDir() + + addrParams := makeStaticAddressParams( + t, lnd, 7, defaultRecoveryServerPubkey, 144, 321, + ) + sourceStaticMgr := &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + params: addrParams, + } + sourceSvc := NewService( + backupDir, "testnet", lnd.Signer, lnd.WalletKit, + sourceStaticMgr, nil, + ) + + writePaidToken( + t, backupDir, 1, time.Date(2026, time.April, 14, 9, 30, 1, 0, time.UTC), + ) + backupFile, err := sourceSvc.WriteBackup(ctx) + require.NoError(t, err) + + destStaticMgr := &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + } + depositMgr := &mockDepositManager{ + depositsFound: 3, + } + destSvc := NewService( + restoreDir, "testnet", lnd.Signer, lnd.WalletKit, + destStaticMgr, depositMgr, + ) + + result, err := destSvc.Restore(ctx, backupFile) + require.NoError(t, err) + require.Equal(t, backupFile, result.BackupFile) + require.True(t, result.RestoredStaticAddress) + require.True(t, result.RestoredL402) + require.Equal(t, 3, result.NumDepositsFound) + require.Empty(t, result.DepositReconciliationError) + + require.Len(t, destStaticMgr.restoreCalls, 1) + restoredParams := destStaticMgr.restoreCalls[0] + require.True(t, restoredParams.ClientPubkey.IsEqual(addrParams.ClientPubkey)) + require.True(t, restoredParams.ServerPubkey.IsEqual(addrParams.ServerPubkey)) + require.Equal(t, addrParams.Expiry, restoredParams.Expiry) + require.Equal(t, addrParams.PkScript, restoredParams.PkScript) + require.Equal(t, addrParams.KeyLocator, restoredParams.KeyLocator) + require.Equal( + t, addrParams.ProtocolVersion, restoredParams.ProtocolVersion, + ) + require.Equal( + t, addrParams.InitiationHeight, restoredParams.InitiationHeight, + ) + + require.Equal(t, 1, depositMgr.calls) + restoredToken, err := os.ReadFile( + filepath.Join(restoreDir, paidTokenFileName), + ) + require.NoError(t, err) + require.NotEmpty(t, restoredToken) + + expectedAddr, err := destStaticMgr.GetTaprootAddress( + addrParams.ClientPubkey, addrParams.ServerPubkey, + int64(addrParams.Expiry), + ) + require.NoError(t, err) + require.Equal(t, expectedAddr.String(), result.StaticAddress) +} + +func TestRestoreReturnsDepositReconciliationError(t *testing.T) { + t.Parallel() + + ctx := context.Background() + lnd := testutils.NewMockLnd() + backupDir := t.TempDir() + restoreDir := t.TempDir() + + addrParams := makeStaticAddressParams( + t, lnd, 7, defaultRecoveryServerPubkey, 144, 321, + ) + sourceStaticMgr := &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + params: addrParams, + } + sourceSvc := NewService( + backupDir, "testnet", lnd.Signer, lnd.WalletKit, + sourceStaticMgr, nil, + ) + writePaidToken( + t, backupDir, 1, time.Date(2026, time.April, 14, 9, 30, 1, 0, time.UTC), + ) + + backupFile, err := sourceSvc.WriteBackup(ctx) + require.NoError(t, err) + + depositErr := errors.New("reconcile failed") + destSvc := NewService( + restoreDir, "testnet", lnd.Signer, lnd.WalletKit, + &mockStaticAddressManager{chainParams: lnd.ChainParams}, + &mockDepositManager{err: depositErr}, + ) + + result, err := destSvc.Restore(ctx, backupFile) + require.NoError(t, err) + require.Equal(t, depositErr.Error(), result.DepositReconciliationError) + require.Equal(t, 0, result.NumDepositsFound) +} + +func TestRestoreReportsNoStaticAddressChangeForIdempotentRestore(t *testing.T) { + t.Parallel() + + ctx := context.Background() + lnd := testutils.NewMockLnd() + backupDir := t.TempDir() + restoreDir := t.TempDir() + + addrParams := makeStaticAddressParams( + t, lnd, 7, defaultRecoveryServerPubkey, 144, 321, + ) + sourceStaticMgr := &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + params: addrParams, + } + sourceSvc := NewService( + backupDir, "testnet", lnd.Signer, lnd.WalletKit, + sourceStaticMgr, nil, + ) + writePaidToken( + t, backupDir, 1, time.Date(2026, time.April, 14, 9, 30, 1, 0, time.UTC), + ) + backupFile, err := sourceSvc.WriteBackup(ctx) + require.NoError(t, err) + + writePaidToken( + t, restoreDir, 1, time.Date(2026, time.April, 14, 9, 30, 1, 0, time.UTC), + ) + destStaticMgr := &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + restoreChanged: false, + restoreChangedSet: true, + } + destSvc := NewService( + restoreDir, "testnet", lnd.Signer, lnd.WalletKit, + destStaticMgr, nil, + ) + + result, err := destSvc.Restore(ctx, backupFile) + require.NoError(t, err) + require.False(t, result.RestoredL402) + require.False(t, result.RestoredStaticAddress) + require.NotEmpty(t, result.StaticAddress) + require.Len(t, destStaticMgr.restoreCalls, 1) +} + +func TestRestoreLatestOnFreshInstallReturnsFalseWithLocalToken(t *testing.T) { + t.Parallel() + + ctx := context.Background() + dir := t.TempDir() + lnd := testutils.NewMockLnd() + + writePaidToken( + t, dir, 1, time.Date(2026, time.April, 14, 9, 30, 1, 0, time.UTC), + ) + + svc := NewService( + dir, "testnet", lnd.Signer, lnd.WalletKit, + &mockStaticAddressManager{chainParams: lnd.ChainParams}, nil, + ) + + result, restored, err := svc.RestoreLatestOnFreshInstall(ctx) + require.NoError(t, err) + require.False(t, restored) + require.Nil(t, result) +} + +func TestRestoreRejectsNetworkMismatch(t *testing.T) { + t.Parallel() + + ctx := context.Background() + dir := t.TempDir() + lnd := testutils.NewMockLnd() + + sourceSvc := NewService( + dir, "testnet", lnd.Signer, lnd.WalletKit, + &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + params: makeStaticAddressParams( + t, lnd, 7, defaultRecoveryServerPubkey, 144, 321, + ), + }, nil, + ) + writePaidToken( + t, dir, 1, time.Date(2026, time.April, 14, 9, 30, 1, 0, time.UTC), + ) + backupFile, err := sourceSvc.WriteBackup(ctx) + require.NoError(t, err) + + restoreSvc := NewService( + t.TempDir(), "mainnet", lnd.Signer, lnd.WalletKit, nil, nil, + ) + + _, err = restoreSvc.Restore(ctx, backupFile) + require.ErrorContains(t, err, "does not match") +} + +func TestRestoreFailsWithoutStaticAddressManager(t *testing.T) { + t.Parallel() + + ctx := context.Background() + lnd := testutils.NewMockLnd() + backupDir := t.TempDir() + + addrParams := makeStaticAddressParams( + t, lnd, 3, defaultRecoveryServerPubkey, 144, 321, + ) + sourceSvc := NewService( + backupDir, "testnet", lnd.Signer, lnd.WalletKit, + &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + params: addrParams, + }, nil, + ) + writePaidToken( + t, backupDir, 1, time.Date(2026, time.April, 14, 9, 30, 1, 0, time.UTC), + ) + backupFile, err := sourceSvc.WriteBackup(ctx) + require.NoError(t, err) + + restoreSvc := NewService( + t.TempDir(), "testnet", lnd.Signer, lnd.WalletKit, nil, nil, + ) + + _, err = restoreSvc.Restore(ctx, backupFile) + require.ErrorContains(t, err, "static address restore is unavailable") +} + +func TestRestoreRejectsDifferentExistingTokenBeforeStaticAddress(t *testing.T) { + t.Parallel() + + ctx := context.Background() + lnd := testutils.NewMockLnd() + backupDir := t.TempDir() + restoreDir := t.TempDir() + + addrParams := makeStaticAddressParams( + t, lnd, 3, defaultRecoveryServerPubkey, 144, 321, + ) + sourceSvc := NewService( + backupDir, "testnet", lnd.Signer, lnd.WalletKit, + &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + params: addrParams, + }, nil, + ) + writePaidToken( + t, backupDir, 1, time.Date(2026, time.April, 14, 9, 30, 1, 0, time.UTC), + ) + backupFile, err := sourceSvc.WriteBackup(ctx) + require.NoError(t, err) + + err = os.WriteFile( + filepath.Join(restoreDir, paidTokenFileName), + []byte("conflicting-token"), 0600, + ) + require.NoError(t, err) + + destStaticMgr := &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + } + restoreSvc := NewService( + restoreDir, "testnet", lnd.Signer, lnd.WalletKit, + destStaticMgr, nil, + ) + + _, err = restoreSvc.Restore(ctx, backupFile) + require.ErrorContains(t, err, "different contents") + require.Empty(t, destStaticMgr.restoreCalls) +} + +func TestRestoreRollsBackTokenFilesOnStaticAddressFailure(t *testing.T) { + t.Parallel() + + ctx := context.Background() + lnd := testutils.NewMockLnd() + backupDir := t.TempDir() + restoreDir := t.TempDir() + + addrParams := makeStaticAddressParams( + t, lnd, 3, defaultRecoveryServerPubkey, 144, 321, + ) + sourceSvc := NewService( + backupDir, "testnet", lnd.Signer, lnd.WalletKit, + &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + params: addrParams, + }, nil, + ) + writePaidToken( + t, backupDir, 1, time.Date(2026, time.April, 14, 9, 30, 1, 0, time.UTC), + ) + backupFile, err := sourceSvc.WriteBackup(ctx) + require.NoError(t, err) + + restoreSvc := NewService( + restoreDir, "testnet", lnd.Signer, lnd.WalletKit, + &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + restoreErr: errors.New("restore address failed"), + }, nil, + ) + + _, err = restoreSvc.Restore(ctx, backupFile) + require.ErrorContains(t, err, "restore address failed") + + _, err = os.Stat(filepath.Join(restoreDir, paidTokenFileName)) + require.ErrorIs(t, err, os.ErrNotExist) +} + +func TestResolveClientKeyFallsBackToReadOnlyGapSearch(t *testing.T) { + t.Parallel() + + ctx := context.Background() + lnd := testutils.NewMockLnd() + targetIndex := uint32(5) + addrParams := makeStaticAddressParams( + t, lnd, targetIndex, defaultRecoveryServerPubkey, 144, 321, + ) + staticMgr := &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + } + svc := NewService( + t.TempDir(), "testnet", lnd.Signer, lnd.WalletKit, + staticMgr, nil, + ) + + taprootAddr, err := staticMgr.GetTaprootAddress( + addrParams.ClientPubkey, addrParams.ServerPubkey, + int64(addrParams.Expiry), + ) + require.NoError(t, err) + + backup := &staticAddressBackup{ + ProtocolVersion: uint32(addrParams.ProtocolVersion), + ClientPubKey: addrParams.ClientPubkey.SerializeCompressed(), + ServerPubKey: addrParams.ServerPubkey.SerializeCompressed(), + Expiry: addrParams.Expiry, + ClientKeyFamily: swap.StaticAddressKeyFamily, + ClientKeyIndex: targetIndex + 2, + PkScript: slices.Clone(addrParams.PkScript), + Address: taprootAddr.String(), + InitiationHeight: addrParams.InitiationHeight, + } + + clientKey, locator, err := svc.resolveClientKey( + ctx, backup, addrParams.ServerPubkey, + ) + require.NoError(t, err) + require.Equal(t, addrParams.KeyLocator, locator) + require.True(t, clientKey.IsEqual(addrParams.ClientPubkey)) + + nextKey, err := lnd.WalletKit.DeriveNextKey( + ctx, swap.StaticAddressKeyFamily, + ) + require.NoError(t, err) + require.EqualValues(t, 0, nextKey.Index) +} + +func TestRestoreTokenFiles(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + svc := &Service{ + dataDir: dir, + } + + restoreResult, err := svc.restoreTokenFiles([]*l402TokenFileEntry{{ + Name: "l402.token", + Data: []byte("paid-token"), + }}) + require.NoError(t, err) + require.True(t, restoreResult.restored) + + paidToken, err := os.ReadFile(filepath.Join(dir, "l402.token")) + require.NoError(t, err) + require.Equal(t, []byte("paid-token"), paidToken) + + restoreResult, err = svc.restoreTokenFiles([]*l402TokenFileEntry{{ + Name: "l402.token", + Data: []byte("paid-token"), + }}) + require.NoError(t, err) + require.False(t, restoreResult.restored) +} + +func TestRestoreTokenFilesRejectsDifferentExistingToken(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + err := os.WriteFile( + filepath.Join(dir, "l402.token"), []byte("current-token"), 0600, + ) + require.NoError(t, err) + + svc := &Service{ + dataDir: dir, + } + + _, err = svc.restoreTokenFiles([]*l402TokenFileEntry{{ + Name: "l402.token", + Data: []byte("backup-token"), + }}) + require.ErrorContains(t, err, "different contents") +} + +func TestRestoreTokenFilesRejectsInvalidName(t *testing.T) { + t.Parallel() + + svc := &Service{ + dataDir: t.TempDir(), + } + + _, err := svc.restoreTokenFiles([]*l402TokenFileEntry{{ + Name: "l402.token.pending", + Data: []byte("pending-token"), + }}) + require.ErrorContains(t, err, "unexpected token file name") +} + +func TestLatestBackupFilePathIgnoresMalformedNames(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + lnd := testutils.NewMockLnd() + + addrParams := makeStaticAddressParams( + t, lnd, 7, defaultRecoveryServerPubkey, 144, 321, + ) + svc := NewService( + dir, "testnet", lnd.Signer, lnd.WalletKit, + &mockStaticAddressManager{ + chainParams: lnd.ChainParams, + params: addrParams, + }, nil, + ) + + writePaidToken( + t, dir, 1, time.Date(2026, time.April, 14, 9, 30, 1, 0, time.UTC), + ) + validPath, err := svc.WriteBackup(context.Background()) + require.NoError(t, err) + + err = os.WriteFile( + filepath.Join(dir, "L402_backup_not-an-id.enc"), + []byte("invalid"), 0600, + ) + require.NoError(t, err) + err = os.WriteFile( + filepath.Join(dir, backupFileName(stringsOfLength(64), 1)), + []byte("invalid"), 0600, + ) + require.NoError(t, err) + + key, err := svc.deriveEncryptionKey(context.Background()) + require.NoError(t, err) + + latestFile, err := latestBackupFilePath(dir, key, "testnet") + require.NoError(t, err) + require.Equal(t, validPath, latestFile) +} + +var defaultRecoveryServerPubkey = func() *btcec.PublicKey { + _, pubKey := testutils.CreateKey(42) + return pubKey +}() + +type mockStaticAddressManager struct { + chainParams *chaincfg.Params + params *address.Parameters + getParamsErr error + restoreErr error + restoreCalls []*address.Parameters + restoreChanged bool + restoreChangedSet bool +} + +func (m *mockStaticAddressManager) GetStaticAddressParameters( + context.Context) (*address.Parameters, error) { + + switch { + case m.getParamsErr != nil: + return nil, m.getParamsErr + + case m.params == nil: + return nil, address.ErrNoStaticAddress + + default: + return cloneAddressParameters(m.params), nil + } +} + +func (m *mockStaticAddressManager) GetTaprootAddress(clientPubkey, + serverPubkey *btcec.PublicKey, expiry int64) (*btcutil.AddressTaproot, + error) { + + return taprootAddress( + clientPubkey, serverPubkey, expiry, m.chainParams, + ) +} + +func (m *mockStaticAddressManager) RestoreAddress(_ context.Context, + params *address.Parameters) (*btcutil.AddressTaproot, bool, error) { + + if m.restoreErr != nil { + return nil, false, m.restoreErr + } + + m.restoreCalls = append(m.restoreCalls, cloneAddressParameters(params)) + + changed := true + if m.restoreChangedSet { + changed = m.restoreChanged + } + + addr, err := m.GetTaprootAddress( + params.ClientPubkey, params.ServerPubkey, int64(params.Expiry), + ) + if err != nil { + return nil, false, err + } + + return addr, changed, nil +} + +type mockDepositManager struct { + depositsFound int + err error + calls int +} + +func (m *mockDepositManager) ReconcileDeposits(context.Context) (int, error) { + m.calls++ + return m.depositsFound, m.err +} + +func makeStaticAddressParams(t *testing.T, lnd *testutils.LndMockServices, + index uint32, serverPubKey *btcec.PublicKey, expiry uint32, + initiationHeight int32) *address.Parameters { + + t.Helper() + + keyDesc, err := lnd.WalletKit.DeriveKey( + context.Background(), &keychain.KeyLocator{ + Family: keychain.KeyFamily(swap.StaticAddressMainKeyFamily), + Index: index, + }, + ) + require.NoError(t, err) + + staticAddress, err := staticaddrscript.NewStaticAddress( + input.MuSig2Version100RC2, int64(expiry), keyDesc.PubKey, + serverPubKey, + ) + require.NoError(t, err) + + pkScript, err := staticAddress.StaticAddressScript() + require.NoError(t, err) + + return &address.Parameters{ + ClientPubkey: keyDesc.PubKey, + ServerPubkey: serverPubKey, + Expiry: expiry, + PkScript: pkScript, + KeyLocator: keyDesc.KeyLocator, + ProtocolVersion: staticaddrversion.ProtocolVersion_V0, + InitiationHeight: initiationHeight, + } +} + +func cloneAddressParameters(params *address.Parameters) *address.Parameters { + if params == nil { + return nil + } + + return &address.Parameters{ + ClientPubkey: params.ClientPubkey, + ServerPubkey: params.ServerPubkey, + Expiry: params.Expiry, + PkScript: slices.Clone(params.PkScript), + KeyLocator: params.KeyLocator, + ProtocolVersion: params.ProtocolVersion, + InitiationHeight: params.InitiationHeight, + } +} + +func taprootAddress(clientPubkey, serverPubkey *btcec.PublicKey, expiry int64, + chainParams *chaincfg.Params) (*btcutil.AddressTaproot, error) { + + staticAddress, err := staticaddrscript.NewStaticAddress( + input.MuSig2Version100RC2, expiry, clientPubkey, serverPubkey, + ) + if err != nil { + return nil, err + } + + return btcutil.NewAddressTaproot( + schnorr.SerializePubKey(staticAddress.TaprootKey), chainParams, + ) +} + +func writePaidToken(t *testing.T, dir string, seed byte, + createdAt time.Time) string { + + t.Helper() + + return writeTokenFile( + t, filepath.Join(dir, paidTokenFileName), seed, createdAt, true, + ) +} + +func writePendingToken(t *testing.T, dir string, seed byte, + createdAt time.Time) string { + + t.Helper() + + return writeTokenFile( + t, filepath.Join(dir, "l402.token.pending"), seed, createdAt, false, + ) +} + +func writeTokenFile(t *testing.T, path string, seed byte, createdAt time.Time, + paid bool) string { + + t.Helper() + + var ( + paymentHash lntypes.Hash + tokenID l402.TokenID + preimage lntypes.Preimage + ) + paymentHash[0] = seed + tokenID[0] = seed + if paid { + preimage[0] = seed + } + + var idBytes bytes.Buffer + err := l402.EncodeIdentifier(&idBytes, &l402.Identifier{ + Version: l402.LatestVersion, + PaymentHash: paymentHash, + TokenID: tokenID, + }) + require.NoError(t, err) + + mac, err := macaroon.New( + []byte("loop-recovery-test-root-key"), + idBytes.Bytes(), "loop.test", macaroon.LatestVersion, + ) + require.NoError(t, err) + + macBytes, err := mac.MarshalBinary() + require.NoError(t, err) + + var serialized bytes.Buffer + err = binary.Write(&serialized, binary.BigEndian, uint32(len(macBytes))) + require.NoError(t, err) + err = binary.Write(&serialized, binary.BigEndian, macBytes) + require.NoError(t, err) + err = binary.Write(&serialized, binary.BigEndian, paymentHash) + require.NoError(t, err) + err = binary.Write(&serialized, binary.BigEndian, preimage) + require.NoError(t, err) + err = binary.Write( + &serialized, binary.BigEndian, lnwire.MilliSatoshi(seed)*1000, + ) + require.NoError(t, err) + err = binary.Write( + &serialized, binary.BigEndian, lnwire.MilliSatoshi(seed)*10, + ) + require.NoError(t, err) + err = binary.Write(&serialized, binary.BigEndian, createdAt.UnixNano()) + require.NoError(t, err) + + err = os.WriteFile(path, serialized.Bytes(), 0600) + require.NoError(t, err) + + return tokenID.String() +} + +func listBackupFiles(t *testing.T, dir string) []string { + t.Helper() + + entries, err := os.ReadDir(dir) + require.NoError(t, err) + + var files []string + for _, entry := range entries { + if _, ok := backupFileTokenID(entry.Name()); ok { + files = append(files, filepath.Join(dir, entry.Name())) + } + } + + slices.Sort(files) + return files +} + +func copyFile(t *testing.T, src, dest string) { + t.Helper() + + data, err := os.ReadFile(src) + require.NoError(t, err) + + err = os.WriteFile(dest, data, 0600) + require.NoError(t, err) +} + +func stringsOfLength(length int) string { + return string(bytes.Repeat([]byte("a"), length)) +} diff --git a/swap/keychain.go b/swap/keychain.go index 37106950c..73b9d2124 100644 --- a/swap/keychain.go +++ b/swap/keychain.go @@ -5,7 +5,15 @@ var ( // spending of the htlc. KeyFamily = int32(99) - // StaticAddressKeyFamily is the key family used to generate static - // address keys. - StaticAddressKeyFamily = int32(42060) + // StaticAddressMainKeyFamily is the key family used to generate + // externally visible static-address receive keys. + StaticAddressMainKeyFamily = int32(42060) + + // StaticAddressChangeKeyFamily is the key family used to generate + // static-address change outputs. + StaticAddressChangeKeyFamily = int32(42061) + + // StaticAddressKeyFamily is kept as the legacy alias for the main + // static-address family. + StaticAddressKeyFamily = StaticAddressMainKeyFamily ) From 12e2dfb80a6f657d7ab6cf63509c855b76c6e4e2 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 14 Apr 2026 12:02:59 +0200 Subject: [PATCH 3/6] looprpc: authorize recovery restore calls Grant the manual recovery RPC the auth-write and static-address loop-in permissions it needs so loopd can restore local L402 material and static-address state through the authenticated client connection. --- looprpc/client.pb.go | 754 ++++++++++++++++++++-------------- looprpc/client.pb.gw.go | 77 ++++ looprpc/client.proto | 48 +++ looprpc/client.swagger.json | 72 ++++ looprpc/client.yaml | 3 + looprpc/client_grpc.pb.go | 42 ++ looprpc/perms.go | 7 + looprpc/swapclient.pb.json.go | 25 ++ 8 files changed, 728 insertions(+), 300 deletions(-) diff --git a/looprpc/client.pb.go b/looprpc/client.pb.go index 35ae71c82..dfbc42232 100644 --- a/looprpc/client.pb.go +++ b/looprpc/client.pb.go @@ -550,17 +550,17 @@ type StaticAddressLoopInSwapState int32 const ( StaticAddressLoopInSwapState_UNKNOWN_STATIC_ADDRESS_SWAP_STATE StaticAddressLoopInSwapState = 0 - StaticAddressLoopInSwapState_INIT_HTLC StaticAddressLoopInSwapState = 1 - StaticAddressLoopInSwapState_SIGN_HTLC_TX StaticAddressLoopInSwapState = 2 - StaticAddressLoopInSwapState_MONITOR_INVOICE_HTLC_TX StaticAddressLoopInSwapState = 3 - StaticAddressLoopInSwapState_PAYMENT_RECEIVED StaticAddressLoopInSwapState = 4 + StaticAddressLoopInSwapState_INIT_HTLC StaticAddressLoopInSwapState = 1 + StaticAddressLoopInSwapState_SIGN_HTLC_TX StaticAddressLoopInSwapState = 2 + StaticAddressLoopInSwapState_MONITOR_INVOICE_HTLC_TX StaticAddressLoopInSwapState = 3 + StaticAddressLoopInSwapState_PAYMENT_RECEIVED StaticAddressLoopInSwapState = 4 StaticAddressLoopInSwapState_SWEEP_STATIC_ADDRESS_HTLC_TIMEOUT StaticAddressLoopInSwapState = 5 - StaticAddressLoopInSwapState_MONITOR_HTLC_TIMEOUT_SWEEP StaticAddressLoopInSwapState = 6 + StaticAddressLoopInSwapState_MONITOR_HTLC_TIMEOUT_SWEEP StaticAddressLoopInSwapState = 6 StaticAddressLoopInSwapState_HTLC_STATIC_ADDRESS_TIMEOUT_SWEPT StaticAddressLoopInSwapState = 7 - StaticAddressLoopInSwapState_SUCCEEDED StaticAddressLoopInSwapState = 8 - StaticAddressLoopInSwapState_SUCCEEDED_TRANSITIONING_FAILED StaticAddressLoopInSwapState = 9 - StaticAddressLoopInSwapState_UNLOCK_DEPOSITS StaticAddressLoopInSwapState = 10 - StaticAddressLoopInSwapState_FAILED_STATIC_ADDRESS_SWAP StaticAddressLoopInSwapState = 11 + StaticAddressLoopInSwapState_SUCCEEDED StaticAddressLoopInSwapState = 8 + StaticAddressLoopInSwapState_SUCCEEDED_TRANSITIONING_FAILED StaticAddressLoopInSwapState = 9 + StaticAddressLoopInSwapState_UNLOCK_DEPOSITS StaticAddressLoopInSwapState = 10 + StaticAddressLoopInSwapState_FAILED_STATIC_ADDRESS_SWAP StaticAddressLoopInSwapState = 11 ) // Enum value maps for StaticAddressLoopInSwapState. @@ -2962,6 +2962,144 @@ func (*FetchL402TokenResponse) Descriptor() ([]byte, []int) { return file_client_proto_rawDescGZIP(), []int{29} } +type RecoverRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional path to the encrypted backup file. If omitted, loopd restores from + // the most recent immutable L402 recovery backup in the active network data + // directory. + BackupFile string `protobuf:"bytes,1,opt,name=backup_file,json=backupFile,proto3" json:"backup_file,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RecoverRequest) Reset() { + *x = RecoverRequest{} + mi := &file_client_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RecoverRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RecoverRequest) ProtoMessage() {} + +func (x *RecoverRequest) ProtoReflect() protoreflect.Message { + mi := &file_client_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RecoverRequest.ProtoReflect.Descriptor instead. +func (*RecoverRequest) Descriptor() ([]byte, []int) { + return file_client_proto_rawDescGZIP(), []int{30} +} + +func (x *RecoverRequest) GetBackupFile() string { + if x != nil { + return x.BackupFile + } + return "" +} + +type RecoverResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The backup file that was restored. + BackupFile string `protobuf:"bytes,1,opt,name=backup_file,json=backupFile,proto3" json:"backup_file,omitempty"` + // Whether a paid L402 token was restored into the local token store. + RestoredL402 bool `protobuf:"varint,2,opt,name=restored_l402,json=restoredL402,proto3" json:"restored_l402,omitempty"` + // Whether static-address state was restored into loopd and lnd. + RestoredStaticAddress bool `protobuf:"varint,3,opt,name=restored_static_address,json=restoredStaticAddress,proto3" json:"restored_static_address,omitempty"` + // The restored static address, if any. + StaticAddress string `protobuf:"bytes,4,opt,name=static_address,json=staticAddress,proto3" json:"static_address,omitempty"` + // The number of deposits found during best-effort reconciliation. + NumDepositsFound uint32 `protobuf:"varint,5,opt,name=num_deposits_found,json=numDepositsFound,proto3" json:"num_deposits_found,omitempty"` + // Best-effort deposit reconciliation error text, if reconciliation failed + // after state restore completed. + DepositReconciliationError string `protobuf:"bytes,6,opt,name=deposit_reconciliation_error,json=depositReconciliationError,proto3" json:"deposit_reconciliation_error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RecoverResponse) Reset() { + *x = RecoverResponse{} + mi := &file_client_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RecoverResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RecoverResponse) ProtoMessage() {} + +func (x *RecoverResponse) ProtoReflect() protoreflect.Message { + mi := &file_client_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RecoverResponse.ProtoReflect.Descriptor instead. +func (*RecoverResponse) Descriptor() ([]byte, []int) { + return file_client_proto_rawDescGZIP(), []int{31} +} + +func (x *RecoverResponse) GetBackupFile() string { + if x != nil { + return x.BackupFile + } + return "" +} + +func (x *RecoverResponse) GetRestoredL402() bool { + if x != nil { + return x.RestoredL402 + } + return false +} + +func (x *RecoverResponse) GetRestoredStaticAddress() bool { + if x != nil { + return x.RestoredStaticAddress + } + return false +} + +func (x *RecoverResponse) GetStaticAddress() string { + if x != nil { + return x.StaticAddress + } + return "" +} + +func (x *RecoverResponse) GetNumDepositsFound() uint32 { + if x != nil { + return x.NumDepositsFound + } + return 0 +} + +func (x *RecoverResponse) GetDepositReconciliationError() string { + if x != nil { + return x.DepositReconciliationError + } + return "" +} + type L402Token struct { state protoimpl.MessageState `protogen:"open.v1"` // The base macaroon that was baked by the auth server. @@ -2991,7 +3129,7 @@ type L402Token struct { func (x *L402Token) Reset() { *x = L402Token{} - mi := &file_client_proto_msgTypes[30] + mi := &file_client_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3003,7 +3141,7 @@ func (x *L402Token) String() string { func (*L402Token) ProtoMessage() {} func (x *L402Token) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[30] + mi := &file_client_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3016,7 +3154,7 @@ func (x *L402Token) ProtoReflect() protoreflect.Message { // Deprecated: Use L402Token.ProtoReflect.Descriptor instead. func (*L402Token) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{30} + return file_client_proto_rawDescGZIP(), []int{32} } func (x *L402Token) GetBaseMacaroon() []byte { @@ -3100,7 +3238,7 @@ type LoopStats struct { func (x *LoopStats) Reset() { *x = LoopStats{} - mi := &file_client_proto_msgTypes[31] + mi := &file_client_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3112,7 +3250,7 @@ func (x *LoopStats) String() string { func (*LoopStats) ProtoMessage() {} func (x *LoopStats) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[31] + mi := &file_client_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3125,7 +3263,7 @@ func (x *LoopStats) ProtoReflect() protoreflect.Message { // Deprecated: Use LoopStats.ProtoReflect.Descriptor instead. func (*LoopStats) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{31} + return file_client_proto_rawDescGZIP(), []int{33} } func (x *LoopStats) GetPendingCount() uint64 { @@ -3171,7 +3309,7 @@ type GetInfoRequest struct { func (x *GetInfoRequest) Reset() { *x = GetInfoRequest{} - mi := &file_client_proto_msgTypes[32] + mi := &file_client_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3183,7 +3321,7 @@ func (x *GetInfoRequest) String() string { func (*GetInfoRequest) ProtoMessage() {} func (x *GetInfoRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[32] + mi := &file_client_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3196,7 +3334,7 @@ func (x *GetInfoRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetInfoRequest.ProtoReflect.Descriptor instead. func (*GetInfoRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{32} + return file_client_proto_rawDescGZIP(), []int{34} } type GetInfoResponse struct { @@ -3227,7 +3365,7 @@ type GetInfoResponse struct { func (x *GetInfoResponse) Reset() { *x = GetInfoResponse{} - mi := &file_client_proto_msgTypes[33] + mi := &file_client_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3239,7 +3377,7 @@ func (x *GetInfoResponse) String() string { func (*GetInfoResponse) ProtoMessage() {} func (x *GetInfoResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[33] + mi := &file_client_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3252,7 +3390,7 @@ func (x *GetInfoResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetInfoResponse.ProtoReflect.Descriptor instead. func (*GetInfoResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{33} + return file_client_proto_rawDescGZIP(), []int{35} } func (x *GetInfoResponse) GetVersion() string { @@ -3326,7 +3464,7 @@ type GetLiquidityParamsRequest struct { func (x *GetLiquidityParamsRequest) Reset() { *x = GetLiquidityParamsRequest{} - mi := &file_client_proto_msgTypes[34] + mi := &file_client_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3338,7 +3476,7 @@ func (x *GetLiquidityParamsRequest) String() string { func (*GetLiquidityParamsRequest) ProtoMessage() {} func (x *GetLiquidityParamsRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[34] + mi := &file_client_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3351,7 +3489,7 @@ func (x *GetLiquidityParamsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLiquidityParamsRequest.ProtoReflect.Descriptor instead. func (*GetLiquidityParamsRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{34} + return file_client_proto_rawDescGZIP(), []int{36} } type LiquidityParameters struct { @@ -3461,7 +3599,7 @@ type LiquidityParameters struct { func (x *LiquidityParameters) Reset() { *x = LiquidityParameters{} - mi := &file_client_proto_msgTypes[35] + mi := &file_client_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3473,7 +3611,7 @@ func (x *LiquidityParameters) String() string { func (*LiquidityParameters) ProtoMessage() {} func (x *LiquidityParameters) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[35] + mi := &file_client_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3486,7 +3624,7 @@ func (x *LiquidityParameters) ProtoReflect() protoreflect.Message { // Deprecated: Use LiquidityParameters.ProtoReflect.Descriptor instead. func (*LiquidityParameters) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{35} + return file_client_proto_rawDescGZIP(), []int{37} } func (x *LiquidityParameters) GetRules() []*LiquidityRule { @@ -3696,7 +3834,7 @@ type EasyAssetAutoloopParams struct { func (x *EasyAssetAutoloopParams) Reset() { *x = EasyAssetAutoloopParams{} - mi := &file_client_proto_msgTypes[36] + mi := &file_client_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3708,7 +3846,7 @@ func (x *EasyAssetAutoloopParams) String() string { func (*EasyAssetAutoloopParams) ProtoMessage() {} func (x *EasyAssetAutoloopParams) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[36] + mi := &file_client_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3721,7 +3859,7 @@ func (x *EasyAssetAutoloopParams) ProtoReflect() protoreflect.Message { // Deprecated: Use EasyAssetAutoloopParams.ProtoReflect.Descriptor instead. func (*EasyAssetAutoloopParams) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{36} + return file_client_proto_rawDescGZIP(), []int{38} } func (x *EasyAssetAutoloopParams) GetEnabled() bool { @@ -3765,7 +3903,7 @@ type LiquidityRule struct { func (x *LiquidityRule) Reset() { *x = LiquidityRule{} - mi := &file_client_proto_msgTypes[37] + mi := &file_client_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3777,7 +3915,7 @@ func (x *LiquidityRule) String() string { func (*LiquidityRule) ProtoMessage() {} func (x *LiquidityRule) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[37] + mi := &file_client_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3790,7 +3928,7 @@ func (x *LiquidityRule) ProtoReflect() protoreflect.Message { // Deprecated: Use LiquidityRule.ProtoReflect.Descriptor instead. func (*LiquidityRule) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{37} + return file_client_proto_rawDescGZIP(), []int{39} } func (x *LiquidityRule) GetChannelId() uint64 { @@ -3848,7 +3986,7 @@ type SetLiquidityParamsRequest struct { func (x *SetLiquidityParamsRequest) Reset() { *x = SetLiquidityParamsRequest{} - mi := &file_client_proto_msgTypes[38] + mi := &file_client_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3860,7 +3998,7 @@ func (x *SetLiquidityParamsRequest) String() string { func (*SetLiquidityParamsRequest) ProtoMessage() {} func (x *SetLiquidityParamsRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[38] + mi := &file_client_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3873,7 +4011,7 @@ func (x *SetLiquidityParamsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetLiquidityParamsRequest.ProtoReflect.Descriptor instead. func (*SetLiquidityParamsRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{38} + return file_client_proto_rawDescGZIP(), []int{40} } func (x *SetLiquidityParamsRequest) GetParameters() *LiquidityParameters { @@ -3891,7 +4029,7 @@ type SetLiquidityParamsResponse struct { func (x *SetLiquidityParamsResponse) Reset() { *x = SetLiquidityParamsResponse{} - mi := &file_client_proto_msgTypes[39] + mi := &file_client_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3903,7 +4041,7 @@ func (x *SetLiquidityParamsResponse) String() string { func (*SetLiquidityParamsResponse) ProtoMessage() {} func (x *SetLiquidityParamsResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[39] + mi := &file_client_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3916,7 +4054,7 @@ func (x *SetLiquidityParamsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetLiquidityParamsResponse.ProtoReflect.Descriptor instead. func (*SetLiquidityParamsResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{39} + return file_client_proto_rawDescGZIP(), []int{41} } type SuggestSwapsRequest struct { @@ -3927,7 +4065,7 @@ type SuggestSwapsRequest struct { func (x *SuggestSwapsRequest) Reset() { *x = SuggestSwapsRequest{} - mi := &file_client_proto_msgTypes[40] + mi := &file_client_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3939,7 +4077,7 @@ func (x *SuggestSwapsRequest) String() string { func (*SuggestSwapsRequest) ProtoMessage() {} func (x *SuggestSwapsRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[40] + mi := &file_client_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3952,7 +4090,7 @@ func (x *SuggestSwapsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SuggestSwapsRequest.ProtoReflect.Descriptor instead. func (*SuggestSwapsRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{40} + return file_client_proto_rawDescGZIP(), []int{42} } type Disqualified struct { @@ -3969,7 +4107,7 @@ type Disqualified struct { func (x *Disqualified) Reset() { *x = Disqualified{} - mi := &file_client_proto_msgTypes[41] + mi := &file_client_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3981,7 +4119,7 @@ func (x *Disqualified) String() string { func (*Disqualified) ProtoMessage() {} func (x *Disqualified) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[41] + mi := &file_client_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3994,7 +4132,7 @@ func (x *Disqualified) ProtoReflect() protoreflect.Message { // Deprecated: Use Disqualified.ProtoReflect.Descriptor instead. func (*Disqualified) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{41} + return file_client_proto_rawDescGZIP(), []int{43} } func (x *Disqualified) GetChannelId() uint64 { @@ -4033,7 +4171,7 @@ type SuggestSwapsResponse struct { func (x *SuggestSwapsResponse) Reset() { *x = SuggestSwapsResponse{} - mi := &file_client_proto_msgTypes[42] + mi := &file_client_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4045,7 +4183,7 @@ func (x *SuggestSwapsResponse) String() string { func (*SuggestSwapsResponse) ProtoMessage() {} func (x *SuggestSwapsResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[42] + mi := &file_client_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4058,7 +4196,7 @@ func (x *SuggestSwapsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SuggestSwapsResponse.ProtoReflect.Descriptor instead. func (*SuggestSwapsResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{42} + return file_client_proto_rawDescGZIP(), []int{44} } func (x *SuggestSwapsResponse) GetLoopOut() []*LoopOutRequest { @@ -4097,7 +4235,7 @@ type AbandonSwapRequest struct { func (x *AbandonSwapRequest) Reset() { *x = AbandonSwapRequest{} - mi := &file_client_proto_msgTypes[43] + mi := &file_client_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4109,7 +4247,7 @@ func (x *AbandonSwapRequest) String() string { func (*AbandonSwapRequest) ProtoMessage() {} func (x *AbandonSwapRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[43] + mi := &file_client_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4122,7 +4260,7 @@ func (x *AbandonSwapRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AbandonSwapRequest.ProtoReflect.Descriptor instead. func (*AbandonSwapRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{43} + return file_client_proto_rawDescGZIP(), []int{45} } func (x *AbandonSwapRequest) GetId() []byte { @@ -4147,7 +4285,7 @@ type AbandonSwapResponse struct { func (x *AbandonSwapResponse) Reset() { *x = AbandonSwapResponse{} - mi := &file_client_proto_msgTypes[44] + mi := &file_client_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4159,7 +4297,7 @@ func (x *AbandonSwapResponse) String() string { func (*AbandonSwapResponse) ProtoMessage() {} func (x *AbandonSwapResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[44] + mi := &file_client_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4172,7 +4310,7 @@ func (x *AbandonSwapResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AbandonSwapResponse.ProtoReflect.Descriptor instead. func (*AbandonSwapResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{44} + return file_client_proto_rawDescGZIP(), []int{46} } type ListReservationsRequest struct { @@ -4183,7 +4321,7 @@ type ListReservationsRequest struct { func (x *ListReservationsRequest) Reset() { *x = ListReservationsRequest{} - mi := &file_client_proto_msgTypes[45] + mi := &file_client_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4195,7 +4333,7 @@ func (x *ListReservationsRequest) String() string { func (*ListReservationsRequest) ProtoMessage() {} func (x *ListReservationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[45] + mi := &file_client_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4208,7 +4346,7 @@ func (x *ListReservationsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListReservationsRequest.ProtoReflect.Descriptor instead. func (*ListReservationsRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{45} + return file_client_proto_rawDescGZIP(), []int{47} } type ListReservationsResponse struct { @@ -4221,7 +4359,7 @@ type ListReservationsResponse struct { func (x *ListReservationsResponse) Reset() { *x = ListReservationsResponse{} - mi := &file_client_proto_msgTypes[46] + mi := &file_client_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4233,7 +4371,7 @@ func (x *ListReservationsResponse) String() string { func (*ListReservationsResponse) ProtoMessage() {} func (x *ListReservationsResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[46] + mi := &file_client_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4246,7 +4384,7 @@ func (x *ListReservationsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListReservationsResponse.ProtoReflect.Descriptor instead. func (*ListReservationsResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{46} + return file_client_proto_rawDescGZIP(), []int{48} } func (x *ListReservationsResponse) GetReservations() []*ClientReservation { @@ -4276,7 +4414,7 @@ type ClientReservation struct { func (x *ClientReservation) Reset() { *x = ClientReservation{} - mi := &file_client_proto_msgTypes[47] + mi := &file_client_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4288,7 +4426,7 @@ func (x *ClientReservation) String() string { func (*ClientReservation) ProtoMessage() {} func (x *ClientReservation) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[47] + mi := &file_client_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4301,7 +4439,7 @@ func (x *ClientReservation) ProtoReflect() protoreflect.Message { // Deprecated: Use ClientReservation.ProtoReflect.Descriptor instead. func (*ClientReservation) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{47} + return file_client_proto_rawDescGZIP(), []int{49} } func (x *ClientReservation) GetReservationId() []byte { @@ -4363,7 +4501,7 @@ type InstantOutRequest struct { func (x *InstantOutRequest) Reset() { *x = InstantOutRequest{} - mi := &file_client_proto_msgTypes[48] + mi := &file_client_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4375,7 +4513,7 @@ func (x *InstantOutRequest) String() string { func (*InstantOutRequest) ProtoMessage() {} func (x *InstantOutRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[48] + mi := &file_client_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4388,7 +4526,7 @@ func (x *InstantOutRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InstantOutRequest.ProtoReflect.Descriptor instead. func (*InstantOutRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{48} + return file_client_proto_rawDescGZIP(), []int{50} } func (x *InstantOutRequest) GetReservationIds() [][]byte { @@ -4426,7 +4564,7 @@ type InstantOutResponse struct { func (x *InstantOutResponse) Reset() { *x = InstantOutResponse{} - mi := &file_client_proto_msgTypes[49] + mi := &file_client_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4438,7 +4576,7 @@ func (x *InstantOutResponse) String() string { func (*InstantOutResponse) ProtoMessage() {} func (x *InstantOutResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[49] + mi := &file_client_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4451,7 +4589,7 @@ func (x *InstantOutResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use InstantOutResponse.ProtoReflect.Descriptor instead. func (*InstantOutResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{49} + return file_client_proto_rawDescGZIP(), []int{51} } func (x *InstantOutResponse) GetInstantOutHash() []byte { @@ -4492,7 +4630,7 @@ type InstantOutQuoteRequest struct { func (x *InstantOutQuoteRequest) Reset() { *x = InstantOutQuoteRequest{} - mi := &file_client_proto_msgTypes[50] + mi := &file_client_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4504,7 +4642,7 @@ func (x *InstantOutQuoteRequest) String() string { func (*InstantOutQuoteRequest) ProtoMessage() {} func (x *InstantOutQuoteRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[50] + mi := &file_client_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4517,7 +4655,7 @@ func (x *InstantOutQuoteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InstantOutQuoteRequest.ProtoReflect.Descriptor instead. func (*InstantOutQuoteRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{50} + return file_client_proto_rawDescGZIP(), []int{52} } func (x *InstantOutQuoteRequest) GetAmt() uint64 { @@ -4555,7 +4693,7 @@ type InstantOutQuoteResponse struct { func (x *InstantOutQuoteResponse) Reset() { *x = InstantOutQuoteResponse{} - mi := &file_client_proto_msgTypes[51] + mi := &file_client_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4567,7 +4705,7 @@ func (x *InstantOutQuoteResponse) String() string { func (*InstantOutQuoteResponse) ProtoMessage() {} func (x *InstantOutQuoteResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[51] + mi := &file_client_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4580,7 +4718,7 @@ func (x *InstantOutQuoteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use InstantOutQuoteResponse.ProtoReflect.Descriptor instead. func (*InstantOutQuoteResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{51} + return file_client_proto_rawDescGZIP(), []int{53} } func (x *InstantOutQuoteResponse) GetServiceFeeSat() int64 { @@ -4605,7 +4743,7 @@ type ListInstantOutsRequest struct { func (x *ListInstantOutsRequest) Reset() { *x = ListInstantOutsRequest{} - mi := &file_client_proto_msgTypes[52] + mi := &file_client_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4617,7 +4755,7 @@ func (x *ListInstantOutsRequest) String() string { func (*ListInstantOutsRequest) ProtoMessage() {} func (x *ListInstantOutsRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[52] + mi := &file_client_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4630,7 +4768,7 @@ func (x *ListInstantOutsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListInstantOutsRequest.ProtoReflect.Descriptor instead. func (*ListInstantOutsRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{52} + return file_client_proto_rawDescGZIP(), []int{54} } type ListInstantOutsResponse struct { @@ -4643,7 +4781,7 @@ type ListInstantOutsResponse struct { func (x *ListInstantOutsResponse) Reset() { *x = ListInstantOutsResponse{} - mi := &file_client_proto_msgTypes[53] + mi := &file_client_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4655,7 +4793,7 @@ func (x *ListInstantOutsResponse) String() string { func (*ListInstantOutsResponse) ProtoMessage() {} func (x *ListInstantOutsResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[53] + mi := &file_client_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4668,7 +4806,7 @@ func (x *ListInstantOutsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListInstantOutsResponse.ProtoReflect.Descriptor instead. func (*ListInstantOutsResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{53} + return file_client_proto_rawDescGZIP(), []int{55} } func (x *ListInstantOutsResponse) GetSwaps() []*InstantOut { @@ -4696,7 +4834,7 @@ type InstantOut struct { func (x *InstantOut) Reset() { *x = InstantOut{} - mi := &file_client_proto_msgTypes[54] + mi := &file_client_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4708,7 +4846,7 @@ func (x *InstantOut) String() string { func (*InstantOut) ProtoMessage() {} func (x *InstantOut) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[54] + mi := &file_client_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4721,7 +4859,7 @@ func (x *InstantOut) ProtoReflect() protoreflect.Message { // Deprecated: Use InstantOut.ProtoReflect.Descriptor instead. func (*InstantOut) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{54} + return file_client_proto_rawDescGZIP(), []int{56} } func (x *InstantOut) GetSwapHash() []byte { @@ -4769,7 +4907,7 @@ type NewStaticAddressRequest struct { func (x *NewStaticAddressRequest) Reset() { *x = NewStaticAddressRequest{} - mi := &file_client_proto_msgTypes[55] + mi := &file_client_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4781,7 +4919,7 @@ func (x *NewStaticAddressRequest) String() string { func (*NewStaticAddressRequest) ProtoMessage() {} func (x *NewStaticAddressRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[55] + mi := &file_client_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4794,7 +4932,7 @@ func (x *NewStaticAddressRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use NewStaticAddressRequest.ProtoReflect.Descriptor instead. func (*NewStaticAddressRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{55} + return file_client_proto_rawDescGZIP(), []int{57} } func (x *NewStaticAddressRequest) GetClientKey() []byte { @@ -4816,7 +4954,7 @@ type NewStaticAddressResponse struct { func (x *NewStaticAddressResponse) Reset() { *x = NewStaticAddressResponse{} - mi := &file_client_proto_msgTypes[56] + mi := &file_client_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4828,7 +4966,7 @@ func (x *NewStaticAddressResponse) String() string { func (*NewStaticAddressResponse) ProtoMessage() {} func (x *NewStaticAddressResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[56] + mi := &file_client_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4841,7 +4979,7 @@ func (x *NewStaticAddressResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use NewStaticAddressResponse.ProtoReflect.Descriptor instead. func (*NewStaticAddressResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{56} + return file_client_proto_rawDescGZIP(), []int{58} } func (x *NewStaticAddressResponse) GetAddress() string { @@ -4871,7 +5009,7 @@ type ListUnspentDepositsRequest struct { func (x *ListUnspentDepositsRequest) Reset() { *x = ListUnspentDepositsRequest{} - mi := &file_client_proto_msgTypes[57] + mi := &file_client_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4883,7 +5021,7 @@ func (x *ListUnspentDepositsRequest) String() string { func (*ListUnspentDepositsRequest) ProtoMessage() {} func (x *ListUnspentDepositsRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[57] + mi := &file_client_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4896,7 +5034,7 @@ func (x *ListUnspentDepositsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListUnspentDepositsRequest.ProtoReflect.Descriptor instead. func (*ListUnspentDepositsRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{57} + return file_client_proto_rawDescGZIP(), []int{59} } func (x *ListUnspentDepositsRequest) GetMinConfs() int32 { @@ -4923,7 +5061,7 @@ type ListUnspentDepositsResponse struct { func (x *ListUnspentDepositsResponse) Reset() { *x = ListUnspentDepositsResponse{} - mi := &file_client_proto_msgTypes[58] + mi := &file_client_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4935,7 +5073,7 @@ func (x *ListUnspentDepositsResponse) String() string { func (*ListUnspentDepositsResponse) ProtoMessage() {} func (x *ListUnspentDepositsResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[58] + mi := &file_client_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4948,7 +5086,7 @@ func (x *ListUnspentDepositsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListUnspentDepositsResponse.ProtoReflect.Descriptor instead. func (*ListUnspentDepositsResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{58} + return file_client_proto_rawDescGZIP(), []int{60} } func (x *ListUnspentDepositsResponse) GetUtxos() []*Utxo { @@ -4974,7 +5112,7 @@ type Utxo struct { func (x *Utxo) Reset() { *x = Utxo{} - mi := &file_client_proto_msgTypes[59] + mi := &file_client_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4986,7 +5124,7 @@ func (x *Utxo) String() string { func (*Utxo) ProtoMessage() {} func (x *Utxo) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[59] + mi := &file_client_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4999,7 +5137,7 @@ func (x *Utxo) ProtoReflect() protoreflect.Message { // Deprecated: Use Utxo.ProtoReflect.Descriptor instead. func (*Utxo) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{59} + return file_client_proto_rawDescGZIP(), []int{61} } func (x *Utxo) GetStaticAddress() string { @@ -5052,7 +5190,7 @@ type WithdrawDepositsRequest struct { func (x *WithdrawDepositsRequest) Reset() { *x = WithdrawDepositsRequest{} - mi := &file_client_proto_msgTypes[60] + mi := &file_client_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5064,7 +5202,7 @@ func (x *WithdrawDepositsRequest) String() string { func (*WithdrawDepositsRequest) ProtoMessage() {} func (x *WithdrawDepositsRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[60] + mi := &file_client_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5077,7 +5215,7 @@ func (x *WithdrawDepositsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WithdrawDepositsRequest.ProtoReflect.Descriptor instead. func (*WithdrawDepositsRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{60} + return file_client_proto_rawDescGZIP(), []int{62} } func (x *WithdrawDepositsRequest) GetOutpoints() []*lnrpc.OutPoint { @@ -5127,7 +5265,7 @@ type WithdrawDepositsResponse struct { func (x *WithdrawDepositsResponse) Reset() { *x = WithdrawDepositsResponse{} - mi := &file_client_proto_msgTypes[61] + mi := &file_client_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5139,7 +5277,7 @@ func (x *WithdrawDepositsResponse) String() string { func (*WithdrawDepositsResponse) ProtoMessage() {} func (x *WithdrawDepositsResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[61] + mi := &file_client_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5152,7 +5290,7 @@ func (x *WithdrawDepositsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WithdrawDepositsResponse.ProtoReflect.Descriptor instead. func (*WithdrawDepositsResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{61} + return file_client_proto_rawDescGZIP(), []int{63} } func (x *WithdrawDepositsResponse) GetWithdrawalTxHash() string { @@ -5181,7 +5319,7 @@ type ListStaticAddressDepositsRequest struct { func (x *ListStaticAddressDepositsRequest) Reset() { *x = ListStaticAddressDepositsRequest{} - mi := &file_client_proto_msgTypes[62] + mi := &file_client_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5193,7 +5331,7 @@ func (x *ListStaticAddressDepositsRequest) String() string { func (*ListStaticAddressDepositsRequest) ProtoMessage() {} func (x *ListStaticAddressDepositsRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[62] + mi := &file_client_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5206,7 +5344,7 @@ func (x *ListStaticAddressDepositsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListStaticAddressDepositsRequest.ProtoReflect.Descriptor instead. func (*ListStaticAddressDepositsRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{62} + return file_client_proto_rawDescGZIP(), []int{64} } func (x *ListStaticAddressDepositsRequest) GetStateFilter() DepositState { @@ -5233,7 +5371,7 @@ type ListStaticAddressDepositsResponse struct { func (x *ListStaticAddressDepositsResponse) Reset() { *x = ListStaticAddressDepositsResponse{} - mi := &file_client_proto_msgTypes[63] + mi := &file_client_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5245,7 +5383,7 @@ func (x *ListStaticAddressDepositsResponse) String() string { func (*ListStaticAddressDepositsResponse) ProtoMessage() {} func (x *ListStaticAddressDepositsResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[63] + mi := &file_client_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5258,7 +5396,7 @@ func (x *ListStaticAddressDepositsResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ListStaticAddressDepositsResponse.ProtoReflect.Descriptor instead. func (*ListStaticAddressDepositsResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{63} + return file_client_proto_rawDescGZIP(), []int{65} } func (x *ListStaticAddressDepositsResponse) GetFilteredDeposits() []*Deposit { @@ -5276,7 +5414,7 @@ type ListStaticAddressWithdrawalRequest struct { func (x *ListStaticAddressWithdrawalRequest) Reset() { *x = ListStaticAddressWithdrawalRequest{} - mi := &file_client_proto_msgTypes[64] + mi := &file_client_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5288,7 +5426,7 @@ func (x *ListStaticAddressWithdrawalRequest) String() string { func (*ListStaticAddressWithdrawalRequest) ProtoMessage() {} func (x *ListStaticAddressWithdrawalRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[64] + mi := &file_client_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5301,7 +5439,7 @@ func (x *ListStaticAddressWithdrawalRequest) ProtoReflect() protoreflect.Message // Deprecated: Use ListStaticAddressWithdrawalRequest.ProtoReflect.Descriptor instead. func (*ListStaticAddressWithdrawalRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{64} + return file_client_proto_rawDescGZIP(), []int{66} } type ListStaticAddressWithdrawalResponse struct { @@ -5314,7 +5452,7 @@ type ListStaticAddressWithdrawalResponse struct { func (x *ListStaticAddressWithdrawalResponse) Reset() { *x = ListStaticAddressWithdrawalResponse{} - mi := &file_client_proto_msgTypes[65] + mi := &file_client_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5326,7 +5464,7 @@ func (x *ListStaticAddressWithdrawalResponse) String() string { func (*ListStaticAddressWithdrawalResponse) ProtoMessage() {} func (x *ListStaticAddressWithdrawalResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[65] + mi := &file_client_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5339,7 +5477,7 @@ func (x *ListStaticAddressWithdrawalResponse) ProtoReflect() protoreflect.Messag // Deprecated: Use ListStaticAddressWithdrawalResponse.ProtoReflect.Descriptor instead. func (*ListStaticAddressWithdrawalResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{65} + return file_client_proto_rawDescGZIP(), []int{67} } func (x *ListStaticAddressWithdrawalResponse) GetWithdrawals() []*StaticAddressWithdrawal { @@ -5357,7 +5495,7 @@ type ListStaticAddressSwapsRequest struct { func (x *ListStaticAddressSwapsRequest) Reset() { *x = ListStaticAddressSwapsRequest{} - mi := &file_client_proto_msgTypes[66] + mi := &file_client_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5369,7 +5507,7 @@ func (x *ListStaticAddressSwapsRequest) String() string { func (*ListStaticAddressSwapsRequest) ProtoMessage() {} func (x *ListStaticAddressSwapsRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[66] + mi := &file_client_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5382,7 +5520,7 @@ func (x *ListStaticAddressSwapsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListStaticAddressSwapsRequest.ProtoReflect.Descriptor instead. func (*ListStaticAddressSwapsRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{66} + return file_client_proto_rawDescGZIP(), []int{68} } type ListStaticAddressSwapsResponse struct { @@ -5395,7 +5533,7 @@ type ListStaticAddressSwapsResponse struct { func (x *ListStaticAddressSwapsResponse) Reset() { *x = ListStaticAddressSwapsResponse{} - mi := &file_client_proto_msgTypes[67] + mi := &file_client_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5407,7 +5545,7 @@ func (x *ListStaticAddressSwapsResponse) String() string { func (*ListStaticAddressSwapsResponse) ProtoMessage() {} func (x *ListStaticAddressSwapsResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[67] + mi := &file_client_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5420,7 +5558,7 @@ func (x *ListStaticAddressSwapsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListStaticAddressSwapsResponse.ProtoReflect.Descriptor instead. func (*ListStaticAddressSwapsResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{67} + return file_client_proto_rawDescGZIP(), []int{69} } func (x *ListStaticAddressSwapsResponse) GetSwaps() []*StaticAddressLoopInSwap { @@ -5438,7 +5576,7 @@ type StaticAddressSummaryRequest struct { func (x *StaticAddressSummaryRequest) Reset() { *x = StaticAddressSummaryRequest{} - mi := &file_client_proto_msgTypes[68] + mi := &file_client_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5450,7 +5588,7 @@ func (x *StaticAddressSummaryRequest) String() string { func (*StaticAddressSummaryRequest) ProtoMessage() {} func (x *StaticAddressSummaryRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[68] + mi := &file_client_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5463,7 +5601,7 @@ func (x *StaticAddressSummaryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticAddressSummaryRequest.ProtoReflect.Descriptor instead. func (*StaticAddressSummaryRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{68} + return file_client_proto_rawDescGZIP(), []int{70} } type StaticAddressSummaryResponse struct { @@ -5494,7 +5632,7 @@ type StaticAddressSummaryResponse struct { func (x *StaticAddressSummaryResponse) Reset() { *x = StaticAddressSummaryResponse{} - mi := &file_client_proto_msgTypes[69] + mi := &file_client_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5506,7 +5644,7 @@ func (x *StaticAddressSummaryResponse) String() string { func (*StaticAddressSummaryResponse) ProtoMessage() {} func (x *StaticAddressSummaryResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[69] + mi := &file_client_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5519,7 +5657,7 @@ func (x *StaticAddressSummaryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticAddressSummaryResponse.ProtoReflect.Descriptor instead. func (*StaticAddressSummaryResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{69} + return file_client_proto_rawDescGZIP(), []int{71} } func (x *StaticAddressSummaryResponse) GetStaticAddress() string { @@ -5616,7 +5754,7 @@ type Deposit struct { func (x *Deposit) Reset() { *x = Deposit{} - mi := &file_client_proto_msgTypes[70] + mi := &file_client_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5628,7 +5766,7 @@ func (x *Deposit) String() string { func (*Deposit) ProtoMessage() {} func (x *Deposit) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[70] + mi := &file_client_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5641,7 +5779,7 @@ func (x *Deposit) ProtoReflect() protoreflect.Message { // Deprecated: Use Deposit.ProtoReflect.Descriptor instead. func (*Deposit) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{70} + return file_client_proto_rawDescGZIP(), []int{72} } func (x *Deposit) GetId() []byte { @@ -5715,7 +5853,7 @@ type StaticAddressWithdrawal struct { func (x *StaticAddressWithdrawal) Reset() { *x = StaticAddressWithdrawal{} - mi := &file_client_proto_msgTypes[71] + mi := &file_client_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5727,7 +5865,7 @@ func (x *StaticAddressWithdrawal) String() string { func (*StaticAddressWithdrawal) ProtoMessage() {} func (x *StaticAddressWithdrawal) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[71] + mi := &file_client_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5740,7 +5878,7 @@ func (x *StaticAddressWithdrawal) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticAddressWithdrawal.ProtoReflect.Descriptor instead. func (*StaticAddressWithdrawal) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{71} + return file_client_proto_rawDescGZIP(), []int{73} } func (x *StaticAddressWithdrawal) GetTxId() string { @@ -5788,9 +5926,9 @@ func (x *StaticAddressWithdrawal) GetConfirmationHeight() uint32 { type StaticAddressLoopInSwap struct { state protoimpl.MessageState `protogen:"open.v1"` // The swap hash of the swap. It represents the unique identifier of the swap. - SwapHash []byte `protobuf:"bytes,1,opt,name=swap_hash,json=swapHash,proto3" json:"swap_hash,omitempty"` - DepositOutpoints []string `protobuf:"bytes,2,rep,name=deposit_outpoints,json=depositOutpoints,proto3" json:"deposit_outpoints,omitempty"` - State StaticAddressLoopInSwapState `protobuf:"varint,3,opt,name=state,proto3,enum=looprpc.StaticAddressLoopInSwapState" json:"state,omitempty"` + SwapHash []byte `protobuf:"bytes,1,opt,name=swap_hash,json=swapHash,proto3" json:"swap_hash,omitempty"` + DepositOutpoints []string `protobuf:"bytes,2,rep,name=deposit_outpoints,json=depositOutpoints,proto3" json:"deposit_outpoints,omitempty"` + State StaticAddressLoopInSwapState `protobuf:"varint,3,opt,name=state,proto3,enum=looprpc.StaticAddressLoopInSwapState" json:"state,omitempty"` // The swap amount of the swap. It is the sum of the values of the deposit // outpoints that were used for this swap. SwapAmountSatoshis int64 `protobuf:"varint,4,opt,name=swap_amount_satoshis,json=swapAmountSatoshis,proto3" json:"swap_amount_satoshis,omitempty"` @@ -5805,7 +5943,7 @@ type StaticAddressLoopInSwap struct { func (x *StaticAddressLoopInSwap) Reset() { *x = StaticAddressLoopInSwap{} - mi := &file_client_proto_msgTypes[72] + mi := &file_client_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5817,7 +5955,7 @@ func (x *StaticAddressLoopInSwap) String() string { func (*StaticAddressLoopInSwap) ProtoMessage() {} func (x *StaticAddressLoopInSwap) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[72] + mi := &file_client_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5830,7 +5968,7 @@ func (x *StaticAddressLoopInSwap) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticAddressLoopInSwap.ProtoReflect.Descriptor instead. func (*StaticAddressLoopInSwap) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{72} + return file_client_proto_rawDescGZIP(), []int{74} } func (x *StaticAddressLoopInSwap) GetSwapHash() []byte { @@ -5928,7 +6066,7 @@ type StaticAddressLoopInRequest struct { func (x *StaticAddressLoopInRequest) Reset() { *x = StaticAddressLoopInRequest{} - mi := &file_client_proto_msgTypes[73] + mi := &file_client_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5940,7 +6078,7 @@ func (x *StaticAddressLoopInRequest) String() string { func (*StaticAddressLoopInRequest) ProtoMessage() {} func (x *StaticAddressLoopInRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[73] + mi := &file_client_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5953,7 +6091,7 @@ func (x *StaticAddressLoopInRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticAddressLoopInRequest.ProtoReflect.Descriptor instead. func (*StaticAddressLoopInRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{73} + return file_client_proto_rawDescGZIP(), []int{75} } func (x *StaticAddressLoopInRequest) GetOutpoints() []string { @@ -6073,7 +6211,7 @@ type StaticAddressLoopInResponse struct { func (x *StaticAddressLoopInResponse) Reset() { *x = StaticAddressLoopInResponse{} - mi := &file_client_proto_msgTypes[74] + mi := &file_client_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6085,7 +6223,7 @@ func (x *StaticAddressLoopInResponse) String() string { func (*StaticAddressLoopInResponse) ProtoMessage() {} func (x *StaticAddressLoopInResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[74] + mi := &file_client_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6098,7 +6236,7 @@ func (x *StaticAddressLoopInResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticAddressLoopInResponse.ProtoReflect.Descriptor instead. func (*StaticAddressLoopInResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{74} + return file_client_proto_rawDescGZIP(), []int{76} } func (x *StaticAddressLoopInResponse) GetSwapHash() []byte { @@ -6227,7 +6365,7 @@ type AssetLoopOutRequest struct { func (x *AssetLoopOutRequest) Reset() { *x = AssetLoopOutRequest{} - mi := &file_client_proto_msgTypes[75] + mi := &file_client_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6239,7 +6377,7 @@ func (x *AssetLoopOutRequest) String() string { func (*AssetLoopOutRequest) ProtoMessage() {} func (x *AssetLoopOutRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[75] + mi := &file_client_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6252,7 +6390,7 @@ func (x *AssetLoopOutRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetLoopOutRequest.ProtoReflect.Descriptor instead. func (*AssetLoopOutRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{75} + return file_client_proto_rawDescGZIP(), []int{77} } func (x *AssetLoopOutRequest) GetAssetId() []byte { @@ -6307,7 +6445,7 @@ type AssetRfqInfo struct { func (x *AssetRfqInfo) Reset() { *x = AssetRfqInfo{} - mi := &file_client_proto_msgTypes[76] + mi := &file_client_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6319,7 +6457,7 @@ func (x *AssetRfqInfo) String() string { func (*AssetRfqInfo) ProtoMessage() {} func (x *AssetRfqInfo) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[76] + mi := &file_client_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6332,7 +6470,7 @@ func (x *AssetRfqInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetRfqInfo.ProtoReflect.Descriptor instead. func (*AssetRfqInfo) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{76} + return file_client_proto_rawDescGZIP(), []int{78} } func (x *AssetRfqInfo) GetPrepayRfqId() []byte { @@ -6421,7 +6559,7 @@ type FixedPoint struct { func (x *FixedPoint) Reset() { *x = FixedPoint{} - mi := &file_client_proto_msgTypes[77] + mi := &file_client_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6433,7 +6571,7 @@ func (x *FixedPoint) String() string { func (*FixedPoint) ProtoMessage() {} func (x *FixedPoint) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[77] + mi := &file_client_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6446,7 +6584,7 @@ func (x *FixedPoint) ProtoReflect() protoreflect.Message { // Deprecated: Use FixedPoint.ProtoReflect.Descriptor instead. func (*FixedPoint) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{77} + return file_client_proto_rawDescGZIP(), []int{79} } func (x *FixedPoint) GetCoefficient() string { @@ -6477,7 +6615,7 @@ type AssetLoopOutInfo struct { func (x *AssetLoopOutInfo) Reset() { *x = AssetLoopOutInfo{} - mi := &file_client_proto_msgTypes[78] + mi := &file_client_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6489,7 +6627,7 @@ func (x *AssetLoopOutInfo) String() string { func (*AssetLoopOutInfo) ProtoMessage() {} func (x *AssetLoopOutInfo) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[78] + mi := &file_client_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6502,7 +6640,7 @@ func (x *AssetLoopOutInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetLoopOutInfo.ProtoReflect.Descriptor instead. func (*AssetLoopOutInfo) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{78} + return file_client_proto_rawDescGZIP(), []int{80} } func (x *AssetLoopOutInfo) GetAssetId() string { @@ -6700,7 +6838,18 @@ const file_client_proto_rawDesc = "" + "\x0eTokensResponse\x12*\n" + "\x06tokens\x18\x01 \x03(\v2\x12.looprpc.L402TokenR\x06tokens\"\x17\n" + "\x15FetchL402TokenRequest\"\x18\n" + - "\x16FetchL402TokenResponse\"\xcb\x02\n" + + "\x16FetchL402TokenResponse\"1\n" + + "\x0eRecoverRequest\x12\x1f\n" + + "\vbackup_file\x18\x01 \x01(\tR\n" + + "backupFile\"\xa6\x02\n" + + "\x0fRecoverResponse\x12\x1f\n" + + "\vbackup_file\x18\x01 \x01(\tR\n" + + "backupFile\x12#\n" + + "\rrestored_l402\x18\x02 \x01(\bR\frestoredL402\x126\n" + + "\x17restored_static_address\x18\x03 \x01(\bR\x15restoredStaticAddress\x12%\n" + + "\x0estatic_address\x18\x04 \x01(\tR\rstaticAddress\x12,\n" + + "\x12num_deposits_found\x18\x05 \x01(\rR\x10numDepositsFound\x12@\n" + + "\x1cdeposit_reconciliation_error\x18\x06 \x01(\tR\x1adepositReconciliationError\"\xcb\x02\n" + "\tL402Token\x12#\n" + "\rbase_macaroon\x18\x01 \x01(\fR\fbaseMacaroon\x12!\n" + "\fpayment_hash\x18\x02 \x01(\fR\vpaymentHash\x12)\n" + @@ -7030,7 +7179,7 @@ const file_client_proto_rawDesc = "" + "\x1eSUCCEEDED_TRANSITIONING_FAILED\x10\t\x12\x13\n" + "\x0fUNLOCK_DEPOSITS\x10\n" + "\x12\x1e\n" + - "\x1aFAILED_STATIC_ADDRESS_SWAP\x10\v2\xca\x14\n" + + "\x1aFAILED_STATIC_ADDRESS_SWAP\x10\v2\x88\x15\n" + "\n" + "SwapClient\x129\n" + "\aLoopOut\x12\x17.looprpc.LoopOutRequest\x1a\x15.looprpc.SwapResponse\x127\n" + @@ -7048,6 +7197,7 @@ const file_client_proto_rawDesc = "" + "\rGetL402Tokens\x12\x16.looprpc.TokensRequest\x1a\x17.looprpc.TokensResponse\x12@\n" + "\rGetLsatTokens\x12\x16.looprpc.TokensRequest\x1a\x17.looprpc.TokensResponse\x12Q\n" + "\x0eFetchL402Token\x12\x1e.looprpc.FetchL402TokenRequest\x1a\x1f.looprpc.FetchL402TokenResponse\x12<\n" + + "\aRecover\x12\x17.looprpc.RecoverRequest\x1a\x18.looprpc.RecoverResponse\x12<\n" + "\aGetInfo\x12\x17.looprpc.GetInfoRequest\x1a\x18.looprpc.GetInfoResponse\x12E\n" + "\n" + "StopDaemon\x12\x1a.looprpc.StopDaemonRequest\x1a\x1b.looprpc.StopDaemonResponse\x12V\n" + @@ -7082,7 +7232,7 @@ func file_client_proto_rawDescGZIP() []byte { } var file_client_proto_enumTypes = make([]protoimpl.EnumInfo, 9) -var file_client_proto_msgTypes = make([]protoimpl.MessageInfo, 80) +var file_client_proto_msgTypes = make([]protoimpl.MessageInfo, 82) var file_client_proto_goTypes = []any{ (AddressType)(0), // 0: looprpc.AddressType (SwapType)(0), // 1: looprpc.SwapType @@ -7123,117 +7273,119 @@ var file_client_proto_goTypes = []any{ (*TokensResponse)(nil), // 36: looprpc.TokensResponse (*FetchL402TokenRequest)(nil), // 37: looprpc.FetchL402TokenRequest (*FetchL402TokenResponse)(nil), // 38: looprpc.FetchL402TokenResponse - (*L402Token)(nil), // 39: looprpc.L402Token - (*LoopStats)(nil), // 40: looprpc.LoopStats - (*GetInfoRequest)(nil), // 41: looprpc.GetInfoRequest - (*GetInfoResponse)(nil), // 42: looprpc.GetInfoResponse - (*GetLiquidityParamsRequest)(nil), // 43: looprpc.GetLiquidityParamsRequest - (*LiquidityParameters)(nil), // 44: looprpc.LiquidityParameters - (*EasyAssetAutoloopParams)(nil), // 45: looprpc.EasyAssetAutoloopParams - (*LiquidityRule)(nil), // 46: looprpc.LiquidityRule - (*SetLiquidityParamsRequest)(nil), // 47: looprpc.SetLiquidityParamsRequest - (*SetLiquidityParamsResponse)(nil), // 48: looprpc.SetLiquidityParamsResponse - (*SuggestSwapsRequest)(nil), // 49: looprpc.SuggestSwapsRequest - (*Disqualified)(nil), // 50: looprpc.Disqualified - (*SuggestSwapsResponse)(nil), // 51: looprpc.SuggestSwapsResponse - (*AbandonSwapRequest)(nil), // 52: looprpc.AbandonSwapRequest - (*AbandonSwapResponse)(nil), // 53: looprpc.AbandonSwapResponse - (*ListReservationsRequest)(nil), // 54: looprpc.ListReservationsRequest - (*ListReservationsResponse)(nil), // 55: looprpc.ListReservationsResponse - (*ClientReservation)(nil), // 56: looprpc.ClientReservation - (*InstantOutRequest)(nil), // 57: looprpc.InstantOutRequest - (*InstantOutResponse)(nil), // 58: looprpc.InstantOutResponse - (*InstantOutQuoteRequest)(nil), // 59: looprpc.InstantOutQuoteRequest - (*InstantOutQuoteResponse)(nil), // 60: looprpc.InstantOutQuoteResponse - (*ListInstantOutsRequest)(nil), // 61: looprpc.ListInstantOutsRequest - (*ListInstantOutsResponse)(nil), // 62: looprpc.ListInstantOutsResponse - (*InstantOut)(nil), // 63: looprpc.InstantOut - (*NewStaticAddressRequest)(nil), // 64: looprpc.NewStaticAddressRequest - (*NewStaticAddressResponse)(nil), // 65: looprpc.NewStaticAddressResponse - (*ListUnspentDepositsRequest)(nil), // 66: looprpc.ListUnspentDepositsRequest - (*ListUnspentDepositsResponse)(nil), // 67: looprpc.ListUnspentDepositsResponse - (*Utxo)(nil), // 68: looprpc.Utxo - (*WithdrawDepositsRequest)(nil), // 69: looprpc.WithdrawDepositsRequest - (*WithdrawDepositsResponse)(nil), // 70: looprpc.WithdrawDepositsResponse - (*ListStaticAddressDepositsRequest)(nil), // 71: looprpc.ListStaticAddressDepositsRequest - (*ListStaticAddressDepositsResponse)(nil), // 72: looprpc.ListStaticAddressDepositsResponse - (*ListStaticAddressWithdrawalRequest)(nil), // 73: looprpc.ListStaticAddressWithdrawalRequest - (*ListStaticAddressWithdrawalResponse)(nil), // 74: looprpc.ListStaticAddressWithdrawalResponse - (*ListStaticAddressSwapsRequest)(nil), // 75: looprpc.ListStaticAddressSwapsRequest - (*ListStaticAddressSwapsResponse)(nil), // 76: looprpc.ListStaticAddressSwapsResponse - (*StaticAddressSummaryRequest)(nil), // 77: looprpc.StaticAddressSummaryRequest - (*StaticAddressSummaryResponse)(nil), // 78: looprpc.StaticAddressSummaryResponse - (*Deposit)(nil), // 79: looprpc.Deposit - (*StaticAddressWithdrawal)(nil), // 80: looprpc.StaticAddressWithdrawal - (*StaticAddressLoopInSwap)(nil), // 81: looprpc.StaticAddressLoopInSwap - (*StaticAddressLoopInRequest)(nil), // 82: looprpc.StaticAddressLoopInRequest - (*StaticAddressLoopInResponse)(nil), // 83: looprpc.StaticAddressLoopInResponse - (*AssetLoopOutRequest)(nil), // 84: looprpc.AssetLoopOutRequest - (*AssetRfqInfo)(nil), // 85: looprpc.AssetRfqInfo - (*FixedPoint)(nil), // 86: looprpc.FixedPoint - (*AssetLoopOutInfo)(nil), // 87: looprpc.AssetLoopOutInfo - nil, // 88: looprpc.LiquidityParameters.EasyAssetParamsEntry - (*lnrpc.OpenChannelRequest)(nil), // 89: lnrpc.OpenChannelRequest - (*swapserverrpc.RouteHint)(nil), // 90: looprpc.RouteHint - (*lnrpc.OutPoint)(nil), // 91: lnrpc.OutPoint + (*RecoverRequest)(nil), // 39: looprpc.RecoverRequest + (*RecoverResponse)(nil), // 40: looprpc.RecoverResponse + (*L402Token)(nil), // 41: looprpc.L402Token + (*LoopStats)(nil), // 42: looprpc.LoopStats + (*GetInfoRequest)(nil), // 43: looprpc.GetInfoRequest + (*GetInfoResponse)(nil), // 44: looprpc.GetInfoResponse + (*GetLiquidityParamsRequest)(nil), // 45: looprpc.GetLiquidityParamsRequest + (*LiquidityParameters)(nil), // 46: looprpc.LiquidityParameters + (*EasyAssetAutoloopParams)(nil), // 47: looprpc.EasyAssetAutoloopParams + (*LiquidityRule)(nil), // 48: looprpc.LiquidityRule + (*SetLiquidityParamsRequest)(nil), // 49: looprpc.SetLiquidityParamsRequest + (*SetLiquidityParamsResponse)(nil), // 50: looprpc.SetLiquidityParamsResponse + (*SuggestSwapsRequest)(nil), // 51: looprpc.SuggestSwapsRequest + (*Disqualified)(nil), // 52: looprpc.Disqualified + (*SuggestSwapsResponse)(nil), // 53: looprpc.SuggestSwapsResponse + (*AbandonSwapRequest)(nil), // 54: looprpc.AbandonSwapRequest + (*AbandonSwapResponse)(nil), // 55: looprpc.AbandonSwapResponse + (*ListReservationsRequest)(nil), // 56: looprpc.ListReservationsRequest + (*ListReservationsResponse)(nil), // 57: looprpc.ListReservationsResponse + (*ClientReservation)(nil), // 58: looprpc.ClientReservation + (*InstantOutRequest)(nil), // 59: looprpc.InstantOutRequest + (*InstantOutResponse)(nil), // 60: looprpc.InstantOutResponse + (*InstantOutQuoteRequest)(nil), // 61: looprpc.InstantOutQuoteRequest + (*InstantOutQuoteResponse)(nil), // 62: looprpc.InstantOutQuoteResponse + (*ListInstantOutsRequest)(nil), // 63: looprpc.ListInstantOutsRequest + (*ListInstantOutsResponse)(nil), // 64: looprpc.ListInstantOutsResponse + (*InstantOut)(nil), // 65: looprpc.InstantOut + (*NewStaticAddressRequest)(nil), // 66: looprpc.NewStaticAddressRequest + (*NewStaticAddressResponse)(nil), // 67: looprpc.NewStaticAddressResponse + (*ListUnspentDepositsRequest)(nil), // 68: looprpc.ListUnspentDepositsRequest + (*ListUnspentDepositsResponse)(nil), // 69: looprpc.ListUnspentDepositsResponse + (*Utxo)(nil), // 70: looprpc.Utxo + (*WithdrawDepositsRequest)(nil), // 71: looprpc.WithdrawDepositsRequest + (*WithdrawDepositsResponse)(nil), // 72: looprpc.WithdrawDepositsResponse + (*ListStaticAddressDepositsRequest)(nil), // 73: looprpc.ListStaticAddressDepositsRequest + (*ListStaticAddressDepositsResponse)(nil), // 74: looprpc.ListStaticAddressDepositsResponse + (*ListStaticAddressWithdrawalRequest)(nil), // 75: looprpc.ListStaticAddressWithdrawalRequest + (*ListStaticAddressWithdrawalResponse)(nil), // 76: looprpc.ListStaticAddressWithdrawalResponse + (*ListStaticAddressSwapsRequest)(nil), // 77: looprpc.ListStaticAddressSwapsRequest + (*ListStaticAddressSwapsResponse)(nil), // 78: looprpc.ListStaticAddressSwapsResponse + (*StaticAddressSummaryRequest)(nil), // 79: looprpc.StaticAddressSummaryRequest + (*StaticAddressSummaryResponse)(nil), // 80: looprpc.StaticAddressSummaryResponse + (*Deposit)(nil), // 81: looprpc.Deposit + (*StaticAddressWithdrawal)(nil), // 82: looprpc.StaticAddressWithdrawal + (*StaticAddressLoopInSwap)(nil), // 83: looprpc.StaticAddressLoopInSwap + (*StaticAddressLoopInRequest)(nil), // 84: looprpc.StaticAddressLoopInRequest + (*StaticAddressLoopInResponse)(nil), // 85: looprpc.StaticAddressLoopInResponse + (*AssetLoopOutRequest)(nil), // 86: looprpc.AssetLoopOutRequest + (*AssetRfqInfo)(nil), // 87: looprpc.AssetRfqInfo + (*FixedPoint)(nil), // 88: looprpc.FixedPoint + (*AssetLoopOutInfo)(nil), // 89: looprpc.AssetLoopOutInfo + nil, // 90: looprpc.LiquidityParameters.EasyAssetParamsEntry + (*lnrpc.OpenChannelRequest)(nil), // 91: lnrpc.OpenChannelRequest + (*swapserverrpc.RouteHint)(nil), // 92: looprpc.RouteHint + (*lnrpc.OutPoint)(nil), // 93: lnrpc.OutPoint } var file_client_proto_depIdxs = []int32{ - 89, // 0: looprpc.StaticOpenChannelRequest.open_channel_request:type_name -> lnrpc.OpenChannelRequest + 91, // 0: looprpc.StaticOpenChannelRequest.open_channel_request:type_name -> lnrpc.OpenChannelRequest 0, // 1: looprpc.LoopOutRequest.account_addr_type:type_name -> looprpc.AddressType - 84, // 2: looprpc.LoopOutRequest.asset_info:type_name -> looprpc.AssetLoopOutRequest - 85, // 3: looprpc.LoopOutRequest.asset_rfq_info:type_name -> looprpc.AssetRfqInfo - 90, // 4: looprpc.LoopInRequest.route_hints:type_name -> looprpc.RouteHint + 86, // 2: looprpc.LoopOutRequest.asset_info:type_name -> looprpc.AssetLoopOutRequest + 87, // 3: looprpc.LoopOutRequest.asset_rfq_info:type_name -> looprpc.AssetRfqInfo + 92, // 4: looprpc.LoopInRequest.route_hints:type_name -> looprpc.RouteHint 1, // 5: looprpc.SwapStatus.type:type_name -> looprpc.SwapType 2, // 6: looprpc.SwapStatus.state:type_name -> looprpc.SwapState 3, // 7: looprpc.SwapStatus.failure_reason:type_name -> looprpc.FailureReason - 87, // 8: looprpc.SwapStatus.asset_info:type_name -> looprpc.AssetLoopOutInfo + 89, // 8: looprpc.SwapStatus.asset_info:type_name -> looprpc.AssetLoopOutInfo 19, // 9: looprpc.ListSwapsRequest.list_swap_filter:type_name -> looprpc.ListSwapsFilter 8, // 10: looprpc.ListSwapsFilter.swap_type:type_name -> looprpc.ListSwapsFilter.SwapTypeFilter 17, // 11: looprpc.ListSwapsResponse.swaps:type_name -> looprpc.SwapStatus 23, // 12: looprpc.SweepHtlcResponse.not_requested:type_name -> looprpc.PublishNotRequested 24, // 13: looprpc.SweepHtlcResponse.published:type_name -> looprpc.PublishSucceeded 25, // 14: looprpc.SweepHtlcResponse.failed:type_name -> looprpc.PublishFailed - 90, // 15: looprpc.QuoteRequest.loop_in_route_hints:type_name -> looprpc.RouteHint - 84, // 16: looprpc.QuoteRequest.asset_info:type_name -> looprpc.AssetLoopOutRequest - 85, // 17: looprpc.OutQuoteResponse.asset_rfq_info:type_name -> looprpc.AssetRfqInfo - 90, // 18: looprpc.ProbeRequest.route_hints:type_name -> looprpc.RouteHint - 39, // 19: looprpc.TokensResponse.tokens:type_name -> looprpc.L402Token - 40, // 20: looprpc.GetInfoResponse.loop_out_stats:type_name -> looprpc.LoopStats - 40, // 21: looprpc.GetInfoResponse.loop_in_stats:type_name -> looprpc.LoopStats - 46, // 22: looprpc.LiquidityParameters.rules:type_name -> looprpc.LiquidityRule + 92, // 15: looprpc.QuoteRequest.loop_in_route_hints:type_name -> looprpc.RouteHint + 86, // 16: looprpc.QuoteRequest.asset_info:type_name -> looprpc.AssetLoopOutRequest + 87, // 17: looprpc.OutQuoteResponse.asset_rfq_info:type_name -> looprpc.AssetRfqInfo + 92, // 18: looprpc.ProbeRequest.route_hints:type_name -> looprpc.RouteHint + 41, // 19: looprpc.TokensResponse.tokens:type_name -> looprpc.L402Token + 42, // 20: looprpc.GetInfoResponse.loop_out_stats:type_name -> looprpc.LoopStats + 42, // 21: looprpc.GetInfoResponse.loop_in_stats:type_name -> looprpc.LoopStats + 48, // 22: looprpc.LiquidityParameters.rules:type_name -> looprpc.LiquidityRule 0, // 23: looprpc.LiquidityParameters.account_addr_type:type_name -> looprpc.AddressType - 88, // 24: looprpc.LiquidityParameters.easy_asset_params:type_name -> looprpc.LiquidityParameters.EasyAssetParamsEntry + 90, // 24: looprpc.LiquidityParameters.easy_asset_params:type_name -> looprpc.LiquidityParameters.EasyAssetParamsEntry 1, // 25: looprpc.LiquidityRule.swap_type:type_name -> looprpc.SwapType 4, // 26: looprpc.LiquidityRule.type:type_name -> looprpc.LiquidityRuleType - 44, // 27: looprpc.SetLiquidityParamsRequest.parameters:type_name -> looprpc.LiquidityParameters + 46, // 27: looprpc.SetLiquidityParamsRequest.parameters:type_name -> looprpc.LiquidityParameters 5, // 28: looprpc.Disqualified.reason:type_name -> looprpc.AutoReason 13, // 29: looprpc.SuggestSwapsResponse.loop_out:type_name -> looprpc.LoopOutRequest 14, // 30: looprpc.SuggestSwapsResponse.loop_in:type_name -> looprpc.LoopInRequest - 50, // 31: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified - 56, // 32: looprpc.ListReservationsResponse.reservations:type_name -> looprpc.ClientReservation - 63, // 33: looprpc.ListInstantOutsResponse.swaps:type_name -> looprpc.InstantOut - 68, // 34: looprpc.ListUnspentDepositsResponse.utxos:type_name -> looprpc.Utxo - 91, // 35: looprpc.WithdrawDepositsRequest.outpoints:type_name -> lnrpc.OutPoint + 52, // 31: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified + 58, // 32: looprpc.ListReservationsResponse.reservations:type_name -> looprpc.ClientReservation + 65, // 33: looprpc.ListInstantOutsResponse.swaps:type_name -> looprpc.InstantOut + 70, // 34: looprpc.ListUnspentDepositsResponse.utxos:type_name -> looprpc.Utxo + 93, // 35: looprpc.WithdrawDepositsRequest.outpoints:type_name -> lnrpc.OutPoint 6, // 36: looprpc.ListStaticAddressDepositsRequest.state_filter:type_name -> looprpc.DepositState - 79, // 37: looprpc.ListStaticAddressDepositsResponse.filtered_deposits:type_name -> looprpc.Deposit - 80, // 38: looprpc.ListStaticAddressWithdrawalResponse.withdrawals:type_name -> looprpc.StaticAddressWithdrawal - 81, // 39: looprpc.ListStaticAddressSwapsResponse.swaps:type_name -> looprpc.StaticAddressLoopInSwap + 81, // 37: looprpc.ListStaticAddressDepositsResponse.filtered_deposits:type_name -> looprpc.Deposit + 82, // 38: looprpc.ListStaticAddressWithdrawalResponse.withdrawals:type_name -> looprpc.StaticAddressWithdrawal + 83, // 39: looprpc.ListStaticAddressSwapsResponse.swaps:type_name -> looprpc.StaticAddressLoopInSwap 6, // 40: looprpc.Deposit.state:type_name -> looprpc.DepositState - 79, // 41: looprpc.StaticAddressWithdrawal.deposits:type_name -> looprpc.Deposit + 81, // 41: looprpc.StaticAddressWithdrawal.deposits:type_name -> looprpc.Deposit 7, // 42: looprpc.StaticAddressLoopInSwap.state:type_name -> looprpc.StaticAddressLoopInSwapState - 79, // 43: looprpc.StaticAddressLoopInSwap.deposits:type_name -> looprpc.Deposit - 90, // 44: looprpc.StaticAddressLoopInRequest.route_hints:type_name -> looprpc.RouteHint - 79, // 45: looprpc.StaticAddressLoopInResponse.used_deposits:type_name -> looprpc.Deposit - 86, // 46: looprpc.AssetRfqInfo.prepay_asset_rate:type_name -> looprpc.FixedPoint - 86, // 47: looprpc.AssetRfqInfo.swap_asset_rate:type_name -> looprpc.FixedPoint - 45, // 48: looprpc.LiquidityParameters.EasyAssetParamsEntry.value:type_name -> looprpc.EasyAssetAutoloopParams + 81, // 43: looprpc.StaticAddressLoopInSwap.deposits:type_name -> looprpc.Deposit + 92, // 44: looprpc.StaticAddressLoopInRequest.route_hints:type_name -> looprpc.RouteHint + 81, // 45: looprpc.StaticAddressLoopInResponse.used_deposits:type_name -> looprpc.Deposit + 88, // 46: looprpc.AssetRfqInfo.prepay_asset_rate:type_name -> looprpc.FixedPoint + 88, // 47: looprpc.AssetRfqInfo.swap_asset_rate:type_name -> looprpc.FixedPoint + 47, // 48: looprpc.LiquidityParameters.EasyAssetParamsEntry.value:type_name -> looprpc.EasyAssetAutoloopParams 13, // 49: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest 14, // 50: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest 16, // 51: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest 18, // 52: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest 21, // 53: looprpc.SwapClient.SweepHtlc:input_type -> looprpc.SweepHtlcRequest 26, // 54: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest - 52, // 55: looprpc.SwapClient.AbandonSwap:input_type -> looprpc.AbandonSwapRequest + 54, // 55: looprpc.SwapClient.AbandonSwap:input_type -> looprpc.AbandonSwapRequest 27, // 56: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest 30, // 57: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest 27, // 58: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest @@ -7242,59 +7394,61 @@ var file_client_proto_depIdxs = []int32{ 35, // 61: looprpc.SwapClient.GetL402Tokens:input_type -> looprpc.TokensRequest 35, // 62: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest 37, // 63: looprpc.SwapClient.FetchL402Token:input_type -> looprpc.FetchL402TokenRequest - 41, // 64: looprpc.SwapClient.GetInfo:input_type -> looprpc.GetInfoRequest - 11, // 65: looprpc.SwapClient.StopDaemon:input_type -> looprpc.StopDaemonRequest - 43, // 66: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest - 47, // 67: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest - 49, // 68: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest - 54, // 69: looprpc.SwapClient.ListReservations:input_type -> looprpc.ListReservationsRequest - 57, // 70: looprpc.SwapClient.InstantOut:input_type -> looprpc.InstantOutRequest - 59, // 71: looprpc.SwapClient.InstantOutQuote:input_type -> looprpc.InstantOutQuoteRequest - 61, // 72: looprpc.SwapClient.ListInstantOuts:input_type -> looprpc.ListInstantOutsRequest - 64, // 73: looprpc.SwapClient.NewStaticAddress:input_type -> looprpc.NewStaticAddressRequest - 66, // 74: looprpc.SwapClient.ListUnspentDeposits:input_type -> looprpc.ListUnspentDepositsRequest - 69, // 75: looprpc.SwapClient.WithdrawDeposits:input_type -> looprpc.WithdrawDepositsRequest - 71, // 76: looprpc.SwapClient.ListStaticAddressDeposits:input_type -> looprpc.ListStaticAddressDepositsRequest - 73, // 77: looprpc.SwapClient.ListStaticAddressWithdrawals:input_type -> looprpc.ListStaticAddressWithdrawalRequest - 75, // 78: looprpc.SwapClient.ListStaticAddressSwaps:input_type -> looprpc.ListStaticAddressSwapsRequest - 77, // 79: looprpc.SwapClient.GetStaticAddressSummary:input_type -> looprpc.StaticAddressSummaryRequest - 82, // 80: looprpc.SwapClient.StaticAddressLoopIn:input_type -> looprpc.StaticAddressLoopInRequest - 9, // 81: looprpc.SwapClient.StaticOpenChannel:input_type -> looprpc.StaticOpenChannelRequest - 15, // 82: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse - 15, // 83: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse - 17, // 84: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus - 20, // 85: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse - 22, // 86: looprpc.SwapClient.SweepHtlc:output_type -> looprpc.SweepHtlcResponse - 17, // 87: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus - 53, // 88: looprpc.SwapClient.AbandonSwap:output_type -> looprpc.AbandonSwapResponse - 29, // 89: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse - 32, // 90: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse - 28, // 91: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse - 31, // 92: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse - 34, // 93: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse - 36, // 94: looprpc.SwapClient.GetL402Tokens:output_type -> looprpc.TokensResponse - 36, // 95: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse - 38, // 96: looprpc.SwapClient.FetchL402Token:output_type -> looprpc.FetchL402TokenResponse - 42, // 97: looprpc.SwapClient.GetInfo:output_type -> looprpc.GetInfoResponse - 12, // 98: looprpc.SwapClient.StopDaemon:output_type -> looprpc.StopDaemonResponse - 44, // 99: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters - 48, // 100: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse - 51, // 101: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse - 55, // 102: looprpc.SwapClient.ListReservations:output_type -> looprpc.ListReservationsResponse - 58, // 103: looprpc.SwapClient.InstantOut:output_type -> looprpc.InstantOutResponse - 60, // 104: looprpc.SwapClient.InstantOutQuote:output_type -> looprpc.InstantOutQuoteResponse - 62, // 105: looprpc.SwapClient.ListInstantOuts:output_type -> looprpc.ListInstantOutsResponse - 65, // 106: looprpc.SwapClient.NewStaticAddress:output_type -> looprpc.NewStaticAddressResponse - 67, // 107: looprpc.SwapClient.ListUnspentDeposits:output_type -> looprpc.ListUnspentDepositsResponse - 70, // 108: looprpc.SwapClient.WithdrawDeposits:output_type -> looprpc.WithdrawDepositsResponse - 72, // 109: looprpc.SwapClient.ListStaticAddressDeposits:output_type -> looprpc.ListStaticAddressDepositsResponse - 74, // 110: looprpc.SwapClient.ListStaticAddressWithdrawals:output_type -> looprpc.ListStaticAddressWithdrawalResponse - 76, // 111: looprpc.SwapClient.ListStaticAddressSwaps:output_type -> looprpc.ListStaticAddressSwapsResponse - 78, // 112: looprpc.SwapClient.GetStaticAddressSummary:output_type -> looprpc.StaticAddressSummaryResponse - 83, // 113: looprpc.SwapClient.StaticAddressLoopIn:output_type -> looprpc.StaticAddressLoopInResponse - 10, // 114: looprpc.SwapClient.StaticOpenChannel:output_type -> looprpc.StaticOpenChannelResponse - 82, // [82:115] is the sub-list for method output_type - 49, // [49:82] is the sub-list for method input_type + 39, // 64: looprpc.SwapClient.Recover:input_type -> looprpc.RecoverRequest + 43, // 65: looprpc.SwapClient.GetInfo:input_type -> looprpc.GetInfoRequest + 11, // 66: looprpc.SwapClient.StopDaemon:input_type -> looprpc.StopDaemonRequest + 45, // 67: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest + 49, // 68: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest + 51, // 69: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest + 56, // 70: looprpc.SwapClient.ListReservations:input_type -> looprpc.ListReservationsRequest + 59, // 71: looprpc.SwapClient.InstantOut:input_type -> looprpc.InstantOutRequest + 61, // 72: looprpc.SwapClient.InstantOutQuote:input_type -> looprpc.InstantOutQuoteRequest + 63, // 73: looprpc.SwapClient.ListInstantOuts:input_type -> looprpc.ListInstantOutsRequest + 66, // 74: looprpc.SwapClient.NewStaticAddress:input_type -> looprpc.NewStaticAddressRequest + 68, // 75: looprpc.SwapClient.ListUnspentDeposits:input_type -> looprpc.ListUnspentDepositsRequest + 71, // 76: looprpc.SwapClient.WithdrawDeposits:input_type -> looprpc.WithdrawDepositsRequest + 73, // 77: looprpc.SwapClient.ListStaticAddressDeposits:input_type -> looprpc.ListStaticAddressDepositsRequest + 75, // 78: looprpc.SwapClient.ListStaticAddressWithdrawals:input_type -> looprpc.ListStaticAddressWithdrawalRequest + 77, // 79: looprpc.SwapClient.ListStaticAddressSwaps:input_type -> looprpc.ListStaticAddressSwapsRequest + 79, // 80: looprpc.SwapClient.GetStaticAddressSummary:input_type -> looprpc.StaticAddressSummaryRequest + 84, // 81: looprpc.SwapClient.StaticAddressLoopIn:input_type -> looprpc.StaticAddressLoopInRequest + 9, // 82: looprpc.SwapClient.StaticOpenChannel:input_type -> looprpc.StaticOpenChannelRequest + 15, // 83: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse + 15, // 84: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse + 17, // 85: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus + 20, // 86: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse + 22, // 87: looprpc.SwapClient.SweepHtlc:output_type -> looprpc.SweepHtlcResponse + 17, // 88: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus + 55, // 89: looprpc.SwapClient.AbandonSwap:output_type -> looprpc.AbandonSwapResponse + 29, // 90: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse + 32, // 91: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse + 28, // 92: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse + 31, // 93: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse + 34, // 94: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse + 36, // 95: looprpc.SwapClient.GetL402Tokens:output_type -> looprpc.TokensResponse + 36, // 96: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse + 38, // 97: looprpc.SwapClient.FetchL402Token:output_type -> looprpc.FetchL402TokenResponse + 40, // 98: looprpc.SwapClient.Recover:output_type -> looprpc.RecoverResponse + 44, // 99: looprpc.SwapClient.GetInfo:output_type -> looprpc.GetInfoResponse + 12, // 100: looprpc.SwapClient.StopDaemon:output_type -> looprpc.StopDaemonResponse + 46, // 101: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters + 50, // 102: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse + 53, // 103: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse + 57, // 104: looprpc.SwapClient.ListReservations:output_type -> looprpc.ListReservationsResponse + 60, // 105: looprpc.SwapClient.InstantOut:output_type -> looprpc.InstantOutResponse + 62, // 106: looprpc.SwapClient.InstantOutQuote:output_type -> looprpc.InstantOutQuoteResponse + 64, // 107: looprpc.SwapClient.ListInstantOuts:output_type -> looprpc.ListInstantOutsResponse + 67, // 108: looprpc.SwapClient.NewStaticAddress:output_type -> looprpc.NewStaticAddressResponse + 69, // 109: looprpc.SwapClient.ListUnspentDeposits:output_type -> looprpc.ListUnspentDepositsResponse + 72, // 110: looprpc.SwapClient.WithdrawDeposits:output_type -> looprpc.WithdrawDepositsResponse + 74, // 111: looprpc.SwapClient.ListStaticAddressDeposits:output_type -> looprpc.ListStaticAddressDepositsResponse + 76, // 112: looprpc.SwapClient.ListStaticAddressWithdrawals:output_type -> looprpc.ListStaticAddressWithdrawalResponse + 78, // 113: looprpc.SwapClient.ListStaticAddressSwaps:output_type -> looprpc.ListStaticAddressSwapsResponse + 80, // 114: looprpc.SwapClient.GetStaticAddressSummary:output_type -> looprpc.StaticAddressSummaryResponse + 85, // 115: looprpc.SwapClient.StaticAddressLoopIn:output_type -> looprpc.StaticAddressLoopInResponse + 10, // 116: looprpc.SwapClient.StaticOpenChannel:output_type -> looprpc.StaticOpenChannelResponse + 83, // [83:117] is the sub-list for method output_type + 49, // [49:83] is the sub-list for method input_type 49, // [49:49] is the sub-list for extension type_name 49, // [49:49] is the sub-list for extension extendee 0, // [0:49] is the sub-list for field type_name @@ -7316,7 +7470,7 @@ func file_client_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_client_proto_rawDesc), len(file_client_proto_rawDesc)), NumEnums: 9, - NumMessages: 80, + NumMessages: 82, NumExtensions: 0, NumServices: 1, }, diff --git a/looprpc/client.pb.gw.go b/looprpc/client.pb.gw.go index c2ef63f88..f80f4ce6b 100644 --- a/looprpc/client.pb.gw.go +++ b/looprpc/client.pb.gw.go @@ -479,6 +479,32 @@ func local_request_SwapClient_GetL402Tokens_1(ctx context.Context, marshaler run } +func request_SwapClient_Recover_0(ctx context.Context, marshaler runtime.Marshaler, client SwapClientClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq RecoverRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.Recover(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_SwapClient_Recover_0(ctx context.Context, marshaler runtime.Marshaler, server SwapClientServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq RecoverRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.Recover(ctx, &protoReq) + return msg, metadata, err + +} + func request_SwapClient_GetInfo_0(ctx context.Context, marshaler runtime.Marshaler, client SwapClientClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq GetInfoRequest var metadata runtime.ServerMetadata @@ -1175,6 +1201,31 @@ func RegisterSwapClientHandlerServer(ctx context.Context, mux *runtime.ServeMux, }) + mux.Handle("POST", pattern_SwapClient_Recover_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/looprpc.SwapClient/Recover", runtime.WithHTTPPathPattern("/v1/recover")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_SwapClient_Recover_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SwapClient_Recover_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_SwapClient_GetInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -1905,6 +1956,28 @@ func RegisterSwapClientHandlerClient(ctx context.Context, mux *runtime.ServeMux, }) + mux.Handle("POST", pattern_SwapClient_Recover_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/looprpc.SwapClient/Recover", runtime.WithHTTPPathPattern("/v1/recover")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_SwapClient_Recover_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SwapClient_Recover_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_SwapClient_GetInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -2307,6 +2380,8 @@ var ( pattern_SwapClient_GetL402Tokens_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "lsat", "tokens"}, "")) + pattern_SwapClient_Recover_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "recover"}, "")) + pattern_SwapClient_GetInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "loop", "info"}, "")) pattern_SwapClient_StopDaemon_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "daemon", "stop"}, "")) @@ -2367,6 +2442,8 @@ var ( forward_SwapClient_GetL402Tokens_1 = runtime.ForwardResponseMessage + forward_SwapClient_Recover_0 = runtime.ForwardResponseMessage + forward_SwapClient_GetInfo_0 = runtime.ForwardResponseMessage forward_SwapClient_StopDaemon_0 = runtime.ForwardResponseMessage diff --git a/looprpc/client.proto b/looprpc/client.proto index cf14ffa0a..dcf1aed48 100644 --- a/looprpc/client.proto +++ b/looprpc/client.proto @@ -102,6 +102,12 @@ service SwapClient { */ rpc FetchL402Token (FetchL402TokenRequest) returns (FetchL402TokenResponse); + /* loop: `recover` + Recover restores the local static-address and L402 state from an encrypted + local backup file. + */ + rpc Recover (RecoverRequest) returns (RecoverResponse); + /* loop: `getinfo` GetInfo gets basic information about the loop daemon. */ @@ -1060,6 +1066,48 @@ message FetchL402TokenRequest { message FetchL402TokenResponse { } +message RecoverRequest { + /* + Optional path to the encrypted backup file. If omitted, loopd restores from + the most recent immutable L402 recovery backup in the active network data + directory. + */ + string backup_file = 1; +} + +message RecoverResponse { + /* + The backup file that was restored. + */ + string backup_file = 1; + + /* + Whether a paid L402 token was restored into the local token store. + */ + bool restored_l402 = 2; + + /* + Whether static-address state was restored into loopd and lnd. + */ + bool restored_static_address = 3; + + /* + The restored static address, if any. + */ + string static_address = 4; + + /* + The number of deposits found during best-effort reconciliation. + */ + uint32 num_deposits_found = 5; + + /* + Best-effort deposit reconciliation error text, if reconciliation failed + after state restore completed. + */ + string deposit_reconciliation_error = 6; +} + message L402Token { /* The base macaroon that was baked by the auth server. diff --git a/looprpc/client.swagger.json b/looprpc/client.swagger.json index 3d75d15da..76502fa24 100644 --- a/looprpc/client.swagger.json +++ b/looprpc/client.swagger.json @@ -868,6 +868,39 @@ ] } }, + "/v1/recover": { + "post": { + "summary": "loop: `recover`\nRecover restores the local static-address and L402 state from an encrypted\nlocal backup file.", + "operationId": "SwapClient_Recover", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/looprpcRecoverResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/looprpcRecoverRequest" + } + } + ], + "tags": [ + "SwapClient" + ] + } + }, "/v1/staticaddr": { "post": { "summary": "loop: `static newstaticaddress`\nNewStaticAddress requests a new static address for loop-ins from the server.", @@ -2599,6 +2632,45 @@ "type": "object", "description": "PublishSucceeded is returned by SweepHtlc if publishing was requested in\nSweepHtlcRequest and it succeeded." }, + "looprpcRecoverRequest": { + "type": "object", + "properties": { + "backup_file": { + "type": "string", + "description": "Optional path to the encrypted backup file. If omitted, loopd restores from\nthe most recent immutable L402 recovery backup in the active network data\ndirectory." + } + } + }, + "looprpcRecoverResponse": { + "type": "object", + "properties": { + "backup_file": { + "type": "string", + "description": "The backup file that was restored." + }, + "restored_l402": { + "type": "boolean", + "description": "Whether a paid L402 token was restored into the local token store." + }, + "restored_static_address": { + "type": "boolean", + "description": "Whether static-address state was restored into loopd and lnd." + }, + "static_address": { + "type": "string", + "description": "The restored static address, if any." + }, + "num_deposits_found": { + "type": "integer", + "format": "int64", + "description": "The number of deposits found during best-effort reconciliation." + }, + "deposit_reconciliation_error": { + "type": "string", + "description": "Best-effort deposit reconciliation error text, if reconciliation failed\nafter state restore completed." + } + } + }, "looprpcRouteHint": { "type": "object", "properties": { diff --git a/looprpc/client.yaml b/looprpc/client.yaml index 5213afe4d..88c038b89 100644 --- a/looprpc/client.yaml +++ b/looprpc/client.yaml @@ -33,6 +33,9 @@ http: get: "/v1/l402/tokens" additional_bindings: - get: "/v1/lsat/tokens" + - selector: looprpc.SwapClient.Recover + post: "/v1/recover" + body: "*" - selector: looprpc.SwapClient.GetLiquidityParams get: "/v1/liquidity/params" - selector: looprpc.SwapClient.SetLiquidityParams diff --git a/looprpc/client_grpc.pb.go b/looprpc/client_grpc.pb.go index b03cc9e87..47fdbd0ef 100644 --- a/looprpc/client_grpc.pb.go +++ b/looprpc/client_grpc.pb.go @@ -76,6 +76,10 @@ type SwapClientClient interface { // FetchL402Token fetches an L402 token from the server, this is required in // order to receive reservation notifications from the server. FetchL402Token(ctx context.Context, in *FetchL402TokenRequest, opts ...grpc.CallOption) (*FetchL402TokenResponse, error) + // loop: `recover` + // Recover restores the local static-address and L402 state from an encrypted + // local backup file. + Recover(ctx context.Context, in *RecoverRequest, opts ...grpc.CallOption) (*RecoverResponse, error) // loop: `getinfo` // GetInfo gets basic information about the loop daemon. GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, error) @@ -312,6 +316,15 @@ func (c *swapClientClient) FetchL402Token(ctx context.Context, in *FetchL402Toke return out, nil } +func (c *swapClientClient) Recover(ctx context.Context, in *RecoverRequest, opts ...grpc.CallOption) (*RecoverResponse, error) { + out := new(RecoverResponse) + err := c.cc.Invoke(ctx, "/looprpc.SwapClient/Recover", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *swapClientClient) GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, error) { out := new(GetInfoResponse) err := c.cc.Invoke(ctx, "/looprpc.SwapClient/GetInfo", in, out, opts...) @@ -536,6 +549,10 @@ type SwapClientServer interface { // FetchL402Token fetches an L402 token from the server, this is required in // order to receive reservation notifications from the server. FetchL402Token(context.Context, *FetchL402TokenRequest) (*FetchL402TokenResponse, error) + // loop: `recover` + // Recover restores the local static-address and L402 state from an encrypted + // local backup file. + Recover(context.Context, *RecoverRequest) (*RecoverResponse, error) // loop: `getinfo` // GetInfo gets basic information about the loop daemon. GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error) @@ -656,6 +673,9 @@ func (UnimplementedSwapClientServer) GetLsatTokens(context.Context, *TokensReque func (UnimplementedSwapClientServer) FetchL402Token(context.Context, *FetchL402TokenRequest) (*FetchL402TokenResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method FetchL402Token not implemented") } +func (UnimplementedSwapClientServer) Recover(context.Context, *RecoverRequest) (*RecoverResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Recover not implemented") +} func (UnimplementedSwapClientServer) GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetInfo not implemented") } @@ -996,6 +1016,24 @@ func _SwapClient_FetchL402Token_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } +func _SwapClient_Recover_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RecoverRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapClientServer).Recover(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.SwapClient/Recover", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapClientServer).Recover(ctx, req.(*RecoverRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _SwapClient_GetInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetInfoRequest) if err := dec(in); err != nil { @@ -1383,6 +1421,10 @@ var SwapClient_ServiceDesc = grpc.ServiceDesc{ MethodName: "FetchL402Token", Handler: _SwapClient_FetchL402Token_Handler, }, + { + MethodName: "Recover", + Handler: _SwapClient_Recover_Handler, + }, { MethodName: "GetInfo", Handler: _SwapClient_GetInfo_Handler, diff --git a/looprpc/perms.go b/looprpc/perms.go index 6ceff7189..d5820bdd3 100644 --- a/looprpc/perms.go +++ b/looprpc/perms.go @@ -151,6 +151,13 @@ var RequiredPermissions = map[string][]bakery.Op{ Entity: "auth", Action: "write", }}, + "/looprpc.SwapClient/Recover": {{ + Entity: "auth", + Action: "write", + }, { + Entity: "loop", + Action: "in", + }}, "/looprpc.SwapClient/SuggestSwaps": {{ Entity: "suggestions", Action: "read", diff --git a/looprpc/swapclient.pb.json.go b/looprpc/swapclient.pb.json.go index ef1297dc3..a168d8152 100644 --- a/looprpc/swapclient.pb.json.go +++ b/looprpc/swapclient.pb.json.go @@ -413,6 +413,31 @@ func RegisterSwapClientJSONCallbacks(registry map[string]func(ctx context.Contex callback(string(respBytes), nil) } + registry["looprpc.SwapClient.Recover"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &RecoverRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewSwapClientClient(conn) + resp, err := client.Recover(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + registry["looprpc.SwapClient.GetInfo"] = func(ctx context.Context, conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { From 06efa4c2a9aba26894bf97785f6c80f4bed006ce Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 14 Apr 2026 12:03:07 +0200 Subject: [PATCH 4/6] loopd: wire recovery service into daemon startup Register a recovery gRPC endpoint, attach the new recovery package to the swap client server, and have daemon startup write an encrypted backup file whenever recoverable static-address or l402 state already exists locally. --- cmd/loop/staticaddr.go | 39 ++++++++------------------------------ loopd/daemon.go | 38 ++++++++++++++++++++++++++++++++++++- loopd/swapclient_server.go | 28 +++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 32 deletions(-) diff --git a/cmd/loop/staticaddr.go b/cmd/loop/staticaddr.go index fc36597e4..35806631a 100644 --- a/cmd/loop/staticaddr.go +++ b/cmd/loop/staticaddr.go @@ -40,13 +40,15 @@ var staticAddressCommands = &cli.Command{ var newStaticAddressCommand = &cli.Command{ Name: "new", Aliases: []string{"n"}, - Usage: "Create a new static loop in address.", + Usage: "Return the static loop in address.", Description: ` - Requests a new static loop in address from the server. Funds that are - sent to this address will be locked by a 2:2 multisig between us and the - loop server, or a timeout path that we can sweep once it opens up. The - funds can either be cooperatively spent with a signature from the server - or looped in. + Returns the current static loop in address. On a fresh installation loopd + initializes the current static-address generation during startup. If the + address is still missing, this call will create it on demand. Funds sent + to the address will be locked by a 2:2 multisig between us and the loop + server, or a timeout path that we can sweep once it opens up. The funds + can either be cooperatively spent with a signature from the server or + looped in. `, Action: newStaticAddress, } @@ -56,11 +58,6 @@ func newStaticAddress(ctx context.Context, cmd *cli.Command) error { return showCommandHelp(ctx, cmd) } - err := displayNewAddressWarning() - if err != nil { - return err - } - client, cleanup, err := getClient(cmd) if err != nil { return err @@ -668,23 +665,3 @@ func depositsToOutpoints(deposits []*looprpc.Deposit) []string { return outpoints } - -func displayNewAddressWarning() error { - fmt.Printf("\nWARNING: Be aware that loosing your l402.token file in " + - ".loop under your home directory will take your ability to " + - "spend funds sent to the static address via loop-ins or " + - "withdrawals. You will have to wait until the deposit " + - "expires and your loop client sweeps the funds back to your " + - "lnd wallet. The deposit expiry could be months in the " + - "future.\n") - - fmt.Printf("\nCONTINUE WITH NEW ADDRESS? (y/n): ") - - var answer string - fmt.Scanln(&answer) - if answer == "y" { - return nil - } - - return errors.New("new address creation canceled") -} diff --git a/loopd/daemon.go b/loopd/daemon.go index 880e19621..ee6e215ed 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -22,6 +22,7 @@ import ( "github.com/lightninglabs/loop/loopdb" loop_looprpc "github.com/lightninglabs/loop/looprpc" "github.com/lightninglabs/loop/notifications" + "github.com/lightninglabs/loop/recovery" "github.com/lightninglabs/loop/staticaddr/address" "github.com/lightninglabs/loop/staticaddr/deposit" "github.com/lightninglabs/loop/staticaddr/loopin" @@ -577,13 +578,16 @@ func (d *Daemon) initialize(withMacaroonService bool) error { withdrawalManager *withdraw.Manager openChannelManager *openchannel.Manager staticLoopInManager *loopin.Manager + recoveryService *recovery.Service ) // Static address manager setup. staticAddressStore := address.NewSqlStore(baseDb) addrCfg := &address.ManagerConfig{ AddressClient: staticAddressClient, - FetchL402: swapClient.Server.FetchL402, + FetchL402: func(ctx context.Context) error { + return swapClient.Server.FetchL402(ctx) + }, Store: staticAddressStore, WalletKit: d.lnd.WalletKit, ChainParams: d.lnd.ChainParams, @@ -689,6 +693,37 @@ func (d *Daemon) initialize(withMacaroonService bool) error { return fmt.Errorf("unable to create loop-in manager: %w", err) } + recoveryService = recovery.NewService( + d.cfg.DataDir, d.cfg.Network, d.lnd.Signer, d.lnd.WalletKit, + staticAddressManager, depositManager, + ) + + restoreResult, restoredFromBackup, err := + recoveryService.RestoreLatestOnFreshInstall(d.mainCtx) + if err != nil { + return fmt.Errorf("unable to restore latest recovery "+ + "backup on fresh install: %w", err) + } + if restoredFromBackup { + infof("Restored fresh install from encrypted recovery "+ + "backup %s", restoreResult.BackupFile) + } else { + _, _, err = staticAddressManager.NewAddress(d.mainCtx) + if err != nil { + return fmt.Errorf("unable to initialize static address "+ + "generation: %w", err) + } + } + + backupFile, err := recoveryService.WriteBackup(d.mainCtx) + if err != nil { + return fmt.Errorf("unable to write backup file: %w", err) + } + if backupFile != "" { + infof("Wrote encrypted recovery backup to %s after "+ + "initializing the current L402 generation", backupFile) + } + var ( reservationManager *reservation.Manager instantOutManager *instantout.Manager @@ -753,6 +788,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error { staticLoopInManager: staticLoopInManager, openChannelManager: openChannelManager, assetClient: d.assetClient, + recoveryService: recoveryService, stopDaemon: d.Stop, } diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index 62187d3e5..3c48f928b 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -29,6 +29,7 @@ import ( "github.com/lightninglabs/loop/liquidity" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/looprpc" + "github.com/lightninglabs/loop/recovery" "github.com/lightninglabs/loop/staticaddr/address" "github.com/lightninglabs/loop/staticaddr/deposit" "github.com/lightninglabs/loop/staticaddr/loopin" @@ -101,6 +102,7 @@ type swapClientServer struct { staticLoopInManager *loopin.Manager openChannelManager *openchannel.Manager assetClient *assets.TapdClient + recoveryService *recovery.Service swaps map[lntypes.Hash]loop.SwapInfo subscribers map[int]chan<- any statusChan chan loop.SwapInfo @@ -1268,6 +1270,32 @@ func (s *swapClientServer) FetchL402Token(ctx context.Context, return &looprpc.FetchL402TokenResponse{}, nil } +// Recover restores the local paid L402 token material and static-address state +// from an encrypted backup file. +func (s *swapClientServer) Recover(ctx context.Context, + req *looprpc.RecoverRequest) (*looprpc.RecoverResponse, error) { + + if s.recoveryService == nil { + return nil, status.Error( + codes.Unavailable, "recovery service not configured", + ) + } + + result, err := s.recoveryService.Restore(ctx, req.GetBackupFile()) + if err != nil { + return nil, err + } + + return &looprpc.RecoverResponse{ + BackupFile: result.BackupFile, + RestoredL402: result.RestoredL402, + RestoredStaticAddress: result.RestoredStaticAddress, + StaticAddress: result.StaticAddress, + NumDepositsFound: uint32(result.NumDepositsFound), + DepositReconciliationError: result.DepositReconciliationError, + }, nil +} + // GetInfo returns basic information about the loop daemon and details to swaps // from the swap store. func (s *swapClientServer) GetInfo(ctx context.Context, From ccf79381d4fb8b3a150694d5a5a8dc558f93a762 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 14 Apr 2026 12:03:11 +0200 Subject: [PATCH 5/6] cmd/loop: add recover command for local backups Expose the new recovery flow through loop-cli with a dedicated recover command that calls loopd over the existing RPC connection and optionally accepts a custom backup file path. --- cmd/loop/main.go | 3 ++- cmd/loop/recover.go | 50 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 cmd/loop/recover.go diff --git a/cmd/loop/main.go b/cmd/loop/main.go index 77dc533ad..b3aae52ec 100644 --- a/cmd/loop/main.go +++ b/cmd/loop/main.go @@ -88,7 +88,8 @@ var ( monitorCommand, quoteCommand, listAuthCommand, fetchL402Command, listSwapsCommand, swapInfoCommand, getLiquidityParamsCommand, setLiquidityRuleCommand, suggestSwapCommand, setParamsCommand, - getInfoCommand, abandonSwapCommand, reservationsCommands, + getInfoCommand, abandonSwapCommand, recoverCommand, + reservationsCommands, instantOutCommand, listInstantOutsCommand, stopCommand, printManCommand, printMarkdownCommand, } diff --git a/cmd/loop/recover.go b/cmd/loop/recover.go new file mode 100644 index 000000000..44cff551d --- /dev/null +++ b/cmd/loop/recover.go @@ -0,0 +1,50 @@ +package main + +import ( + "context" + + "github.com/lightninglabs/loop/looprpc" + "github.com/urfave/cli/v3" +) + +var recoverCommand = &cli.Command{ + Name: "recover", + Usage: "restore static address and L402 state from a local backup file", + Description: "Restores the local static-address state and L402 token " + + "from an encrypted backup file. If --backup_file is omitted, " + + "loopd will use the most recent immutable network-specific " + + "L402 backup file.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "backup_file", + Usage: "path to an encrypted backup file; if omitted, " + + "loopd uses the most recent immutable L402 " + + "backup file path", + }, + }, + Action: runRecover, +} + +func runRecover(ctx context.Context, cmd *cli.Command) error { + if cmd.NArg() > 0 { + return showCommandHelp(ctx, cmd) + } + + client, cleanup, err := getClient(cmd) + if err != nil { + return err + } + defer cleanup() + + resp, err := client.Recover( + ctx, &looprpc.RecoverRequest{ + BackupFile: cmd.String("backup_file"), + }, + ) + if err != nil { + return err + } + + printRespJSON(resp) + return nil +} From 5790124e0e423665721fc6c3792f423a110a7f27 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 14 Apr 2026 16:49:00 +0200 Subject: [PATCH 6/6] docs: update loop.md --- README.md | 17 ++++++ docs/loop.1 | 12 +++- docs/loop.md | 23 ++++++- staticaddr/deposit/manager.go | 23 +++++++ staticaddr/deposit/manager_test.go | 98 +++++++++++++++++++++++++----- 5 files changed, 156 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 0bba28ef8..fde02a4e4 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,23 @@ To execute a Loop In: loop in ``` +### Static Address Recovery +Loop now keeps one encrypted immutable recovery backup per paid L402 +generation in the active network data directory. Each backup contains the raw +paid `l402.token` plus the legacy static-address parameters needed to recreate +the current address locally. + +Existing static-address users get this backup backfilled on the next startup +with the upgraded client. Fresh installs materialize the initial paid-L402 and +legacy static-address generation during startup so the first backup can be +written immediately. + +The follow-up multi-address work is expected to keep this one-backup-per-L402 +model and extend it with deterministic root metadata for synthetic-xpub / +BIP328-style address derivation and scanning. See +[recovery/README.md](./recovery/README.md) for the full recovery model and the +planned multi-address outlook. + ### More info - [Loop FAQs](./docs/faqs.md) diff --git a/docs/loop.1 b/docs/loop.1 index 7d767cdff..df046fa3a 100644 --- a/docs/loop.1 +++ b/docs/loop.1 @@ -403,6 +403,16 @@ abandon a swap with a given swap hash .PP \fB--i_know_what_i_am_doing\fP: Specify this flag if you made sure that you read and understood the following consequence of applying this command. +.SH recover +.PP +restore static address and L402 state from a local backup file + +.PP +\fB--backup_file\fP="": path to an encrypted backup file; if omitted, loopd uses the most recent immutable L402 backup file path + +.PP +\fB--help, -h\fP: show help + .SH reservations, r .PP manage reservations @@ -456,7 +466,7 @@ perform on-chain to off-chain swaps using static addresses. .SS new, n .PP -Create a new static loop in address. +Return the static loop in address. .PP \fB--help, -h\fP: show help diff --git a/docs/loop.md b/docs/loop.md index 3a847e96c..6b2eddde4 100644 --- a/docs/loop.md +++ b/docs/loop.md @@ -418,6 +418,25 @@ The following flags are supported: | `--i_know_what_i_am_doing` | Specify this flag if you made sure that you read and understood the following consequence of applying this command | bool | `false` | | `--help` (`-h`) | show help | bool | `false` | +### `recover` command + +restore static address and L402 state from a local backup file. + +Restores the local static-address state and L402 token from an encrypted backup file. If --backup_file is omitted, loopd will use the most recent immutable network-specific L402 backup file. + +Usage: + +```bash +$ loop [GLOBAL FLAGS] recover [COMMAND FLAGS] [ARGUMENTS...] +``` + +The following flags are supported: + +| Name | Description | Type | Default value | +|---------------------|----------------------------------------------------------------------------------------------------------|--------|:-------------:| +| `--backup_file="…"` | path to an encrypted backup file; if omitted, loopd uses the most recent immutable L402 backup file path | string | +| `--help` (`-h`) | show help | bool | `false` | + ### `reservations` command (aliases: `r`) manage reservations. @@ -529,9 +548,9 @@ The following flags are supported: ### `static new` subcommand (aliases: `n`) -Create a new static loop in address. +Return the static loop in address. -Requests a new static loop in address from the server. Funds that are sent to this address will be locked by a 2:2 multisig between us and the loop server, or a timeout path that we can sweep once it opens up. The funds can either be cooperatively spent with a signature from the server or looped in. +Returns the current static loop in address. On a fresh installation loopd initializes the current static-address generation during startup. If the address is still missing, this call will create it on demand. Funds sent to the address will be locked by a 2:2 multisig between us and the loop server, or a timeout path that we can sweep once it opens up. The funds can either be cooperatively spent with a signature from the server or looped in. Usage: diff --git a/staticaddr/deposit/manager.go b/staticaddr/deposit/manager.go index 9376bfea3..32653329e 100644 --- a/staticaddr/deposit/manager.go +++ b/staticaddr/deposit/manager.go @@ -171,6 +171,20 @@ func (m *Manager) recoverDeposits(ctx context.Context) error { log.Infof("Recovering static address parameters and deposits...") + // Deposits that are still in the plain Deposited state must remain + // unspent across a restart. Other active states may have already spent + // the deposit and rely on their owning managers to finish recovery. + utxos, err := m.cfg.AddressManager.ListUnspent(ctx, 0, 0) + if err != nil { + return fmt.Errorf("unable to list unspent deposits for "+ + "recovery: %w", err) + } + + unspentOutpoints := make(map[wire.OutPoint]struct{}, len(utxos)) + for _, utxo := range utxos { + unspentOutpoints[utxo.OutPoint] = struct{}{} + } + // Recover deposits. deposits, err := m.cfg.Store.AllDeposits(ctx) if err != nil { @@ -187,6 +201,15 @@ func (m *Manager) recoverDeposits(ctx context.Context) error { continue } + if d.GetState() == Deposited { + if _, ok := unspentOutpoints[d.OutPoint]; !ok { + log.Warnf("Skipping recovery of spent deposited "+ + "outpoint %v", d.OutPoint) + + continue + } + } + log.Debugf("Recovering deposit %x", d.ID) // Create a state machine for a given deposit. diff --git a/staticaddr/deposit/manager_test.go b/staticaddr/deposit/manager_test.go index ab8aaa7a8..9ce036c59 100644 --- a/staticaddr/deposit/manager_test.go +++ b/staticaddr/deposit/manager_test.go @@ -304,6 +304,66 @@ func TestManager(t *testing.T) { } } +func TestRecoverDepositsSkipsSpentDeposited(t *testing.T) { + ctx := context.Background() + + id, err := GetRandomDepositID() + require.NoError(t, err) + + storedDeposit := &Deposit{ + ID: id, + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{1}, + Index: 1, + }, + state: Deposited, + Value: btcutil.Amount(100000), + ConfirmationHeight: 42, + } + + testContext := newManagerTestContextWithStoredDeposits( + t, []*Deposit{storedDeposit}, nil, + ) + + err = testContext.manager.recoverDeposits(ctx) + require.NoError(t, err) + require.Empty(t, testContext.manager.activeDeposits) + + deposits, err := testContext.manager.GetActiveDepositsInState(Deposited) + require.NoError(t, err) + require.Empty(t, deposits) +} + +func TestRecoverDepositsKeepsSpentWithdrawing(t *testing.T) { + ctx := context.Background() + + id, err := GetRandomDepositID() + require.NoError(t, err) + + storedDeposit := &Deposit{ + ID: id, + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{2}, + Index: 2, + }, + state: Withdrawing, + Value: btcutil.Amount(100000), + ConfirmationHeight: 42, + } + + testContext := newManagerTestContextWithStoredDeposits( + t, []*Deposit{storedDeposit}, nil, + ) + + err = testContext.manager.recoverDeposits(ctx) + require.NoError(t, err) + + deposits, err := testContext.manager.GetActiveDepositsInState(Withdrawing) + require.NoError(t, err) + require.Len(t, deposits, 1) + require.Equal(t, storedDeposit.OutPoint, deposits[0].OutPoint) +} + // ManagerTestContext is a helper struct that contains all the necessary // components to test the reservation manager. type ManagerTestContext struct { @@ -320,19 +380,9 @@ type ManagerTestContext struct { // newManagerTestContext creates a new test context for the reservation manager. func newManagerTestContext(t *testing.T) *ManagerTestContext { - mockLnd := test.NewMockLnd() - lndContext := test.NewContext(t, mockLnd) - - mockStaticAddressClient := new(mockStaticAddressClient) - mockAddressManager := new(mockAddressManager) - mockStore := new(mockStore) - mockChainNotifier := new(MockChainNotifier) - confChan := make(chan *chainntnfs.TxConfirmation) - confErrChan := make(chan error) - blockChan := make(chan int32) - blockErrChan := make(chan error) - ID, err := GetRandomDepositID() + require.NoError(t, err) + utxo := &lnwallet.Utxo{ AddressType: lnwallet.TaprootPubkey, Value: btcutil.Amount(100000), @@ -343,7 +393,7 @@ func newManagerTestContext(t *testing.T) *ManagerTestContext { Index: 0xffffffff, }, } - require.NoError(t, err) + storedDeposits := []*Deposit{ { ID: ID, @@ -355,6 +405,26 @@ func newManagerTestContext(t *testing.T) *ManagerTestContext { }, } + return newManagerTestContextWithStoredDeposits( + t, storedDeposits, []*lnwallet.Utxo{utxo}, + ) +} + +func newManagerTestContextWithStoredDeposits(t *testing.T, + storedDeposits []*Deposit, utxos []*lnwallet.Utxo) *ManagerTestContext { + + mockLnd := test.NewMockLnd() + lndContext := test.NewContext(t, mockLnd) + + mockStaticAddressClient := new(mockStaticAddressClient) + mockAddressManager := new(mockAddressManager) + mockStore := new(mockStore) + mockChainNotifier := new(MockChainNotifier) + confChan := make(chan *chainntnfs.TxConfirmation) + confErrChan := make(chan error) + blockChan := make(chan int32) + blockErrChan := make(chan error) + mockStore.On( "AllDeposits", mock.Anything, ).Return(storedDeposits, nil) @@ -371,7 +441,7 @@ func newManagerTestContext(t *testing.T) *ManagerTestContext { mockAddressManager.On( "ListUnspent", mock.Anything, mock.Anything, mock.Anything, - ).Return([]*lnwallet.Utxo{utxo}, nil) + ).Return(utxos, nil) // Define the expected return values for the mocks. mockChainNotifier.On(