From 1f9e38887cb53148aa9fbbf4127604f36bf298f9 Mon Sep 17 00:00:00 2001 From: octobocto Date: Fri, 25 Sep 2026 21:18:58 -0700 Subject: [PATCH 01/13] orchestrator: store the script type a path names --- .../api/generate_wallet_path_test.go | 2 +- sidechain-orchestrator/api/wallet_handler.go | 28 +------ sidechain-orchestrator/wallet/core_backend.go | 36 +-------- sidechain-orchestrator/wallet/derivation.go | 44 ++++++++++- .../wallet/derivation_test.go | 74 +++++++++++++++++++ sidechain-orchestrator/wallet/descriptor.go | 17 +---- .../wallet/migrate_script_type_test.go | 42 +++++++++++ sidechain-orchestrator/wallet/service.go | 29 +++++++- 8 files changed, 195 insertions(+), 77 deletions(-) create mode 100644 sidechain-orchestrator/wallet/migrate_script_type_test.go diff --git a/sidechain-orchestrator/api/generate_wallet_path_test.go b/sidechain-orchestrator/api/generate_wallet_path_test.go index a68db2fd2..d2077b5a5 100644 --- a/sidechain-orchestrator/api/generate_wallet_path_test.go +++ b/sidechain-orchestrator/api/generate_wallet_path_test.go @@ -22,7 +22,7 @@ func TestGenerateWalletAcceptsAStandardPathWithNoScriptType(t *testing.T) { DerivationPath: "m/86'/1'/0'", })) require.NoError(t, err) - assert.NotEmpty(t, resp.Msg.WalletId) + assert.Equal(t, "taproot", h.svc.GetWalletByID(resp.Msg.WalletId).ScriptType) } // A stated type and a path that disagree would scan addresses the wallet does diff --git a/sidechain-orchestrator/api/wallet_handler.go b/sidechain-orchestrator/api/wallet_handler.go index f306a2dad..dcea2bb70 100644 --- a/sidechain-orchestrator/api/wallet_handler.go +++ b/sidechain-orchestrator/api/wallet_handler.go @@ -118,33 +118,13 @@ func (h *WalletHandler) GetWalletStatus(ctx context.Context, req *connect.Reques }), nil } -// checkPathMatchesScriptType rejects a derivation path whose BIP purpose names -// a different address kind than scriptType. One wallet derives one kind, so a -// mismatch scans and signs addresses it does not own. An empty scriptType -// states nothing, and the kind then comes from the path itself. -func checkPathMatchesScriptType(path, scriptType string) error { - if path == "" || scriptType == "" { - return nil - } - ap, err := wallet.ParseAccountPath(path) - if err != nil { - return connect.NewError(connect.CodeInvalidArgument, err) - } - want, ok := wallet.HotScriptKind(scriptType).Purpose() - if !ap.Standard() || !ok || ap.Purpose == want { - return nil - } - return connect.NewError(connect.CodeInvalidArgument, - fmt.Errorf("derivation purpose %d' does not match script type %q (want %d')", ap.Purpose, scriptType, want)) -} - func (h *WalletHandler) GenerateWallet(ctx context.Context, req *connect.Request[pb.GenerateWalletRequest]) (*connect.Response[pb.GenerateWalletResponse], error) { account, path, err := wallet.ResolveCreateDerivationPath(req.Msg.Account, req.Msg.DerivationPath) if err != nil { return nil, connect.NewError(connect.CodeInvalidArgument, err) } - if err := checkPathMatchesScriptType(path, req.Msg.ScriptType); err != nil { - return nil, err + if _, err := wallet.ResolveScriptType(req.Msg.ScriptType, path); err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) } w, err := h.svc.GenerateWalletWithPath(req.Msg.Name, req.Msg.CustomMnemonic, req.Msg.Passphrase, account, path, req.Msg.ScriptType, allSidechainSlots()) if err != nil { @@ -435,8 +415,8 @@ func (h *WalletHandler) CreateElectrumWallet(ctx context.Context, req *connect.R return nil, connect.NewError(connect.CodeInvalidArgument, err) } if req.Msg.XpubOrDescriptor == "" { - if err := checkPathMatchesScriptType(path, req.Msg.ScriptType); err != nil { - return nil, err + if _, err := wallet.ResolveScriptType(req.Msg.ScriptType, path); err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) } } // A bare extended key states no kind, so its own history places it. With diff --git a/sidechain-orchestrator/wallet/core_backend.go b/sidechain-orchestrator/wallet/core_backend.go index 271c2b138..858b3f2a6 100644 --- a/sidechain-orchestrator/wallet/core_backend.go +++ b/sidechain-orchestrator/wallet/core_backend.go @@ -1685,20 +1685,10 @@ func (p *CoreBackend) createBitcoinCoreWallet(ctx context.Context, walletName st } fingerprint := masterFingerprint(masterKey) - var purposes []ScriptKind - if w.pinsOneKind() { - if _, err := ParseAccountPath(w.DerivationPath); err != nil { - return fmt.Errorf("invalid derivation path: %w", err) - } - purposes = []ScriptKind{coreScriptKind(w)} - } else { - // Exactly what the wallet advertises on the Receive page. Core hands out - // no address it holds no descriptor for, so the two sets must be one. - purposes = ReceiveKinds(w) - } - + // Exactly what the wallet advertises on the Receive page. Core hands out no + // address it holds no descriptor for, so the two sets must be one. var descriptors []ImportDescriptor - for _, kind := range purposes { + for _, kind := range ReceiveKinds(w) { ap, err := accountPathFor(w, kind, net) if err != nil { return err @@ -1912,29 +1902,11 @@ func createAndImport( return nil } -// walletScriptKind resolves the script kind a Core wallet receives to: the -// purpose of an explicit derivation path, else native segwit. +// walletScriptKind resolves the script kind a Core wallet receives to. func (p *CoreBackend) walletScriptKind(walletID string) ScriptKind { w := p.svc.GetWalletByID(walletID) if w == nil { return ScriptNativeSegwit } - return coreScriptKind(w) -} - -// coreScriptKind is the address kind a Core wallet derives: the BIP purpose of -// an explicit path when it names one, else the kind the wallet stores. The -// import reads it too, or Core is asked for a family it holds no descriptor for. -func coreScriptKind(w *WalletData) ScriptKind { - if !w.usesExplicitPath() { - return w.scriptKind() - } - ap, err := ParseAccountPath(w.DerivationPath) - if err != nil { - return w.scriptKind() - } - if kind, ok := purposeToCoreKind(ap.Purpose); ok { - return kind - } return w.scriptKind() } diff --git a/sidechain-orchestrator/wallet/derivation.go b/sidechain-orchestrator/wallet/derivation.go index dbce56e10..3017585df 100644 --- a/sidechain-orchestrator/wallet/derivation.go +++ b/sidechain-orchestrator/wallet/derivation.go @@ -365,5 +365,47 @@ func (w *WalletData) pinsOneKind() bool { if PinsOneKind(w.DerivationPath) { return true } - return w.usesExplicitPath() && walletReceiveKind(w) != w.scriptKind() + kind, ok := pathKind(w.DerivationPath) + return ok && kind != w.scriptKind() +} + +// pathKind returns the script kind that the purpose of a standard account path +// names. +func pathKind(derivationPath string) (ScriptKind, bool) { + if strings.TrimSpace(derivationPath) == "" { + return ScriptUnknown, false + } + ap, err := ParseAccountPath(derivationPath) + if err != nil || !ap.Standard() { + return ScriptUnknown, false + } + return purposeToCoreKind(ap.Purpose) +} + +// ResolveScriptType returns the script type a new hot wallet stores: the +// requested type, or the type a standard path names when the request has none. +func ResolveScriptType(requested, derivationPath string) (string, error) { + scriptType, err := validateHotScriptType(requested) + if err != nil { + return "", err + } + kind, ok := pathKind(derivationPath) + switch { + case !ok: + return scriptType, nil + case requested == "": + return hotScriptType(kind), nil + case kind != HotScriptKind(scriptType): + purpose, _ := kind.Purpose() + return "", fmt.Errorf("derivation purpose %d' does not match script type %q", purpose, requested) + } + return scriptType, nil +} + +// hotScriptType is the stored ScriptType of a single-sig kind. +func hotScriptType(kind ScriptKind) string { + if kind == ScriptNativeSegwit { + return "" + } + return kind.String() } diff --git a/sidechain-orchestrator/wallet/derivation_test.go b/sidechain-orchestrator/wallet/derivation_test.go index cf2888a3e..d785f9424 100644 --- a/sidechain-orchestrator/wallet/derivation_test.go +++ b/sidechain-orchestrator/wallet/derivation_test.go @@ -1,10 +1,13 @@ package wallet import ( + "context" "encoding/hex" + "encoding/json" "testing" "github.com/btcsuite/btcd/chaincfg" + "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -156,6 +159,77 @@ func TestCustomAccountAddressVector(t *testing.T) { assert.Equal(t, "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu", ds0.address.EncodeAddress()) } +func TestResolveScriptType(t *testing.T) { + for _, c := range []struct { + requested, path, want, wantErr string + }{ + {requested: "", path: "", want: ""}, + {requested: "taproot", path: "", want: "taproot"}, + {requested: "", path: "m/44'/1'/0'", want: "legacy"}, + {requested: "", path: "m/49'/0'/3'", want: "nested-segwit"}, + {requested: "", path: "m/84'/0'/0'", want: ""}, + {requested: "", path: "m/86'/0'/0'", want: "taproot"}, + {requested: "native-segwit", path: "m/84'/0'/0'", want: ""}, + {requested: "legacy", path: "m/1234'/9'/0'/7", want: "legacy"}, + {requested: "legacy", path: "m/86'/1'/0'", wantErr: "does not match"}, + {requested: "native-segwit", path: "m/86'/0'/0'", wantErr: "does not match"}, + {requested: "bogus", path: "", wantErr: "unsupported script type"}, + } { + got, err := ResolveScriptType(c.requested, c.path) + if c.wantErr != "" { + require.ErrorContains(t, err, c.wantErr, "%q %q", c.requested, c.path) + continue + } + require.NoError(t, err, "%q %q", c.requested, c.path) + assert.Equal(t, c.want, got, "%q %q", c.requested, c.path) + } +} + +// One seed and one path give one address, whichever backend serves the wallet. +func TestAStandardPathGivesOneAddressOnEveryBackend(t *testing.T) { + net := &chaincfg.MainNetParams + for path, want := range map[string]string{ + "m/44'/0'/0'": "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA", + "m/49'/0'/0'": "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf", + "m/86'/0'/0'": "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr", + } { + t.Run(path, func(t *testing.T) { + svc := newTestService(t) + elec, err := svc.CreateElectrumWallet("Electrum", nil, nil, testMnemonic, "", "", "", 0, path) + require.NoError(t, err) + core, err := svc.GenerateWalletWithPath("Core", testMnemonic, "", 0, path, "", nil) + require.NoError(t, err) + + eb := NewElectrumBackend(svc, newFakeEsplora(), StaticParams(net), zerolog.Nop()) + d, err := eb.walletDescriptorFor(elec, ReceiveKinds(elec)[0]) + require.NoError(t, err) + a, err := eb.deriveAddr(d, false, 0) + require.NoError(t, err) + assert.Equal(t, want, a.address, "electrum") + + fake := newFakeBitcoind(t) + fake.stubEnsureFlow() + cb := NewCoreBackend(svc, fake.client(t), StaticParams(net), zerolog.Nop()) + t.Cleanup(cb.bip47Imports.Wait) + _, err = cb.Ensure(context.Background(), core.ID) + require.NoError(t, err) + var imports []ImportDescriptor + require.NoError(t, json.Unmarshal(fake.callsFor("importdescriptors")[0].Params[0], &imports)) + receive, err := ParseDescriptor(imports[0].Desc) + require.NoError(t, err) + ds, _, err := receive.DeriveScript(false, 0, net) + require.NoError(t, err) + assert.Equal(t, want, ds.address.EncodeAddress(), "core") + + for _, w := range []*WalletData{elec, core} { + preview, err := DeriveWalletReceiveAddresses(w, net, 0, 1) + require.NoError(t, err) + assert.Equal(t, want, preview[0], "preview of the %s wallet", w.WalletType) + } + }) + } +} + func TestCoreDescriptorWrapper(t *testing.T) { cases := []struct { kind ScriptKind diff --git a/sidechain-orchestrator/wallet/descriptor.go b/sidechain-orchestrator/wallet/descriptor.go index 25fd982c8..2ab684af8 100644 --- a/sidechain-orchestrator/wallet/descriptor.go +++ b/sidechain-orchestrator/wallet/descriptor.go @@ -520,21 +520,6 @@ func accountKeyFromSeed(seedHex string, kind ScriptKind, net *chaincfg.Params) ( return acct, err } -// walletReceiveKind resolves the script kind a wallet's receive addresses use: -// an explicit derivation path's purpose wins (BIP44/49/84/86), otherwise the -// wallet's stored ScriptType (default native segwit). This is the kind the -// address preview must derive against so it matches the real receive addresses. -func walletReceiveKind(w *WalletData) ScriptKind { - if w.usesExplicitPath() { - if ap, err := ParseAccountPath(w.DerivationPath); err == nil { - if kind, ok := purposeToCoreKind(ap.Purpose); ok { - return kind - } - } - } - return w.scriptKind() -} - // DeriveWalletReceiveAddresses derives receive addresses without an address allocation. func DeriveWalletReceiveAddresses(w *WalletData, net *chaincfg.Params, start, count int) ([]string, error) { if net == nil { @@ -561,7 +546,7 @@ func DeriveWalletReceiveAddresses(w *WalletData, net *chaincfg.Params, start, co if w.Master.SeedHex == "" { return nil, errors.New("wallet has no seed; cannot derive addresses") } - kind := walletReceiveKind(w) + kind := w.scriptKind() ap, err := accountPathFor(w, kind, net) if err != nil { return nil, err diff --git a/sidechain-orchestrator/wallet/migrate_script_type_test.go b/sidechain-orchestrator/wallet/migrate_script_type_test.go new file mode 100644 index 000000000..bfa888478 --- /dev/null +++ b/sidechain-orchestrator/wallet/migrate_script_type_test.go @@ -0,0 +1,42 @@ +package wallet + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/config" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Core imported the kind a standard path names, so a Core wallet stores that +// kind. An electrum wallet keeps the chain it already scans. +func TestCoreWalletStoresTheScriptTypeOfItsPath(t *testing.T) { + dir := t.TempDir() + body, err := json.Marshal(map[string]any{ + "wallets": []map[string]any{ + {"id": "CORE44", "name": "Core legacy", "wallet_type": "bitcoinCore", "derivation_path": "m/44'/1'/0'"}, + {"id": "CORE86", "name": "Core taproot", "wallet_type": "bitcoinCore", "derivation_path": "m/86'/1'/0'", "script_type": "taproot"}, + {"id": "ELEC86", "name": "Electrum", "wallet_type": "electrum", "derivation_path": "m/86'/1'/0'"}, + }, + "activeWalletId": "CORE44", + }) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(dir, "wallet.json"), body, 0o600)) + + for range 2 { + svc := NewService(dir, zerolog.Nop()) + svc.SetNetwork(string(config.NetworkSignet)) + require.NoError(t, svc.Init()) + + assert.Equal(t, "legacy", svc.GetWalletByID("CORE44").ScriptType) + assert.Equal(t, "taproot", svc.GetWalletByID("CORE86").ScriptType) + elec := svc.GetWalletByID("ELEC86") + assert.Empty(t, elec.ScriptType) + assert.Equal(t, []ScriptKind{ScriptNativeSegwit}, ReceiveKinds(elec)) + svc.Close() + } +} diff --git a/sidechain-orchestrator/wallet/service.go b/sidechain-orchestrator/wallet/service.go index 7c671da81..86afcd805 100644 --- a/sidechain-orchestrator/wallet/service.go +++ b/sidechain-orchestrator/wallet/service.go @@ -753,7 +753,7 @@ func (s *Service) CreateBitcoinCoreWallet(name string, gradientJSON json.RawMess // xpubOrDescriptor instead creates a watch-only electrum wallet with no // private keys; it is mutually exclusive with customMnemonic. func (s *Service) CreateElectrumWallet(name string, gradient json.RawMessage, slots []uint32, customMnemonic, passphrase, xpubOrDescriptor, scriptType string, accountIndex uint32, derivationPath string) (*WalletData, error) { - st, err := validateHotScriptType(scriptType) + st, err := ResolveScriptType(scriptType, derivationPath) if err != nil { return nil, err } @@ -1056,7 +1056,7 @@ func (s *Service) GenerateWallet(name, customMnemonic, passphrase string, slots // derivation-path override stored on the wallet so descriptor derivation honors // it. Both override args must be pre-validated (see ResolveCreateDerivationPath). func (s *Service) GenerateWalletWithPath(name, customMnemonic, passphrase string, accountIndex uint32, derivationPath, scriptType string, slots []SidechainSlot) (*WalletData, error) { - st, err := validateHotScriptType(scriptType) + st, err := ResolveScriptType(scriptType, derivationPath) if err != nil { return nil, err } @@ -1867,6 +1867,9 @@ func (s *Service) loadWalletFile() error { if enforcerMigrated { migrated = true } + if s.alignCoreScriptTypes() { + migrated = true + } if s.adoptStarterWallet() { migrated = true } @@ -1978,6 +1981,26 @@ func (s *Service) enforcerLegacyWallet(w *WalletData, target WalletType) (*Walle return &legacy, nil } +// alignCoreScriptTypes stores on each Core wallet the script type that its +// standard path names, because Core imported that type. Must be called with mu +// held. +func (s *Service) alignCoreScriptTypes() bool { + changed := false + for i := range s.wallets { + w := &s.wallets[i] + if w.WalletType != WalletTypeBitcoinCore || w.Multisig != nil || w.IsWatchOnly() { + continue + } + kind, ok := pathKind(w.DerivationPath) + if !ok || kind == w.scriptKind() { + continue + } + w.ScriptType = hotScriptType(kind) + changed = true + } + return changed +} + // saveWalletFile writes wallet.json atomically, keeping every wallet the file // on disk holds. Must be called with mu held. func (s *Service) saveWalletFile() error { return s.saveWalletFileWithOptions(false) } @@ -2134,7 +2157,7 @@ func (s *Service) derivesEnforcerAccount(w *WalletData) bool { // startup, so this cannot happen. panic(fmt.Sprintf("unknown network %q: %v", s.network, err)) } - ap, err := accountPathFor(w, walletReceiveKind(w), net) + ap, err := accountPathFor(w, w.scriptKind(), net) if err != nil { return false } From e13c8d44178d8aefc9c819ef6bfd9ff39c796a63 Mon Sep 17 00:00:00 2001 From: octobocto Date: Fri, 25 Sep 2026 21:27:46 -0700 Subject: [PATCH 02/13] orchestrator: build descriptors in one place --- sidechain-orchestrator/wallet/core_backend.go | 72 +------- .../wallet/derive_keystore.go | 8 +- sidechain-orchestrator/wallet/descriptor.go | 132 +++++++++------ .../wallet/descriptor_test.go | 28 ++++ .../wallet/electrum_backend.go | 154 ++++-------------- sidechain-orchestrator/wallet/keys.go | 91 +---------- .../wallet/sidechain_core_wallet.go | 45 +---- .../wallet/sidechain_core_wallet_test.go | 39 +++++ .../wallet/wallet_descriptors.go | 123 ++++++++++++++ .../wallet/wallet_descriptors_test.go | 57 +++++++ 10 files changed, 377 insertions(+), 372 deletions(-) create mode 100644 sidechain-orchestrator/wallet/sidechain_core_wallet_test.go create mode 100644 sidechain-orchestrator/wallet/wallet_descriptors.go create mode 100644 sidechain-orchestrator/wallet/wallet_descriptors_test.go diff --git a/sidechain-orchestrator/wallet/core_backend.go b/sidechain-orchestrator/wallet/core_backend.go index 858b3f2a6..2535575eb 100644 --- a/sidechain-orchestrator/wallet/core_backend.go +++ b/sidechain-orchestrator/wallet/core_backend.go @@ -21,7 +21,6 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/rs/zerolog" "github.com/samber/lo" - "github.com/tyler-smith/go-bip32" "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/replay" "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/wallet/bip47" @@ -1665,76 +1664,23 @@ func (p *CoreBackend) retryBip47NotificationDescriptor(ctx context.Context, wall delete(p.bip47NotifRetry, walletID) } -// createBitcoinCoreWallet creates a Bitcoin Core descriptor wallet from a seed. -// With no derivation override it imports the standard BIP84 + BIP86 descriptors -// at account 0; an AccountIndex shifts both to that account; a DerivationPath -// that pins one kind imports the single descriptor for that path's purpose. +// createBitcoinCoreWallet creates a Bitcoin Core descriptor wallet that holds +// every descriptor the wallet derives. func (p *CoreBackend) createBitcoinCoreWallet(ctx context.Context, walletName string, w *WalletData) error { net := p.net() - if net == nil { - return fmt.Errorf("no chain params for this network; cannot derive wallet descriptors") - } - seed, err := hex.DecodeString(w.Master.SeedHex) + descriptors, err := WalletDescriptors(w, net) if err != nil { - return fmt.Errorf("decode seed hex: %w", err) - } - - masterKey, err := bip32.NewMasterKey(seed) - if err != nil { - return fmt.Errorf("create master key: %w", err) + return err } - fingerprint := masterFingerprint(masterKey) - - // Exactly what the wallet advertises on the Receive page. Core hands out no - // address it holds no descriptor for, so the two sets must be one. - var descriptors []ImportDescriptor - for _, kind := range ReceiveKinds(w) { - ap, err := accountPathFor(w, kind, net) - if err != nil { - return err - } - acct, err := deriveAccountKey(masterKey, ap) + var imports []ImportDescriptor + for _, d := range descriptors { + pair, err := d.coreImports(net, importTimestamp(w)) if err != nil { return err } - acctXprv := serializeKeyForNetwork(acct, net) - open, close, ok := coreDescriptorWrapper(kind) - if !ok { - return fmt.Errorf("unsupported core descriptor kind %s", kind) - } - origin := ap.Origin("'") - descriptors = append(descriptors, - ImportDescriptor{ - Desc: mustAddChecksum(fmt.Sprintf("%s[%s/%s]%s/0/*%s", open, fingerprint, origin, acctXprv, close)), - Active: true, - Timestamp: importTimestamp(w), - Internal: false, - Range: []int{0, 999}, - }, - ImportDescriptor{ - Desc: mustAddChecksum(fmt.Sprintf("%s[%s/%s]%s/1/*%s", open, fingerprint, origin, acctXprv, close)), - Active: true, - Timestamp: importTimestamp(w), - Internal: true, - Range: []int{0, 999}, - }, - ) - } - - return p.createAndImport(ctx, walletName, false, descriptors) -} - -// deriveAccountKey derives the hardened account-level key for an AccountPath. -func deriveAccountKey(masterKey *bip32.Key, ap AccountPath) (*bip32.Key, error) { - cur := masterKey - for _, index := range ap.Indices() { - next, err := cur.NewChildKey(index) - if err != nil { - return nil, fmt.Errorf("derive %d: %w", index, err) - } - cur = next + imports = append(imports, pair...) } - return cur, nil + return p.createAndImport(ctx, walletName, false, imports) } // coreDescriptorWrapper returns the open/close fragments wrapping the key diff --git a/sidechain-orchestrator/wallet/derive_keystore.go b/sidechain-orchestrator/wallet/derive_keystore.go index 6849b0d00..50fbc689e 100644 --- a/sidechain-orchestrator/wallet/derive_keystore.go +++ b/sidechain-orchestrator/wallet/derive_keystore.go @@ -160,14 +160,14 @@ func parseRawKey(raw string) (xpub, fingerprint, originPath string, err error) { // singleSigWatchDescriptor builds the canonical checksummed watch descriptor // wrap([fp/origin]xpub/<0;1>/*) for a single-sig kind. func singleSigWatchDescriptor(kind ScriptKind, fingerprint, originPath, xpub string) (string, error) { - open, close, ok := coreDescriptorWrapper(kind) - if !ok { + if _, ok := kind.Purpose(); !ok { return "", fmt.Errorf("no single-sig descriptor for kind %s", kind) } - raw := fmt.Sprintf("%s[%s/%s]%s/<0;1>/*%s", open, fingerprint, originPath, xpub, close) - d, err := ParseDescriptor(raw) + key, err := parseKeyExpr(xpub) if err != nil { return "", err } + key.Origin = fingerprint + "/" + originPath + d := &Descriptor{Kind: kind, Threshold: 1, Keys: []DescriptorKey{key}} return d.String() } diff --git a/sidechain-orchestrator/wallet/descriptor.go b/sidechain-orchestrator/wallet/descriptor.go index 2ab684af8..ab05416ab 100644 --- a/sidechain-orchestrator/wallet/descriptor.go +++ b/sidechain-orchestrator/wallet/descriptor.go @@ -206,8 +206,7 @@ func validateBranchSuffix(suffix string) error { // String renders the descriptor in canonical form with a checksum, using the // multipath /<0;1>/* branch and neutering any private account keys. func (d *Descriptor) String() (string, error) { - exprs := make([]string, len(d.Keys)) - for i, k := range d.Keys { + body, err := d.render(func(k DescriptorKey) (string, error) { acct := k.Account if acct.IsPrivate() { pub, err := acct.Neuter() @@ -220,35 +219,96 @@ func (d *Descriptor) String() (string, error) { if k.Origin != "" { expr = "[" + k.Origin + "]" + expr } - exprs[i] = expr + "/<0;1>/*" + return expr + "/<0;1>/*", nil + }) + if err != nil { + return "", err } + return AddDescriptorChecksum(body) +} + +// coreImports renders the descriptor as the receive and change descriptors +// Bitcoin Core imports, each key as the wallet holds it. +func (d *Descriptor) coreImports(net *chaincfg.Params, timestamp any) ([]ImportDescriptor, error) { + imports := make([]ImportDescriptor, 0, 2) + for _, change := range []bool{false, true} { + body, err := d.render(func(k DescriptorKey) (string, error) { + return coreKeyExpr(k, chainIndex(change), net) + }) + if err != nil { + return nil, err + } + desc, err := AddDescriptorChecksum(body) + if err != nil { + return nil, err + } + imports = append(imports, ImportDescriptor{ + Desc: desc, + Active: true, + Timestamp: timestamp, + Internal: change, + Range: []int{0, 999}, + }) + } + return imports, nil +} - var body string +// coreKeyExpr writes one key of a branch as Core reads it: the version bytes +// of net, and ' as the hardened marker of the origin. +func coreKeyExpr(k DescriptorKey, chain uint32, net *chaincfg.Params) (string, error) { + version := net.HDPublicKeyID[:] + if k.Account.IsPrivate() { + version = net.HDPrivateKeyID[:] + } + key, err := k.Account.CloneWithVersion(version) + if err != nil { + return "", err + } + expr := fmt.Sprintf("%s/%d/*", key.String(), chain) + if k.Origin == "" { + return expr, nil + } + fingerprint, originPath, _ := strings.Cut(k.Origin, "/") + path, ok := parsePathSegments(strings.Split(originPath, "/")) + if !ok { + return "", fmt.Errorf("key origin %q has an invalid path", k.Origin) + } + if len(path) == 0 { + return "[" + fingerprint + "]" + expr, nil + } + return "[" + fingerprint + "/" + formatOriginPath(path) + "]" + expr, nil +} + +// render writes the script of the descriptor around each key expression. +func (d *Descriptor) render(keyExpr func(DescriptorKey) (string, error)) (string, error) { + exprs := make([]string, len(d.Keys)) + for i, k := range d.Keys { + expr, err := keyExpr(k) + if err != nil { + return "", err + } + exprs[i] = expr + } switch d.Kind { case ScriptLegacy: - body = "pkh(" + exprs[0] + ")" + return "pkh(" + exprs[0] + ")", nil case ScriptNativeSegwit: - body = "wpkh(" + exprs[0] + ")" + return "wpkh(" + exprs[0] + ")", nil case ScriptNestedSegwit: - body = "sh(wpkh(" + exprs[0] + "))" + return "sh(wpkh(" + exprs[0] + "))", nil case ScriptTaproot: - body = "tr(" + exprs[0] + ")" + return "tr(" + exprs[0] + ")", nil case ScriptMultisig: - body = fmt.Sprintf("wsh(sortedmulti(%d,%s))", d.Threshold, strings.Join(exprs, ",")) + return fmt.Sprintf("wsh(sortedmulti(%d,%s))", d.Threshold, strings.Join(exprs, ",")), nil case ScriptMultisigP2SH: - body = fmt.Sprintf("sh(sortedmulti(%d,%s))", d.Threshold, strings.Join(exprs, ",")) + return fmt.Sprintf("sh(sortedmulti(%d,%s))", d.Threshold, strings.Join(exprs, ",")), nil case ScriptMultisigNested: - body = fmt.Sprintf("sh(wsh(sortedmulti(%d,%s)))", d.Threshold, strings.Join(exprs, ",")) + return fmt.Sprintf("sh(wsh(sortedmulti(%d,%s)))", d.Threshold, strings.Join(exprs, ",")), nil case ScriptMultisigTaproot: - body = fmt.Sprintf("tr(%s,sortedmulti_a(%d,%s))", numsInternalKeyHex, d.Threshold, strings.Join(exprs, ",")) + return fmt.Sprintf("tr(%s,sortedmulti_a(%d,%s))", numsInternalKeyHex, d.Threshold, strings.Join(exprs, ",")), nil default: return "", fmt.Errorf("cannot serialize script kind %s", d.Kind) } - sum, err := DescriptorChecksum(body) - if err != nil { - return "", err - } - return body + "#" + sum, nil } // DeriveScript resolves the address + scripts at (change, index). For single-sig @@ -522,42 +582,10 @@ func accountKeyFromSeed(seedHex string, kind ScriptKind, net *chaincfg.Params) ( // DeriveWalletReceiveAddresses derives receive addresses without an address allocation. func DeriveWalletReceiveAddresses(w *WalletData, net *chaincfg.Params, start, count int) ([]string, error) { - if net == nil { - return nil, errors.New("no chain params for this network; cannot derive addresses") - } - var d *Descriptor - if w.Multisig != nil { - group := MultisigLoungeGroup{M: w.Multisig.M, N: w.Multisig.N} - for _, c := range w.Multisig.Cosigners { - group.Keys = append(group.Keys, MultisigLoungeKey{ - Xpub: c.Xpub, Fingerprint: c.Fingerprint, OriginPath: c.OriginPath, - IsWallet: c.Fingerprint != "", - }) - } - receive, _, err := BuildMultisigLoungeDescriptorsTyped(group, w.MultisigScriptType()) - if err != nil { - return nil, err - } - d, err = ParseDescriptor(receive) - if err != nil { - return nil, err - } - } else { - if w.Master.SeedHex == "" { - return nil, errors.New("wallet has no seed; cannot derive addresses") - } - kind := w.scriptKind() - ap, err := accountPathFor(w, kind, net) - if err != nil { - return nil, err - } - acct, _, err := accountKeyAndOrigin(w.Master.SeedHex, ap, net) - if err != nil { - return nil, err - } - d = &Descriptor{Kind: kind, Threshold: 1, Keys: []DescriptorKey{{Account: acct}}} + d, err := DescriptorFor(w, w.scriptKind(), net) + if err != nil { + return nil, err } - addrs := make([]string, 0, count) for i := start; i < start+count; i++ { ds, _, err := d.DeriveScript(false, uint32(i), net) diff --git a/sidechain-orchestrator/wallet/descriptor_test.go b/sidechain-orchestrator/wallet/descriptor_test.go index 299e4ee84..141776b80 100644 --- a/sidechain-orchestrator/wallet/descriptor_test.go +++ b/sidechain-orchestrator/wallet/descriptor_test.go @@ -426,3 +426,31 @@ func TestWatchOnlyImportHonoursTheChosenAddressType(t *testing.T) { require.NoError(t, err) assert.Equal(t, byte('1'), ds.address.EncodeAddress()[0], "a legacy import must scan 1-prefixed addresses") } + +// Core takes each branch as its own descriptor, with the key encoded for the +// network it runs on. +func TestCoreImportsRenderBothBranchesForTheNetwork(t *testing.T) { + net := &chaincfg.SigNetParams + seedHex := hex.EncodeToString(MnemonicToSeed(testMnemonic, "")) + watch, err := ParseDescriptor("wpkh([73c5da0a/84h/0h/0h]" + accountXpub(t, seedHex, &chaincfg.MainNetParams) + "/0/*)") + require.NoError(t, err) + hot, err := DescriptorFor(&WalletData{Master: MasterWallet{SeedHex: seedHex}}, ScriptNativeSegwit, net) + require.NoError(t, err) + + for prefix, d := range map[string]*Descriptor{"tpub": watch, "tprv": hot} { + imports, err := d.coreImports(net, 0) + require.NoError(t, err) + require.Len(t, imports, 2) + assert.Regexp(t, `^wpkh\(\[73c5da0a/84'/[01]'/0'\]`+prefix+`[1-9A-HJ-NP-Za-km-z]+/0/\*\)#[a-z0-9]{8}$`, imports[0].Desc) + assert.False(t, imports[0].Internal) + assert.Contains(t, imports[1].Desc, "/1/*)#") + assert.True(t, imports[1].Internal) + for _, imp := range imports { + assert.True(t, imp.Active) + assert.Equal(t, []int{0, 999}, imp.Range) + assert.Equal(t, 0, imp.Timestamp) + _, err := stripDescriptorChecksum(imp.Desc) + require.NoError(t, err) + } + } +} diff --git a/sidechain-orchestrator/wallet/electrum_backend.go b/sidechain-orchestrator/wallet/electrum_backend.go index af7c965f4..ae04a1981 100644 --- a/sidechain-orchestrator/wallet/electrum_backend.go +++ b/sidechain-orchestrator/wallet/electrum_backend.go @@ -2093,125 +2093,50 @@ func (p *ElectrumBackend) walletDescriptor(w *WalletData) (*Descriptor, error) { return p.walletDescriptorFor(w, w.scriptKind()) } -// walletDescriptorFor builds the parsed output descriptor for one of a wallet's -// script kinds: from its seed + the kind when hot (the account key is private, -// so its addresses can be signed), or from its single stored watch-only -// descriptor otherwise (watch-only wallets are single-kind, so kind is ignored). +// walletDescriptorFor builds the descriptor for one of a wallet's script kinds. func (p *ElectrumBackend) walletDescriptorFor(w *WalletData, kind ScriptKind) (*Descriptor, error) { - net := p.params() - if net == nil { - return nil, errors.New("no chain params for this network; cannot derive electrum wallet") - } - if w.Multisig != nil { - return p.multisigSigningDescriptor(w) - } - if w.Master.SeedHex != "" { - ap, err := accountPathFor(w, kind, net) - if err != nil { - return nil, err - } - acct, origin, err := accountKeyAndOrigin(w.Master.SeedHex, ap, net) - if err != nil { - return nil, err - } - return &Descriptor{Kind: kind, Threshold: 1, Keys: []DescriptorKey{{Origin: origin, Account: acct}}}, nil - } - desc, err := watchOnlyDescriptorString(w) - if err != nil { - return nil, err - } - // A bare xpub states no kind, so the one recorded at import decides. - return ParseDescriptorAs(desc, w.scriptKind()) + p.warnNoOrigin(w) + return DescriptorFor(w, kind, p.params()) } -// multisigSigningDescriptor builds the wallet's multisig descriptor, substituting -// each held cosigner's account xprv for its xpub (so deriveAddr yields signing -// keys) while external legs stay xpubs. The resulting descriptor derives the same -// addresses as the watch-only one and is signed through the same PSBT path as any -// other input — held keys are just signers that happen to live on disk. -func (p *ElectrumBackend) multisigSigningDescriptor(w *WalletData) (*Descriptor, error) { - return p.multisigSigningDescriptorFor(w, "") +// multisigSigningDescriptorFor builds the multisig descriptor that signs with +// the held cosigners, or with the one cosigner onlyXpub names. +func (p *ElectrumBackend) multisigSigningDescriptorFor(w *WalletData, onlyXpub string) (*Descriptor, error) { + p.warnNoOrigin(w) + return multisigDescriptor(w, onlyXpub, p.params()) } -// shortXpub names a key in a log line without printing the whole thing. -func shortXpub(xpub string) string { - if len(xpub) <= 16 { - return xpub +// warnNoOrigin names each multisig cosigner that has no key origin, one time +// for each wallet. A hardware signer cannot place such a key in the script it +// rebuilds, and the device reports only a bad signature. +func (p *ElectrumBackend) warnNoOrigin(w *WalletData) { + if w.Multisig == nil { + return } - return xpub[:12] + "…" + xpub[len(xpub)-4:] -} - -// multisigSigningDescriptorFor builds the multisig descriptor with held cosigners -// substituted as their xprv. When onlyXpub is non-empty, only that cosigner's -// xprv is substituted (the rest stay xpubs), so signing adds a single cosigner's -// signature — the per-keystore signing path. -func (p *ElectrumBackend) multisigSigningDescriptorFor(w *WalletData, onlyXpub string) (*Descriptor, error) { - net := p.params() - ms := w.Multisig - if ms == nil { - return nil, errors.New("wallet has no multisig config") - } - group := MultisigLoungeGroup{M: ms.M, N: ms.N} - signWithXprv := map[string]string{} - // A cosigner with no origin reaches the PSBT as a bare key, and a hardware - // signer cannot place it in the script it rebuilds. Name it here: the device - // reports only a bad signature, which says nothing about which leg is short. var noOrigin []string - for _, c := range ms.Cosigners { + for _, c := range w.Multisig.Cosigners { if c.Fingerprint == "" { noOrigin = append(noOrigin, shortXpub(c.Xpub)) } - group.Keys = append(group.Keys, MultisigLoungeKey{ - Xpub: c.Xpub, - Fingerprint: c.Fingerprint, - OriginPath: c.OriginPath, - // Emit a key-origin prefix for every cosigner we know a fingerprint - // for, so each origin lands in the PSBT. A hardware signer needs all - // of them to rebuild the multisig script it signs over. - IsWallet: c.Fingerprint != "", - }) - if !c.Held() { - continue - } - if onlyXpub != "" && c.Xpub != onlyXpub { - continue - } - xprv := c.Xprv - if xprv == "" { - seedHex := hex.EncodeToString(MnemonicToSeed(c.Mnemonic, c.Passphrase)) - x, _, err := DeriveAccountXprv(seedHex, "m/"+c.OriginPath, net) - if err != nil { - return nil, fmt.Errorf("derive cosigner xprv: %w", err) - } - xprv = x - } - signWithXprv[c.Xpub] = xprv } - - if len(noOrigin) > 0 { - if _, seen := p.warnedNoOrigin.LoadOrStore(w.ID, true); seen { - noOrigin = nil - } + if len(noOrigin) == 0 { + return } - if len(noOrigin) > 0 { - p.log.Warn(). - Str("wallet", w.ID). - Strs("cosigners", noOrigin). - Msg("cosigner has no key origin; a hardware signer cannot rebuild this script") + if _, seen := p.warnedNoOrigin.LoadOrStore(w.ID, true); seen { + return } + p.log.Warn(). + Str("wallet", w.ID). + Strs("cosigners", noOrigin). + Msg("cosigner has no key origin; a hardware signer cannot rebuild this script") +} - scriptType := multisigTypeString(w.scriptKind()) - var receive string - var err error - if len(signWithXprv) > 0 { - receive, _, err = BuildMultisigSigningDescriptorsTyped(group, signWithXprv, scriptType) - } else { - receive, _, err = BuildMultisigLoungeDescriptorsTyped(group, scriptType) - } - if err != nil { - return nil, err +// shortXpub names a key in a log line without printing the whole thing. +func shortXpub(xpub string) string { + if len(xpub) <= 16 { + return xpub } - return ParseDescriptor(receive) + return xpub[:12] + "…" + xpub[len(xpub)-4:] } // deriveAddr resolves one address of a descriptor, enriching it with the signing @@ -2955,27 +2880,6 @@ func (p *ElectrumBackend) hydrate(ctx context.Context, walletID string, a *scann return nil } -// watchOnlyDescriptorString returns the descriptor (or bare xpub) stored in a -// watch-only wallet's payload, for ParseDescriptor. -func watchOnlyDescriptorString(w *WalletData) (string, error) { - var stored struct { - Xpub string `json:"xpub"` - Descriptor string `json:"descriptor"` - } - if len(w.WatchOnly) > 0 { - if err := json.Unmarshal(w.WatchOnly, &stored); err != nil { - return "", fmt.Errorf("parse watch-only data: %w", err) - } - } - if stored.Descriptor != "" { - return stored.Descriptor, nil - } - if stored.Xpub != "" { - return stored.Xpub, nil - } - return "", errors.New("watch-only electrum wallet has no descriptor or xpub") -} - func (p *ElectrumBackend) watchKeyAddr(ctx context.Context, walletID string, k WatchKey, prior *electrumScan) (scannedAddr, error) { net := p.params() wif, err := btcutil.DecodeWIF(k.WIF) diff --git a/sidechain-orchestrator/wallet/keys.go b/sidechain-orchestrator/wallet/keys.go index f73791da6..f13c05e69 100644 --- a/sidechain-orchestrator/wallet/keys.go +++ b/sidechain-orchestrator/wallet/keys.go @@ -1,98 +1,19 @@ package wallet import ( - "crypto/sha256" - "encoding/hex" "fmt" - "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/base58" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/tyler-smith/go-bip32" - "golang.org/x/crypto/ripemd160" //nolint:staticcheck // Bitcoin protocol requires RIPEMD160 "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/wallet/bip47" ) -// serializeKeyForNetwork serializes a BIP32 key, patching the version bytes to -// match the network. go-bip32 hardcodes mainnet xprv version bytes; for any -// other chain Core rejects the serialized key, so we strip + replace + re-sum. -func serializeKeyForNetwork(key *bip32.Key, network *chaincfg.Params) string { - if network == nil { - panic("serializeKeyForNetwork: no network; a key's version bytes name its chain") - } - if network.HDPrivateKeyID == chaincfg.MainNetParams.HDPrivateKeyID { - return key.String() - } - - // key.Serialize() returns the full 82-byte payload (78 data + 4 checksum). - // We need to strip the existing checksum, replace version bytes, then - // re-encode with a fresh checksum so Bitcoin Core accepts the key. - serialized, err := key.Serialize() - if err != nil { - return key.String() - } - - // serialized is 82 bytes: [4 version][74 data][4 checksum]. - // Strip old checksum, patch version, recompute. - raw := serialized[:78] - copy(raw[0:4], network.HDPrivateKeyID[:]) - return base58CheckEncode(raw) -} - // DeriveBIP84Addresses derives external-chain P2WPKH receive addresses -// (m/84'/coin'/0'/0/i) from a BIP32 seed, locally, without any backend. +// (m/84'/coin'/0'/0/i) of a BIP32 seed. func DeriveBIP84Addresses(seedHex string, net *chaincfg.Params, start, count int) ([]string, error) { - if net == nil { - return nil, fmt.Errorf("no chain params for this network; cannot derive addresses") - } - seed, err := hex.DecodeString(seedHex) - if err != nil { - return nil, fmt.Errorf("decode seed hex: %w", err) - } - masterKey, err := bip32.NewMasterKey(seed) - if err != nil { - return nil, fmt.Errorf("create master key: %w", err) - } - purpose, err := masterKey.NewChildKey(bip32.FirstHardenedChild + 84) - if err != nil { - return nil, fmt.Errorf("derive purpose: %w", err) - } - coin, err := purpose.NewChildKey(bip32.FirstHardenedChild + net.HDCoinType) - if err != nil { - return nil, fmt.Errorf("derive coin: %w", err) - } - account, err := coin.NewChildKey(bip32.FirstHardenedChild + 0) - if err != nil { - return nil, fmt.Errorf("derive account: %w", err) - } - external, err := account.NewChildKey(0) - if err != nil { - return nil, fmt.Errorf("derive external chain: %w", err) - } - - addrs := make([]string, 0, count) - for i := start; i < start+count; i++ { - child, err := external.NewChildKey(uint32(i)) - if err != nil { - return nil, fmt.Errorf("derive index %d: %w", i, err) - } - addr, err := btcutil.NewAddressWitnessPubKeyHash(hash160(child.PublicKey().Key), net) - if err != nil { - return nil, fmt.Errorf("address at index %d: %w", i, err) - } - addrs = append(addrs, addr.EncodeAddress()) - } - return addrs, nil -} - -// masterFingerprint computes the master fingerprint from a BIP32 key. -// Hash160(compressed_pubkey)[:4], hex encoded. -func masterFingerprint(masterKey *bip32.Key) string { - pubKey := masterKey.PublicKey() - h := hash160(pubKey.Key) - return hex.EncodeToString(h[:4]) + return DeriveWalletReceiveAddresses(&WalletData{Master: MasterWallet{SeedHex: seedHex}}, net, start, count) } // Bip47PaymentCodeFromSeed returns the BIP47 v1 spec-compliant payment code @@ -114,14 +35,6 @@ func Bip47PaymentCodeFromSeed(seedHex string, net *chaincfg.Params) (string, err return pc.Base58(), nil } -// hash160 computes RIPEMD160(SHA256(data)). -func hash160(data []byte) []byte { - sha := sha256.Sum256(data) - ripemd := ripemd160.New() - ripemd.Write(sha[:]) - return ripemd.Sum(nil) -} - // mustAddChecksum adds a descriptor checksum, panicking on error (for known-good descriptors). func mustAddChecksum(desc string) string { result, err := AddDescriptorChecksum(desc) diff --git a/sidechain-orchestrator/wallet/sidechain_core_wallet.go b/sidechain-orchestrator/wallet/sidechain_core_wallet.go index 6a8f06732..9965a9022 100644 --- a/sidechain-orchestrator/wallet/sidechain_core_wallet.go +++ b/sidechain-orchestrator/wallet/sidechain_core_wallet.go @@ -2,12 +2,11 @@ package wallet import ( "context" + "encoding/hex" "fmt" "github.com/btcsuite/btcd/chaincfg" "github.com/rs/zerolog" - bip32 "github.com/tyler-smith/go-bip32" - bip39 "github.com/tyler-smith/go-bip39" ) // EnsureCoreWalletFromMnemonic creates a Bitcoin Core wallet holding the keys a @@ -18,49 +17,17 @@ func EnsureCoreWalletFromMnemonic( ctx context.Context, rpc *CoreRPCClient, log zerolog.Logger, walletName, mnemonic string, net *chaincfg.Params, ) error { - if net == nil { - return fmt.Errorf("no chain params for this network; cannot derive wallet descriptors") - } if mnemonic == "" { return fmt.Errorf("empty mnemonic") } - - masterKey, err := bip32.NewMasterKey(bip39.NewSeed(mnemonic, "")) + starter := &WalletData{Master: MasterWallet{SeedHex: hex.EncodeToString(MnemonicToSeed(mnemonic, ""))}} + d, err := DescriptorFor(starter, ScriptNativeSegwit, net) if err != nil { - return fmt.Errorf("create master key: %w", err) - } - - const kind = ScriptNativeSegwit - purpose, ok := kind.Purpose() - if !ok { - return fmt.Errorf("no derivation purpose for script kind %s", kind) + return err } - path := AccountPath{Purpose: purpose, Coin: net.HDCoinType} - account, err := deriveAccountKey(masterKey, path) + imports, err := d.coreImports(net, "now") if err != nil { return err } - - open, close, ok := coreDescriptorWrapper(kind) - if !ok { - return fmt.Errorf("unsupported core descriptor kind %s", kind) - } - origin := fmt.Sprintf("[%s/%s]%s", masterFingerprint(masterKey), path.Origin("'"), serializeKeyForNetwork(account, net)) - descriptors := []ImportDescriptor{ - { - Desc: mustAddChecksum(fmt.Sprintf("%s%s/0/*%s", open, origin, close)), - Active: true, - Timestamp: "now", - Range: []int{0, 999}, - }, - { - Desc: mustAddChecksum(fmt.Sprintf("%s%s/1/*%s", open, origin, close)), - Active: true, - Timestamp: "now", - Internal: true, - Range: []int{0, 999}, - }, - } - - return createAndImport(ctx, rpc, log, walletName, false, descriptors) + return createAndImport(ctx, rpc, log, walletName, false, imports) } diff --git a/sidechain-orchestrator/wallet/sidechain_core_wallet_test.go b/sidechain-orchestrator/wallet/sidechain_core_wallet_test.go new file mode 100644 index 000000000..ce3d2b043 --- /dev/null +++ b/sidechain-orchestrator/wallet/sidechain_core_wallet_test.go @@ -0,0 +1,39 @@ +package wallet + +import ( + "context" + "encoding/hex" + "encoding/json" + "strings" + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// A Core derived sidechain gets its starter as one native segwit pair that +// scans from the tip. +func TestEnsureCoreWalletFromMnemonicImportsOneSegwitPair(t *testing.T) { + net := &chaincfg.RegressionNetParams + fake := newFakeBitcoind(t) + fake.stubEnsureFlow() + + require.NoError(t, EnsureCoreWalletFromMnemonic(context.Background(), fake.client(t), zerolog.Nop(), "starter", testMnemonic, net)) + + var imports []ImportDescriptor + require.NoError(t, json.Unmarshal(fake.callsFor("importdescriptors")[0].Params[0], &imports)) + require.Len(t, imports, 2) + assert.True(t, strings.HasPrefix(imports[0].Desc, "wpkh([73c5da0a/84'/1'/0']tprv"), imports[0].Desc) + assert.Equal(t, "now", imports[0].Timestamp) + assert.True(t, imports[1].Internal) + + receive, err := ParseDescriptor(imports[0].Desc) + require.NoError(t, err) + ds, _, err := receive.DeriveScript(false, 0, net) + require.NoError(t, err) + want, err := DeriveBIP84Addresses(hex.EncodeToString(MnemonicToSeed(testMnemonic, "")), net, 0, 1) + require.NoError(t, err) + assert.Equal(t, want[0], ds.address.EncodeAddress()) +} diff --git a/sidechain-orchestrator/wallet/wallet_descriptors.go b/sidechain-orchestrator/wallet/wallet_descriptors.go new file mode 100644 index 000000000..12e5365e7 --- /dev/null +++ b/sidechain-orchestrator/wallet/wallet_descriptors.go @@ -0,0 +1,123 @@ +package wallet + +import ( + "encoding/hex" + "encoding/json" + "errors" + "fmt" + + "github.com/btcsuite/btcd/chaincfg" +) + +// WalletDescriptors returns every descriptor a wallet derives, its receive +// default first. +func WalletDescriptors(w *WalletData, net *chaincfg.Params) ([]*Descriptor, error) { + kinds := ReceiveKinds(w) + out := make([]*Descriptor, 0, len(kinds)) + for _, kind := range kinds { + d, err := DescriptorFor(w, kind, net) + if err != nil { + return nil, err + } + out = append(out, d) + } + return out, nil +} + +// DescriptorFor returns the descriptor a wallet derives for one script kind. A +// multisig or watch-only wallet has one descriptor for every kind. +func DescriptorFor(w *WalletData, kind ScriptKind, net *chaincfg.Params) (*Descriptor, error) { + if net == nil { + return nil, errors.New("no chain params for this network; cannot derive the wallet") + } + switch { + case w.Multisig != nil: + return multisigDescriptor(w, "", net) + case w.Master.SeedHex != "": + ap, err := accountPathFor(w, kind, net) + if err != nil { + return nil, err + } + acct, origin, err := accountKeyAndOrigin(w.Master.SeedHex, ap, net) + if err != nil { + return nil, err + } + return &Descriptor{Kind: kind, Threshold: 1, Keys: []DescriptorKey{{Origin: origin, Account: acct}}}, nil + default: + desc, err := watchOnlyDescriptorString(w) + if err != nil { + return nil, err + } + // A bare xpub states no kind, so the one recorded at import decides. + return ParseDescriptorAs(desc, w.scriptKind()) + } +} + +// multisigDescriptor builds the multisig descriptor with each held cosigner as +// its account xprv, so the keys it derives can sign. A non-empty onlyXpub holds +// that one cosigner and leaves the rest as xpubs. +func multisigDescriptor(w *WalletData, onlyXpub string, net *chaincfg.Params) (*Descriptor, error) { + ms := w.Multisig + if ms == nil { + return nil, errors.New("wallet has no multisig config") + } + group := MultisigLoungeGroup{M: ms.M, N: ms.N} + signWithXprv := map[string]string{} + for _, c := range ms.Cosigners { + group.Keys = append(group.Keys, MultisigLoungeKey{ + Xpub: c.Xpub, + Fingerprint: c.Fingerprint, + OriginPath: c.OriginPath, + // A hardware signer needs the origin of every cosigner to rebuild the + // multisig script it signs over. + IsWallet: c.Fingerprint != "", + }) + if !c.Held() || (onlyXpub != "" && c.Xpub != onlyXpub) { + continue + } + xprv := c.Xprv + if xprv == "" { + seedHex := hex.EncodeToString(MnemonicToSeed(c.Mnemonic, c.Passphrase)) + x, _, err := DeriveAccountXprv(seedHex, "m/"+c.OriginPath, net) + if err != nil { + return nil, fmt.Errorf("derive cosigner xprv: %w", err) + } + xprv = x + } + signWithXprv[c.Xpub] = xprv + } + + scriptType := multisigTypeString(w.scriptKind()) + var receive string + var err error + if len(signWithXprv) > 0 { + receive, _, err = BuildMultisigSigningDescriptorsTyped(group, signWithXprv, scriptType) + } else { + receive, _, err = BuildMultisigLoungeDescriptorsTyped(group, scriptType) + } + if err != nil { + return nil, err + } + return ParseDescriptor(receive) +} + +// watchOnlyDescriptorString returns the descriptor (or bare xpub) stored in a +// watch-only wallet's payload, for ParseDescriptor. +func watchOnlyDescriptorString(w *WalletData) (string, error) { + var stored struct { + Xpub string `json:"xpub"` + Descriptor string `json:"descriptor"` + } + if len(w.WatchOnly) > 0 { + if err := json.Unmarshal(w.WatchOnly, &stored); err != nil { + return "", fmt.Errorf("parse watch-only data: %w", err) + } + } + if stored.Descriptor != "" { + return stored.Descriptor, nil + } + if stored.Xpub != "" { + return stored.Xpub, nil + } + return "", errors.New("watch-only wallet has no descriptor or xpub") +} diff --git a/sidechain-orchestrator/wallet/wallet_descriptors_test.go b/sidechain-orchestrator/wallet/wallet_descriptors_test.go new file mode 100644 index 000000000..2339fe1e1 --- /dev/null +++ b/sidechain-orchestrator/wallet/wallet_descriptors_test.go @@ -0,0 +1,57 @@ +package wallet + +import ( + "encoding/hex" + "encoding/json" + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWalletDescriptorsCoverEveryWalletShape(t *testing.T) { + net := &chaincfg.SigNetParams + seedHex := hex.EncodeToString(MnemonicToSeed(testMnemonic, "")) + otherSeedHex := hex.EncodeToString(MnemonicToSeed(testMnemonic, "other")) + cosigner := func(seed string) MultisigCosigner { + xpub, err := DeriveAccountXpub(seed, "m/48'/1'/0'/2'", net) + require.NoError(t, err) + return MultisigCosigner{Xpub: xpub, OriginPath: "48'/1'/0'/2'"} + } + + for _, c := range []struct { + name string + w *WalletData + kinds []ScriptKind + }{ + {"hot segwit", &WalletData{Master: MasterWallet{SeedHex: seedHex}}, []ScriptKind{ScriptNativeSegwit, ScriptTaproot}}, + {"hot legacy", &WalletData{Master: MasterWallet{SeedHex: seedHex}, ScriptType: "legacy"}, []ScriptKind{ScriptLegacy}}, + {"watch-only", &WalletData{WatchOnly: json.RawMessage(`{"xpub":"` + accountXpub(t, seedHex, net) + `"}`)}, []ScriptKind{ScriptNativeSegwit}}, + {"multisig", &WalletData{ScriptType: "multisig", Multisig: &MultisigWalletData{ + M: 1, N: 2, Cosigners: []MultisigCosigner{cosigner(seedHex), cosigner(otherSeedHex)}, + }}, []ScriptKind{ScriptMultisig}}, + } { + t.Run(c.name, func(t *testing.T) { + descriptors, err := WalletDescriptors(c.w, net) + require.NoError(t, err) + kinds := make([]ScriptKind, len(descriptors)) + for i, d := range descriptors { + kinds[i] = d.Kind + } + assert.Equal(t, c.kinds, kinds) + }) + } +} + +func TestWatchOnlyPreviewDerivesItsAddresses(t *testing.T) { + net := &chaincfg.SigNetParams + seedHex := hex.EncodeToString(MnemonicToSeed(testMnemonic, "")) + w := &WalletData{WatchOnly: json.RawMessage(`{"xpub":"` + accountXpub(t, seedHex, net) + `"}`)} + + got, err := DeriveWalletReceiveAddresses(w, net, 0, 2) + require.NoError(t, err) + want, err := DeriveBIP84Addresses(seedHex, net, 0, 2) + require.NoError(t, err) + assert.Equal(t, want, got) +} From f7463fdd0fa7cde7ce4d75323dceda6e7ae819de Mon Sep 17 00:00:00 2001 From: octobocto Date: Fri, 25 Sep 2026 21:33:41 -0700 Subject: [PATCH 03/13] orchestrator: hash BIP39 seeds from normal input --- sidechain-orchestrator/api/wallet_handler.go | 12 +++++-- sidechain-orchestrator/go.mod | 2 +- .../wallet/derive_keystore.go | 6 +++- .../wallet/derive_keystore_test.go | 5 +++ sidechain-orchestrator/wallet/keygen.go | 28 +++++++++++---- sidechain-orchestrator/wallet/keygen_test.go | 30 ++++++++++++++++ sidechain-orchestrator/wallet/service.go | 3 +- .../wallet/wallet_descriptors.go | 36 +++++++++++++++++-- .../wallet/wallet_descriptors_test.go | 26 ++++++++++++++ 9 files changed, 132 insertions(+), 16 deletions(-) diff --git a/sidechain-orchestrator/api/wallet_handler.go b/sidechain-orchestrator/api/wallet_handler.go index dcea2bb70..d47b4a418 100644 --- a/sidechain-orchestrator/api/wallet_handler.go +++ b/sidechain-orchestrator/api/wallet_handler.go @@ -450,11 +450,17 @@ func (h *WalletHandler) CreateMultisigWallet(ctx context.Context, req *connect.R cosigners := make([]wallet.MultisigCosigner, 0, len(req.Msg.Cosigners)) for _, c := range req.Msg.Cosigners { xpub := c.Xpub + mnemonic := c.Mnemonic // For a held cosigner, derive the authoritative xpub from its seed + // passphrase so the stored (watch-only) key can never disagree with the // signing key — the wallet always watches exactly what it can sign. - if c.Mnemonic != "" { - seedHex := hex.EncodeToString(wallet.MnemonicToSeed(c.Mnemonic, c.Passphrase)) + if mnemonic != "" { + var err error + mnemonic, err = wallet.ParseMnemonic(mnemonic) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("cosigner mnemonic: %w", err)) + } + seedHex := hex.EncodeToString(wallet.MnemonicToSeed(mnemonic, c.Passphrase)) _, derived, err := wallet.DeriveAccountXprv(seedHex, "m/"+c.OriginPath, net) if err != nil { return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("derive cosigner xpub: %w", err)) @@ -468,7 +474,7 @@ func (h *WalletHandler) CreateMultisigWallet(ctx context.Context, req *connect.R Xpub: xpub, OriginPath: c.OriginPath, Fingerprint: c.Fingerprint, - Mnemonic: c.Mnemonic, + Mnemonic: mnemonic, Passphrase: c.Passphrase, Xprv: c.Xprv, HardwareDeviceType: c.HardwareDeviceType, diff --git a/sidechain-orchestrator/go.mod b/sidechain-orchestrator/go.mod index 205a12f41..dfaf4e3c5 100644 --- a/sidechain-orchestrator/go.mod +++ b/sidechain-orchestrator/go.mod @@ -23,6 +23,7 @@ require ( github.com/urfave/cli/v2 v2.27.7 golang.org/x/crypto v0.53.0 golang.org/x/net v0.55.0 + golang.org/x/text v0.38.0 google.golang.org/protobuf v1.36.11 lukechampine.com/blake3 v1.4.1 ) @@ -60,7 +61,6 @@ require ( github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect golang.org/x/sys v0.46.0 - golang.org/x/text v0.38.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/sidechain-orchestrator/wallet/derive_keystore.go b/sidechain-orchestrator/wallet/derive_keystore.go index 50fbc689e..b889e49c9 100644 --- a/sidechain-orchestrator/wallet/derive_keystore.go +++ b/sidechain-orchestrator/wallet/derive_keystore.go @@ -57,7 +57,11 @@ func DeriveKeystore( var out DerivedKeystore switch { case src.Mnemonic != "": - seedHex := hex.EncodeToString(MnemonicToSeed(src.Mnemonic, src.Passphrase)) + mnemonic, merr := ParseMnemonic(src.Mnemonic) + if merr != nil { + return DerivedKeystore{}, merr + } + seedHex := hex.EncodeToString(MnemonicToSeed(mnemonic, src.Passphrase)) xpub, xerr := DeriveAccountXpub(seedHex, path, net) if xerr != nil { return DerivedKeystore{}, xerr diff --git a/sidechain-orchestrator/wallet/derive_keystore_test.go b/sidechain-orchestrator/wallet/derive_keystore_test.go index fc4f6f4b0..198edd0c0 100644 --- a/sidechain-orchestrator/wallet/derive_keystore_test.go +++ b/sidechain-orchestrator/wallet/derive_keystore_test.go @@ -45,6 +45,11 @@ func TestDeriveKeystoreSingleSigMnemonic(t *testing.T) { require.Equal(t, "84'/0'/0'", out.OriginPath) // AccountPath renders hardened with ' } +func TestDeriveKeystoreRefusesAnInvalidMnemonic(t *testing.T) { + _, err := DeriveKeystore(context.Background(), KeystoreSource{Mnemonic: "abandon abandon"}, "native-segwit", false, 0, "", &chaincfg.MainNetParams) + require.ErrorContains(t, err, "invalid mnemonic") +} + func TestDeriveKeystoreMultisigMnemonic(t *testing.T) { net := &chaincfg.MainNetParams out, err := DeriveKeystore(context.Background(), KeystoreSource{Mnemonic: testMnemonic}, "wsh", true, 0, "", net) diff --git a/sidechain-orchestrator/wallet/keygen.go b/sidechain-orchestrator/wallet/keygen.go index 0af7c420e..11e292950 100644 --- a/sidechain-orchestrator/wallet/keygen.go +++ b/sidechain-orchestrator/wallet/keygen.go @@ -3,12 +3,14 @@ package wallet import ( "crypto/sha256" "encoding/hex" + "errors" "fmt" "strconv" "strings" "github.com/tyler-smith/go-bip32" "github.com/tyler-smith/go-bip39" + "golang.org/x/text/unicode/norm" ) // GenerateMnemonic generates a new 12-word BIP39 mnemonic. @@ -24,9 +26,24 @@ func GenerateMnemonic() (string, error) { return mnemonic, nil } -// MnemonicToSeed converts a mnemonic to a seed using PBKDF2-HMAC-SHA512. +// MnemonicToSeed returns the BIP39 seed of a mnemonic and a passphrase. func MnemonicToSeed(mnemonic, passphrase string) []byte { - return bip39.NewSeed(mnemonic, passphrase) + return bip39.NewSeed(NormalizeMnemonic(mnemonic), norm.NFKD.String(passphrase)) +} + +// NormalizeMnemonic returns a mnemonic in the form BIP39 hashes: NFKD, lower +// case, and one space between words. +func NormalizeMnemonic(mnemonic string) string { + return strings.Join(strings.Fields(strings.ToLower(norm.NFKD.String(mnemonic))), " ") +} + +// ParseMnemonic returns a valid BIP39 mnemonic in its normal form. +func ParseMnemonic(mnemonic string) (string, error) { + normal := NormalizeMnemonic(mnemonic) + if !bip39.IsMnemonicValid(normal) { + return "", errors.New("invalid mnemonic") + } + return normal, nil } // DeriveStarter derives a child mnemonic from a seed at the given derivation path. @@ -138,11 +155,10 @@ func GenerateFullWallet(name string, customMnemonic string, passphrase string, s var err error if customMnemonic != "" { - // Validate the provided mnemonic - if !bip39.IsMnemonicValid(customMnemonic) { - return nil, fmt.Errorf("invalid mnemonic") + mnemonic, err = ParseMnemonic(customMnemonic) + if err != nil { + return nil, err } - mnemonic = customMnemonic } else { mnemonic, err = GenerateMnemonic() if err != nil { diff --git a/sidechain-orchestrator/wallet/keygen_test.go b/sidechain-orchestrator/wallet/keygen_test.go index c89245e8c..15816f277 100644 --- a/sidechain-orchestrator/wallet/keygen_test.go +++ b/sidechain-orchestrator/wallet/keygen_test.go @@ -1,6 +1,7 @@ package wallet import ( + "crypto/sha512" "encoding/hex" "strings" "testing" @@ -9,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/tyler-smith/go-bip32" "github.com/tyler-smith/go-bip39" + "golang.org/x/crypto/pbkdf2" ) func TestGenerateMnemonic(t *testing.T) { @@ -65,6 +67,16 @@ func TestMnemonicToSeedWithPassphrase(t *testing.T) { assert.NotEqual(t, seed1, seed2, "different passphrases should produce different seeds") } +// BIP39 hashes the passphrase in NFKD, so a composed and a decomposed é give +// one seed. +func TestMnemonicToSeedAppliesNFKDToThePassphrase(t *testing.T) { + composed := MnemonicToSeed(testMnemonic, "caf\u00e9") + decomposed := MnemonicToSeed(testMnemonic, "cafe\u0301") + assert.Equal(t, decomposed, composed) + assert.Equal(t, pbkdf2.Key([]byte(testMnemonic), []byte("mnemonic"+"cafe\u0301"), 2048, 64, sha512.New), composed) + assert.NotEqual(t, bip39.NewSeed(testMnemonic, "caf\u00e9"), composed) +} + func TestMnemonicToSeedDeterministic(t *testing.T) { mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" seed1 := MnemonicToSeed(mnemonic, "test") @@ -218,6 +230,24 @@ func TestGenerateFullWalletWithCustomMnemonic(t *testing.T) { assert.Equal(t, expectedSeedHex, wallet.Master.SeedHex) } +// A pasted phrase keeps its words, not its whitespace or its case. +func TestGenerateFullWalletNormalizesThePastedPhrase(t *testing.T) { + const seedHex = "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4" + for _, pasted := range []string{ + testMnemonic + "\n", + " " + testMnemonic, + strings.ReplaceAll(testMnemonic, " ", "\t"), + strings.ReplaceAll(testMnemonic, " ", " "), + strings.ReplaceAll(testMnemonic, " ", "\u00a0"), + "Abandon" + strings.TrimPrefix(testMnemonic, "abandon"), + } { + wallet, err := GenerateFullWallet("Pasted", pasted, "", nil, WalletTypeElectrum) + require.NoError(t, err, "%q", pasted) + assert.Equal(t, testMnemonic, wallet.Master.Mnemonic, "%q", pasted) + assert.Equal(t, seedHex, wallet.Master.SeedHex, "%q", pasted) + } +} + func TestGenerateFullWalletInvalidCustomMnemonic(t *testing.T) { _, err := GenerateFullWallet("Bad", "invalid mnemonic words that are not real bip39", "", nil, "enforcer") assert.Error(t, err) diff --git a/sidechain-orchestrator/wallet/service.go b/sidechain-orchestrator/wallet/service.go index 86afcd805..c803849a4 100644 --- a/sidechain-orchestrator/wallet/service.go +++ b/sidechain-orchestrator/wallet/service.go @@ -631,8 +631,7 @@ func (s *Service) GenerateWalletFromEntropy(entropy []byte, passphrase string, d return nil, fmt.Errorf("create mnemonic from entropy: %w", err) } - // Generate seed (bip39 library uses PBKDF2-HMAC-SHA512 internally) - seed := bip39.NewSeed(mnemonic, passphrase) + seed := MnemonicToSeed(mnemonic, passphrase) seedHex := hex.EncodeToString(seed) // Create master key diff --git a/sidechain-orchestrator/wallet/wallet_descriptors.go b/sidechain-orchestrator/wallet/wallet_descriptors.go index 12e5365e7..c189948cf 100644 --- a/sidechain-orchestrator/wallet/wallet_descriptors.go +++ b/sidechain-orchestrator/wallet/wallet_descriptors.go @@ -1,11 +1,13 @@ package wallet import ( + "bytes" "encoding/hex" "encoding/json" "errors" "fmt" + "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" ) @@ -77,10 +79,9 @@ func multisigDescriptor(w *WalletData, onlyXpub string, net *chaincfg.Params) (* } xprv := c.Xprv if xprv == "" { - seedHex := hex.EncodeToString(MnemonicToSeed(c.Mnemonic, c.Passphrase)) - x, _, err := DeriveAccountXprv(seedHex, "m/"+c.OriginPath, net) + x, err := cosignerXprv(c, net) if err != nil { - return nil, fmt.Errorf("derive cosigner xprv: %w", err) + return nil, err } xprv = x } @@ -101,6 +102,35 @@ func multisigDescriptor(w *WalletData, onlyXpub string, net *chaincfg.Params) (* return ParseDescriptor(receive) } +// cosignerXprv derives the account xprv of a cosigner held as a mnemonic. It +// must be the key the stored xpub names. +func cosignerXprv(c MultisigCosigner, net *chaincfg.Params) (string, error) { + stored, err := parseKeyExpr(c.Xpub) + if err != nil { + return "", fmt.Errorf("cosigner xpub: %w", err) + } + seedHex := hex.EncodeToString(MnemonicToSeed(c.Mnemonic, c.Passphrase)) + key, err := deriveMultisigAccountKey(seedHex, "m/"+c.OriginPath, net) + if err != nil { + return "", fmt.Errorf("derive cosigner xprv: %w", err) + } + if !sameAccountKey(key, stored.Account) { + return "", fmt.Errorf("the seed of cosigner %s does not derive its stored key", shortXpub(c.Xpub)) + } + return key.String(), nil +} + +// sameAccountKey reports whether two extended keys hold the same public key +// and chain code. +func sameAccountKey(a, b *hdkeychain.ExtendedKey) bool { + pubA, errA := a.ECPubKey() + pubB, errB := b.ECPubKey() + if errA != nil || errB != nil { + return false + } + return bytes.Equal(pubA.SerializeCompressed(), pubB.SerializeCompressed()) && bytes.Equal(a.ChainCode(), b.ChainCode()) +} + // watchOnlyDescriptorString returns the descriptor (or bare xpub) stored in a // watch-only wallet's payload, for ParseDescriptor. func watchOnlyDescriptorString(w *WalletData) (string, error) { diff --git a/sidechain-orchestrator/wallet/wallet_descriptors_test.go b/sidechain-orchestrator/wallet/wallet_descriptors_test.go index 2339fe1e1..543c88547 100644 --- a/sidechain-orchestrator/wallet/wallet_descriptors_test.go +++ b/sidechain-orchestrator/wallet/wallet_descriptors_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "testing" + "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -55,3 +56,28 @@ func TestWatchOnlyPreviewDerivesItsAddresses(t *testing.T) { require.NoError(t, err) assert.Equal(t, want, got) } + +// A cosigner signs only with the key its stored xpub names. +func TestCosignerSignsWithTheKeyItsXpubNames(t *testing.T) { + net := &chaincfg.SigNetParams + const passphrase = "bl\u00e5b\u00e6r" + const path = "48'/1'/0'/2'" + xpubOf := func(seed []byte) string { + xpub, err := DeriveAccountXpub(hex.EncodeToString(seed), "m/"+path, net) + require.NoError(t, err) + return xpub + } + + own := xpubOf(MnemonicToSeed(testMnemonic, passphrase)) + xprv, err := cosignerXprv(MultisigCosigner{Xpub: own, OriginPath: path, Mnemonic: testMnemonic, Passphrase: passphrase}, net) + require.NoError(t, err) + key, err := hdkeychain.NewKeyFromString(xprv) + require.NoError(t, err) + pub, err := key.Neuter() + require.NoError(t, err) + assert.Equal(t, own, pub.String()) + + foreign := MultisigCosigner{Xpub: xpubOf(MnemonicToSeed(testMnemonic, "other")), OriginPath: path, Mnemonic: testMnemonic, Passphrase: passphrase} + _, err = cosignerXprv(foreign, net) + require.ErrorContains(t, err, "does not derive its stored key") +} From 090bbd156d8ea05d02bb5a03a6e24abfa481517d Mon Sep 17 00:00:00 2001 From: octobocto Date: Fri, 25 Sep 2026 21:33:41 -0700 Subject: [PATCH 04/13] sailui: count seed words on any whitespace --- sail_ui/lib/pages/create_wallet_page.dart | 2 +- .../test/pages/create_wallet_screen_test.dart | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/sail_ui/lib/pages/create_wallet_page.dart b/sail_ui/lib/pages/create_wallet_page.dart index 33100aa2e..7e5a8a8f0 100644 --- a/sail_ui/lib/pages/create_wallet_page.dart +++ b/sail_ui/lib/pages/create_wallet_page.dart @@ -783,7 +783,7 @@ class _SailCreateWalletPageState extends State { } bool _isValidMnemonic(String mnemonic) { - final words = mnemonic.trim().split(' '); + final words = mnemonic.trim().split(RegExp(r'\s+')); return words.length == 12 || words.length == 24; } diff --git a/sail_ui/test/pages/create_wallet_screen_test.dart b/sail_ui/test/pages/create_wallet_screen_test.dart index 02fed7902..ae31ea8ae 100644 --- a/sail_ui/test/pages/create_wallet_screen_test.dart +++ b/sail_ui/test/pages/create_wallet_screen_test.dart @@ -17,6 +17,27 @@ class _FakeWriter extends WalletWriterProvider { Future hasExistingWallet() async => false; } +/// Records the phrase a restore hands the backend. +class _RecordingWriter extends _FakeWriter { + String? restored; + + @override + Future createElectrumWallet({ + required String name, + required WalletGradient gradient, + String? customMnemonic, + String? passphrase, + String? xpubOrDescriptor, + String? scriptType, + int account = 0, + String? derivationPath, + String? hardwareDeviceType, + String? hardwareFingerprint, + }) async { + restored = customMnemonic; + } +} + Future _pumpPage(WidgetTester tester) async { await tester.binding.setSurfaceSize(const Size(1440, 1024)); addTearDown(() => tester.binding.setSurfaceSize(null)); @@ -70,6 +91,28 @@ void main() { ); }); + // A phrase pasted with more than one space between words is still 12 words. + testWidgets('a seed with extra spaces reaches the backend', (tester) async { + final writer = _RecordingWriter(); + GetIt.I.unregister(); + GetIt.I.registerSingleton(writer); + await _pumpPage(tester); + await tester.tap(find.text('Paste a seed phrase')); + await tester.pumpAndSettle(); + + const pasted = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\n'; + await tester.enterText( + find.widgetWithText(TextField, 'Paste your seed phrase — 12 or 24 words, separated by spaces'), + pasted, + ); + // The button draws its label twice, so both finds name the one button. + await tester.tap(find.widgetWithText(SailButton, 'Restore').first); + await tester.pump(); + + expect(find.textContaining('Invalid mnemonic format'), findsNothing); + expect(writer.restored, pasted); + }); + // Electrum takes a passphrase like every other backend; the field used to be // disabled there, which made a typed value unclearable and every restore fail. testWidgets('the passphrase field stays usable on electrum', (tester) async { From 5be05cf1ae81409fe8ef80a44945b5c99b05cd4b Mon Sep 17 00:00:00 2001 From: octobocto Date: Fri, 25 Sep 2026 21:36:23 -0700 Subject: [PATCH 05/13] orchestrator: name an Electrum seed on restore --- sidechain-orchestrator/wallet/keygen.go | 26 +++++++++++++++++--- sidechain-orchestrator/wallet/keygen_test.go | 13 ++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/sidechain-orchestrator/wallet/keygen.go b/sidechain-orchestrator/wallet/keygen.go index 11e292950..ccfdd16c5 100644 --- a/sidechain-orchestrator/wallet/keygen.go +++ b/sidechain-orchestrator/wallet/keygen.go @@ -1,7 +1,9 @@ package wallet import ( + "crypto/hmac" "crypto/sha256" + "crypto/sha512" "encoding/hex" "errors" "fmt" @@ -40,10 +42,28 @@ func NormalizeMnemonic(mnemonic string) string { // ParseMnemonic returns a valid BIP39 mnemonic in its normal form. func ParseMnemonic(mnemonic string) (string, error) { normal := NormalizeMnemonic(mnemonic) - if !bip39.IsMnemonicValid(normal) { - return "", errors.New("invalid mnemonic") + if bip39.IsMnemonicValid(normal) { + return normal, nil } - return normal, nil + if isElectrumSeed(normal) { + return "", errors.New("the phrase is an Electrum seed, not a BIP39 seed; restore it in Electrum") + } + return "", errors.New("invalid mnemonic") +} + +// isElectrumSeed reports whether a phrase in normal form is a seed of the +// Electrum app, whose words hash to a version prefix. The normal form is the +// Electrum form for a phrase with no accents and no CJK characters. +func isElectrumSeed(normal string) bool { + mac := hmac.New(sha512.New, []byte("Seed version")) + mac.Write([]byte(normal)) + version := hex.EncodeToString(mac.Sum(nil)) + for _, prefix := range []string{"01", "100", "101", "102"} { + if strings.HasPrefix(version, prefix) { + return true + } + } + return false } // DeriveStarter derives a child mnemonic from a seed at the given derivation path. diff --git a/sidechain-orchestrator/wallet/keygen_test.go b/sidechain-orchestrator/wallet/keygen_test.go index 15816f277..212853a4f 100644 --- a/sidechain-orchestrator/wallet/keygen_test.go +++ b/sidechain-orchestrator/wallet/keygen_test.go @@ -248,6 +248,19 @@ func TestGenerateFullWalletNormalizesThePastedPhrase(t *testing.T) { } } +// An Electrum-app seed fails the BIP39 checksum, and the error names it. +func TestParseMnemonicNamesAnElectrumSeed(t *testing.T) { + for _, seed := range []string{ + "bitter grass shiver impose acquire brush forget axis eager alone wine silver", + "cycle rocket west magnet parrot shuffle foot correct salt library feed song", + } { + _, err := ParseMnemonic(seed) + require.ErrorContains(t, err, "Electrum seed", seed) + } + _, err := ParseMnemonic("abandon abandon abandon") + require.EqualError(t, err, "invalid mnemonic") +} + func TestGenerateFullWalletInvalidCustomMnemonic(t *testing.T) { _, err := GenerateFullWallet("Bad", "invalid mnemonic words that are not real bip39", "", nil, "enforcer") assert.Error(t, err) From 8657b0a1df897dddca86ba31a24aea27b075f487 Mon Sep 17 00:00:00 2001 From: octobocto Date: Fri, 25 Sep 2026 21:36:24 -0700 Subject: [PATCH 06/13] bitwindow: name an Electrum seed on import --- .../lib/pages/welcome/wallet_backup_page.dart | 16 +++++++++++++--- bitwindow/test/wallet_backup_page_test.dart | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/bitwindow/lib/pages/welcome/wallet_backup_page.dart b/bitwindow/lib/pages/welcome/wallet_backup_page.dart index a1ba43685..b67ffd823 100644 --- a/bitwindow/lib/pages/welcome/wallet_backup_page.dart +++ b/bitwindow/lib/pages/welcome/wallet_backup_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:bip39_mnemonic/bip39_mnemonic.dart'; +import 'package:crypto/crypto.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get_it/get_it.dart'; @@ -14,6 +15,13 @@ class SeedBackup { const SeedBackup(this.mnemonic, this.passphrase); } +/// Whether a phrase is a seed of the Electrum app, whose words hash to a +/// version prefix. A BIP39 wallet cannot restore it. +bool _isElectrumSeed(String phrase) { + final version = Hmac(sha512, utf8.encode('Seed version')).convert(utf8.encode(phrase)).toString(); + return const ['01', '100', '101', '102'].any(version.startsWith); +} + /// Where the seed comes from: minted here, or one the user already has. enum SeedEntryMode { generate, importExisting } @@ -876,9 +884,11 @@ class _WalletBackupPageState extends State { ], ) : SailText.secondary13( - complete - ? 'These words are not a valid BIP39 phrase — check for a typo' - : '$_importedCount of ${_imported.length} words entered', + !complete + ? '$_importedCount of ${_imported.length} words entered' + : _isElectrumSeed(_importedWords.join(' ')) + ? 'These words are an Electrum seed, not a BIP39 phrase — restore them in Electrum' + : 'These words are not a valid BIP39 phrase — check for a typo', color: complete ? theme.colors.error : theme.colors.textSecondary, ), ), diff --git a/bitwindow/test/wallet_backup_page_test.dart b/bitwindow/test/wallet_backup_page_test.dart index f6d9c6b7b..1904121e7 100644 --- a/bitwindow/test/wallet_backup_page_test.dart +++ b/bitwindow/test/wallet_backup_page_test.dart @@ -462,6 +462,20 @@ void main() { expect(_createWalletButton(tester).disabled, isTrue); }); + // An Electrum-app seed fails the checksum too, and the line names it. + testWidgets('an Electrum seed is named as one', (tester) async { + await _pumpPage(tester, mode: SeedEntryMode.importExisting); + + await tester.enterText( + find.byType(TextField).first, + 'bitter grass shiver impose acquire brush forget axis eager alone wine silver', + ); + await tester.pump(); + + expect(find.textContaining('an Electrum seed'), findsOneWidget); + expect(_createWalletButton(tester).disabled, isTrue); + }); + // The grid opens at 12, so a 24-word backup has to grow it on paste. testWidgets('pasting 24 words switches the grid to 24', (tester) async { await _pumpPage(tester, mode: SeedEntryMode.importExisting); From e6c41dbd53d78ab876cc1b976be5dee8aa511a09 Mon Sep 17 00:00:00 2001 From: octobocto Date: Fri, 25 Sep 2026 21:41:27 -0700 Subject: [PATCH 07/13] orchestrator: create watch-only wallets one way --- sidechain-orchestrator/wallet/core_backend.go | 93 +++++---------- .../wallet/core_backend_test.go | 93 +++++++++++++++ .../core_watch_only_integration_test.go | 91 ++++++++++++++ .../wallet/migrate_script_type_test.go | 29 +++++ sidechain-orchestrator/wallet/service.go | 111 ++++++++---------- 5 files changed, 292 insertions(+), 125 deletions(-) create mode 100644 sidechain-orchestrator/wallet/core_watch_only_integration_test.go diff --git a/sidechain-orchestrator/wallet/core_backend.go b/sidechain-orchestrator/wallet/core_backend.go index 2535575eb..4f3def2de 100644 --- a/sidechain-orchestrator/wallet/core_backend.go +++ b/sidechain-orchestrator/wallet/core_backend.go @@ -174,13 +174,7 @@ func (p *CoreBackend) Ensure(ctx context.Context, walletID string) (string, erro var err error switch targetWallet.WalletType { case WalletTypeBitcoinCore: - // Watch-only Core wallets import a descriptor; full wallets create from - // the seed. Both run on the same Core backend. - if targetWallet.IsWatchOnly() { - err = p.createWatchOnlyWallet(ctx, walletName, targetWallet) - } else { - err = p.createBitcoinCoreWallet(ctx, walletName, targetWallet) - } + err = p.createCoreWallet(ctx, walletName, targetWallet) default: return "", fmt.Errorf("wallet type %s does not use Bitcoin Core", targetWallet.WalletType) } @@ -1664,9 +1658,33 @@ func (p *CoreBackend) retryBip47NotificationDescriptor(ctx context.Context, wall delete(p.bip47NotifRetry, walletID) } -// createBitcoinCoreWallet creates a Bitcoin Core descriptor wallet that holds -// every descriptor the wallet derives. -func (p *CoreBackend) createBitcoinCoreWallet(ctx context.Context, walletName string, w *WalletData) error { +// descriptorOnlyCoreReads returns the stored descriptor of a watch-only wallet +// when the parser rejects it. Core reads more policies, such as wsh(multi(...)). +func descriptorOnlyCoreReads(w *WalletData) (string, bool) { + stored := w.watchOnlyField("descriptor") + if stored == "" { + return "", false + } + _, err := ParseDescriptor(stored) + return stored, err != nil +} + +// createCoreWallet creates a Bitcoin Core descriptor wallet that holds every +// descriptor the wallet derives. A watch-only wallet holds no private key. +func (p *CoreBackend) createCoreWallet(ctx context.Context, walletName string, w *WalletData) error { + if stored, ok := descriptorOnlyCoreReads(w); ok { + body, err := stripDescriptorChecksum(stored) + if err != nil { + return err + } + desc, err := AddDescriptorChecksum(body) + if err != nil { + return err + } + return p.createAndImport(ctx, walletName, true, []ImportDescriptor{{ + Desc: desc, Active: true, Timestamp: importTimestamp(w), Range: []int{0, 999}, + }}) + } net := p.net() descriptors, err := WalletDescriptors(w, net) if err != nil { @@ -1680,7 +1698,7 @@ func (p *CoreBackend) createBitcoinCoreWallet(ctx context.Context, walletName st } imports = append(imports, pair...) } - return p.createAndImport(ctx, walletName, false, imports) + return p.createAndImport(ctx, walletName, w.IsWatchOnly(), imports) } // coreDescriptorWrapper returns the open/close fragments wrapping the key @@ -1716,59 +1734,6 @@ func purposeToCoreKind(purpose uint32) (ScriptKind, bool) { } } -// createWatchOnlyWallet creates a watch-only Bitcoin Core wallet. -func (p *CoreBackend) createWatchOnlyWallet(ctx context.Context, walletName string, w *WalletData) error { - if w.WatchOnly == nil { - return fmt.Errorf("watch-only wallet missing watch_only data") - } - - var watchOnly struct { - Descriptor string `json:"descriptor"` - Xpub string `json:"xpub"` - } - if err := json.Unmarshal(w.WatchOnly, &watchOnly); err != nil { - return fmt.Errorf("parse watch_only: %w", err) - } - - var descriptors []ImportDescriptor - if watchOnly.Descriptor != "" { - desc := watchOnly.Descriptor - if !strings.Contains(desc, "#") { - var err error - desc, err = AddDescriptorChecksum(desc) - if err != nil { - return fmt.Errorf("add checksum: %w", err) - } - } - descriptors = append(descriptors, ImportDescriptor{ - Desc: desc, - Active: true, - Timestamp: "now", - Range: []int{0, 1000}, - }) - } else if watchOnly.Xpub != "" { - descriptors = append(descriptors, - ImportDescriptor{ - Desc: mustAddChecksum(fmt.Sprintf("wpkh(%s/0/*)", watchOnly.Xpub)), - Active: true, - Timestamp: "now", - Range: []int{0, 1000}, - }, - ImportDescriptor{ - Desc: mustAddChecksum(fmt.Sprintf("wpkh(%s/1/*)", watchOnly.Xpub)), - Active: true, - Timestamp: "now", - Internal: true, - Range: []int{0, 1000}, - }, - ) - } else { - return fmt.Errorf("watch-only wallet requires descriptor or xpub") - } - - return p.createAndImport(ctx, walletName, true, descriptors) -} - // createAndImport creates a Core wallet and imports descriptors. func (p *CoreBackend) createAndImport(ctx context.Context, walletName string, disablePrivateKeys bool, descriptors []ImportDescriptor) error { return createAndImport(ctx, p.rpc, p.log, walletName, disablePrivateKeys, descriptors) diff --git a/sidechain-orchestrator/wallet/core_backend_test.go b/sidechain-orchestrator/wallet/core_backend_test.go index 11effa38d..cbda9ff19 100644 --- a/sidechain-orchestrator/wallet/core_backend_test.go +++ b/sidechain-orchestrator/wallet/core_backend_test.go @@ -9,6 +9,7 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "strconv" "strings" "sync" @@ -2561,3 +2562,95 @@ func TestCoreReceiveListAddsEveryCoinOnAnAddress(t *testing.T) { require.Len(t, out, 1) require.EqualValues(t, 3, out[0].BalanceSats) } + +// Core reads more policies than the parser. An old watch-only wallet that holds +// one imports it as the user gave it, and scans from genesis. +func TestCoreBackendImportsAPolicyTheParserRejectsAsStored(t *testing.T) { + net := &chaincfg.RegressionNetParams + first := accountXpub(t, hex.EncodeToString(MnemonicToSeed(testMnemonic, "")), net) + second := accountXpub(t, hex.EncodeToString(MnemonicToSeed(testMnemonic, "second")), net) + multi := "wsh(multi(1," + first + "/0/*," + second + "/0/*))" + _, err := ParseDescriptor(multi) + require.Error(t, err) + summed, err := AddDescriptorChecksum(multi) + require.NoError(t, err) + + dir := t.TempDir() + body, err := json.Marshal(map[string]any{ + "wallets": []map[string]any{ + {"id": "WATCHBARE", "name": "Bare", "wallet_type": "bitcoinCore", "watch_only": map[string]string{"descriptor": multi}}, + {"id": "WATCHSUMMED", "name": "Summed", "wallet_type": "bitcoinCore", "watch_only": map[string]string{"descriptor": summed}}, + }, + "activeWalletId": "WATCHBARE", + }) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(dir, "wallet.json"), body, 0o600)) + svc := NewService(dir, zerolog.New(zerolog.NewTestWriter(t))) + require.NoError(t, svc.Init()) + t.Cleanup(svc.Close) + + fake := newFakeBitcoind(t) + fake.stubEnsureFlow() + backend := NewCoreBackend(svc, fake.client(t), StaticParams(net), zerolog.New(zerolog.NewTestWriter(t))) + for _, id := range []string{"WATCHBARE", "WATCHSUMMED"} { + _, err := backend.Ensure(context.Background(), id) + require.NoError(t, err, id) + + creates := fake.callsFor("createwallet") + var disablePrivateKeys bool + require.NoError(t, json.Unmarshal(creates[len(creates)-1].Params[1], &disablePrivateKeys)) + assert.True(t, disablePrivateKeys, id) + + calls := fake.callsFor("importdescriptors") + var imports []ImportDescriptor + require.NoError(t, json.Unmarshal(calls[len(calls)-1].Params[0], &imports)) + require.Len(t, imports, 1, id) + assert.Equal(t, summed, imports[0].Desc, id) + assert.True(t, imports[0].Active, id) + assert.False(t, imports[0].Internal, id) + assert.Equal(t, float64(0), asFloat(t, imports[0].Timestamp), id) + } +} + +// A watch-only Core wallet imports both branches of its key with no private +// keys. The key can hold history of any age, so Core scans from genesis. +func TestCoreBackendImportsAWatchOnlyKeyOnBothBranches(t *testing.T) { + net := &chaincfg.RegressionNetParams + svc := newTestService(t) + seedHex := hex.EncodeToString(MnemonicToSeed(testMnemonic, "")) + zpub := reencodeVersion(t, accountXpub(t, seedHex, &chaincfg.MainNetParams), 0x04B24746) + require.NoError(t, svc.CreateWatchOnlyWallet("Watch", zpub, `{"background_svg":""}`)) + w := svc.GetWalletByID(svc.ActiveWalletID()) + require.NotNil(t, w) + assert.True(t, w.Imported) + assert.Equal(t, ScriptNativeSegwit, w.scriptKind()) + + fake := newFakeBitcoind(t) + fake.stubEnsureFlow() + backend := NewCoreBackend(svc, fake.client(t), StaticParams(net), zerolog.New(zerolog.NewTestWriter(t))) + _, err := backend.Ensure(context.Background(), w.ID) + require.NoError(t, err) + + creates := fake.callsFor("createwallet") + require.Len(t, creates, 1) + var disablePrivateKeys bool + require.NoError(t, json.Unmarshal(creates[0].Params[1], &disablePrivateKeys)) + assert.True(t, disablePrivateKeys) + + var imports []ImportDescriptor + require.NoError(t, json.Unmarshal(fake.callsFor("importdescriptors")[0].Params[0], &imports)) + require.Len(t, imports, 2) + assert.True(t, strings.HasPrefix(imports[0].Desc, "wpkh(tpub"), imports[0].Desc) + assert.Contains(t, imports[0].Desc, "/0/*)#") + assert.False(t, imports[0].Internal) + assert.Contains(t, imports[1].Desc, "/1/*)#") + assert.True(t, imports[1].Internal) + assert.Equal(t, float64(0), asFloat(t, imports[0].Timestamp)) + + receive, err := ParseDescriptor(imports[0].Desc) + require.NoError(t, err) + ds, _, err := receive.DeriveScript(false, 0, net) + require.NoError(t, err) + // The key hash of the BIP84 test vector, in the regtest encoding. + assert.Equal(t, "bcrt1qcr8te4kr609gcawutmrza0j4xv80jy8zeqchgx", ds.address.EncodeAddress()) +} diff --git a/sidechain-orchestrator/wallet/core_watch_only_integration_test.go b/sidechain-orchestrator/wallet/core_watch_only_integration_test.go new file mode 100644 index 000000000..dece9c34e --- /dev/null +++ b/sidechain-orchestrator/wallet/core_watch_only_integration_test.go @@ -0,0 +1,91 @@ +//go:build integration + +package wallet_test + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/json" + "os" + "path/filepath" + "testing" + "time" + + "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/testharness" + "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/wallet" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/btcutil/hdkeychain" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" +) + +// Core reads more policies than the parser. An old watch-only wallet with +// wsh(multi(...)) loads into Core, and Core keeps the sequence of its keys. +func TestCoreWatchOnlyKeepsAPolicyTheParserRejects(t *testing.T) { + h := testharness.New(t, 1) + defer h.Close() + node := h.Nodes[0] + net := &chaincfg.RegressionNetParams + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + const mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + var keys []*hdkeychain.ExtendedKey + var pubs [][]byte + for _, passphrase := range []string{"", "second"} { + master, err := hdkeychain.NewMaster(wallet.MnemonicToSeed(mnemonic, passphrase), net) + require.NoError(t, err) + key, err := master.Neuter() + require.NoError(t, err) + child, err := key.Derive(0) + require.NoError(t, err) + child, err = child.Derive(0) + require.NoError(t, err) + pub, err := child.ECPubKey() + require.NoError(t, err) + keys = append(keys, key) + pubs = append(pubs, pub.SerializeCompressed()) + } + // Out of sorted sequence, so a sortedmulti import gives another address. + if bytes.Compare(pubs[0], pubs[1]) < 0 { + keys[0], keys[1] = keys[1], keys[0] + pubs[0], pubs[1] = pubs[1], pubs[0] + } + multi := "wsh(multi(1," + keys[0].String() + "/0/*," + keys[1].String() + "/0/*))" + + dir := t.TempDir() + body, err := json.Marshal(map[string]any{ + "wallets": []map[string]any{ + {"id": "WATCHMULTI", "name": "Watch", "wallet_type": "bitcoinCore", "watch_only": map[string]string{"descriptor": multi}}, + }, + "activeWalletId": "WATCHMULTI", + }) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(dir, "wallet.json"), body, 0o600)) + svc := wallet.NewService(dir, zerolog.Nop()) + require.NoError(t, svc.Init()) + defer svc.Close() + + backend := wallet.NewCoreBackend(svc, node.CoreRPC, wallet.StaticParams(net), zerolog.Nop()) + name, err := backend.Ensure(ctx, "WATCHMULTI") + require.NoError(t, err) + got, err := node.CoreRPC.GetNewAddress(ctx, name, "", "bech32") + require.NoError(t, err) + + var given []*btcutil.AddressPubKey + for _, pub := range pubs { + a, err := btcutil.NewAddressPubKey(pub, net) + require.NoError(t, err) + given = append(given, a) + } + script, err := txscript.MultiSigScript(given, 1) + require.NoError(t, err) + hash := sha256.Sum256(script) + want, err := btcutil.NewAddressWitnessScriptHash(hash[:], net) + require.NoError(t, err) + require.Equal(t, want.EncodeAddress(), got) +} diff --git a/sidechain-orchestrator/wallet/migrate_script_type_test.go b/sidechain-orchestrator/wallet/migrate_script_type_test.go index bfa888478..1c9448aa2 100644 --- a/sidechain-orchestrator/wallet/migrate_script_type_test.go +++ b/sidechain-orchestrator/wallet/migrate_script_type_test.go @@ -1,12 +1,14 @@ package wallet import ( + "encoding/hex" "encoding/json" "os" "path/filepath" "testing" "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/config" + "github.com/btcsuite/btcd/chaincfg" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -40,3 +42,30 @@ func TestCoreWalletStoresTheScriptTypeOfItsPath(t *testing.T) { svc.Close() } } + +// An old Core watch-only wallet stores the kind its descriptor states, and it +// scans from genesis. +func TestWatchOnlyWalletStoresTheKindOfItsDescriptor(t *testing.T) { + dir := t.TempDir() + seedHex := hex.EncodeToString(MnemonicToSeed(testMnemonic, "")) + legacy := "pkh(" + accountXpub(t, seedHex, &chaincfg.SigNetParams) + "/0/*)" + body, err := json.Marshal(map[string]any{ + "wallets": []map[string]any{ + {"id": "WATCH", "name": "Watch", "wallet_type": "bitcoinCore", "watch_only": map[string]string{"descriptor": legacy}}, + }, + "activeWalletId": "WATCH", + }) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(dir, "wallet.json"), body, 0o600)) + + for range 2 { + svc := NewService(dir, zerolog.Nop()) + svc.SetNetwork(string(config.NetworkSignet)) + require.NoError(t, svc.Init()) + + w := svc.GetWalletByID("WATCH") + assert.Equal(t, "legacy", w.ScriptType) + assert.True(t, w.Imported) + svc.Close() + } +} diff --git a/sidechain-orchestrator/wallet/service.go b/sidechain-orchestrator/wallet/service.go index c803849a4..d72e8dd1b 100644 --- a/sidechain-orchestrator/wallet/service.go +++ b/sidechain-orchestrator/wallet/service.go @@ -758,7 +758,7 @@ func (s *Service) CreateElectrumWallet(name string, gradient json.RawMessage, sl } if xpubOrDescriptor != "" { - return s.createElectrumWatchOnly(name, gradient, xpubOrDescriptor, HotScriptKind(st)) + return s.createWatchOnly(name, gradient, xpubOrDescriptor, HotScriptKind(st), WalletTypeElectrum) } sidechainSlots := make([]SidechainSlot, len(slots)) @@ -798,19 +798,14 @@ func validateHotScriptType(s string) (string, error) { } } -// createElectrumWatchOnly creates a watch-only electrum wallet from an xpub or -// descriptor. Addresses derive from the public key material; with no seed the -// ElectrumBackend can read balances/history but cannot sign or send. requested -// is the kind to record when the key states none of its own. -func (s *Service) createElectrumWatchOnly(name string, gradient json.RawMessage, xpubOrDescriptor string, requested ScriptKind) (*WalletData, error) { - // Parse-validate the descriptor and record its script kind so derivation - // scans the addresses the descriptor actually owns. +// createWatchOnly creates a watch-only wallet of a backend type from an xpub or +// descriptor. requested is the kind to store when the key states none of its +// own. The key can hold history of any age, so the wallet counts as imported. +func (s *Service) createWatchOnly(name string, gradient json.RawMessage, xpubOrDescriptor string, requested ScriptKind, walletType WalletType) (*WalletData, error) { desc, err := ParseDescriptorAs(xpubOrDescriptor, requested) if err != nil { return nil, fmt.Errorf("invalid watch-only descriptor: %w", err) } - // Watch-only stays public-only; reject any private extended key so the - // wallet can never store or sign with private material. for _, k := range desc.Keys { if k.Account.IsPrivate() { return nil, errors.New("watch-only import must not contain private keys") @@ -831,7 +826,10 @@ func (s *Service) createElectrumWatchOnly(name string, gradient json.RawMessage, } else { watchOnly["xpub"] = xpubOrDescriptor } - watchOnlyJSON, _ := json.Marshal(watchOnly) + watchOnlyJSON, err := json.Marshal(watchOnly) + if err != nil { + return nil, fmt.Errorf("encode watch-only data: %w", err) + } wallet := WalletData{ Version: 1, @@ -842,18 +840,19 @@ func (s *Service) createElectrumWatchOnly(name string, gradient json.RawMessage, Name: name, Gradient: gradient, CreatedAt: time.Now(), - WalletType: WalletTypeElectrum, + WalletType: walletType, WatchOnly: json.RawMessage(watchOnlyJSON), ScriptType: desc.Kind.String(), + Imported: true, } s.wallets = append(s.wallets, wallet) s.activeWalletID = walletID s.adoptStarterWallet() if err := s.saveWalletFile(); err != nil { - return nil, fmt.Errorf("save watch-only electrum wallet: %w", err) + return nil, fmt.Errorf("save watch-only wallet: %w", err) } - s.log.Info().Str("id", walletID).Msg("watch-only electrum wallet created") + s.log.Info().Str("id", walletID).Str("type", string(walletType)).Msg("watch-only wallet created") return &wallet, nil } @@ -913,54 +912,11 @@ func (s *Service) CreateElectrumMultisig( return &wallet, nil } -// CreateWatchOnlyWallet creates a watch-only wallet from an xpub or descriptor. -// Dart: WalletWriterProvider.createWatchOnlyWallet (L156-214) +// CreateWatchOnlyWallet creates a watch-only Bitcoin Core wallet from an xpub or +// descriptor. A bare key states no kind, so it is native segwit. func (s *Service) CreateWatchOnlyWallet(name, xpubOrDescriptor, gradientJSON string) error { - s.mu.Lock() - defer s.mu.Unlock() - - if s.locked() { - return fmt.Errorf("wallet is locked, unlock before creating a wallet") - } - - s.log.Info().Str("name", name).Msg("creating watch-only wallet") - - walletID := generateWalletID() - - // Detect if descriptor (contains '(' and ')') - isDescriptor := strings.Contains(xpubOrDescriptor, "(") && strings.Contains(xpubOrDescriptor, ")") - - watchOnly := map[string]string{} - if isDescriptor { - watchOnly["descriptor"] = xpubOrDescriptor - } else { - watchOnly["xpub"] = xpubOrDescriptor - } - watchOnlyJSON, _ := json.Marshal(watchOnly) - - wallet := WalletData{ - Version: 1, - Master: MasterWallet{SeedHex: ""}, - L1: L1Wallet{Mnemonic: ""}, - Sidechains: []SidechainWallet{}, - ID: walletID, - Name: name, - Gradient: json.RawMessage(gradientJSON), - CreatedAt: time.Now(), - WalletType: WalletTypeBitcoinCore, - WatchOnly: json.RawMessage(watchOnlyJSON), - } - - s.wallets = append(s.wallets, wallet) - s.activeWalletID = walletID - s.adoptStarterWallet() - - if err := s.saveWalletFile(); err != nil { - return fmt.Errorf("save watch-only wallet: %w", err) - } - - s.log.Info().Str("id", walletID).Msg("watch-only wallet created") - return nil + _, err := s.createWatchOnly(name, json.RawMessage(gradientJSON), xpubOrDescriptor, ScriptNativeSegwit, WalletTypeBitcoinCore) + return err } // SetWalletHardwareDevice tags a watch-only wallet to sign on a USB device. @@ -1869,6 +1825,9 @@ func (s *Service) loadWalletFile() error { if s.alignCoreScriptTypes() { migrated = true } + if s.alignWatchOnlyWallets() { + migrated = true + } if s.adoptStarterWallet() { migrated = true } @@ -2000,6 +1959,36 @@ func (s *Service) alignCoreScriptTypes() bool { return changed } +// alignWatchOnlyWallets stores on each watch-only wallet the script type its +// descriptor states, and marks it imported, because its key can hold history +// of any age. Must be called with mu held. +func (s *Service) alignWatchOnlyWallets() bool { + changed := false + for i := range s.wallets { + w := &s.wallets[i] + if w.Multisig != nil || !w.IsWatchOnly() { + continue + } + if !w.Imported { + w.Imported = true + changed = true + } + stored, err := watchOnlyDescriptorString(w) + if err != nil { + // A payload that does not parse states no kind; the backend reports + // the error when it derives. + continue + } + d, err := ParseDescriptorAs(stored, w.scriptKind()) + if err != nil || d.Kind == w.scriptKind() { + continue + } + w.ScriptType = d.Kind.String() + changed = true + } + return changed +} + // saveWalletFile writes wallet.json atomically, keeping every wallet the file // on disk holds. Must be called with mu held. func (s *Service) saveWalletFile() error { return s.saveWalletFileWithOptions(false) } From 20b4a73629940d3e1af54fe072f0fafcab587ce0 Mon Sep 17 00:00:00 2001 From: octobocto Date: Fri, 25 Sep 2026 21:43:31 -0700 Subject: [PATCH 08/13] orchestrator: never hand out a BIP47 key address --- sidechain-orchestrator/wallet/core_backend.go | 16 ++++-- .../wallet/core_backend_test.go | 44 ++++++++++++++- .../wallet/core_receive_integration_test.go | 54 +++++++++++++++++++ sidechain-orchestrator/wallet/core_rpc.go | 1 + 4 files changed, 109 insertions(+), 6 deletions(-) diff --git a/sidechain-orchestrator/wallet/core_backend.go b/sidechain-orchestrator/wallet/core_backend.go index 4f3def2de..847fb7bbb 100644 --- a/sidechain-orchestrator/wallet/core_backend.go +++ b/sidechain-orchestrator/wallet/core_backend.go @@ -496,10 +496,9 @@ func (p *CoreBackend) OwnedAddresses(ctx context.Context, walletID string, addre // mempool receives). Lets the receive page poll without burning the keypool, // while staying entirely stateless across orchestrator restarts. // -// Candidates are filtered to the chain's bech32 prefix because the Core wallet -// also imports P2PKH addresses for BIP47 (the notification address + per-sender -// derived payment addresses) — those must never leak into the regular receive -// flow. +// Core also holds single BIP47 keys: the notification key and the per-sender +// payment keys. Their P2PKH addresses never enter the receive flow, because +// only an address from a ranged descriptor is reused. func (p *CoreBackend) NextReceiveAddress(ctx context.Context, walletID string, kind ScriptKind) (DerivedAddress, error) { name, err := p.walletName(ctx, walletID) if err != nil { @@ -527,7 +526,14 @@ func (p *CoreBackend) NextReceiveAddress(ctx context.Context, walletID string, k if !p.addressMatchesKind(a.Address, kind) { continue } - return p.describeAddress(ctx, walletID, a.Address), nil + info, err := p.rpc.GetAddressInfo(ctx, name, a.Address) + if err != nil { + return DerivedAddress{}, err + } + if !strings.Contains(info.ParentDesc, "*") { + continue + } + return DerivedAddress{Address: a.Address, HDPath: info.HDKeyPath, Index: -1}, nil } addr, err := p.rpc.GetNewAddress(ctx, name, "", addressType) if err != nil { diff --git a/sidechain-orchestrator/wallet/core_backend_test.go b/sidechain-orchestrator/wallet/core_backend_test.go index cbda9ff19..6374fe6ac 100644 --- a/sidechain-orchestrator/wallet/core_backend_test.go +++ b/sidechain-orchestrator/wallet/core_backend_test.go @@ -138,7 +138,12 @@ func (f *fakeBitcoind) stubEnsureFlow() { f.handle("getaddressinfo", func(c bitcoindCall) (any, string) { var address string _ = json.Unmarshal(c.Params[0], &address) - return map[string]any{"address": address, "hdkeypath": "m/84'/1'/0'/0/0", "ismine": true}, "" + return map[string]any{ + "address": address, + "hdkeypath": "m/84h/1h/0h/0/0", + "ismine": true, + "parent_desc": "wpkh([d34db33f/84h/1h/0h]tpub.../0/*)#aaaaaaaa", + }, "" }) } @@ -2654,3 +2659,40 @@ func TestCoreBackendImportsAWatchOnlyKeyOnBothBranches(t *testing.T) { // The key hash of the BIP84 test vector, in the regtest encoding. assert.Equal(t, "bcrt1qcr8te4kr609gcawutmrza0j4xv80jy8zeqchgx", ds.address.EncodeAddress()) } + +// Core holds the BIP47 notification key as a single key. Its P2PKH address is +// unused, but a legacy receive request must not hand it out. +func TestCoreBackendLegacyReceiveSkipsTheBip47Key(t *testing.T) { + backend, fake, coreID := newCoreBackendFixture(t) + fake.stubEnsureFlow() + ctx := context.Background() + + net := &chaincfg.RegressionNetParams + notification := p2pkhAddr(t, fixedKey(0x47), net) + minted := p2pkhAddr(t, fixedKey(0x44), net) + fake.handle("listreceivedbyaddress", func(bitcoindCall) (any, string) { + return []map[string]any{{"address": notification, "amount": 0.0, "txids": []string{}}}, "" + }) + stubbed := fake.handlerFor("getaddressinfo") + fake.handle("getaddressinfo", func(c bitcoindCall) (any, string) { + if mustString(t, c.Params[0]) != notification { + return stubbed(c) + } + return map[string]any{ + "address": notification, + "hdkeypath": "m", + "ismine": true, + "parent_desc": "pkh([2bd5a2f9]02ab...)#bbbbbbbb", + }, "" + }) + var mintedType string + fake.handle("getnewaddress", func(c bitcoindCall) (any, string) { + mintedType = mustString(t, c.Params[1]) + return minted, "" + }) + + addr, err := nextAddr(backend, ctx, coreID, ScriptLegacy) + require.NoError(t, err) + assert.Equal(t, minted, addr) + assert.Equal(t, "legacy", mintedType) +} diff --git a/sidechain-orchestrator/wallet/core_receive_integration_test.go b/sidechain-orchestrator/wallet/core_receive_integration_test.go index d09f9cab2..18ad73604 100644 --- a/sidechain-orchestrator/wallet/core_receive_integration_test.go +++ b/sidechain-orchestrator/wallet/core_receive_integration_test.go @@ -4,12 +4,16 @@ package wallet_test import ( "context" + "encoding/hex" "testing" "time" "connectrpc.com/connect" pb "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/gen/walletmanager/v1" "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/testharness" + "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/wallet" + "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/wallet/bip47" + "github.com/btcsuite/btcd/chaincfg" "github.com/stretchr/testify/require" ) @@ -63,3 +67,53 @@ func TestCoreReceiveBalancesMatchTheCoins(t *testing.T) { // TestDescriptorIsChange. A send here adds no coverage and costs a // broadcast, which outruns the RPC client on a loaded runner. } + +// Core holds the BIP47 notification key as a single key, and lists its unused +// address beside the receive addresses. A legacy receive request must never +// hand it out. +func TestCoreLegacyReceiveSkipsTheBip47Key(t *testing.T) { + h := testharness.New(t, 1) + defer h.Close() + node := h.Nodes[0] + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + client := node.WalletClient + + const mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + gen, err := client.GenerateWallet(ctx, connect.NewRequest(&pb.GenerateWalletRequest{ + Name: "Legacy", + CustomMnemonic: mnemonic, + ScriptType: "legacy", + })) + require.NoError(t, err) + walletID := gen.Msg.WalletId + requireCoreWallet(t, ctx, client, walletID) + coreName := "wallet_" + walletID[:8] + + _, notification, err := bip47.DeriveOwnNotificationKey( + hex.EncodeToString(wallet.MnemonicToSeed(mnemonic, "")), &chaincfg.RegressionNetParams) + require.NoError(t, err) + + // The first read loads the Core wallet, and the backend imports the + // notification key in the background. Its address is then the only + // unused one Core lists. + _, err = client.GetBalance(ctx, connect.NewRequest(&pb.GetBalanceRequest{WalletId: walletID})) + require.NoError(t, err) + require.Eventually(t, func() bool { + info, err := node.CoreRPC.GetAddressInfo(ctx, coreName, notification.EncodeAddress()) + return err == nil && info.IsMine + }, 2*time.Minute, time.Second) + info, err := node.CoreRPC.GetAddressInfo(ctx, coreName, notification.EncodeAddress()) + require.NoError(t, err) + require.NotContains(t, info.ParentDesc, "*") + + for range 3 { + resp, err := client.GetNewAddress(ctx, connect.NewRequest(&pb.GetNewAddressRequest{WalletId: walletID})) + require.NoError(t, err) + require.NotEqual(t, notification.EncodeAddress(), resp.Msg.Address) + info, err := node.CoreRPC.GetAddressInfo(ctx, coreName, resp.Msg.Address) + require.NoError(t, err) + require.Contains(t, info.ParentDesc, "*", resp.Msg.Address) + } +} diff --git a/sidechain-orchestrator/wallet/core_rpc.go b/sidechain-orchestrator/wallet/core_rpc.go index 24165c897..ac2ab0fb5 100644 --- a/sidechain-orchestrator/wallet/core_rpc.go +++ b/sidechain-orchestrator/wallet/core_rpc.go @@ -745,6 +745,7 @@ type AddressInfo struct { IsWitness bool `json:"iswitness"` WitnessVer int `json:"witness_version"` PubKey string `json:"pubkey"` + ParentDesc string `json:"parent_desc"` } // GetAddressInfo returns information about an address in the wallet, including From 4e0643dbb5c77db9f453608fa2bfe2e58b99aa49 Mon Sep 17 00:00:00 2001 From: octobocto Date: Fri, 25 Sep 2026 21:45:24 -0700 Subject: [PATCH 09/13] orchestrator: use the BIP45 path a device signs --- sidechain-orchestrator/wallet/derivation.go | 5 +++-- sidechain-orchestrator/wallet/derive_keystore_test.go | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/sidechain-orchestrator/wallet/derivation.go b/sidechain-orchestrator/wallet/derivation.go index 3017585df..8958f7b7c 100644 --- a/sidechain-orchestrator/wallet/derivation.go +++ b/sidechain-orchestrator/wallet/derivation.go @@ -317,7 +317,8 @@ func accountPathFor(w *WalletData, kind ScriptKind, net *chaincfg.Params) (Accou } // multisigAccountPath returns the cosigner account path for a script type: -// BIP48 for segwit variants, BIP45 for legacy P2SH. +// BIP48 for segwit variants, BIP45 for legacy P2SH. BIP45 takes the account as +// its unhardened cosigner index. func multisigAccountPath(scriptType string, account uint32, net *chaincfg.Params) (string, error) { if account >= 1<<31 { return "", fmt.Errorf("account index %d out of hardened range", account) @@ -328,7 +329,7 @@ func multisigAccountPath(scriptType string, account uint32, net *chaincfg.Params } switch scriptType { case "sh": - return fmt.Sprintf("m/45'/%d'", account), nil + return fmt.Sprintf("m/45'/%d", account), nil case "sh-wsh": return fmt.Sprintf("m/48'/%d'/%d'/1'", coin, account), nil case "tr": diff --git a/sidechain-orchestrator/wallet/derive_keystore_test.go b/sidechain-orchestrator/wallet/derive_keystore_test.go index 198edd0c0..9eb92565b 100644 --- a/sidechain-orchestrator/wallet/derive_keystore_test.go +++ b/sidechain-orchestrator/wallet/derive_keystore_test.go @@ -15,13 +15,17 @@ func TestMultisigAccountPath(t *testing.T) { "wsh": "m/48'/0'/0'/2'", "sh-wsh": "m/48'/0'/0'/1'", "tr": "m/48'/0'/0'/3'", - "sh": "m/45'/0'", + "sh": "m/45'/0", } for st, want := range cases { got, err := multisigAccountPath(st, 0, net) require.NoError(t, err) require.Equal(t, want, got, st) } + // A hardware signer accepts a key on the default legacy path. + sh, err := parseHDPath(cases["sh"]) + require.NoError(t, err) + require.True(t, multisigPathIsStandard(append(sh, 0, 5))) // Testnet coin 1 for a different account. got, err := multisigAccountPath("wsh", 3, &chaincfg.TestNet3Params) require.NoError(t, err) @@ -133,7 +137,7 @@ func TestStandardDerivationPaths(t *testing.T) { require.Equal(t, "m/48'/1'/1'/2'", def) require.Len(t, opts, 4) require.Equal(t, "m/48'/1'/1'/2'", opts[0].Path) - require.Equal(t, "m/45'/1'", opts[3].Path) + require.Equal(t, "m/45'/1", opts[3].Path) opts, def, err = StandardDerivationPaths("taproot", false, 0, &chaincfg.MainNetParams) require.NoError(t, err) @@ -157,6 +161,7 @@ func TestKeystorePathScriptType(t *testing.T) { {"m/48'/1'/0'/2'", true, "wsh"}, {"m/48'/1'/0'/1'", true, "sh-wsh"}, {"m/48'/1'/0'/3'", true, "tr"}, + {"m/45'/0", true, "sh"}, {"m/45'/0'", true, "sh"}, // A path that is standard for the other policy, or custom, has no type. {"m/84'/0'/0'", true, ""}, From db8c6abb37653e46efc196c346c2cc3eaa26e7e6 Mon Sep 17 00:00:00 2001 From: octobocto Date: Fri, 25 Sep 2026 22:02:59 -0700 Subject: [PATCH 10/13] orchestrator: find the key of a receive address --- .../wallet/wallet_descriptors.go | 38 +++++++++++++++++++ .../wallet/wallet_descriptors_test.go | 33 ++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/sidechain-orchestrator/wallet/wallet_descriptors.go b/sidechain-orchestrator/wallet/wallet_descriptors.go index c189948cf..257913975 100644 --- a/sidechain-orchestrator/wallet/wallet_descriptors.go +++ b/sidechain-orchestrator/wallet/wallet_descriptors.go @@ -7,8 +7,10 @@ import ( "errors" "fmt" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" ) // WalletDescriptors returns every descriptor a wallet derives, its receive @@ -55,6 +57,42 @@ func DescriptorFor(w *WalletData, kind ScriptKind, net *chaincfg.Params) (*Descr } } +// SigningKey returns the private key behind one of the first depth receive +// addresses of each descriptor a wallet derives. A taproot key comes tweaked, +// because the address commits to the tweaked key. +func SigningKey(w *WalletData, net *chaincfg.Params, address string, depth uint32) (*btcec.PrivateKey, error) { + descriptors, err := WalletDescriptors(w, net) + if err != nil { + return nil, err + } + for _, d := range descriptors { + if len(d.Keys) != 1 { + continue + } + for index := uint32(0); index < depth; index++ { + ds, _, err := d.DeriveScript(false, index, net) + if err != nil { + return nil, err + } + if ds.address.EncodeAddress() != address { + continue + } + priv, ok, err := deriveChildPrivIfPossible(d.Keys[0].Account, 0, index) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("the wallet holds no private key for address %s", address) + } + if d.Kind == ScriptTaproot { + return txscript.TweakTaprootPrivKey(*priv, []byte{}), nil + } + return priv, nil + } + } + return nil, fmt.Errorf("address %s is not one of the wallet's first %d receiving addresses", address, depth) +} + // multisigDescriptor builds the multisig descriptor with each held cosigner as // its account xprv, so the keys it derives can sign. A non-empty onlyXpub holds // that one cosigner and leaves the rest as xpubs. diff --git a/sidechain-orchestrator/wallet/wallet_descriptors_test.go b/sidechain-orchestrator/wallet/wallet_descriptors_test.go index 543c88547..a4fd448f1 100644 --- a/sidechain-orchestrator/wallet/wallet_descriptors_test.go +++ b/sidechain-orchestrator/wallet/wallet_descriptors_test.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -81,3 +82,35 @@ func TestCosignerSignsWithTheKeyItsXpubNames(t *testing.T) { _, err = cosignerXprv(foreign, net) require.ErrorContains(t, err, "does not derive its stored key") } + +func TestSigningKeyFindsTheKeyBehindAReceiveAddress(t *testing.T) { + net := &chaincfg.SigNetParams + seedHex := hex.EncodeToString(MnemonicToSeed(testMnemonic, "")) + w := &WalletData{Master: MasterWallet{SeedHex: seedHex}} + descriptors, err := WalletDescriptors(w, net) + require.NoError(t, err) + + for _, d := range descriptors { + ds, pub, err := d.DeriveScript(false, 3, net) + require.NoError(t, err) + key, err := SigningKey(w, net, ds.address.EncodeAddress(), 10) + require.NoError(t, err) + want := pub.SerializeCompressed() + if d.Kind == ScriptTaproot { + // A taproot address commits to the tweaked key. + want = txscript.ComputeTaprootKeyNoScript(pub).SerializeCompressed() + } + assert.Equal(t, want, key.PubKey().SerializeCompressed(), d.Kind.String()) + } + + beyond, _, err := descriptors[0].DeriveScript(false, 10, net) + require.NoError(t, err) + _, err = SigningKey(w, net, beyond.address.EncodeAddress(), 10) + require.ErrorContains(t, err, "not one of the wallet") + + first, _, err := descriptors[0].DeriveScript(false, 0, net) + require.NoError(t, err) + watch := &WalletData{WatchOnly: json.RawMessage(`{"xpub":"` + accountXpub(t, seedHex, net) + `"}`)} + _, err = SigningKey(watch, net, first.address.EncodeAddress(), 10) + require.ErrorContains(t, err, "no private key") +} From 7676d596b070f0511bc6490dcfa0f78075923a3d Mon Sep 17 00:00:00 2001 From: octobocto Date: Fri, 25 Sep 2026 22:03:10 -0700 Subject: [PATCH 11/13] bitwindow: drop the unused Core wallet test RPC --- bitwindow/server/api/wallet/wallet.go | 44 - .../gen/wallet/v1/v1connect/wallet.connect.go | 968 ------------------ bitwindow/server/gen/wallet/v1/wallet.pb.go | 494 ++++----- .../v1/walletv1connect/wallet.connect.go | 29 - bitwindow/server/proto/wallet/v1/wallet.proto | 18 - .../gen/wallet/v1/wallet.connect.client.dart | 17 - .../gen/wallet/v1/wallet.connect.spec.dart | 7 - .../lib/gen/wallet/v1/wallet.pb.dart | 151 --- .../lib/gen/wallet/v1/wallet.pbjson.dart | 137 +-- .../lib/gen/wallet/v1/wallet.pbserver.dart | 3 - 10 files changed, 233 insertions(+), 1635 deletions(-) delete mode 100644 bitwindow/server/gen/wallet/v1/v1connect/wallet.connect.go diff --git a/bitwindow/server/api/wallet/wallet.go b/bitwindow/server/api/wallet/wallet.go index 1cc347785..11d4e9310 100644 --- a/bitwindow/server/api/wallet/wallet.go +++ b/bitwindow/server/api/wallet/wallet.go @@ -88,50 +88,6 @@ type Server struct { restartL1 func(context.Context) error } -// CreateBitcoinCoreWallet implements walletv1connect.WalletServiceHandler. -// Test endpoint to verify descriptor import to Bitcoin Core. -func (s *Server) CreateBitcoinCoreWallet(ctx context.Context, c *connect.Request[pb.CreateBitcoinCoreWalletRequest]) (*connect.Response[pb.CreateBitcoinCoreWalletResponse], error) { - seedHex := strings.TrimSpace(c.Msg.SeedHex) - if seedHex == "" { - return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("seed_hex required")) - } - - coreWalletName := c.Msg.Name - if coreWalletName == "" { - return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("name required")) - } - - // Directly import to Bitcoin Core - no wallet.json needed - if err := s.walletEngine.CreateBitcoinCoreWalletFromSeed(ctx, coreWalletName, seedHex, 0, ""); err != nil { - zerolog.Ctx(ctx).Error().Err(err).Msg("create Bitcoin Core wallet failed") - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("create wallet: %w", err)) - } - - // Get first address for verification - bitcoindClient, err := s.bitcoind.Get(ctx) - if err != nil { - return nil, err - } - - addrResp, err := bitcoindClient.GetNewAddress(ctx, connect.NewRequest(&corepb.GetNewAddressRequest{ - Wallet: coreWalletName, - })) - if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("get new address: %w", err)) - } - - zerolog.Ctx(ctx).Info(). - Str("core_wallet_name", coreWalletName). - Str("first_address", addrResp.Msg.Address). - Msg("Created Bitcoin Core wallet from seed") - - return connect.NewResponse(&pb.CreateBitcoinCoreWalletResponse{ - WalletId: "", // Not used in test - CoreWalletName: coreWalletName, - FirstAddress: addrResp.Msg.Address, - }), nil -} - // SendTransaction implements drivechainv1connect.DrivechainServiceHandler. func (s *Server) SendTransaction(ctx context.Context, c *connect.Request[pb.SendTransactionRequest]) (*connect.Response[pb.SendTransactionResponse], error) { walletId := c.Msg.WalletId diff --git a/bitwindow/server/gen/wallet/v1/v1connect/wallet.connect.go b/bitwindow/server/gen/wallet/v1/v1connect/wallet.connect.go deleted file mode 100644 index 1c56a9ad2..000000000 --- a/bitwindow/server/gen/wallet/v1/v1connect/wallet.connect.go +++ /dev/null @@ -1,968 +0,0 @@ -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: wallet/v1/wallet.proto - -package v1connect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - v1 "github.com/LayerTwo-Labs/sidesail/bitwindow/server/gen/wallet/v1" - emptypb "google.golang.org/protobuf/types/known/emptypb" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // WalletServiceName is the fully-qualified name of the WalletService service. - WalletServiceName = "wallet.v1.WalletService" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // WalletServiceCreateBitcoinCoreWalletProcedure is the fully-qualified name of the WalletService's - // CreateBitcoinCoreWallet RPC. - WalletServiceCreateBitcoinCoreWalletProcedure = "/wallet.v1.WalletService/CreateBitcoinCoreWallet" - // WalletServiceSendTransactionProcedure is the fully-qualified name of the WalletService's - // SendTransaction RPC. - WalletServiceSendTransactionProcedure = "/wallet.v1.WalletService/SendTransaction" - // WalletServiceGetBalanceProcedure is the fully-qualified name of the WalletService's GetBalance - // RPC. - WalletServiceGetBalanceProcedure = "/wallet.v1.WalletService/GetBalance" - // WalletServiceGetNewAddressProcedure is the fully-qualified name of the WalletService's - // GetNewAddress RPC. - WalletServiceGetNewAddressProcedure = "/wallet.v1.WalletService/GetNewAddress" - // WalletServiceListTransactionsProcedure is the fully-qualified name of the WalletService's - // ListTransactions RPC. - WalletServiceListTransactionsProcedure = "/wallet.v1.WalletService/ListTransactions" - // WalletServiceListUnspentProcedure is the fully-qualified name of the WalletService's ListUnspent - // RPC. - WalletServiceListUnspentProcedure = "/wallet.v1.WalletService/ListUnspent" - // WalletServiceListReceiveAddressesProcedure is the fully-qualified name of the WalletService's - // ListReceiveAddresses RPC. - WalletServiceListReceiveAddressesProcedure = "/wallet.v1.WalletService/ListReceiveAddresses" - // WalletServiceListSidechainDepositsProcedure is the fully-qualified name of the WalletService's - // ListSidechainDeposits RPC. - WalletServiceListSidechainDepositsProcedure = "/wallet.v1.WalletService/ListSidechainDeposits" - // WalletServiceCreateSidechainDepositProcedure is the fully-qualified name of the WalletService's - // CreateSidechainDeposit RPC. - WalletServiceCreateSidechainDepositProcedure = "/wallet.v1.WalletService/CreateSidechainDeposit" - // WalletServiceSignMessageProcedure is the fully-qualified name of the WalletService's SignMessage - // RPC. - WalletServiceSignMessageProcedure = "/wallet.v1.WalletService/SignMessage" - // WalletServiceVerifyMessageProcedure is the fully-qualified name of the WalletService's - // VerifyMessage RPC. - WalletServiceVerifyMessageProcedure = "/wallet.v1.WalletService/VerifyMessage" - // WalletServiceGetStatsProcedure is the fully-qualified name of the WalletService's GetStats RPC. - WalletServiceGetStatsProcedure = "/wallet.v1.WalletService/GetStats" - // WalletServiceUnlockWalletProcedure is the fully-qualified name of the WalletService's - // UnlockWallet RPC. - WalletServiceUnlockWalletProcedure = "/wallet.v1.WalletService/UnlockWallet" - // WalletServiceLockWalletProcedure is the fully-qualified name of the WalletService's LockWallet - // RPC. - WalletServiceLockWalletProcedure = "/wallet.v1.WalletService/LockWallet" - // WalletServiceIsWalletUnlockedProcedure is the fully-qualified name of the WalletService's - // IsWalletUnlocked RPC. - WalletServiceIsWalletUnlockedProcedure = "/wallet.v1.WalletService/IsWalletUnlocked" - // WalletServiceCreateChequeProcedure is the fully-qualified name of the WalletService's - // CreateCheque RPC. - WalletServiceCreateChequeProcedure = "/wallet.v1.WalletService/CreateCheque" - // WalletServiceGetChequeProcedure is the fully-qualified name of the WalletService's GetCheque RPC. - WalletServiceGetChequeProcedure = "/wallet.v1.WalletService/GetCheque" - // WalletServiceGetChequePrivateKeyProcedure is the fully-qualified name of the WalletService's - // GetChequePrivateKey RPC. - WalletServiceGetChequePrivateKeyProcedure = "/wallet.v1.WalletService/GetChequePrivateKey" - // WalletServiceListChequesProcedure is the fully-qualified name of the WalletService's ListCheques - // RPC. - WalletServiceListChequesProcedure = "/wallet.v1.WalletService/ListCheques" - // WalletServiceCheckChequeFundingProcedure is the fully-qualified name of the WalletService's - // CheckChequeFunding RPC. - WalletServiceCheckChequeFundingProcedure = "/wallet.v1.WalletService/CheckChequeFunding" - // WalletServiceSweepChequeProcedure is the fully-qualified name of the WalletService's SweepCheque - // RPC. - WalletServiceSweepChequeProcedure = "/wallet.v1.WalletService/SweepCheque" - // WalletServiceDeleteChequeProcedure is the fully-qualified name of the WalletService's - // DeleteCheque RPC. - WalletServiceDeleteChequeProcedure = "/wallet.v1.WalletService/DeleteCheque" - // WalletServiceSetUTXOMetadataProcedure is the fully-qualified name of the WalletService's - // SetUTXOMetadata RPC. - WalletServiceSetUTXOMetadataProcedure = "/wallet.v1.WalletService/SetUTXOMetadata" - // WalletServiceGetUTXOMetadataProcedure is the fully-qualified name of the WalletService's - // GetUTXOMetadata RPC. - WalletServiceGetUTXOMetadataProcedure = "/wallet.v1.WalletService/GetUTXOMetadata" - // WalletServiceSetCoinSelectionStrategyProcedure is the fully-qualified name of the WalletService's - // SetCoinSelectionStrategy RPC. - WalletServiceSetCoinSelectionStrategyProcedure = "/wallet.v1.WalletService/SetCoinSelectionStrategy" - // WalletServiceGetCoinSelectionStrategyProcedure is the fully-qualified name of the WalletService's - // GetCoinSelectionStrategy RPC. - WalletServiceGetCoinSelectionStrategyProcedure = "/wallet.v1.WalletService/GetCoinSelectionStrategy" - // WalletServiceGetTransactionDetailsProcedure is the fully-qualified name of the WalletService's - // GetTransactionDetails RPC. - WalletServiceGetTransactionDetailsProcedure = "/wallet.v1.WalletService/GetTransactionDetails" - // WalletServiceGetUTXODistributionProcedure is the fully-qualified name of the WalletService's - // GetUTXODistribution RPC. - WalletServiceGetUTXODistributionProcedure = "/wallet.v1.WalletService/GetUTXODistribution" - // WalletServiceBumpFeeProcedure is the fully-qualified name of the WalletService's BumpFee RPC. - WalletServiceBumpFeeProcedure = "/wallet.v1.WalletService/BumpFee" - // WalletServiceSelectCoinsProcedure is the fully-qualified name of the WalletService's SelectCoins - // RPC. - WalletServiceSelectCoinsProcedure = "/wallet.v1.WalletService/SelectCoins" -) - -// WalletServiceClient is a client for the wallet.v1.WalletService service. -type WalletServiceClient interface { - CreateBitcoinCoreWallet(context.Context, *connect.Request[v1.CreateBitcoinCoreWalletRequest]) (*connect.Response[v1.CreateBitcoinCoreWalletResponse], error) - SendTransaction(context.Context, *connect.Request[v1.SendTransactionRequest]) (*connect.Response[v1.SendTransactionResponse], error) - GetBalance(context.Context, *connect.Request[v1.GetBalanceRequest]) (*connect.Response[v1.GetBalanceResponse], error) - // Problem: deriving nilly willy here is potentially problematic. There's no way of listing - // out unused addresses, so we risk crossing the sync gap. - GetNewAddress(context.Context, *connect.Request[v1.GetNewAddressRequest]) (*connect.Response[v1.GetNewAddressResponse], error) - ListTransactions(context.Context, *connect.Request[v1.ListTransactionsRequest]) (*connect.Response[v1.ListTransactionsResponse], error) - ListUnspent(context.Context, *connect.Request[v1.ListUnspentRequest]) (*connect.Response[v1.ListUnspentResponse], error) - ListReceiveAddresses(context.Context, *connect.Request[v1.ListReceiveAddressesRequest]) (*connect.Response[v1.ListReceiveAddressesResponse], error) - ListSidechainDeposits(context.Context, *connect.Request[v1.ListSidechainDepositsRequest]) (*connect.Response[v1.ListSidechainDepositsResponse], error) - CreateSidechainDeposit(context.Context, *connect.Request[v1.CreateSidechainDepositRequest]) (*connect.Response[v1.CreateSidechainDepositResponse], error) - SignMessage(context.Context, *connect.Request[v1.SignMessageRequest]) (*connect.Response[v1.SignMessageResponse], error) - VerifyMessage(context.Context, *connect.Request[v1.VerifyMessageRequest]) (*connect.Response[v1.VerifyMessageResponse], error) - GetStats(context.Context, *connect.Request[v1.GetStatsRequest]) (*connect.Response[v1.GetStatsResponse], error) - // Wallet unlock/lock for cheque operations - UnlockWallet(context.Context, *connect.Request[v1.UnlockWalletRequest]) (*connect.Response[emptypb.Empty], error) - LockWallet(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[emptypb.Empty], error) - IsWalletUnlocked(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[emptypb.Empty], error) - // Cheque operations - CreateCheque(context.Context, *connect.Request[v1.CreateChequeRequest]) (*connect.Response[v1.CreateChequeResponse], error) - GetCheque(context.Context, *connect.Request[v1.GetChequeRequest]) (*connect.Response[v1.GetChequeResponse], error) - GetChequePrivateKey(context.Context, *connect.Request[v1.GetChequePrivateKeyRequest]) (*connect.Response[v1.GetChequePrivateKeyResponse], error) - ListCheques(context.Context, *connect.Request[v1.ListChequesRequest]) (*connect.Response[v1.ListChequesResponse], error) - CheckChequeFunding(context.Context, *connect.Request[v1.CheckChequeFundingRequest]) (*connect.Response[v1.CheckChequeFundingResponse], error) - SweepCheque(context.Context, *connect.Request[v1.SweepChequeRequest]) (*connect.Response[v1.SweepChequeResponse], error) - DeleteCheque(context.Context, *connect.Request[v1.DeleteChequeRequest]) (*connect.Response[emptypb.Empty], error) - // UTXO Coin Control - SetUTXOMetadata(context.Context, *connect.Request[v1.SetUTXOMetadataRequest]) (*connect.Response[emptypb.Empty], error) - GetUTXOMetadata(context.Context, *connect.Request[v1.GetUTXOMetadataRequest]) (*connect.Response[v1.GetUTXOMetadataResponse], error) - // Coin Selection Preferences - SetCoinSelectionStrategy(context.Context, *connect.Request[v1.SetCoinSelectionStrategyRequest]) (*connect.Response[emptypb.Empty], error) - GetCoinSelectionStrategy(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.GetCoinSelectionStrategyResponse], error) - // Transaction Details (enriched with input values/addresses) - GetTransactionDetails(context.Context, *connect.Request[v1.GetTransactionDetailsRequest]) (*connect.Response[v1.GetTransactionDetailsResponse], error) - // UTXO Distribution (for chart visualization) - GetUTXODistribution(context.Context, *connect.Request[v1.GetUTXODistributionRequest]) (*connect.Response[v1.GetUTXODistributionResponse], error) - // RBF - Replace-By-Fee for unconfirmed transactions - BumpFee(context.Context, *connect.Request[v1.BumpFeeRequest]) (*connect.Response[v1.BumpFeeResponse], error) - // Coin Selection - Select UTXOs for a transaction - SelectCoins(context.Context, *connect.Request[v1.SelectCoinsRequest]) (*connect.Response[v1.SelectCoinsResponse], error) -} - -// NewWalletServiceClient constructs a client for the wallet.v1.WalletService service. By default, -// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and -// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() -// or connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewWalletServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) WalletServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - walletServiceMethods := v1.File_wallet_v1_wallet_proto.Services().ByName("WalletService").Methods() - return &walletServiceClient{ - createBitcoinCoreWallet: connect.NewClient[v1.CreateBitcoinCoreWalletRequest, v1.CreateBitcoinCoreWalletResponse]( - httpClient, - baseURL+WalletServiceCreateBitcoinCoreWalletProcedure, - connect.WithSchema(walletServiceMethods.ByName("CreateBitcoinCoreWallet")), - connect.WithClientOptions(opts...), - ), - sendTransaction: connect.NewClient[v1.SendTransactionRequest, v1.SendTransactionResponse]( - httpClient, - baseURL+WalletServiceSendTransactionProcedure, - connect.WithSchema(walletServiceMethods.ByName("SendTransaction")), - connect.WithClientOptions(opts...), - ), - getBalance: connect.NewClient[v1.GetBalanceRequest, v1.GetBalanceResponse]( - httpClient, - baseURL+WalletServiceGetBalanceProcedure, - connect.WithSchema(walletServiceMethods.ByName("GetBalance")), - connect.WithClientOptions(opts...), - ), - getNewAddress: connect.NewClient[v1.GetNewAddressRequest, v1.GetNewAddressResponse]( - httpClient, - baseURL+WalletServiceGetNewAddressProcedure, - connect.WithSchema(walletServiceMethods.ByName("GetNewAddress")), - connect.WithClientOptions(opts...), - ), - listTransactions: connect.NewClient[v1.ListTransactionsRequest, v1.ListTransactionsResponse]( - httpClient, - baseURL+WalletServiceListTransactionsProcedure, - connect.WithSchema(walletServiceMethods.ByName("ListTransactions")), - connect.WithClientOptions(opts...), - ), - listUnspent: connect.NewClient[v1.ListUnspentRequest, v1.ListUnspentResponse]( - httpClient, - baseURL+WalletServiceListUnspentProcedure, - connect.WithSchema(walletServiceMethods.ByName("ListUnspent")), - connect.WithClientOptions(opts...), - ), - listReceiveAddresses: connect.NewClient[v1.ListReceiveAddressesRequest, v1.ListReceiveAddressesResponse]( - httpClient, - baseURL+WalletServiceListReceiveAddressesProcedure, - connect.WithSchema(walletServiceMethods.ByName("ListReceiveAddresses")), - connect.WithClientOptions(opts...), - ), - listSidechainDeposits: connect.NewClient[v1.ListSidechainDepositsRequest, v1.ListSidechainDepositsResponse]( - httpClient, - baseURL+WalletServiceListSidechainDepositsProcedure, - connect.WithSchema(walletServiceMethods.ByName("ListSidechainDeposits")), - connect.WithClientOptions(opts...), - ), - createSidechainDeposit: connect.NewClient[v1.CreateSidechainDepositRequest, v1.CreateSidechainDepositResponse]( - httpClient, - baseURL+WalletServiceCreateSidechainDepositProcedure, - connect.WithSchema(walletServiceMethods.ByName("CreateSidechainDeposit")), - connect.WithClientOptions(opts...), - ), - signMessage: connect.NewClient[v1.SignMessageRequest, v1.SignMessageResponse]( - httpClient, - baseURL+WalletServiceSignMessageProcedure, - connect.WithSchema(walletServiceMethods.ByName("SignMessage")), - connect.WithClientOptions(opts...), - ), - verifyMessage: connect.NewClient[v1.VerifyMessageRequest, v1.VerifyMessageResponse]( - httpClient, - baseURL+WalletServiceVerifyMessageProcedure, - connect.WithSchema(walletServiceMethods.ByName("VerifyMessage")), - connect.WithClientOptions(opts...), - ), - getStats: connect.NewClient[v1.GetStatsRequest, v1.GetStatsResponse]( - httpClient, - baseURL+WalletServiceGetStatsProcedure, - connect.WithSchema(walletServiceMethods.ByName("GetStats")), - connect.WithClientOptions(opts...), - ), - unlockWallet: connect.NewClient[v1.UnlockWalletRequest, emptypb.Empty]( - httpClient, - baseURL+WalletServiceUnlockWalletProcedure, - connect.WithSchema(walletServiceMethods.ByName("UnlockWallet")), - connect.WithClientOptions(opts...), - ), - lockWallet: connect.NewClient[emptypb.Empty, emptypb.Empty]( - httpClient, - baseURL+WalletServiceLockWalletProcedure, - connect.WithSchema(walletServiceMethods.ByName("LockWallet")), - connect.WithClientOptions(opts...), - ), - isWalletUnlocked: connect.NewClient[emptypb.Empty, emptypb.Empty]( - httpClient, - baseURL+WalletServiceIsWalletUnlockedProcedure, - connect.WithSchema(walletServiceMethods.ByName("IsWalletUnlocked")), - connect.WithClientOptions(opts...), - ), - createCheque: connect.NewClient[v1.CreateChequeRequest, v1.CreateChequeResponse]( - httpClient, - baseURL+WalletServiceCreateChequeProcedure, - connect.WithSchema(walletServiceMethods.ByName("CreateCheque")), - connect.WithClientOptions(opts...), - ), - getCheque: connect.NewClient[v1.GetChequeRequest, v1.GetChequeResponse]( - httpClient, - baseURL+WalletServiceGetChequeProcedure, - connect.WithSchema(walletServiceMethods.ByName("GetCheque")), - connect.WithClientOptions(opts...), - ), - getChequePrivateKey: connect.NewClient[v1.GetChequePrivateKeyRequest, v1.GetChequePrivateKeyResponse]( - httpClient, - baseURL+WalletServiceGetChequePrivateKeyProcedure, - connect.WithSchema(walletServiceMethods.ByName("GetChequePrivateKey")), - connect.WithClientOptions(opts...), - ), - listCheques: connect.NewClient[v1.ListChequesRequest, v1.ListChequesResponse]( - httpClient, - baseURL+WalletServiceListChequesProcedure, - connect.WithSchema(walletServiceMethods.ByName("ListCheques")), - connect.WithClientOptions(opts...), - ), - checkChequeFunding: connect.NewClient[v1.CheckChequeFundingRequest, v1.CheckChequeFundingResponse]( - httpClient, - baseURL+WalletServiceCheckChequeFundingProcedure, - connect.WithSchema(walletServiceMethods.ByName("CheckChequeFunding")), - connect.WithClientOptions(opts...), - ), - sweepCheque: connect.NewClient[v1.SweepChequeRequest, v1.SweepChequeResponse]( - httpClient, - baseURL+WalletServiceSweepChequeProcedure, - connect.WithSchema(walletServiceMethods.ByName("SweepCheque")), - connect.WithClientOptions(opts...), - ), - deleteCheque: connect.NewClient[v1.DeleteChequeRequest, emptypb.Empty]( - httpClient, - baseURL+WalletServiceDeleteChequeProcedure, - connect.WithSchema(walletServiceMethods.ByName("DeleteCheque")), - connect.WithClientOptions(opts...), - ), - setUTXOMetadata: connect.NewClient[v1.SetUTXOMetadataRequest, emptypb.Empty]( - httpClient, - baseURL+WalletServiceSetUTXOMetadataProcedure, - connect.WithSchema(walletServiceMethods.ByName("SetUTXOMetadata")), - connect.WithClientOptions(opts...), - ), - getUTXOMetadata: connect.NewClient[v1.GetUTXOMetadataRequest, v1.GetUTXOMetadataResponse]( - httpClient, - baseURL+WalletServiceGetUTXOMetadataProcedure, - connect.WithSchema(walletServiceMethods.ByName("GetUTXOMetadata")), - connect.WithClientOptions(opts...), - ), - setCoinSelectionStrategy: connect.NewClient[v1.SetCoinSelectionStrategyRequest, emptypb.Empty]( - httpClient, - baseURL+WalletServiceSetCoinSelectionStrategyProcedure, - connect.WithSchema(walletServiceMethods.ByName("SetCoinSelectionStrategy")), - connect.WithClientOptions(opts...), - ), - getCoinSelectionStrategy: connect.NewClient[emptypb.Empty, v1.GetCoinSelectionStrategyResponse]( - httpClient, - baseURL+WalletServiceGetCoinSelectionStrategyProcedure, - connect.WithSchema(walletServiceMethods.ByName("GetCoinSelectionStrategy")), - connect.WithClientOptions(opts...), - ), - getTransactionDetails: connect.NewClient[v1.GetTransactionDetailsRequest, v1.GetTransactionDetailsResponse]( - httpClient, - baseURL+WalletServiceGetTransactionDetailsProcedure, - connect.WithSchema(walletServiceMethods.ByName("GetTransactionDetails")), - connect.WithClientOptions(opts...), - ), - getUTXODistribution: connect.NewClient[v1.GetUTXODistributionRequest, v1.GetUTXODistributionResponse]( - httpClient, - baseURL+WalletServiceGetUTXODistributionProcedure, - connect.WithSchema(walletServiceMethods.ByName("GetUTXODistribution")), - connect.WithClientOptions(opts...), - ), - bumpFee: connect.NewClient[v1.BumpFeeRequest, v1.BumpFeeResponse]( - httpClient, - baseURL+WalletServiceBumpFeeProcedure, - connect.WithSchema(walletServiceMethods.ByName("BumpFee")), - connect.WithClientOptions(opts...), - ), - selectCoins: connect.NewClient[v1.SelectCoinsRequest, v1.SelectCoinsResponse]( - httpClient, - baseURL+WalletServiceSelectCoinsProcedure, - connect.WithSchema(walletServiceMethods.ByName("SelectCoins")), - connect.WithClientOptions(opts...), - ), - } -} - -// walletServiceClient implements WalletServiceClient. -type walletServiceClient struct { - createBitcoinCoreWallet *connect.Client[v1.CreateBitcoinCoreWalletRequest, v1.CreateBitcoinCoreWalletResponse] - sendTransaction *connect.Client[v1.SendTransactionRequest, v1.SendTransactionResponse] - getBalance *connect.Client[v1.GetBalanceRequest, v1.GetBalanceResponse] - getNewAddress *connect.Client[v1.GetNewAddressRequest, v1.GetNewAddressResponse] - listTransactions *connect.Client[v1.ListTransactionsRequest, v1.ListTransactionsResponse] - listUnspent *connect.Client[v1.ListUnspentRequest, v1.ListUnspentResponse] - listReceiveAddresses *connect.Client[v1.ListReceiveAddressesRequest, v1.ListReceiveAddressesResponse] - listSidechainDeposits *connect.Client[v1.ListSidechainDepositsRequest, v1.ListSidechainDepositsResponse] - createSidechainDeposit *connect.Client[v1.CreateSidechainDepositRequest, v1.CreateSidechainDepositResponse] - signMessage *connect.Client[v1.SignMessageRequest, v1.SignMessageResponse] - verifyMessage *connect.Client[v1.VerifyMessageRequest, v1.VerifyMessageResponse] - getStats *connect.Client[v1.GetStatsRequest, v1.GetStatsResponse] - unlockWallet *connect.Client[v1.UnlockWalletRequest, emptypb.Empty] - lockWallet *connect.Client[emptypb.Empty, emptypb.Empty] - isWalletUnlocked *connect.Client[emptypb.Empty, emptypb.Empty] - createCheque *connect.Client[v1.CreateChequeRequest, v1.CreateChequeResponse] - getCheque *connect.Client[v1.GetChequeRequest, v1.GetChequeResponse] - getChequePrivateKey *connect.Client[v1.GetChequePrivateKeyRequest, v1.GetChequePrivateKeyResponse] - listCheques *connect.Client[v1.ListChequesRequest, v1.ListChequesResponse] - checkChequeFunding *connect.Client[v1.CheckChequeFundingRequest, v1.CheckChequeFundingResponse] - sweepCheque *connect.Client[v1.SweepChequeRequest, v1.SweepChequeResponse] - deleteCheque *connect.Client[v1.DeleteChequeRequest, emptypb.Empty] - setUTXOMetadata *connect.Client[v1.SetUTXOMetadataRequest, emptypb.Empty] - getUTXOMetadata *connect.Client[v1.GetUTXOMetadataRequest, v1.GetUTXOMetadataResponse] - setCoinSelectionStrategy *connect.Client[v1.SetCoinSelectionStrategyRequest, emptypb.Empty] - getCoinSelectionStrategy *connect.Client[emptypb.Empty, v1.GetCoinSelectionStrategyResponse] - getTransactionDetails *connect.Client[v1.GetTransactionDetailsRequest, v1.GetTransactionDetailsResponse] - getUTXODistribution *connect.Client[v1.GetUTXODistributionRequest, v1.GetUTXODistributionResponse] - bumpFee *connect.Client[v1.BumpFeeRequest, v1.BumpFeeResponse] - selectCoins *connect.Client[v1.SelectCoinsRequest, v1.SelectCoinsResponse] -} - -// CreateBitcoinCoreWallet calls wallet.v1.WalletService.CreateBitcoinCoreWallet. -func (c *walletServiceClient) CreateBitcoinCoreWallet(ctx context.Context, req *connect.Request[v1.CreateBitcoinCoreWalletRequest]) (*connect.Response[v1.CreateBitcoinCoreWalletResponse], error) { - return c.createBitcoinCoreWallet.CallUnary(ctx, req) -} - -// SendTransaction calls wallet.v1.WalletService.SendTransaction. -func (c *walletServiceClient) SendTransaction(ctx context.Context, req *connect.Request[v1.SendTransactionRequest]) (*connect.Response[v1.SendTransactionResponse], error) { - return c.sendTransaction.CallUnary(ctx, req) -} - -// GetBalance calls wallet.v1.WalletService.GetBalance. -func (c *walletServiceClient) GetBalance(ctx context.Context, req *connect.Request[v1.GetBalanceRequest]) (*connect.Response[v1.GetBalanceResponse], error) { - return c.getBalance.CallUnary(ctx, req) -} - -// GetNewAddress calls wallet.v1.WalletService.GetNewAddress. -func (c *walletServiceClient) GetNewAddress(ctx context.Context, req *connect.Request[v1.GetNewAddressRequest]) (*connect.Response[v1.GetNewAddressResponse], error) { - return c.getNewAddress.CallUnary(ctx, req) -} - -// ListTransactions calls wallet.v1.WalletService.ListTransactions. -func (c *walletServiceClient) ListTransactions(ctx context.Context, req *connect.Request[v1.ListTransactionsRequest]) (*connect.Response[v1.ListTransactionsResponse], error) { - return c.listTransactions.CallUnary(ctx, req) -} - -// ListUnspent calls wallet.v1.WalletService.ListUnspent. -func (c *walletServiceClient) ListUnspent(ctx context.Context, req *connect.Request[v1.ListUnspentRequest]) (*connect.Response[v1.ListUnspentResponse], error) { - return c.listUnspent.CallUnary(ctx, req) -} - -// ListReceiveAddresses calls wallet.v1.WalletService.ListReceiveAddresses. -func (c *walletServiceClient) ListReceiveAddresses(ctx context.Context, req *connect.Request[v1.ListReceiveAddressesRequest]) (*connect.Response[v1.ListReceiveAddressesResponse], error) { - return c.listReceiveAddresses.CallUnary(ctx, req) -} - -// ListSidechainDeposits calls wallet.v1.WalletService.ListSidechainDeposits. -func (c *walletServiceClient) ListSidechainDeposits(ctx context.Context, req *connect.Request[v1.ListSidechainDepositsRequest]) (*connect.Response[v1.ListSidechainDepositsResponse], error) { - return c.listSidechainDeposits.CallUnary(ctx, req) -} - -// CreateSidechainDeposit calls wallet.v1.WalletService.CreateSidechainDeposit. -func (c *walletServiceClient) CreateSidechainDeposit(ctx context.Context, req *connect.Request[v1.CreateSidechainDepositRequest]) (*connect.Response[v1.CreateSidechainDepositResponse], error) { - return c.createSidechainDeposit.CallUnary(ctx, req) -} - -// SignMessage calls wallet.v1.WalletService.SignMessage. -func (c *walletServiceClient) SignMessage(ctx context.Context, req *connect.Request[v1.SignMessageRequest]) (*connect.Response[v1.SignMessageResponse], error) { - return c.signMessage.CallUnary(ctx, req) -} - -// VerifyMessage calls wallet.v1.WalletService.VerifyMessage. -func (c *walletServiceClient) VerifyMessage(ctx context.Context, req *connect.Request[v1.VerifyMessageRequest]) (*connect.Response[v1.VerifyMessageResponse], error) { - return c.verifyMessage.CallUnary(ctx, req) -} - -// GetStats calls wallet.v1.WalletService.GetStats. -func (c *walletServiceClient) GetStats(ctx context.Context, req *connect.Request[v1.GetStatsRequest]) (*connect.Response[v1.GetStatsResponse], error) { - return c.getStats.CallUnary(ctx, req) -} - -// UnlockWallet calls wallet.v1.WalletService.UnlockWallet. -func (c *walletServiceClient) UnlockWallet(ctx context.Context, req *connect.Request[v1.UnlockWalletRequest]) (*connect.Response[emptypb.Empty], error) { - return c.unlockWallet.CallUnary(ctx, req) -} - -// LockWallet calls wallet.v1.WalletService.LockWallet. -func (c *walletServiceClient) LockWallet(ctx context.Context, req *connect.Request[emptypb.Empty]) (*connect.Response[emptypb.Empty], error) { - return c.lockWallet.CallUnary(ctx, req) -} - -// IsWalletUnlocked calls wallet.v1.WalletService.IsWalletUnlocked. -func (c *walletServiceClient) IsWalletUnlocked(ctx context.Context, req *connect.Request[emptypb.Empty]) (*connect.Response[emptypb.Empty], error) { - return c.isWalletUnlocked.CallUnary(ctx, req) -} - -// CreateCheque calls wallet.v1.WalletService.CreateCheque. -func (c *walletServiceClient) CreateCheque(ctx context.Context, req *connect.Request[v1.CreateChequeRequest]) (*connect.Response[v1.CreateChequeResponse], error) { - return c.createCheque.CallUnary(ctx, req) -} - -// GetCheque calls wallet.v1.WalletService.GetCheque. -func (c *walletServiceClient) GetCheque(ctx context.Context, req *connect.Request[v1.GetChequeRequest]) (*connect.Response[v1.GetChequeResponse], error) { - return c.getCheque.CallUnary(ctx, req) -} - -// GetChequePrivateKey calls wallet.v1.WalletService.GetChequePrivateKey. -func (c *walletServiceClient) GetChequePrivateKey(ctx context.Context, req *connect.Request[v1.GetChequePrivateKeyRequest]) (*connect.Response[v1.GetChequePrivateKeyResponse], error) { - return c.getChequePrivateKey.CallUnary(ctx, req) -} - -// ListCheques calls wallet.v1.WalletService.ListCheques. -func (c *walletServiceClient) ListCheques(ctx context.Context, req *connect.Request[v1.ListChequesRequest]) (*connect.Response[v1.ListChequesResponse], error) { - return c.listCheques.CallUnary(ctx, req) -} - -// CheckChequeFunding calls wallet.v1.WalletService.CheckChequeFunding. -func (c *walletServiceClient) CheckChequeFunding(ctx context.Context, req *connect.Request[v1.CheckChequeFundingRequest]) (*connect.Response[v1.CheckChequeFundingResponse], error) { - return c.checkChequeFunding.CallUnary(ctx, req) -} - -// SweepCheque calls wallet.v1.WalletService.SweepCheque. -func (c *walletServiceClient) SweepCheque(ctx context.Context, req *connect.Request[v1.SweepChequeRequest]) (*connect.Response[v1.SweepChequeResponse], error) { - return c.sweepCheque.CallUnary(ctx, req) -} - -// DeleteCheque calls wallet.v1.WalletService.DeleteCheque. -func (c *walletServiceClient) DeleteCheque(ctx context.Context, req *connect.Request[v1.DeleteChequeRequest]) (*connect.Response[emptypb.Empty], error) { - return c.deleteCheque.CallUnary(ctx, req) -} - -// SetUTXOMetadata calls wallet.v1.WalletService.SetUTXOMetadata. -func (c *walletServiceClient) SetUTXOMetadata(ctx context.Context, req *connect.Request[v1.SetUTXOMetadataRequest]) (*connect.Response[emptypb.Empty], error) { - return c.setUTXOMetadata.CallUnary(ctx, req) -} - -// GetUTXOMetadata calls wallet.v1.WalletService.GetUTXOMetadata. -func (c *walletServiceClient) GetUTXOMetadata(ctx context.Context, req *connect.Request[v1.GetUTXOMetadataRequest]) (*connect.Response[v1.GetUTXOMetadataResponse], error) { - return c.getUTXOMetadata.CallUnary(ctx, req) -} - -// SetCoinSelectionStrategy calls wallet.v1.WalletService.SetCoinSelectionStrategy. -func (c *walletServiceClient) SetCoinSelectionStrategy(ctx context.Context, req *connect.Request[v1.SetCoinSelectionStrategyRequest]) (*connect.Response[emptypb.Empty], error) { - return c.setCoinSelectionStrategy.CallUnary(ctx, req) -} - -// GetCoinSelectionStrategy calls wallet.v1.WalletService.GetCoinSelectionStrategy. -func (c *walletServiceClient) GetCoinSelectionStrategy(ctx context.Context, req *connect.Request[emptypb.Empty]) (*connect.Response[v1.GetCoinSelectionStrategyResponse], error) { - return c.getCoinSelectionStrategy.CallUnary(ctx, req) -} - -// GetTransactionDetails calls wallet.v1.WalletService.GetTransactionDetails. -func (c *walletServiceClient) GetTransactionDetails(ctx context.Context, req *connect.Request[v1.GetTransactionDetailsRequest]) (*connect.Response[v1.GetTransactionDetailsResponse], error) { - return c.getTransactionDetails.CallUnary(ctx, req) -} - -// GetUTXODistribution calls wallet.v1.WalletService.GetUTXODistribution. -func (c *walletServiceClient) GetUTXODistribution(ctx context.Context, req *connect.Request[v1.GetUTXODistributionRequest]) (*connect.Response[v1.GetUTXODistributionResponse], error) { - return c.getUTXODistribution.CallUnary(ctx, req) -} - -// BumpFee calls wallet.v1.WalletService.BumpFee. -func (c *walletServiceClient) BumpFee(ctx context.Context, req *connect.Request[v1.BumpFeeRequest]) (*connect.Response[v1.BumpFeeResponse], error) { - return c.bumpFee.CallUnary(ctx, req) -} - -// SelectCoins calls wallet.v1.WalletService.SelectCoins. -func (c *walletServiceClient) SelectCoins(ctx context.Context, req *connect.Request[v1.SelectCoinsRequest]) (*connect.Response[v1.SelectCoinsResponse], error) { - return c.selectCoins.CallUnary(ctx, req) -} - -// WalletServiceHandler is an implementation of the wallet.v1.WalletService service. -type WalletServiceHandler interface { - CreateBitcoinCoreWallet(context.Context, *connect.Request[v1.CreateBitcoinCoreWalletRequest]) (*connect.Response[v1.CreateBitcoinCoreWalletResponse], error) - SendTransaction(context.Context, *connect.Request[v1.SendTransactionRequest]) (*connect.Response[v1.SendTransactionResponse], error) - GetBalance(context.Context, *connect.Request[v1.GetBalanceRequest]) (*connect.Response[v1.GetBalanceResponse], error) - // Problem: deriving nilly willy here is potentially problematic. There's no way of listing - // out unused addresses, so we risk crossing the sync gap. - GetNewAddress(context.Context, *connect.Request[v1.GetNewAddressRequest]) (*connect.Response[v1.GetNewAddressResponse], error) - ListTransactions(context.Context, *connect.Request[v1.ListTransactionsRequest]) (*connect.Response[v1.ListTransactionsResponse], error) - ListUnspent(context.Context, *connect.Request[v1.ListUnspentRequest]) (*connect.Response[v1.ListUnspentResponse], error) - ListReceiveAddresses(context.Context, *connect.Request[v1.ListReceiveAddressesRequest]) (*connect.Response[v1.ListReceiveAddressesResponse], error) - ListSidechainDeposits(context.Context, *connect.Request[v1.ListSidechainDepositsRequest]) (*connect.Response[v1.ListSidechainDepositsResponse], error) - CreateSidechainDeposit(context.Context, *connect.Request[v1.CreateSidechainDepositRequest]) (*connect.Response[v1.CreateSidechainDepositResponse], error) - SignMessage(context.Context, *connect.Request[v1.SignMessageRequest]) (*connect.Response[v1.SignMessageResponse], error) - VerifyMessage(context.Context, *connect.Request[v1.VerifyMessageRequest]) (*connect.Response[v1.VerifyMessageResponse], error) - GetStats(context.Context, *connect.Request[v1.GetStatsRequest]) (*connect.Response[v1.GetStatsResponse], error) - // Wallet unlock/lock for cheque operations - UnlockWallet(context.Context, *connect.Request[v1.UnlockWalletRequest]) (*connect.Response[emptypb.Empty], error) - LockWallet(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[emptypb.Empty], error) - IsWalletUnlocked(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[emptypb.Empty], error) - // Cheque operations - CreateCheque(context.Context, *connect.Request[v1.CreateChequeRequest]) (*connect.Response[v1.CreateChequeResponse], error) - GetCheque(context.Context, *connect.Request[v1.GetChequeRequest]) (*connect.Response[v1.GetChequeResponse], error) - GetChequePrivateKey(context.Context, *connect.Request[v1.GetChequePrivateKeyRequest]) (*connect.Response[v1.GetChequePrivateKeyResponse], error) - ListCheques(context.Context, *connect.Request[v1.ListChequesRequest]) (*connect.Response[v1.ListChequesResponse], error) - CheckChequeFunding(context.Context, *connect.Request[v1.CheckChequeFundingRequest]) (*connect.Response[v1.CheckChequeFundingResponse], error) - SweepCheque(context.Context, *connect.Request[v1.SweepChequeRequest]) (*connect.Response[v1.SweepChequeResponse], error) - DeleteCheque(context.Context, *connect.Request[v1.DeleteChequeRequest]) (*connect.Response[emptypb.Empty], error) - // UTXO Coin Control - SetUTXOMetadata(context.Context, *connect.Request[v1.SetUTXOMetadataRequest]) (*connect.Response[emptypb.Empty], error) - GetUTXOMetadata(context.Context, *connect.Request[v1.GetUTXOMetadataRequest]) (*connect.Response[v1.GetUTXOMetadataResponse], error) - // Coin Selection Preferences - SetCoinSelectionStrategy(context.Context, *connect.Request[v1.SetCoinSelectionStrategyRequest]) (*connect.Response[emptypb.Empty], error) - GetCoinSelectionStrategy(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.GetCoinSelectionStrategyResponse], error) - // Transaction Details (enriched with input values/addresses) - GetTransactionDetails(context.Context, *connect.Request[v1.GetTransactionDetailsRequest]) (*connect.Response[v1.GetTransactionDetailsResponse], error) - // UTXO Distribution (for chart visualization) - GetUTXODistribution(context.Context, *connect.Request[v1.GetUTXODistributionRequest]) (*connect.Response[v1.GetUTXODistributionResponse], error) - // RBF - Replace-By-Fee for unconfirmed transactions - BumpFee(context.Context, *connect.Request[v1.BumpFeeRequest]) (*connect.Response[v1.BumpFeeResponse], error) - // Coin Selection - Select UTXOs for a transaction - SelectCoins(context.Context, *connect.Request[v1.SelectCoinsRequest]) (*connect.Response[v1.SelectCoinsResponse], error) -} - -// NewWalletServiceHandler builds an HTTP handler from the service implementation. It returns the -// path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewWalletServiceHandler(svc WalletServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - walletServiceMethods := v1.File_wallet_v1_wallet_proto.Services().ByName("WalletService").Methods() - walletServiceCreateBitcoinCoreWalletHandler := connect.NewUnaryHandler( - WalletServiceCreateBitcoinCoreWalletProcedure, - svc.CreateBitcoinCoreWallet, - connect.WithSchema(walletServiceMethods.ByName("CreateBitcoinCoreWallet")), - connect.WithHandlerOptions(opts...), - ) - walletServiceSendTransactionHandler := connect.NewUnaryHandler( - WalletServiceSendTransactionProcedure, - svc.SendTransaction, - connect.WithSchema(walletServiceMethods.ByName("SendTransaction")), - connect.WithHandlerOptions(opts...), - ) - walletServiceGetBalanceHandler := connect.NewUnaryHandler( - WalletServiceGetBalanceProcedure, - svc.GetBalance, - connect.WithSchema(walletServiceMethods.ByName("GetBalance")), - connect.WithHandlerOptions(opts...), - ) - walletServiceGetNewAddressHandler := connect.NewUnaryHandler( - WalletServiceGetNewAddressProcedure, - svc.GetNewAddress, - connect.WithSchema(walletServiceMethods.ByName("GetNewAddress")), - connect.WithHandlerOptions(opts...), - ) - walletServiceListTransactionsHandler := connect.NewUnaryHandler( - WalletServiceListTransactionsProcedure, - svc.ListTransactions, - connect.WithSchema(walletServiceMethods.ByName("ListTransactions")), - connect.WithHandlerOptions(opts...), - ) - walletServiceListUnspentHandler := connect.NewUnaryHandler( - WalletServiceListUnspentProcedure, - svc.ListUnspent, - connect.WithSchema(walletServiceMethods.ByName("ListUnspent")), - connect.WithHandlerOptions(opts...), - ) - walletServiceListReceiveAddressesHandler := connect.NewUnaryHandler( - WalletServiceListReceiveAddressesProcedure, - svc.ListReceiveAddresses, - connect.WithSchema(walletServiceMethods.ByName("ListReceiveAddresses")), - connect.WithHandlerOptions(opts...), - ) - walletServiceListSidechainDepositsHandler := connect.NewUnaryHandler( - WalletServiceListSidechainDepositsProcedure, - svc.ListSidechainDeposits, - connect.WithSchema(walletServiceMethods.ByName("ListSidechainDeposits")), - connect.WithHandlerOptions(opts...), - ) - walletServiceCreateSidechainDepositHandler := connect.NewUnaryHandler( - WalletServiceCreateSidechainDepositProcedure, - svc.CreateSidechainDeposit, - connect.WithSchema(walletServiceMethods.ByName("CreateSidechainDeposit")), - connect.WithHandlerOptions(opts...), - ) - walletServiceSignMessageHandler := connect.NewUnaryHandler( - WalletServiceSignMessageProcedure, - svc.SignMessage, - connect.WithSchema(walletServiceMethods.ByName("SignMessage")), - connect.WithHandlerOptions(opts...), - ) - walletServiceVerifyMessageHandler := connect.NewUnaryHandler( - WalletServiceVerifyMessageProcedure, - svc.VerifyMessage, - connect.WithSchema(walletServiceMethods.ByName("VerifyMessage")), - connect.WithHandlerOptions(opts...), - ) - walletServiceGetStatsHandler := connect.NewUnaryHandler( - WalletServiceGetStatsProcedure, - svc.GetStats, - connect.WithSchema(walletServiceMethods.ByName("GetStats")), - connect.WithHandlerOptions(opts...), - ) - walletServiceUnlockWalletHandler := connect.NewUnaryHandler( - WalletServiceUnlockWalletProcedure, - svc.UnlockWallet, - connect.WithSchema(walletServiceMethods.ByName("UnlockWallet")), - connect.WithHandlerOptions(opts...), - ) - walletServiceLockWalletHandler := connect.NewUnaryHandler( - WalletServiceLockWalletProcedure, - svc.LockWallet, - connect.WithSchema(walletServiceMethods.ByName("LockWallet")), - connect.WithHandlerOptions(opts...), - ) - walletServiceIsWalletUnlockedHandler := connect.NewUnaryHandler( - WalletServiceIsWalletUnlockedProcedure, - svc.IsWalletUnlocked, - connect.WithSchema(walletServiceMethods.ByName("IsWalletUnlocked")), - connect.WithHandlerOptions(opts...), - ) - walletServiceCreateChequeHandler := connect.NewUnaryHandler( - WalletServiceCreateChequeProcedure, - svc.CreateCheque, - connect.WithSchema(walletServiceMethods.ByName("CreateCheque")), - connect.WithHandlerOptions(opts...), - ) - walletServiceGetChequeHandler := connect.NewUnaryHandler( - WalletServiceGetChequeProcedure, - svc.GetCheque, - connect.WithSchema(walletServiceMethods.ByName("GetCheque")), - connect.WithHandlerOptions(opts...), - ) - walletServiceGetChequePrivateKeyHandler := connect.NewUnaryHandler( - WalletServiceGetChequePrivateKeyProcedure, - svc.GetChequePrivateKey, - connect.WithSchema(walletServiceMethods.ByName("GetChequePrivateKey")), - connect.WithHandlerOptions(opts...), - ) - walletServiceListChequesHandler := connect.NewUnaryHandler( - WalletServiceListChequesProcedure, - svc.ListCheques, - connect.WithSchema(walletServiceMethods.ByName("ListCheques")), - connect.WithHandlerOptions(opts...), - ) - walletServiceCheckChequeFundingHandler := connect.NewUnaryHandler( - WalletServiceCheckChequeFundingProcedure, - svc.CheckChequeFunding, - connect.WithSchema(walletServiceMethods.ByName("CheckChequeFunding")), - connect.WithHandlerOptions(opts...), - ) - walletServiceSweepChequeHandler := connect.NewUnaryHandler( - WalletServiceSweepChequeProcedure, - svc.SweepCheque, - connect.WithSchema(walletServiceMethods.ByName("SweepCheque")), - connect.WithHandlerOptions(opts...), - ) - walletServiceDeleteChequeHandler := connect.NewUnaryHandler( - WalletServiceDeleteChequeProcedure, - svc.DeleteCheque, - connect.WithSchema(walletServiceMethods.ByName("DeleteCheque")), - connect.WithHandlerOptions(opts...), - ) - walletServiceSetUTXOMetadataHandler := connect.NewUnaryHandler( - WalletServiceSetUTXOMetadataProcedure, - svc.SetUTXOMetadata, - connect.WithSchema(walletServiceMethods.ByName("SetUTXOMetadata")), - connect.WithHandlerOptions(opts...), - ) - walletServiceGetUTXOMetadataHandler := connect.NewUnaryHandler( - WalletServiceGetUTXOMetadataProcedure, - svc.GetUTXOMetadata, - connect.WithSchema(walletServiceMethods.ByName("GetUTXOMetadata")), - connect.WithHandlerOptions(opts...), - ) - walletServiceSetCoinSelectionStrategyHandler := connect.NewUnaryHandler( - WalletServiceSetCoinSelectionStrategyProcedure, - svc.SetCoinSelectionStrategy, - connect.WithSchema(walletServiceMethods.ByName("SetCoinSelectionStrategy")), - connect.WithHandlerOptions(opts...), - ) - walletServiceGetCoinSelectionStrategyHandler := connect.NewUnaryHandler( - WalletServiceGetCoinSelectionStrategyProcedure, - svc.GetCoinSelectionStrategy, - connect.WithSchema(walletServiceMethods.ByName("GetCoinSelectionStrategy")), - connect.WithHandlerOptions(opts...), - ) - walletServiceGetTransactionDetailsHandler := connect.NewUnaryHandler( - WalletServiceGetTransactionDetailsProcedure, - svc.GetTransactionDetails, - connect.WithSchema(walletServiceMethods.ByName("GetTransactionDetails")), - connect.WithHandlerOptions(opts...), - ) - walletServiceGetUTXODistributionHandler := connect.NewUnaryHandler( - WalletServiceGetUTXODistributionProcedure, - svc.GetUTXODistribution, - connect.WithSchema(walletServiceMethods.ByName("GetUTXODistribution")), - connect.WithHandlerOptions(opts...), - ) - walletServiceBumpFeeHandler := connect.NewUnaryHandler( - WalletServiceBumpFeeProcedure, - svc.BumpFee, - connect.WithSchema(walletServiceMethods.ByName("BumpFee")), - connect.WithHandlerOptions(opts...), - ) - walletServiceSelectCoinsHandler := connect.NewUnaryHandler( - WalletServiceSelectCoinsProcedure, - svc.SelectCoins, - connect.WithSchema(walletServiceMethods.ByName("SelectCoins")), - connect.WithHandlerOptions(opts...), - ) - return "/wallet.v1.WalletService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case WalletServiceCreateBitcoinCoreWalletProcedure: - walletServiceCreateBitcoinCoreWalletHandler.ServeHTTP(w, r) - case WalletServiceSendTransactionProcedure: - walletServiceSendTransactionHandler.ServeHTTP(w, r) - case WalletServiceGetBalanceProcedure: - walletServiceGetBalanceHandler.ServeHTTP(w, r) - case WalletServiceGetNewAddressProcedure: - walletServiceGetNewAddressHandler.ServeHTTP(w, r) - case WalletServiceListTransactionsProcedure: - walletServiceListTransactionsHandler.ServeHTTP(w, r) - case WalletServiceListUnspentProcedure: - walletServiceListUnspentHandler.ServeHTTP(w, r) - case WalletServiceListReceiveAddressesProcedure: - walletServiceListReceiveAddressesHandler.ServeHTTP(w, r) - case WalletServiceListSidechainDepositsProcedure: - walletServiceListSidechainDepositsHandler.ServeHTTP(w, r) - case WalletServiceCreateSidechainDepositProcedure: - walletServiceCreateSidechainDepositHandler.ServeHTTP(w, r) - case WalletServiceSignMessageProcedure: - walletServiceSignMessageHandler.ServeHTTP(w, r) - case WalletServiceVerifyMessageProcedure: - walletServiceVerifyMessageHandler.ServeHTTP(w, r) - case WalletServiceGetStatsProcedure: - walletServiceGetStatsHandler.ServeHTTP(w, r) - case WalletServiceUnlockWalletProcedure: - walletServiceUnlockWalletHandler.ServeHTTP(w, r) - case WalletServiceLockWalletProcedure: - walletServiceLockWalletHandler.ServeHTTP(w, r) - case WalletServiceIsWalletUnlockedProcedure: - walletServiceIsWalletUnlockedHandler.ServeHTTP(w, r) - case WalletServiceCreateChequeProcedure: - walletServiceCreateChequeHandler.ServeHTTP(w, r) - case WalletServiceGetChequeProcedure: - walletServiceGetChequeHandler.ServeHTTP(w, r) - case WalletServiceGetChequePrivateKeyProcedure: - walletServiceGetChequePrivateKeyHandler.ServeHTTP(w, r) - case WalletServiceListChequesProcedure: - walletServiceListChequesHandler.ServeHTTP(w, r) - case WalletServiceCheckChequeFundingProcedure: - walletServiceCheckChequeFundingHandler.ServeHTTP(w, r) - case WalletServiceSweepChequeProcedure: - walletServiceSweepChequeHandler.ServeHTTP(w, r) - case WalletServiceDeleteChequeProcedure: - walletServiceDeleteChequeHandler.ServeHTTP(w, r) - case WalletServiceSetUTXOMetadataProcedure: - walletServiceSetUTXOMetadataHandler.ServeHTTP(w, r) - case WalletServiceGetUTXOMetadataProcedure: - walletServiceGetUTXOMetadataHandler.ServeHTTP(w, r) - case WalletServiceSetCoinSelectionStrategyProcedure: - walletServiceSetCoinSelectionStrategyHandler.ServeHTTP(w, r) - case WalletServiceGetCoinSelectionStrategyProcedure: - walletServiceGetCoinSelectionStrategyHandler.ServeHTTP(w, r) - case WalletServiceGetTransactionDetailsProcedure: - walletServiceGetTransactionDetailsHandler.ServeHTTP(w, r) - case WalletServiceGetUTXODistributionProcedure: - walletServiceGetUTXODistributionHandler.ServeHTTP(w, r) - case WalletServiceBumpFeeProcedure: - walletServiceBumpFeeHandler.ServeHTTP(w, r) - case WalletServiceSelectCoinsProcedure: - walletServiceSelectCoinsHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedWalletServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedWalletServiceHandler struct{} - -func (UnimplementedWalletServiceHandler) CreateBitcoinCoreWallet(context.Context, *connect.Request[v1.CreateBitcoinCoreWalletRequest]) (*connect.Response[v1.CreateBitcoinCoreWalletResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.CreateBitcoinCoreWallet is not implemented")) -} - -func (UnimplementedWalletServiceHandler) SendTransaction(context.Context, *connect.Request[v1.SendTransactionRequest]) (*connect.Response[v1.SendTransactionResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.SendTransaction is not implemented")) -} - -func (UnimplementedWalletServiceHandler) GetBalance(context.Context, *connect.Request[v1.GetBalanceRequest]) (*connect.Response[v1.GetBalanceResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.GetBalance is not implemented")) -} - -func (UnimplementedWalletServiceHandler) GetNewAddress(context.Context, *connect.Request[v1.GetNewAddressRequest]) (*connect.Response[v1.GetNewAddressResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.GetNewAddress is not implemented")) -} - -func (UnimplementedWalletServiceHandler) ListTransactions(context.Context, *connect.Request[v1.ListTransactionsRequest]) (*connect.Response[v1.ListTransactionsResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.ListTransactions is not implemented")) -} - -func (UnimplementedWalletServiceHandler) ListUnspent(context.Context, *connect.Request[v1.ListUnspentRequest]) (*connect.Response[v1.ListUnspentResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.ListUnspent is not implemented")) -} - -func (UnimplementedWalletServiceHandler) ListReceiveAddresses(context.Context, *connect.Request[v1.ListReceiveAddressesRequest]) (*connect.Response[v1.ListReceiveAddressesResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.ListReceiveAddresses is not implemented")) -} - -func (UnimplementedWalletServiceHandler) ListSidechainDeposits(context.Context, *connect.Request[v1.ListSidechainDepositsRequest]) (*connect.Response[v1.ListSidechainDepositsResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.ListSidechainDeposits is not implemented")) -} - -func (UnimplementedWalletServiceHandler) CreateSidechainDeposit(context.Context, *connect.Request[v1.CreateSidechainDepositRequest]) (*connect.Response[v1.CreateSidechainDepositResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.CreateSidechainDeposit is not implemented")) -} - -func (UnimplementedWalletServiceHandler) SignMessage(context.Context, *connect.Request[v1.SignMessageRequest]) (*connect.Response[v1.SignMessageResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.SignMessage is not implemented")) -} - -func (UnimplementedWalletServiceHandler) VerifyMessage(context.Context, *connect.Request[v1.VerifyMessageRequest]) (*connect.Response[v1.VerifyMessageResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.VerifyMessage is not implemented")) -} - -func (UnimplementedWalletServiceHandler) GetStats(context.Context, *connect.Request[v1.GetStatsRequest]) (*connect.Response[v1.GetStatsResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.GetStats is not implemented")) -} - -func (UnimplementedWalletServiceHandler) UnlockWallet(context.Context, *connect.Request[v1.UnlockWalletRequest]) (*connect.Response[emptypb.Empty], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.UnlockWallet is not implemented")) -} - -func (UnimplementedWalletServiceHandler) LockWallet(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[emptypb.Empty], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.LockWallet is not implemented")) -} - -func (UnimplementedWalletServiceHandler) IsWalletUnlocked(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[emptypb.Empty], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.IsWalletUnlocked is not implemented")) -} - -func (UnimplementedWalletServiceHandler) CreateCheque(context.Context, *connect.Request[v1.CreateChequeRequest]) (*connect.Response[v1.CreateChequeResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.CreateCheque is not implemented")) -} - -func (UnimplementedWalletServiceHandler) GetCheque(context.Context, *connect.Request[v1.GetChequeRequest]) (*connect.Response[v1.GetChequeResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.GetCheque is not implemented")) -} - -func (UnimplementedWalletServiceHandler) GetChequePrivateKey(context.Context, *connect.Request[v1.GetChequePrivateKeyRequest]) (*connect.Response[v1.GetChequePrivateKeyResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.GetChequePrivateKey is not implemented")) -} - -func (UnimplementedWalletServiceHandler) ListCheques(context.Context, *connect.Request[v1.ListChequesRequest]) (*connect.Response[v1.ListChequesResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.ListCheques is not implemented")) -} - -func (UnimplementedWalletServiceHandler) CheckChequeFunding(context.Context, *connect.Request[v1.CheckChequeFundingRequest]) (*connect.Response[v1.CheckChequeFundingResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.CheckChequeFunding is not implemented")) -} - -func (UnimplementedWalletServiceHandler) SweepCheque(context.Context, *connect.Request[v1.SweepChequeRequest]) (*connect.Response[v1.SweepChequeResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.SweepCheque is not implemented")) -} - -func (UnimplementedWalletServiceHandler) DeleteCheque(context.Context, *connect.Request[v1.DeleteChequeRequest]) (*connect.Response[emptypb.Empty], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.DeleteCheque is not implemented")) -} - -func (UnimplementedWalletServiceHandler) SetUTXOMetadata(context.Context, *connect.Request[v1.SetUTXOMetadataRequest]) (*connect.Response[emptypb.Empty], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.SetUTXOMetadata is not implemented")) -} - -func (UnimplementedWalletServiceHandler) GetUTXOMetadata(context.Context, *connect.Request[v1.GetUTXOMetadataRequest]) (*connect.Response[v1.GetUTXOMetadataResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.GetUTXOMetadata is not implemented")) -} - -func (UnimplementedWalletServiceHandler) SetCoinSelectionStrategy(context.Context, *connect.Request[v1.SetCoinSelectionStrategyRequest]) (*connect.Response[emptypb.Empty], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.SetCoinSelectionStrategy is not implemented")) -} - -func (UnimplementedWalletServiceHandler) GetCoinSelectionStrategy(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.GetCoinSelectionStrategyResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.GetCoinSelectionStrategy is not implemented")) -} - -func (UnimplementedWalletServiceHandler) GetTransactionDetails(context.Context, *connect.Request[v1.GetTransactionDetailsRequest]) (*connect.Response[v1.GetTransactionDetailsResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.GetTransactionDetails is not implemented")) -} - -func (UnimplementedWalletServiceHandler) GetUTXODistribution(context.Context, *connect.Request[v1.GetUTXODistributionRequest]) (*connect.Response[v1.GetUTXODistributionResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.GetUTXODistribution is not implemented")) -} - -func (UnimplementedWalletServiceHandler) BumpFee(context.Context, *connect.Request[v1.BumpFeeRequest]) (*connect.Response[v1.BumpFeeResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.BumpFee is not implemented")) -} - -func (UnimplementedWalletServiceHandler) SelectCoins(context.Context, *connect.Request[v1.SelectCoinsRequest]) (*connect.Response[v1.SelectCoinsResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.SelectCoins is not implemented")) -} diff --git a/bitwindow/server/gen/wallet/v1/wallet.pb.go b/bitwindow/server/gen/wallet/v1/wallet.pb.go index fa462c94e..7019ccc14 100644 --- a/bitwindow/server/gen/wallet/v1/wallet.pb.go +++ b/bitwindow/server/gen/wallet/v1/wallet.pb.go @@ -2878,124 +2878,6 @@ func (x *DeleteChequeRequest) GetId() int64 { return 0 } -type CreateBitcoinCoreWalletRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // BIP32 seed as hex string (64 bytes = 128 hex chars) - // This is the output of BIP39 PBKDF2(mnemonic + passphrase) - SeedHex string `protobuf:"bytes,1,opt,name=seed_hex,json=seedHex,proto3" json:"seed_hex,omitempty"` - // Wallet name for display - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateBitcoinCoreWalletRequest) Reset() { - *x = CreateBitcoinCoreWalletRequest{} - mi := &file_wallet_v1_wallet_proto_msgTypes[44] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateBitcoinCoreWalletRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateBitcoinCoreWalletRequest) ProtoMessage() {} - -func (x *CreateBitcoinCoreWalletRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[44] - 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 CreateBitcoinCoreWalletRequest.ProtoReflect.Descriptor instead. -func (*CreateBitcoinCoreWalletRequest) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{44} -} - -func (x *CreateBitcoinCoreWalletRequest) GetSeedHex() string { - if x != nil { - return x.SeedHex - } - return "" -} - -func (x *CreateBitcoinCoreWalletRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -type CreateBitcoinCoreWalletResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The wallet ID that was created - WalletId string `protobuf:"bytes,1,opt,name=wallet_id,json=walletId,proto3" json:"wallet_id,omitempty"` - // The Bitcoin Core wallet name - CoreWalletName string `protobuf:"bytes,2,opt,name=core_wallet_name,json=coreWalletName,proto3" json:"core_wallet_name,omitempty"` - // First receiving address for verification - FirstAddress string `protobuf:"bytes,3,opt,name=first_address,json=firstAddress,proto3" json:"first_address,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateBitcoinCoreWalletResponse) Reset() { - *x = CreateBitcoinCoreWalletResponse{} - mi := &file_wallet_v1_wallet_proto_msgTypes[45] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateBitcoinCoreWalletResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateBitcoinCoreWalletResponse) ProtoMessage() {} - -func (x *CreateBitcoinCoreWalletResponse) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[45] - 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 CreateBitcoinCoreWalletResponse.ProtoReflect.Descriptor instead. -func (*CreateBitcoinCoreWalletResponse) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{45} -} - -func (x *CreateBitcoinCoreWalletResponse) GetWalletId() string { - if x != nil { - return x.WalletId - } - return "" -} - -func (x *CreateBitcoinCoreWalletResponse) GetCoreWalletName() string { - if x != nil { - return x.CoreWalletName - } - return "" -} - -func (x *CreateBitcoinCoreWalletResponse) GetFirstAddress() string { - if x != nil { - return x.FirstAddress - } - return "" -} - // UTXO Coin Control metadata type UTXOMetadata struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -3008,7 +2890,7 @@ type UTXOMetadata struct { func (x *UTXOMetadata) Reset() { *x = UTXOMetadata{} - mi := &file_wallet_v1_wallet_proto_msgTypes[46] + mi := &file_wallet_v1_wallet_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3020,7 +2902,7 @@ func (x *UTXOMetadata) String() string { func (*UTXOMetadata) ProtoMessage() {} func (x *UTXOMetadata) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[46] + mi := &file_wallet_v1_wallet_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3033,7 +2915,7 @@ func (x *UTXOMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use UTXOMetadata.ProtoReflect.Descriptor instead. func (*UTXOMetadata) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{46} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{44} } func (x *UTXOMetadata) GetOutpoint() string { @@ -3068,7 +2950,7 @@ type SetUTXOMetadataRequest struct { func (x *SetUTXOMetadataRequest) Reset() { *x = SetUTXOMetadataRequest{} - mi := &file_wallet_v1_wallet_proto_msgTypes[47] + mi := &file_wallet_v1_wallet_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3080,7 +2962,7 @@ func (x *SetUTXOMetadataRequest) String() string { func (*SetUTXOMetadataRequest) ProtoMessage() {} func (x *SetUTXOMetadataRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[47] + mi := &file_wallet_v1_wallet_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3093,7 +2975,7 @@ func (x *SetUTXOMetadataRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetUTXOMetadataRequest.ProtoReflect.Descriptor instead. func (*SetUTXOMetadataRequest) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{47} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{45} } func (x *SetUTXOMetadataRequest) GetOutpoint() string { @@ -3126,7 +3008,7 @@ type GetUTXOMetadataRequest struct { func (x *GetUTXOMetadataRequest) Reset() { *x = GetUTXOMetadataRequest{} - mi := &file_wallet_v1_wallet_proto_msgTypes[48] + mi := &file_wallet_v1_wallet_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3138,7 +3020,7 @@ func (x *GetUTXOMetadataRequest) String() string { func (*GetUTXOMetadataRequest) ProtoMessage() {} func (x *GetUTXOMetadataRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[48] + mi := &file_wallet_v1_wallet_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3151,7 +3033,7 @@ func (x *GetUTXOMetadataRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetUTXOMetadataRequest.ProtoReflect.Descriptor instead. func (*GetUTXOMetadataRequest) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{48} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{46} } func (x *GetUTXOMetadataRequest) GetOutpoints() []string { @@ -3170,7 +3052,7 @@ type GetUTXOMetadataResponse struct { func (x *GetUTXOMetadataResponse) Reset() { *x = GetUTXOMetadataResponse{} - mi := &file_wallet_v1_wallet_proto_msgTypes[49] + mi := &file_wallet_v1_wallet_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3182,7 +3064,7 @@ func (x *GetUTXOMetadataResponse) String() string { func (*GetUTXOMetadataResponse) ProtoMessage() {} func (x *GetUTXOMetadataResponse) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[49] + mi := &file_wallet_v1_wallet_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3195,7 +3077,7 @@ func (x *GetUTXOMetadataResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetUTXOMetadataResponse.ProtoReflect.Descriptor instead. func (*GetUTXOMetadataResponse) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{49} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{47} } func (x *GetUTXOMetadataResponse) GetMetadata() map[string]*UTXOMetadata { @@ -3214,7 +3096,7 @@ type SetCoinSelectionStrategyRequest struct { func (x *SetCoinSelectionStrategyRequest) Reset() { *x = SetCoinSelectionStrategyRequest{} - mi := &file_wallet_v1_wallet_proto_msgTypes[50] + mi := &file_wallet_v1_wallet_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3226,7 +3108,7 @@ func (x *SetCoinSelectionStrategyRequest) String() string { func (*SetCoinSelectionStrategyRequest) ProtoMessage() {} func (x *SetCoinSelectionStrategyRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[50] + mi := &file_wallet_v1_wallet_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3239,7 +3121,7 @@ func (x *SetCoinSelectionStrategyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetCoinSelectionStrategyRequest.ProtoReflect.Descriptor instead. func (*SetCoinSelectionStrategyRequest) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{50} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{48} } func (x *SetCoinSelectionStrategyRequest) GetStrategy() CoinSelectionStrategy { @@ -3258,7 +3140,7 @@ type GetCoinSelectionStrategyResponse struct { func (x *GetCoinSelectionStrategyResponse) Reset() { *x = GetCoinSelectionStrategyResponse{} - mi := &file_wallet_v1_wallet_proto_msgTypes[51] + mi := &file_wallet_v1_wallet_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3270,7 +3152,7 @@ func (x *GetCoinSelectionStrategyResponse) String() string { func (*GetCoinSelectionStrategyResponse) ProtoMessage() {} func (x *GetCoinSelectionStrategyResponse) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[51] + mi := &file_wallet_v1_wallet_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3283,7 +3165,7 @@ func (x *GetCoinSelectionStrategyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCoinSelectionStrategyResponse.ProtoReflect.Descriptor instead. func (*GetCoinSelectionStrategyResponse) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{51} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{49} } func (x *GetCoinSelectionStrategyResponse) GetStrategy() CoinSelectionStrategy { @@ -3303,7 +3185,7 @@ type GetTransactionDetailsRequest struct { func (x *GetTransactionDetailsRequest) Reset() { *x = GetTransactionDetailsRequest{} - mi := &file_wallet_v1_wallet_proto_msgTypes[52] + mi := &file_wallet_v1_wallet_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3315,7 +3197,7 @@ func (x *GetTransactionDetailsRequest) String() string { func (*GetTransactionDetailsRequest) ProtoMessage() {} func (x *GetTransactionDetailsRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[52] + mi := &file_wallet_v1_wallet_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3328,7 +3210,7 @@ func (x *GetTransactionDetailsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetTransactionDetailsRequest.ProtoReflect.Descriptor instead. func (*GetTransactionDetailsRequest) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{52} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{50} } func (x *GetTransactionDetailsRequest) GetTxid() string { @@ -3370,7 +3252,7 @@ type GetTransactionDetailsResponse struct { func (x *GetTransactionDetailsResponse) Reset() { *x = GetTransactionDetailsResponse{} - mi := &file_wallet_v1_wallet_proto_msgTypes[53] + mi := &file_wallet_v1_wallet_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3382,7 +3264,7 @@ func (x *GetTransactionDetailsResponse) String() string { func (*GetTransactionDetailsResponse) ProtoMessage() {} func (x *GetTransactionDetailsResponse) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[53] + mi := &file_wallet_v1_wallet_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3395,7 +3277,7 @@ func (x *GetTransactionDetailsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetTransactionDetailsResponse.ProtoReflect.Descriptor instead. func (*GetTransactionDetailsResponse) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{53} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{51} } func (x *GetTransactionDetailsResponse) GetTxid() string { @@ -3537,7 +3419,7 @@ type TransactionInput struct { func (x *TransactionInput) Reset() { *x = TransactionInput{} - mi := &file_wallet_v1_wallet_proto_msgTypes[54] + mi := &file_wallet_v1_wallet_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3549,7 +3431,7 @@ func (x *TransactionInput) String() string { func (*TransactionInput) ProtoMessage() {} func (x *TransactionInput) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[54] + mi := &file_wallet_v1_wallet_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3562,7 +3444,7 @@ func (x *TransactionInput) ProtoReflect() protoreflect.Message { // Deprecated: Use TransactionInput.ProtoReflect.Descriptor instead. func (*TransactionInput) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{54} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{52} } func (x *TransactionInput) GetIndex() int32 { @@ -3653,7 +3535,7 @@ type TransactionOutput struct { func (x *TransactionOutput) Reset() { *x = TransactionOutput{} - mi := &file_wallet_v1_wallet_proto_msgTypes[55] + mi := &file_wallet_v1_wallet_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3665,7 +3547,7 @@ func (x *TransactionOutput) String() string { func (*TransactionOutput) ProtoMessage() {} func (x *TransactionOutput) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[55] + mi := &file_wallet_v1_wallet_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3678,7 +3560,7 @@ func (x *TransactionOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use TransactionOutput.ProtoReflect.Descriptor instead. func (*TransactionOutput) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{55} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{53} } func (x *TransactionOutput) GetIndex() int32 { @@ -3748,7 +3630,7 @@ type GetUTXODistributionRequest struct { func (x *GetUTXODistributionRequest) Reset() { *x = GetUTXODistributionRequest{} - mi := &file_wallet_v1_wallet_proto_msgTypes[56] + mi := &file_wallet_v1_wallet_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3760,7 +3642,7 @@ func (x *GetUTXODistributionRequest) String() string { func (*GetUTXODistributionRequest) ProtoMessage() {} func (x *GetUTXODistributionRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[56] + mi := &file_wallet_v1_wallet_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3773,7 +3655,7 @@ func (x *GetUTXODistributionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetUTXODistributionRequest.ProtoReflect.Descriptor instead. func (*GetUTXODistributionRequest) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{56} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{54} } func (x *GetUTXODistributionRequest) GetWalletId() string { @@ -3799,7 +3681,7 @@ type GetUTXODistributionResponse struct { func (x *GetUTXODistributionResponse) Reset() { *x = GetUTXODistributionResponse{} - mi := &file_wallet_v1_wallet_proto_msgTypes[57] + mi := &file_wallet_v1_wallet_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3811,7 +3693,7 @@ func (x *GetUTXODistributionResponse) String() string { func (*GetUTXODistributionResponse) ProtoMessage() {} func (x *GetUTXODistributionResponse) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[57] + mi := &file_wallet_v1_wallet_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3824,7 +3706,7 @@ func (x *GetUTXODistributionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetUTXODistributionResponse.ProtoReflect.Descriptor instead. func (*GetUTXODistributionResponse) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{57} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{55} } func (x *GetUTXODistributionResponse) GetBuckets() []*UTXOBucket { @@ -3846,7 +3728,7 @@ type UTXOBucket struct { func (x *UTXOBucket) Reset() { *x = UTXOBucket{} - mi := &file_wallet_v1_wallet_proto_msgTypes[58] + mi := &file_wallet_v1_wallet_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3858,7 +3740,7 @@ func (x *UTXOBucket) String() string { func (*UTXOBucket) ProtoMessage() {} func (x *UTXOBucket) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[58] + mi := &file_wallet_v1_wallet_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3871,7 +3753,7 @@ func (x *UTXOBucket) ProtoReflect() protoreflect.Message { // Deprecated: Use UTXOBucket.ProtoReflect.Descriptor instead. func (*UTXOBucket) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{58} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{56} } func (x *UTXOBucket) GetLabel() string { @@ -3912,7 +3794,7 @@ type BumpFeeRequest struct { func (x *BumpFeeRequest) Reset() { *x = BumpFeeRequest{} - mi := &file_wallet_v1_wallet_proto_msgTypes[59] + mi := &file_wallet_v1_wallet_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3924,7 +3806,7 @@ func (x *BumpFeeRequest) String() string { func (*BumpFeeRequest) ProtoMessage() {} func (x *BumpFeeRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[59] + mi := &file_wallet_v1_wallet_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3937,7 +3819,7 @@ func (x *BumpFeeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use BumpFeeRequest.ProtoReflect.Descriptor instead. func (*BumpFeeRequest) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{59} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{57} } func (x *BumpFeeRequest) GetTxid() string { @@ -3958,7 +3840,7 @@ type BumpFeeResponse struct { func (x *BumpFeeResponse) Reset() { *x = BumpFeeResponse{} - mi := &file_wallet_v1_wallet_proto_msgTypes[60] + mi := &file_wallet_v1_wallet_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3970,7 +3852,7 @@ func (x *BumpFeeResponse) String() string { func (*BumpFeeResponse) ProtoMessage() {} func (x *BumpFeeResponse) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[60] + mi := &file_wallet_v1_wallet_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3983,7 +3865,7 @@ func (x *BumpFeeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BumpFeeResponse.ProtoReflect.Descriptor instead. func (*BumpFeeResponse) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{60} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{58} } func (x *BumpFeeResponse) GetTxid() string { @@ -4023,7 +3905,7 @@ type SelectCoinsRequest struct { func (x *SelectCoinsRequest) Reset() { *x = SelectCoinsRequest{} - mi := &file_wallet_v1_wallet_proto_msgTypes[61] + mi := &file_wallet_v1_wallet_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4035,7 +3917,7 @@ func (x *SelectCoinsRequest) String() string { func (*SelectCoinsRequest) ProtoMessage() {} func (x *SelectCoinsRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[61] + mi := &file_wallet_v1_wallet_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4048,7 +3930,7 @@ func (x *SelectCoinsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SelectCoinsRequest.ProtoReflect.Descriptor instead. func (*SelectCoinsRequest) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{61} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{59} } func (x *SelectCoinsRequest) GetWalletId() string { @@ -4112,7 +3994,7 @@ type SelectCoinsResponse struct { func (x *SelectCoinsResponse) Reset() { *x = SelectCoinsResponse{} - mi := &file_wallet_v1_wallet_proto_msgTypes[62] + mi := &file_wallet_v1_wallet_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4124,7 +4006,7 @@ func (x *SelectCoinsResponse) String() string { func (*SelectCoinsResponse) ProtoMessage() {} func (x *SelectCoinsResponse) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[62] + mi := &file_wallet_v1_wallet_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4137,7 +4019,7 @@ func (x *SelectCoinsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SelectCoinsResponse.ProtoReflect.Descriptor instead. func (*SelectCoinsResponse) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{62} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{60} } func (x *SelectCoinsResponse) GetSelectedUtxos() []*UnspentOutput { @@ -4181,7 +4063,7 @@ type CreateBackupResponse struct { func (x *CreateBackupResponse) Reset() { *x = CreateBackupResponse{} - mi := &file_wallet_v1_wallet_proto_msgTypes[63] + mi := &file_wallet_v1_wallet_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4193,7 +4075,7 @@ func (x *CreateBackupResponse) String() string { func (*CreateBackupResponse) ProtoMessage() {} func (x *CreateBackupResponse) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[63] + mi := &file_wallet_v1_wallet_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4206,7 +4088,7 @@ func (x *CreateBackupResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateBackupResponse.ProtoReflect.Descriptor instead. func (*CreateBackupResponse) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{63} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{61} } func (x *CreateBackupResponse) GetBackupData() []byte { @@ -4235,7 +4117,7 @@ type RestoreBackupRequest struct { func (x *RestoreBackupRequest) Reset() { *x = RestoreBackupRequest{} - mi := &file_wallet_v1_wallet_proto_msgTypes[64] + mi := &file_wallet_v1_wallet_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4247,7 +4129,7 @@ func (x *RestoreBackupRequest) String() string { func (*RestoreBackupRequest) ProtoMessage() {} func (x *RestoreBackupRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[64] + mi := &file_wallet_v1_wallet_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4260,7 +4142,7 @@ func (x *RestoreBackupRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreBackupRequest.ProtoReflect.Descriptor instead. func (*RestoreBackupRequest) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{64} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{62} } func (x *RestoreBackupRequest) GetBackupData() []byte { @@ -4289,7 +4171,7 @@ type ValidateBackupRequest struct { func (x *ValidateBackupRequest) Reset() { *x = ValidateBackupRequest{} - mi := &file_wallet_v1_wallet_proto_msgTypes[65] + mi := &file_wallet_v1_wallet_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4301,7 +4183,7 @@ func (x *ValidateBackupRequest) String() string { func (*ValidateBackupRequest) ProtoMessage() {} func (x *ValidateBackupRequest) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[65] + mi := &file_wallet_v1_wallet_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4314,7 +4196,7 @@ func (x *ValidateBackupRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateBackupRequest.ProtoReflect.Descriptor instead. func (*ValidateBackupRequest) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{65} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{63} } func (x *ValidateBackupRequest) GetBackupData() []byte { @@ -4345,7 +4227,7 @@ type ValidateBackupResponse struct { func (x *ValidateBackupResponse) Reset() { *x = ValidateBackupResponse{} - mi := &file_wallet_v1_wallet_proto_msgTypes[66] + mi := &file_wallet_v1_wallet_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4357,7 +4239,7 @@ func (x *ValidateBackupResponse) String() string { func (*ValidateBackupResponse) ProtoMessage() {} func (x *ValidateBackupResponse) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[66] + mi := &file_wallet_v1_wallet_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4370,7 +4252,7 @@ func (x *ValidateBackupResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateBackupResponse.ProtoReflect.Descriptor instead. func (*ValidateBackupResponse) Descriptor() ([]byte, []int) { - return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{66} + return file_wallet_v1_wallet_proto_rawDescGZIP(), []int{64} } func (x *ValidateBackupResponse) GetValid() bool { @@ -4420,7 +4302,7 @@ type ListSidechainDepositsResponse_SidechainDeposit struct { func (x *ListSidechainDepositsResponse_SidechainDeposit) Reset() { *x = ListSidechainDepositsResponse_SidechainDeposit{} - mi := &file_wallet_v1_wallet_proto_msgTypes[68] + mi := &file_wallet_v1_wallet_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4432,7 +4314,7 @@ func (x *ListSidechainDepositsResponse_SidechainDeposit) String() string { func (*ListSidechainDepositsResponse_SidechainDeposit) ProtoMessage() {} func (x *ListSidechainDepositsResponse_SidechainDeposit) ProtoReflect() protoreflect.Message { - mi := &file_wallet_v1_wallet_proto_msgTypes[68] + mi := &file_wallet_v1_wallet_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4691,14 +4573,7 @@ const file_wallet_v1_wallet_proto_rawDesc = "" + "\x11fee_sat_per_vbyte\x18\x03 \x01(\x04R\x0efeeSatPerVbyte\"B\n" + "\x13DeleteChequeRequest\x12\x1b\n" + "\twallet_id\x18\x01 \x01(\tR\bwalletId\x12\x0e\n" + - "\x02id\x18\x02 \x01(\x03R\x02id\"O\n" + - "\x1eCreateBitcoinCoreWalletRequest\x12\x19\n" + - "\bseed_hex\x18\x01 \x01(\tR\aseedHex\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04name\"\x8d\x01\n" + - "\x1fCreateBitcoinCoreWalletResponse\x12\x1b\n" + - "\twallet_id\x18\x01 \x01(\tR\bwalletId\x12(\n" + - "\x10core_wallet_name\x18\x02 \x01(\tR\x0ecoreWalletName\x12#\n" + - "\rfirst_address\x18\x03 \x01(\tR\ffirstAddress\"]\n" + + "\x02id\x18\x02 \x01(\x03R\x02id\"]\n" + "\fUTXOMetadata\x12\x1a\n" + "\boutpoint\x18\x01 \x01(\tR\boutpoint\x12\x1b\n" + "\tis_frozen\x18\x02 \x01(\bR\bisFrozen\x12\x14\n" + @@ -4836,9 +4711,8 @@ const file_wallet_v1_wallet_proto_rawDesc = "" + "%COIN_SELECTION_STRATEGY_LARGEST_FIRST\x10\x01\x12*\n" + "&COIN_SELECTION_STRATEGY_SMALLEST_FIRST\x10\x02\x12\"\n" + "\x1eCOIN_SELECTION_STRATEGY_RANDOM\x10\x03\x12,\n" + - "(COIN_SELECTION_STRATEGY_BRANCH_AND_BOUND\x10\x042\xe6\x16\n" + - "\rWalletService\x12p\n" + - "\x17CreateBitcoinCoreWallet\x12).wallet.v1.CreateBitcoinCoreWalletRequest\x1a*.wallet.v1.CreateBitcoinCoreWalletResponse\x12X\n" + + "(COIN_SELECTION_STRATEGY_BRANCH_AND_BOUND\x10\x042\xf4\x15\n" + + "\rWalletService\x12X\n" + "\x0fSendTransaction\x12!.wallet.v1.SendTransactionRequest\x1a\".wallet.v1.SendTransactionResponse\x12I\n" + "\n" + "GetBalance\x12\x1c.wallet.v1.GetBalanceRequest\x1a\x1d.wallet.v1.GetBalanceResponse\x12R\n" + @@ -4890,7 +4764,7 @@ func file_wallet_v1_wallet_proto_rawDescGZIP() []byte { } var file_wallet_v1_wallet_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_wallet_v1_wallet_proto_msgTypes = make([]protoimpl.MessageInfo, 70) +var file_wallet_v1_wallet_proto_msgTypes = make([]protoimpl.MessageInfo, 68) var file_wallet_v1_wallet_proto_goTypes = []any{ (AddressType)(0), // 0: wallet.v1.AddressType (SweepAddressKind)(0), // 1: wallet.v1.SweepAddressKind @@ -4939,137 +4813,133 @@ var file_wallet_v1_wallet_proto_goTypes = []any{ (*SweepChequeRequest)(nil), // 44: wallet.v1.SweepChequeRequest (*SweepChequeResponse)(nil), // 45: wallet.v1.SweepChequeResponse (*DeleteChequeRequest)(nil), // 46: wallet.v1.DeleteChequeRequest - (*CreateBitcoinCoreWalletRequest)(nil), // 47: wallet.v1.CreateBitcoinCoreWalletRequest - (*CreateBitcoinCoreWalletResponse)(nil), // 48: wallet.v1.CreateBitcoinCoreWalletResponse - (*UTXOMetadata)(nil), // 49: wallet.v1.UTXOMetadata - (*SetUTXOMetadataRequest)(nil), // 50: wallet.v1.SetUTXOMetadataRequest - (*GetUTXOMetadataRequest)(nil), // 51: wallet.v1.GetUTXOMetadataRequest - (*GetUTXOMetadataResponse)(nil), // 52: wallet.v1.GetUTXOMetadataResponse - (*SetCoinSelectionStrategyRequest)(nil), // 53: wallet.v1.SetCoinSelectionStrategyRequest - (*GetCoinSelectionStrategyResponse)(nil), // 54: wallet.v1.GetCoinSelectionStrategyResponse - (*GetTransactionDetailsRequest)(nil), // 55: wallet.v1.GetTransactionDetailsRequest - (*GetTransactionDetailsResponse)(nil), // 56: wallet.v1.GetTransactionDetailsResponse - (*TransactionInput)(nil), // 57: wallet.v1.TransactionInput - (*TransactionOutput)(nil), // 58: wallet.v1.TransactionOutput - (*GetUTXODistributionRequest)(nil), // 59: wallet.v1.GetUTXODistributionRequest - (*GetUTXODistributionResponse)(nil), // 60: wallet.v1.GetUTXODistributionResponse - (*UTXOBucket)(nil), // 61: wallet.v1.UTXOBucket - (*BumpFeeRequest)(nil), // 62: wallet.v1.BumpFeeRequest - (*BumpFeeResponse)(nil), // 63: wallet.v1.BumpFeeResponse - (*SelectCoinsRequest)(nil), // 64: wallet.v1.SelectCoinsRequest - (*SelectCoinsResponse)(nil), // 65: wallet.v1.SelectCoinsResponse - (*CreateBackupResponse)(nil), // 66: wallet.v1.CreateBackupResponse - (*RestoreBackupRequest)(nil), // 67: wallet.v1.RestoreBackupRequest - (*ValidateBackupRequest)(nil), // 68: wallet.v1.ValidateBackupRequest - (*ValidateBackupResponse)(nil), // 69: wallet.v1.ValidateBackupResponse - nil, // 70: wallet.v1.SendTransactionRequest.DestinationsEntry - (*ListSidechainDepositsResponse_SidechainDeposit)(nil), // 71: wallet.v1.ListSidechainDepositsResponse.SidechainDeposit - nil, // 72: wallet.v1.GetUTXOMetadataResponse.MetadataEntry - (*timestamppb.Timestamp)(nil), // 73: google.protobuf.Timestamp - (*v1.DenialInfo)(nil), // 74: bitwindowd.v1.DenialInfo - (*emptypb.Empty)(nil), // 75: google.protobuf.Empty + (*UTXOMetadata)(nil), // 47: wallet.v1.UTXOMetadata + (*SetUTXOMetadataRequest)(nil), // 48: wallet.v1.SetUTXOMetadataRequest + (*GetUTXOMetadataRequest)(nil), // 49: wallet.v1.GetUTXOMetadataRequest + (*GetUTXOMetadataResponse)(nil), // 50: wallet.v1.GetUTXOMetadataResponse + (*SetCoinSelectionStrategyRequest)(nil), // 51: wallet.v1.SetCoinSelectionStrategyRequest + (*GetCoinSelectionStrategyResponse)(nil), // 52: wallet.v1.GetCoinSelectionStrategyResponse + (*GetTransactionDetailsRequest)(nil), // 53: wallet.v1.GetTransactionDetailsRequest + (*GetTransactionDetailsResponse)(nil), // 54: wallet.v1.GetTransactionDetailsResponse + (*TransactionInput)(nil), // 55: wallet.v1.TransactionInput + (*TransactionOutput)(nil), // 56: wallet.v1.TransactionOutput + (*GetUTXODistributionRequest)(nil), // 57: wallet.v1.GetUTXODistributionRequest + (*GetUTXODistributionResponse)(nil), // 58: wallet.v1.GetUTXODistributionResponse + (*UTXOBucket)(nil), // 59: wallet.v1.UTXOBucket + (*BumpFeeRequest)(nil), // 60: wallet.v1.BumpFeeRequest + (*BumpFeeResponse)(nil), // 61: wallet.v1.BumpFeeResponse + (*SelectCoinsRequest)(nil), // 62: wallet.v1.SelectCoinsRequest + (*SelectCoinsResponse)(nil), // 63: wallet.v1.SelectCoinsResponse + (*CreateBackupResponse)(nil), // 64: wallet.v1.CreateBackupResponse + (*RestoreBackupRequest)(nil), // 65: wallet.v1.RestoreBackupRequest + (*ValidateBackupRequest)(nil), // 66: wallet.v1.ValidateBackupRequest + (*ValidateBackupResponse)(nil), // 67: wallet.v1.ValidateBackupResponse + nil, // 68: wallet.v1.SendTransactionRequest.DestinationsEntry + (*ListSidechainDepositsResponse_SidechainDeposit)(nil), // 69: wallet.v1.ListSidechainDepositsResponse.SidechainDeposit + nil, // 70: wallet.v1.GetUTXOMetadataResponse.MetadataEntry + (*timestamppb.Timestamp)(nil), // 71: google.protobuf.Timestamp + (*v1.DenialInfo)(nil), // 72: bitwindowd.v1.DenialInfo + (*emptypb.Empty)(nil), // 73: google.protobuf.Empty } var file_wallet_v1_wallet_proto_depIdxs = []int32{ 0, // 0: wallet.v1.GetNewAddressRequest.address_type:type_name -> wallet.v1.AddressType - 70, // 1: wallet.v1.SendTransactionRequest.destinations:type_name -> wallet.v1.SendTransactionRequest.DestinationsEntry + 68, // 1: wallet.v1.SendTransactionRequest.destinations:type_name -> wallet.v1.SendTransactionRequest.DestinationsEntry 14, // 2: wallet.v1.SendTransactionRequest.required_inputs:type_name -> wallet.v1.UnspentOutput 19, // 3: wallet.v1.ListTransactionsResponse.transactions:type_name -> wallet.v1.WalletTransaction - 73, // 4: wallet.v1.UnspentOutput.received_at:type_name -> google.protobuf.Timestamp - 74, // 5: wallet.v1.UnspentOutput.denial_info:type_name -> bitwindowd.v1.DenialInfo + 71, // 4: wallet.v1.UnspentOutput.received_at:type_name -> google.protobuf.Timestamp + 72, // 5: wallet.v1.UnspentOutput.denial_info:type_name -> bitwindowd.v1.DenialInfo 14, // 6: wallet.v1.ListUnspentResponse.utxos:type_name -> wallet.v1.UnspentOutput 17, // 7: wallet.v1.ListReceiveAddressesResponse.addresses:type_name -> wallet.v1.ReceiveAddress - 73, // 8: wallet.v1.ReceiveAddress.last_used_at:type_name -> google.protobuf.Timestamp - 73, // 9: wallet.v1.Confirmation.timestamp:type_name -> google.protobuf.Timestamp + 71, // 8: wallet.v1.ReceiveAddress.last_used_at:type_name -> google.protobuf.Timestamp + 71, // 9: wallet.v1.Confirmation.timestamp:type_name -> google.protobuf.Timestamp 18, // 10: wallet.v1.WalletTransaction.confirmation_time:type_name -> wallet.v1.Confirmation 20, // 11: wallet.v1.WalletTransaction.bmm_bid:type_name -> wallet.v1.BmmBid - 71, // 12: wallet.v1.ListSidechainDepositsResponse.deposits:type_name -> wallet.v1.ListSidechainDepositsResponse.SidechainDeposit - 73, // 13: wallet.v1.GetStatsResponse.last_tx_at:type_name -> google.protobuf.Timestamp + 69, // 12: wallet.v1.ListSidechainDepositsResponse.deposits:type_name -> wallet.v1.ListSidechainDepositsResponse.SidechainDeposit + 71, // 13: wallet.v1.GetStatsResponse.last_tx_at:type_name -> google.protobuf.Timestamp 37, // 14: wallet.v1.GetChequeResponse.cheque:type_name -> wallet.v1.Cheque - 73, // 15: wallet.v1.Cheque.created_at:type_name -> google.protobuf.Timestamp - 73, // 16: wallet.v1.Cheque.funded_at:type_name -> google.protobuf.Timestamp - 73, // 17: wallet.v1.Cheque.swept_at:type_name -> google.protobuf.Timestamp + 71, // 15: wallet.v1.Cheque.created_at:type_name -> google.protobuf.Timestamp + 71, // 16: wallet.v1.Cheque.funded_at:type_name -> google.protobuf.Timestamp + 71, // 17: wallet.v1.Cheque.swept_at:type_name -> google.protobuf.Timestamp 37, // 18: wallet.v1.ListChequesResponse.cheques:type_name -> wallet.v1.Cheque - 73, // 19: wallet.v1.CheckChequeFundingResponse.funded_at:type_name -> google.protobuf.Timestamp + 71, // 19: wallet.v1.CheckChequeFundingResponse.funded_at:type_name -> google.protobuf.Timestamp 1, // 20: wallet.v1.PreviewSweepResponse.address_kind:type_name -> wallet.v1.SweepAddressKind - 72, // 21: wallet.v1.GetUTXOMetadataResponse.metadata:type_name -> wallet.v1.GetUTXOMetadataResponse.MetadataEntry + 70, // 21: wallet.v1.GetUTXOMetadataResponse.metadata:type_name -> wallet.v1.GetUTXOMetadataResponse.MetadataEntry 2, // 22: wallet.v1.SetCoinSelectionStrategyRequest.strategy:type_name -> wallet.v1.CoinSelectionStrategy 2, // 23: wallet.v1.GetCoinSelectionStrategyResponse.strategy:type_name -> wallet.v1.CoinSelectionStrategy - 57, // 24: wallet.v1.GetTransactionDetailsResponse.inputs:type_name -> wallet.v1.TransactionInput - 58, // 25: wallet.v1.GetTransactionDetailsResponse.outputs:type_name -> wallet.v1.TransactionOutput - 61, // 26: wallet.v1.GetUTXODistributionResponse.buckets:type_name -> wallet.v1.UTXOBucket + 55, // 24: wallet.v1.GetTransactionDetailsResponse.inputs:type_name -> wallet.v1.TransactionInput + 56, // 25: wallet.v1.GetTransactionDetailsResponse.outputs:type_name -> wallet.v1.TransactionOutput + 59, // 26: wallet.v1.GetUTXODistributionResponse.buckets:type_name -> wallet.v1.UTXOBucket 2, // 27: wallet.v1.SelectCoinsRequest.strategy:type_name -> wallet.v1.CoinSelectionStrategy 14, // 28: wallet.v1.SelectCoinsResponse.selected_utxos:type_name -> wallet.v1.UnspentOutput - 49, // 29: wallet.v1.GetUTXOMetadataResponse.MetadataEntry.value:type_name -> wallet.v1.UTXOMetadata - 47, // 30: wallet.v1.WalletService.CreateBitcoinCoreWallet:input_type -> wallet.v1.CreateBitcoinCoreWalletRequest - 10, // 31: wallet.v1.WalletService.SendTransaction:input_type -> wallet.v1.SendTransactionRequest - 3, // 32: wallet.v1.WalletService.GetBalance:input_type -> wallet.v1.GetBalanceRequest - 4, // 33: wallet.v1.WalletService.GetNewAddress:input_type -> wallet.v1.GetNewAddressRequest - 6, // 34: wallet.v1.WalletService.ListTransactions:input_type -> wallet.v1.ListTransactionsRequest - 7, // 35: wallet.v1.WalletService.ListUnspent:input_type -> wallet.v1.ListUnspentRequest - 8, // 36: wallet.v1.WalletService.ListReceiveAddresses:input_type -> wallet.v1.ListReceiveAddressesRequest - 21, // 37: wallet.v1.WalletService.ListSidechainDeposits:input_type -> wallet.v1.ListSidechainDepositsRequest - 23, // 38: wallet.v1.WalletService.CreateSidechainDeposit:input_type -> wallet.v1.CreateSidechainDepositRequest - 25, // 39: wallet.v1.WalletService.SignMessage:input_type -> wallet.v1.SignMessageRequest - 27, // 40: wallet.v1.WalletService.VerifyMessage:input_type -> wallet.v1.VerifyMessageRequest - 9, // 41: wallet.v1.WalletService.GetStats:input_type -> wallet.v1.GetStatsRequest - 30, // 42: wallet.v1.WalletService.UnlockWallet:input_type -> wallet.v1.UnlockWalletRequest - 75, // 43: wallet.v1.WalletService.LockWallet:input_type -> google.protobuf.Empty - 75, // 44: wallet.v1.WalletService.IsWalletUnlocked:input_type -> google.protobuf.Empty - 31, // 45: wallet.v1.WalletService.CreateCheque:input_type -> wallet.v1.CreateChequeRequest - 33, // 46: wallet.v1.WalletService.GetCheque:input_type -> wallet.v1.GetChequeRequest - 35, // 47: wallet.v1.WalletService.GetChequePrivateKey:input_type -> wallet.v1.GetChequePrivateKeyRequest - 38, // 48: wallet.v1.WalletService.ListCheques:input_type -> wallet.v1.ListChequesRequest - 40, // 49: wallet.v1.WalletService.CheckChequeFunding:input_type -> wallet.v1.CheckChequeFundingRequest - 42, // 50: wallet.v1.WalletService.PreviewSweep:input_type -> wallet.v1.PreviewSweepRequest - 44, // 51: wallet.v1.WalletService.SweepCheque:input_type -> wallet.v1.SweepChequeRequest - 46, // 52: wallet.v1.WalletService.DeleteCheque:input_type -> wallet.v1.DeleteChequeRequest - 50, // 53: wallet.v1.WalletService.SetUTXOMetadata:input_type -> wallet.v1.SetUTXOMetadataRequest - 51, // 54: wallet.v1.WalletService.GetUTXOMetadata:input_type -> wallet.v1.GetUTXOMetadataRequest - 53, // 55: wallet.v1.WalletService.SetCoinSelectionStrategy:input_type -> wallet.v1.SetCoinSelectionStrategyRequest - 75, // 56: wallet.v1.WalletService.GetCoinSelectionStrategy:input_type -> google.protobuf.Empty - 55, // 57: wallet.v1.WalletService.GetTransactionDetails:input_type -> wallet.v1.GetTransactionDetailsRequest - 59, // 58: wallet.v1.WalletService.GetUTXODistribution:input_type -> wallet.v1.GetUTXODistributionRequest - 62, // 59: wallet.v1.WalletService.BumpFee:input_type -> wallet.v1.BumpFeeRequest - 64, // 60: wallet.v1.WalletService.SelectCoins:input_type -> wallet.v1.SelectCoinsRequest - 75, // 61: wallet.v1.WalletService.CreateBackup:input_type -> google.protobuf.Empty - 67, // 62: wallet.v1.WalletService.RestoreBackup:input_type -> wallet.v1.RestoreBackupRequest - 68, // 63: wallet.v1.WalletService.ValidateBackup:input_type -> wallet.v1.ValidateBackupRequest - 48, // 64: wallet.v1.WalletService.CreateBitcoinCoreWallet:output_type -> wallet.v1.CreateBitcoinCoreWalletResponse - 11, // 65: wallet.v1.WalletService.SendTransaction:output_type -> wallet.v1.SendTransactionResponse - 12, // 66: wallet.v1.WalletService.GetBalance:output_type -> wallet.v1.GetBalanceResponse - 5, // 67: wallet.v1.WalletService.GetNewAddress:output_type -> wallet.v1.GetNewAddressResponse - 13, // 68: wallet.v1.WalletService.ListTransactions:output_type -> wallet.v1.ListTransactionsResponse - 15, // 69: wallet.v1.WalletService.ListUnspent:output_type -> wallet.v1.ListUnspentResponse - 16, // 70: wallet.v1.WalletService.ListReceiveAddresses:output_type -> wallet.v1.ListReceiveAddressesResponse - 22, // 71: wallet.v1.WalletService.ListSidechainDeposits:output_type -> wallet.v1.ListSidechainDepositsResponse - 24, // 72: wallet.v1.WalletService.CreateSidechainDeposit:output_type -> wallet.v1.CreateSidechainDepositResponse - 26, // 73: wallet.v1.WalletService.SignMessage:output_type -> wallet.v1.SignMessageResponse - 28, // 74: wallet.v1.WalletService.VerifyMessage:output_type -> wallet.v1.VerifyMessageResponse - 29, // 75: wallet.v1.WalletService.GetStats:output_type -> wallet.v1.GetStatsResponse - 75, // 76: wallet.v1.WalletService.UnlockWallet:output_type -> google.protobuf.Empty - 75, // 77: wallet.v1.WalletService.LockWallet:output_type -> google.protobuf.Empty - 75, // 78: wallet.v1.WalletService.IsWalletUnlocked:output_type -> google.protobuf.Empty - 32, // 79: wallet.v1.WalletService.CreateCheque:output_type -> wallet.v1.CreateChequeResponse - 34, // 80: wallet.v1.WalletService.GetCheque:output_type -> wallet.v1.GetChequeResponse - 36, // 81: wallet.v1.WalletService.GetChequePrivateKey:output_type -> wallet.v1.GetChequePrivateKeyResponse - 39, // 82: wallet.v1.WalletService.ListCheques:output_type -> wallet.v1.ListChequesResponse - 41, // 83: wallet.v1.WalletService.CheckChequeFunding:output_type -> wallet.v1.CheckChequeFundingResponse - 43, // 84: wallet.v1.WalletService.PreviewSweep:output_type -> wallet.v1.PreviewSweepResponse - 45, // 85: wallet.v1.WalletService.SweepCheque:output_type -> wallet.v1.SweepChequeResponse - 75, // 86: wallet.v1.WalletService.DeleteCheque:output_type -> google.protobuf.Empty - 75, // 87: wallet.v1.WalletService.SetUTXOMetadata:output_type -> google.protobuf.Empty - 52, // 88: wallet.v1.WalletService.GetUTXOMetadata:output_type -> wallet.v1.GetUTXOMetadataResponse - 75, // 89: wallet.v1.WalletService.SetCoinSelectionStrategy:output_type -> google.protobuf.Empty - 54, // 90: wallet.v1.WalletService.GetCoinSelectionStrategy:output_type -> wallet.v1.GetCoinSelectionStrategyResponse - 56, // 91: wallet.v1.WalletService.GetTransactionDetails:output_type -> wallet.v1.GetTransactionDetailsResponse - 60, // 92: wallet.v1.WalletService.GetUTXODistribution:output_type -> wallet.v1.GetUTXODistributionResponse - 63, // 93: wallet.v1.WalletService.BumpFee:output_type -> wallet.v1.BumpFeeResponse - 65, // 94: wallet.v1.WalletService.SelectCoins:output_type -> wallet.v1.SelectCoinsResponse - 66, // 95: wallet.v1.WalletService.CreateBackup:output_type -> wallet.v1.CreateBackupResponse - 75, // 96: wallet.v1.WalletService.RestoreBackup:output_type -> google.protobuf.Empty - 69, // 97: wallet.v1.WalletService.ValidateBackup:output_type -> wallet.v1.ValidateBackupResponse - 64, // [64:98] is the sub-list for method output_type - 30, // [30:64] is the sub-list for method input_type + 47, // 29: wallet.v1.GetUTXOMetadataResponse.MetadataEntry.value:type_name -> wallet.v1.UTXOMetadata + 10, // 30: wallet.v1.WalletService.SendTransaction:input_type -> wallet.v1.SendTransactionRequest + 3, // 31: wallet.v1.WalletService.GetBalance:input_type -> wallet.v1.GetBalanceRequest + 4, // 32: wallet.v1.WalletService.GetNewAddress:input_type -> wallet.v1.GetNewAddressRequest + 6, // 33: wallet.v1.WalletService.ListTransactions:input_type -> wallet.v1.ListTransactionsRequest + 7, // 34: wallet.v1.WalletService.ListUnspent:input_type -> wallet.v1.ListUnspentRequest + 8, // 35: wallet.v1.WalletService.ListReceiveAddresses:input_type -> wallet.v1.ListReceiveAddressesRequest + 21, // 36: wallet.v1.WalletService.ListSidechainDeposits:input_type -> wallet.v1.ListSidechainDepositsRequest + 23, // 37: wallet.v1.WalletService.CreateSidechainDeposit:input_type -> wallet.v1.CreateSidechainDepositRequest + 25, // 38: wallet.v1.WalletService.SignMessage:input_type -> wallet.v1.SignMessageRequest + 27, // 39: wallet.v1.WalletService.VerifyMessage:input_type -> wallet.v1.VerifyMessageRequest + 9, // 40: wallet.v1.WalletService.GetStats:input_type -> wallet.v1.GetStatsRequest + 30, // 41: wallet.v1.WalletService.UnlockWallet:input_type -> wallet.v1.UnlockWalletRequest + 73, // 42: wallet.v1.WalletService.LockWallet:input_type -> google.protobuf.Empty + 73, // 43: wallet.v1.WalletService.IsWalletUnlocked:input_type -> google.protobuf.Empty + 31, // 44: wallet.v1.WalletService.CreateCheque:input_type -> wallet.v1.CreateChequeRequest + 33, // 45: wallet.v1.WalletService.GetCheque:input_type -> wallet.v1.GetChequeRequest + 35, // 46: wallet.v1.WalletService.GetChequePrivateKey:input_type -> wallet.v1.GetChequePrivateKeyRequest + 38, // 47: wallet.v1.WalletService.ListCheques:input_type -> wallet.v1.ListChequesRequest + 40, // 48: wallet.v1.WalletService.CheckChequeFunding:input_type -> wallet.v1.CheckChequeFundingRequest + 42, // 49: wallet.v1.WalletService.PreviewSweep:input_type -> wallet.v1.PreviewSweepRequest + 44, // 50: wallet.v1.WalletService.SweepCheque:input_type -> wallet.v1.SweepChequeRequest + 46, // 51: wallet.v1.WalletService.DeleteCheque:input_type -> wallet.v1.DeleteChequeRequest + 48, // 52: wallet.v1.WalletService.SetUTXOMetadata:input_type -> wallet.v1.SetUTXOMetadataRequest + 49, // 53: wallet.v1.WalletService.GetUTXOMetadata:input_type -> wallet.v1.GetUTXOMetadataRequest + 51, // 54: wallet.v1.WalletService.SetCoinSelectionStrategy:input_type -> wallet.v1.SetCoinSelectionStrategyRequest + 73, // 55: wallet.v1.WalletService.GetCoinSelectionStrategy:input_type -> google.protobuf.Empty + 53, // 56: wallet.v1.WalletService.GetTransactionDetails:input_type -> wallet.v1.GetTransactionDetailsRequest + 57, // 57: wallet.v1.WalletService.GetUTXODistribution:input_type -> wallet.v1.GetUTXODistributionRequest + 60, // 58: wallet.v1.WalletService.BumpFee:input_type -> wallet.v1.BumpFeeRequest + 62, // 59: wallet.v1.WalletService.SelectCoins:input_type -> wallet.v1.SelectCoinsRequest + 73, // 60: wallet.v1.WalletService.CreateBackup:input_type -> google.protobuf.Empty + 65, // 61: wallet.v1.WalletService.RestoreBackup:input_type -> wallet.v1.RestoreBackupRequest + 66, // 62: wallet.v1.WalletService.ValidateBackup:input_type -> wallet.v1.ValidateBackupRequest + 11, // 63: wallet.v1.WalletService.SendTransaction:output_type -> wallet.v1.SendTransactionResponse + 12, // 64: wallet.v1.WalletService.GetBalance:output_type -> wallet.v1.GetBalanceResponse + 5, // 65: wallet.v1.WalletService.GetNewAddress:output_type -> wallet.v1.GetNewAddressResponse + 13, // 66: wallet.v1.WalletService.ListTransactions:output_type -> wallet.v1.ListTransactionsResponse + 15, // 67: wallet.v1.WalletService.ListUnspent:output_type -> wallet.v1.ListUnspentResponse + 16, // 68: wallet.v1.WalletService.ListReceiveAddresses:output_type -> wallet.v1.ListReceiveAddressesResponse + 22, // 69: wallet.v1.WalletService.ListSidechainDeposits:output_type -> wallet.v1.ListSidechainDepositsResponse + 24, // 70: wallet.v1.WalletService.CreateSidechainDeposit:output_type -> wallet.v1.CreateSidechainDepositResponse + 26, // 71: wallet.v1.WalletService.SignMessage:output_type -> wallet.v1.SignMessageResponse + 28, // 72: wallet.v1.WalletService.VerifyMessage:output_type -> wallet.v1.VerifyMessageResponse + 29, // 73: wallet.v1.WalletService.GetStats:output_type -> wallet.v1.GetStatsResponse + 73, // 74: wallet.v1.WalletService.UnlockWallet:output_type -> google.protobuf.Empty + 73, // 75: wallet.v1.WalletService.LockWallet:output_type -> google.protobuf.Empty + 73, // 76: wallet.v1.WalletService.IsWalletUnlocked:output_type -> google.protobuf.Empty + 32, // 77: wallet.v1.WalletService.CreateCheque:output_type -> wallet.v1.CreateChequeResponse + 34, // 78: wallet.v1.WalletService.GetCheque:output_type -> wallet.v1.GetChequeResponse + 36, // 79: wallet.v1.WalletService.GetChequePrivateKey:output_type -> wallet.v1.GetChequePrivateKeyResponse + 39, // 80: wallet.v1.WalletService.ListCheques:output_type -> wallet.v1.ListChequesResponse + 41, // 81: wallet.v1.WalletService.CheckChequeFunding:output_type -> wallet.v1.CheckChequeFundingResponse + 43, // 82: wallet.v1.WalletService.PreviewSweep:output_type -> wallet.v1.PreviewSweepResponse + 45, // 83: wallet.v1.WalletService.SweepCheque:output_type -> wallet.v1.SweepChequeResponse + 73, // 84: wallet.v1.WalletService.DeleteCheque:output_type -> google.protobuf.Empty + 73, // 85: wallet.v1.WalletService.SetUTXOMetadata:output_type -> google.protobuf.Empty + 50, // 86: wallet.v1.WalletService.GetUTXOMetadata:output_type -> wallet.v1.GetUTXOMetadataResponse + 73, // 87: wallet.v1.WalletService.SetCoinSelectionStrategy:output_type -> google.protobuf.Empty + 52, // 88: wallet.v1.WalletService.GetCoinSelectionStrategy:output_type -> wallet.v1.GetCoinSelectionStrategyResponse + 54, // 89: wallet.v1.WalletService.GetTransactionDetails:output_type -> wallet.v1.GetTransactionDetailsResponse + 58, // 90: wallet.v1.WalletService.GetUTXODistribution:output_type -> wallet.v1.GetUTXODistributionResponse + 61, // 91: wallet.v1.WalletService.BumpFee:output_type -> wallet.v1.BumpFeeResponse + 63, // 92: wallet.v1.WalletService.SelectCoins:output_type -> wallet.v1.SelectCoinsResponse + 64, // 93: wallet.v1.WalletService.CreateBackup:output_type -> wallet.v1.CreateBackupResponse + 73, // 94: wallet.v1.WalletService.RestoreBackup:output_type -> google.protobuf.Empty + 67, // 95: wallet.v1.WalletService.ValidateBackup:output_type -> wallet.v1.ValidateBackupResponse + 63, // [63:96] is the sub-list for method output_type + 30, // [30:63] is the sub-list for method input_type 30, // [30:30] is the sub-list for extension type_name 30, // [30:30] is the sub-list for extension extendee 0, // [0:30] is the sub-list for field type_name @@ -5083,14 +4953,14 @@ func file_wallet_v1_wallet_proto_init() { file_wallet_v1_wallet_proto_msgTypes[11].OneofWrappers = []any{} file_wallet_v1_wallet_proto_msgTypes[34].OneofWrappers = []any{} file_wallet_v1_wallet_proto_msgTypes[38].OneofWrappers = []any{} - file_wallet_v1_wallet_proto_msgTypes[47].OneofWrappers = []any{} + file_wallet_v1_wallet_proto_msgTypes[45].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_wallet_v1_wallet_proto_rawDesc), len(file_wallet_v1_wallet_proto_rawDesc)), NumEnums: 3, - NumMessages: 70, + NumMessages: 68, NumExtensions: 0, NumServices: 1, }, diff --git a/bitwindow/server/gen/wallet/v1/walletv1connect/wallet.connect.go b/bitwindow/server/gen/wallet/v1/walletv1connect/wallet.connect.go index ec83e04f7..632a2a05a 100644 --- a/bitwindow/server/gen/wallet/v1/walletv1connect/wallet.connect.go +++ b/bitwindow/server/gen/wallet/v1/walletv1connect/wallet.connect.go @@ -34,9 +34,6 @@ const ( // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( - // WalletServiceCreateBitcoinCoreWalletProcedure is the fully-qualified name of the WalletService's - // CreateBitcoinCoreWallet RPC. - WalletServiceCreateBitcoinCoreWalletProcedure = "/wallet.v1.WalletService/CreateBitcoinCoreWallet" // WalletServiceSendTransactionProcedure is the fully-qualified name of the WalletService's // SendTransaction RPC. WalletServiceSendTransactionProcedure = "/wallet.v1.WalletService/SendTransaction" @@ -137,7 +134,6 @@ const ( // WalletServiceClient is a client for the wallet.v1.WalletService service. type WalletServiceClient interface { - CreateBitcoinCoreWallet(context.Context, *connect.Request[v1.CreateBitcoinCoreWalletRequest]) (*connect.Response[v1.CreateBitcoinCoreWalletResponse], error) SendTransaction(context.Context, *connect.Request[v1.SendTransactionRequest]) (*connect.Response[v1.SendTransactionResponse], error) GetBalance(context.Context, *connect.Request[v1.GetBalanceRequest]) (*connect.Response[v1.GetBalanceResponse], error) // Problem: deriving nilly willy here is potentially problematic. There's no way of listing @@ -195,12 +191,6 @@ func NewWalletServiceClient(httpClient connect.HTTPClient, baseURL string, opts baseURL = strings.TrimRight(baseURL, "/") walletServiceMethods := v1.File_wallet_v1_wallet_proto.Services().ByName("WalletService").Methods() return &walletServiceClient{ - createBitcoinCoreWallet: connect.NewClient[v1.CreateBitcoinCoreWalletRequest, v1.CreateBitcoinCoreWalletResponse]( - httpClient, - baseURL+WalletServiceCreateBitcoinCoreWalletProcedure, - connect.WithSchema(walletServiceMethods.ByName("CreateBitcoinCoreWallet")), - connect.WithClientOptions(opts...), - ), sendTransaction: connect.NewClient[v1.SendTransactionRequest, v1.SendTransactionResponse]( httpClient, baseURL+WalletServiceSendTransactionProcedure, @@ -404,7 +394,6 @@ func NewWalletServiceClient(httpClient connect.HTTPClient, baseURL string, opts // walletServiceClient implements WalletServiceClient. type walletServiceClient struct { - createBitcoinCoreWallet *connect.Client[v1.CreateBitcoinCoreWalletRequest, v1.CreateBitcoinCoreWalletResponse] sendTransaction *connect.Client[v1.SendTransactionRequest, v1.SendTransactionResponse] getBalance *connect.Client[v1.GetBalanceRequest, v1.GetBalanceResponse] getNewAddress *connect.Client[v1.GetNewAddressRequest, v1.GetNewAddressResponse] @@ -440,11 +429,6 @@ type walletServiceClient struct { validateBackup *connect.Client[v1.ValidateBackupRequest, v1.ValidateBackupResponse] } -// CreateBitcoinCoreWallet calls wallet.v1.WalletService.CreateBitcoinCoreWallet. -func (c *walletServiceClient) CreateBitcoinCoreWallet(ctx context.Context, req *connect.Request[v1.CreateBitcoinCoreWalletRequest]) (*connect.Response[v1.CreateBitcoinCoreWalletResponse], error) { - return c.createBitcoinCoreWallet.CallUnary(ctx, req) -} - // SendTransaction calls wallet.v1.WalletService.SendTransaction. func (c *walletServiceClient) SendTransaction(ctx context.Context, req *connect.Request[v1.SendTransactionRequest]) (*connect.Response[v1.SendTransactionResponse], error) { return c.sendTransaction.CallUnary(ctx, req) @@ -612,7 +596,6 @@ func (c *walletServiceClient) ValidateBackup(ctx context.Context, req *connect.R // WalletServiceHandler is an implementation of the wallet.v1.WalletService service. type WalletServiceHandler interface { - CreateBitcoinCoreWallet(context.Context, *connect.Request[v1.CreateBitcoinCoreWalletRequest]) (*connect.Response[v1.CreateBitcoinCoreWalletResponse], error) SendTransaction(context.Context, *connect.Request[v1.SendTransactionRequest]) (*connect.Response[v1.SendTransactionResponse], error) GetBalance(context.Context, *connect.Request[v1.GetBalanceRequest]) (*connect.Response[v1.GetBalanceResponse], error) // Problem: deriving nilly willy here is potentially problematic. There's no way of listing @@ -666,12 +649,6 @@ type WalletServiceHandler interface { // and JSON codecs. They also support gzip compression. func NewWalletServiceHandler(svc WalletServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { walletServiceMethods := v1.File_wallet_v1_wallet_proto.Services().ByName("WalletService").Methods() - walletServiceCreateBitcoinCoreWalletHandler := connect.NewUnaryHandler( - WalletServiceCreateBitcoinCoreWalletProcedure, - svc.CreateBitcoinCoreWallet, - connect.WithSchema(walletServiceMethods.ByName("CreateBitcoinCoreWallet")), - connect.WithHandlerOptions(opts...), - ) walletServiceSendTransactionHandler := connect.NewUnaryHandler( WalletServiceSendTransactionProcedure, svc.SendTransaction, @@ -872,8 +849,6 @@ func NewWalletServiceHandler(svc WalletServiceHandler, opts ...connect.HandlerOp ) return "/wallet.v1.WalletService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case WalletServiceCreateBitcoinCoreWalletProcedure: - walletServiceCreateBitcoinCoreWalletHandler.ServeHTTP(w, r) case WalletServiceSendTransactionProcedure: walletServiceSendTransactionHandler.ServeHTTP(w, r) case WalletServiceGetBalanceProcedure: @@ -949,10 +924,6 @@ func NewWalletServiceHandler(svc WalletServiceHandler, opts ...connect.HandlerOp // UnimplementedWalletServiceHandler returns CodeUnimplemented from all methods. type UnimplementedWalletServiceHandler struct{} -func (UnimplementedWalletServiceHandler) CreateBitcoinCoreWallet(context.Context, *connect.Request[v1.CreateBitcoinCoreWalletRequest]) (*connect.Response[v1.CreateBitcoinCoreWalletResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.CreateBitcoinCoreWallet is not implemented")) -} - func (UnimplementedWalletServiceHandler) SendTransaction(context.Context, *connect.Request[v1.SendTransactionRequest]) (*connect.Response[v1.SendTransactionResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("wallet.v1.WalletService.SendTransaction is not implemented")) } diff --git a/bitwindow/server/proto/wallet/v1/wallet.proto b/bitwindow/server/proto/wallet/v1/wallet.proto index 56007bde3..04e6afb43 100644 --- a/bitwindow/server/proto/wallet/v1/wallet.proto +++ b/bitwindow/server/proto/wallet/v1/wallet.proto @@ -7,7 +7,6 @@ import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; service WalletService { - rpc CreateBitcoinCoreWallet(CreateBitcoinCoreWalletRequest) returns (CreateBitcoinCoreWalletResponse); rpc SendTransaction(SendTransactionRequest) returns (SendTransactionResponse); rpc GetBalance(GetBalanceRequest) returns (GetBalanceResponse); // Problem: deriving nilly willy here is potentially problematic. There's no way of listing @@ -402,23 +401,6 @@ message DeleteChequeRequest { int64 id = 2; } -message CreateBitcoinCoreWalletRequest { - // BIP32 seed as hex string (64 bytes = 128 hex chars) - // This is the output of BIP39 PBKDF2(mnemonic + passphrase) - string seed_hex = 1; - // Wallet name for display - string name = 2; -} - -message CreateBitcoinCoreWalletResponse { - // The wallet ID that was created - string wallet_id = 1; - // The Bitcoin Core wallet name - string core_wallet_name = 2; - // First receiving address for verification - string first_address = 3; -} - // UTXO Coin Control metadata message UTXOMetadata { string outpoint = 1; diff --git a/sidechain_core/lib/gen/wallet/v1/wallet.connect.client.dart b/sidechain_core/lib/gen/wallet/v1/wallet.connect.client.dart index dd150b909..9c6e07ac5 100644 --- a/sidechain_core/lib/gen/wallet/v1/wallet.connect.client.dart +++ b/sidechain_core/lib/gen/wallet/v1/wallet.connect.client.dart @@ -9,23 +9,6 @@ import "wallet.connect.spec.dart" as specs; import "../../google/protobuf/empty.pb.dart" as googleprotobufempty; extension type WalletServiceClient (connect.Transport _transport) { - Future createBitcoinCoreWallet( - walletv1wallet.CreateBitcoinCoreWalletRequest input, { - connect.Headers? headers, - connect.AbortSignal? signal, - Function(connect.Headers)? onHeader, - Function(connect.Headers)? onTrailer, - }) { - return connect.Client(_transport).unary( - specs.WalletService.createBitcoinCoreWallet, - input, - signal: signal, - headers: headers, - onHeader: onHeader, - onTrailer: onTrailer, - ); - } - Future sendTransaction( walletv1wallet.SendTransactionRequest input, { connect.Headers? headers, diff --git a/sidechain_core/lib/gen/wallet/v1/wallet.connect.spec.dart b/sidechain_core/lib/gen/wallet/v1/wallet.connect.spec.dart index c5a3cfec6..2ae14fb8e 100644 --- a/sidechain_core/lib/gen/wallet/v1/wallet.connect.spec.dart +++ b/sidechain_core/lib/gen/wallet/v1/wallet.connect.spec.dart @@ -11,13 +11,6 @@ abstract final class WalletService { /// Fully-qualified name of the WalletService service. static const name = 'wallet.v1.WalletService'; - static const createBitcoinCoreWallet = connect.Spec( - '/$name/CreateBitcoinCoreWallet', - connect.StreamType.unary, - walletv1wallet.CreateBitcoinCoreWalletRequest.new, - walletv1wallet.CreateBitcoinCoreWalletResponse.new, - ); - static const sendTransaction = connect.Spec( '/$name/SendTransaction', connect.StreamType.unary, diff --git a/sidechain_core/lib/gen/wallet/v1/wallet.pb.dart b/sidechain_core/lib/gen/wallet/v1/wallet.pb.dart index 08b84d2b0..28fc28fc9 100644 --- a/sidechain_core/lib/gen/wallet/v1/wallet.pb.dart +++ b/sidechain_core/lib/gen/wallet/v1/wallet.pb.dart @@ -3587,154 +3587,6 @@ class DeleteChequeRequest extends $pb.GeneratedMessage { void clearId() => clearField(2); } -class CreateBitcoinCoreWalletRequest extends $pb.GeneratedMessage { - factory CreateBitcoinCoreWalletRequest({ - $core.String? seedHex, - $core.String? name, - }) { - final $result = create(); - if (seedHex != null) { - $result.seedHex = seedHex; - } - if (name != null) { - $result.name = name; - } - return $result; - } - CreateBitcoinCoreWalletRequest._() : super(); - factory CreateBitcoinCoreWalletRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory CreateBitcoinCoreWalletRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CreateBitcoinCoreWalletRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'wallet.v1'), createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'seedHex') - ..aOS(2, _omitFieldNames ? '' : 'name') - ..hasRequiredFields = false - ; - - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - CreateBitcoinCoreWalletRequest clone() => CreateBitcoinCoreWalletRequest()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - CreateBitcoinCoreWalletRequest copyWith(void Function(CreateBitcoinCoreWalletRequest) updates) => super.copyWith((message) => updates(message as CreateBitcoinCoreWalletRequest)) as CreateBitcoinCoreWalletRequest; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static CreateBitcoinCoreWalletRequest create() => CreateBitcoinCoreWalletRequest._(); - CreateBitcoinCoreWalletRequest createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static CreateBitcoinCoreWalletRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static CreateBitcoinCoreWalletRequest? _defaultInstance; - - /// BIP32 seed as hex string (64 bytes = 128 hex chars) - /// This is the output of BIP39 PBKDF2(mnemonic + passphrase) - @$pb.TagNumber(1) - $core.String get seedHex => $_getSZ(0); - @$pb.TagNumber(1) - set seedHex($core.String v) { $_setString(0, v); } - @$pb.TagNumber(1) - $core.bool hasSeedHex() => $_has(0); - @$pb.TagNumber(1) - void clearSeedHex() => clearField(1); - - /// Wallet name for display - @$pb.TagNumber(2) - $core.String get name => $_getSZ(1); - @$pb.TagNumber(2) - set name($core.String v) { $_setString(1, v); } - @$pb.TagNumber(2) - $core.bool hasName() => $_has(1); - @$pb.TagNumber(2) - void clearName() => clearField(2); -} - -class CreateBitcoinCoreWalletResponse extends $pb.GeneratedMessage { - factory CreateBitcoinCoreWalletResponse({ - $core.String? walletId, - $core.String? coreWalletName, - $core.String? firstAddress, - }) { - final $result = create(); - if (walletId != null) { - $result.walletId = walletId; - } - if (coreWalletName != null) { - $result.coreWalletName = coreWalletName; - } - if (firstAddress != null) { - $result.firstAddress = firstAddress; - } - return $result; - } - CreateBitcoinCoreWalletResponse._() : super(); - factory CreateBitcoinCoreWalletResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory CreateBitcoinCoreWalletResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CreateBitcoinCoreWalletResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'wallet.v1'), createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'walletId') - ..aOS(2, _omitFieldNames ? '' : 'coreWalletName') - ..aOS(3, _omitFieldNames ? '' : 'firstAddress') - ..hasRequiredFields = false - ; - - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - CreateBitcoinCoreWalletResponse clone() => CreateBitcoinCoreWalletResponse()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - CreateBitcoinCoreWalletResponse copyWith(void Function(CreateBitcoinCoreWalletResponse) updates) => super.copyWith((message) => updates(message as CreateBitcoinCoreWalletResponse)) as CreateBitcoinCoreWalletResponse; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static CreateBitcoinCoreWalletResponse create() => CreateBitcoinCoreWalletResponse._(); - CreateBitcoinCoreWalletResponse createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static CreateBitcoinCoreWalletResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static CreateBitcoinCoreWalletResponse? _defaultInstance; - - /// The wallet ID that was created - @$pb.TagNumber(1) - $core.String get walletId => $_getSZ(0); - @$pb.TagNumber(1) - set walletId($core.String v) { $_setString(0, v); } - @$pb.TagNumber(1) - $core.bool hasWalletId() => $_has(0); - @$pb.TagNumber(1) - void clearWalletId() => clearField(1); - - /// The Bitcoin Core wallet name - @$pb.TagNumber(2) - $core.String get coreWalletName => $_getSZ(1); - @$pb.TagNumber(2) - set coreWalletName($core.String v) { $_setString(1, v); } - @$pb.TagNumber(2) - $core.bool hasCoreWalletName() => $_has(1); - @$pb.TagNumber(2) - void clearCoreWalletName() => clearField(2); - - /// First receiving address for verification - @$pb.TagNumber(3) - $core.String get firstAddress => $_getSZ(2); - @$pb.TagNumber(3) - set firstAddress($core.String v) { $_setString(2, v); } - @$pb.TagNumber(3) - $core.bool hasFirstAddress() => $_has(2); - @$pb.TagNumber(3) - void clearFirstAddress() => clearField(3); -} - /// UTXO Coin Control metadata class UTXOMetadata extends $pb.GeneratedMessage { factory UTXOMetadata({ @@ -5565,9 +5417,6 @@ class WalletServiceApi { $pb.RpcClient _client; WalletServiceApi(this._client); - $async.Future createBitcoinCoreWallet($pb.ClientContext? ctx, CreateBitcoinCoreWalletRequest request) => - _client.invoke(ctx, 'WalletService', 'CreateBitcoinCoreWallet', request, CreateBitcoinCoreWalletResponse()) - ; $async.Future sendTransaction($pb.ClientContext? ctx, SendTransactionRequest request) => _client.invoke(ctx, 'WalletService', 'SendTransaction', request, SendTransactionResponse()) ; diff --git a/sidechain_core/lib/gen/wallet/v1/wallet.pbjson.dart b/sidechain_core/lib/gen/wallet/v1/wallet.pbjson.dart index 31b2d1659..c0b4d1190 100644 --- a/sidechain_core/lib/gen/wallet/v1/wallet.pbjson.dart +++ b/sidechain_core/lib/gen/wallet/v1/wallet.pbjson.dart @@ -811,36 +811,6 @@ final $typed_data.Uint8List deleteChequeRequestDescriptor = $convert.base64Decod 'ChNEZWxldGVDaGVxdWVSZXF1ZXN0EhsKCXdhbGxldF9pZBgBIAEoCVIId2FsbGV0SWQSDgoCaW' 'QYAiABKANSAmlk'); -@$core.Deprecated('Use createBitcoinCoreWalletRequestDescriptor instead') -const CreateBitcoinCoreWalletRequest$json = { - '1': 'CreateBitcoinCoreWalletRequest', - '2': [ - {'1': 'seed_hex', '3': 1, '4': 1, '5': 9, '10': 'seedHex'}, - {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'}, - ], -}; - -/// Descriptor for `CreateBitcoinCoreWalletRequest`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List createBitcoinCoreWalletRequestDescriptor = $convert.base64Decode( - 'Ch5DcmVhdGVCaXRjb2luQ29yZVdhbGxldFJlcXVlc3QSGQoIc2VlZF9oZXgYASABKAlSB3NlZW' - 'RIZXgSEgoEbmFtZRgCIAEoCVIEbmFtZQ=='); - -@$core.Deprecated('Use createBitcoinCoreWalletResponseDescriptor instead') -const CreateBitcoinCoreWalletResponse$json = { - '1': 'CreateBitcoinCoreWalletResponse', - '2': [ - {'1': 'wallet_id', '3': 1, '4': 1, '5': 9, '10': 'walletId'}, - {'1': 'core_wallet_name', '3': 2, '4': 1, '5': 9, '10': 'coreWalletName'}, - {'1': 'first_address', '3': 3, '4': 1, '5': 9, '10': 'firstAddress'}, - ], -}; - -/// Descriptor for `CreateBitcoinCoreWalletResponse`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List createBitcoinCoreWalletResponseDescriptor = $convert.base64Decode( - 'Ch9DcmVhdGVCaXRjb2luQ29yZVdhbGxldFJlc3BvbnNlEhsKCXdhbGxldF9pZBgBIAEoCVIId2' - 'FsbGV0SWQSKAoQY29yZV93YWxsZXRfbmFtZRgCIAEoCVIOY29yZVdhbGxldE5hbWUSIwoNZmly' - 'c3RfYWRkcmVzcxgDIAEoCVIMZmlyc3RBZGRyZXNz'); - @$core.Deprecated('Use uTXOMetadataDescriptor instead') const UTXOMetadata$json = { '1': 'UTXOMetadata', @@ -1216,7 +1186,6 @@ final $typed_data.Uint8List validateBackupResponseDescriptor = $convert.base64De const $core.Map<$core.String, $core.dynamic> WalletServiceBase$json = { '1': 'WalletService', '2': [ - {'1': 'CreateBitcoinCoreWallet', '2': '.wallet.v1.CreateBitcoinCoreWalletRequest', '3': '.wallet.v1.CreateBitcoinCoreWalletResponse'}, {'1': 'SendTransaction', '2': '.wallet.v1.SendTransactionRequest', '3': '.wallet.v1.SendTransactionResponse'}, {'1': 'GetBalance', '2': '.wallet.v1.GetBalanceRequest', '3': '.wallet.v1.GetBalanceResponse'}, {'1': 'GetNewAddress', '2': '.wallet.v1.GetNewAddressRequest', '3': '.wallet.v1.GetNewAddressResponse'}, @@ -1255,8 +1224,6 @@ const $core.Map<$core.String, $core.dynamic> WalletServiceBase$json = { @$core.Deprecated('Use walletServiceDescriptor instead') const $core.Map<$core.String, $core.Map<$core.String, $core.dynamic>> WalletServiceBase$messageJson = { - '.wallet.v1.CreateBitcoinCoreWalletRequest': CreateBitcoinCoreWalletRequest$json, - '.wallet.v1.CreateBitcoinCoreWalletResponse': CreateBitcoinCoreWalletResponse$json, '.wallet.v1.SendTransactionRequest': SendTransactionRequest$json, '.wallet.v1.SendTransactionRequest.DestinationsEntry': SendTransactionRequest_DestinationsEntry$json, '.wallet.v1.UnspentOutput': UnspentOutput$json, @@ -1333,57 +1300,55 @@ const $core.Map<$core.String, $core.Map<$core.String, $core.dynamic>> WalletServ /// Descriptor for `WalletService`. Decode as a `google.protobuf.ServiceDescriptorProto`. final $typed_data.Uint8List walletServiceDescriptor = $convert.base64Decode( - 'Cg1XYWxsZXRTZXJ2aWNlEnAKF0NyZWF0ZUJpdGNvaW5Db3JlV2FsbGV0Eikud2FsbGV0LnYxLk' - 'NyZWF0ZUJpdGNvaW5Db3JlV2FsbGV0UmVxdWVzdBoqLndhbGxldC52MS5DcmVhdGVCaXRjb2lu' - 'Q29yZVdhbGxldFJlc3BvbnNlElgKD1NlbmRUcmFuc2FjdGlvbhIhLndhbGxldC52MS5TZW5kVH' - 'JhbnNhY3Rpb25SZXF1ZXN0GiIud2FsbGV0LnYxLlNlbmRUcmFuc2FjdGlvblJlc3BvbnNlEkkK' - 'CkdldEJhbGFuY2USHC53YWxsZXQudjEuR2V0QmFsYW5jZVJlcXVlc3QaHS53YWxsZXQudjEuR2' - 'V0QmFsYW5jZVJlc3BvbnNlElIKDUdldE5ld0FkZHJlc3MSHy53YWxsZXQudjEuR2V0TmV3QWRk' - 'cmVzc1JlcXVlc3QaIC53YWxsZXQudjEuR2V0TmV3QWRkcmVzc1Jlc3BvbnNlElsKEExpc3RUcm' - 'Fuc2FjdGlvbnMSIi53YWxsZXQudjEuTGlzdFRyYW5zYWN0aW9uc1JlcXVlc3QaIy53YWxsZXQu' - 'djEuTGlzdFRyYW5zYWN0aW9uc1Jlc3BvbnNlEkwKC0xpc3RVbnNwZW50Eh0ud2FsbGV0LnYxLk' - 'xpc3RVbnNwZW50UmVxdWVzdBoeLndhbGxldC52MS5MaXN0VW5zcGVudFJlc3BvbnNlEmcKFExp' - 'c3RSZWNlaXZlQWRkcmVzc2VzEiYud2FsbGV0LnYxLkxpc3RSZWNlaXZlQWRkcmVzc2VzUmVxdW' - 'VzdBonLndhbGxldC52MS5MaXN0UmVjZWl2ZUFkZHJlc3Nlc1Jlc3BvbnNlEmoKFUxpc3RTaWRl' - 'Y2hhaW5EZXBvc2l0cxInLndhbGxldC52MS5MaXN0U2lkZWNoYWluRGVwb3NpdHNSZXF1ZXN0Gi' - 'gud2FsbGV0LnYxLkxpc3RTaWRlY2hhaW5EZXBvc2l0c1Jlc3BvbnNlEm0KFkNyZWF0ZVNpZGVj' - 'aGFpbkRlcG9zaXQSKC53YWxsZXQudjEuQ3JlYXRlU2lkZWNoYWluRGVwb3NpdFJlcXVlc3QaKS' - '53YWxsZXQudjEuQ3JlYXRlU2lkZWNoYWluRGVwb3NpdFJlc3BvbnNlEkwKC1NpZ25NZXNzYWdl' - 'Eh0ud2FsbGV0LnYxLlNpZ25NZXNzYWdlUmVxdWVzdBoeLndhbGxldC52MS5TaWduTWVzc2FnZV' - 'Jlc3BvbnNlElIKDVZlcmlmeU1lc3NhZ2USHy53YWxsZXQudjEuVmVyaWZ5TWVzc2FnZVJlcXVl' - 'c3QaIC53YWxsZXQudjEuVmVyaWZ5TWVzc2FnZVJlc3BvbnNlEkMKCEdldFN0YXRzEhoud2FsbG' - 'V0LnYxLkdldFN0YXRzUmVxdWVzdBobLndhbGxldC52MS5HZXRTdGF0c1Jlc3BvbnNlEkYKDFVu' - 'bG9ja1dhbGxldBIeLndhbGxldC52MS5VbmxvY2tXYWxsZXRSZXF1ZXN0GhYuZ29vZ2xlLnByb3' - 'RvYnVmLkVtcHR5EjwKCkxvY2tXYWxsZXQSFi5nb29nbGUucHJvdG9idWYuRW1wdHkaFi5nb29n' - 'bGUucHJvdG9idWYuRW1wdHkSQgoQSXNXYWxsZXRVbmxvY2tlZBIWLmdvb2dsZS5wcm90b2J1Zi' - '5FbXB0eRoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRJPCgxDcmVhdGVDaGVxdWUSHi53YWxsZXQu' - 'djEuQ3JlYXRlQ2hlcXVlUmVxdWVzdBofLndhbGxldC52MS5DcmVhdGVDaGVxdWVSZXNwb25zZR' - 'JGCglHZXRDaGVxdWUSGy53YWxsZXQudjEuR2V0Q2hlcXVlUmVxdWVzdBocLndhbGxldC52MS5H' - 'ZXRDaGVxdWVSZXNwb25zZRJkChNHZXRDaGVxdWVQcml2YXRlS2V5EiUud2FsbGV0LnYxLkdldE' - 'NoZXF1ZVByaXZhdGVLZXlSZXF1ZXN0GiYud2FsbGV0LnYxLkdldENoZXF1ZVByaXZhdGVLZXlS' - 'ZXNwb25zZRJMCgtMaXN0Q2hlcXVlcxIdLndhbGxldC52MS5MaXN0Q2hlcXVlc1JlcXVlc3QaHi' - '53YWxsZXQudjEuTGlzdENoZXF1ZXNSZXNwb25zZRJhChJDaGVja0NoZXF1ZUZ1bmRpbmcSJC53' - 'YWxsZXQudjEuQ2hlY2tDaGVxdWVGdW5kaW5nUmVxdWVzdBolLndhbGxldC52MS5DaGVja0NoZX' - 'F1ZUZ1bmRpbmdSZXNwb25zZRJPCgxQcmV2aWV3U3dlZXASHi53YWxsZXQudjEuUHJldmlld1N3' - 'ZWVwUmVxdWVzdBofLndhbGxldC52MS5QcmV2aWV3U3dlZXBSZXNwb25zZRJMCgtTd2VlcENoZX' - 'F1ZRIdLndhbGxldC52MS5Td2VlcENoZXF1ZVJlcXVlc3QaHi53YWxsZXQudjEuU3dlZXBDaGVx' - 'dWVSZXNwb25zZRJGCgxEZWxldGVDaGVxdWUSHi53YWxsZXQudjEuRGVsZXRlQ2hlcXVlUmVxdW' - 'VzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRJMCg9TZXRVVFhPTWV0YWRhdGESIS53YWxsZXQu' - 'djEuU2V0VVRYT01ldGFkYXRhUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRJYCg9HZX' - 'RVVFhPTWV0YWRhdGESIS53YWxsZXQudjEuR2V0VVRYT01ldGFkYXRhUmVxdWVzdBoiLndhbGxl' - 'dC52MS5HZXRVVFhPTWV0YWRhdGFSZXNwb25zZRJeChhTZXRDb2luU2VsZWN0aW9uU3RyYXRlZ3' - 'kSKi53YWxsZXQudjEuU2V0Q29pblNlbGVjdGlvblN0cmF0ZWd5UmVxdWVzdBoWLmdvb2dsZS5w' - 'cm90b2J1Zi5FbXB0eRJfChhHZXRDb2luU2VsZWN0aW9uU3RyYXRlZ3kSFi5nb29nbGUucHJvdG' - '9idWYuRW1wdHkaKy53YWxsZXQudjEuR2V0Q29pblNlbGVjdGlvblN0cmF0ZWd5UmVzcG9uc2US' - 'agoVR2V0VHJhbnNhY3Rpb25EZXRhaWxzEicud2FsbGV0LnYxLkdldFRyYW5zYWN0aW9uRGV0YW' - 'lsc1JlcXVlc3QaKC53YWxsZXQudjEuR2V0VHJhbnNhY3Rpb25EZXRhaWxzUmVzcG9uc2USZAoT' - 'R2V0VVRYT0Rpc3RyaWJ1dGlvbhIlLndhbGxldC52MS5HZXRVVFhPRGlzdHJpYnV0aW9uUmVxdW' - 'VzdBomLndhbGxldC52MS5HZXRVVFhPRGlzdHJpYnV0aW9uUmVzcG9uc2USQAoHQnVtcEZlZRIZ' - 'LndhbGxldC52MS5CdW1wRmVlUmVxdWVzdBoaLndhbGxldC52MS5CdW1wRmVlUmVzcG9uc2USTA' - 'oLU2VsZWN0Q29pbnMSHS53YWxsZXQudjEuU2VsZWN0Q29pbnNSZXF1ZXN0Gh4ud2FsbGV0LnYx' - 'LlNlbGVjdENvaW5zUmVzcG9uc2USRwoMQ3JlYXRlQmFja3VwEhYuZ29vZ2xlLnByb3RvYnVmLk' - 'VtcHR5Gh8ud2FsbGV0LnYxLkNyZWF0ZUJhY2t1cFJlc3BvbnNlEkgKDVJlc3RvcmVCYWNrdXAS' - 'Hy53YWxsZXQudjEuUmVzdG9yZUJhY2t1cFJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdH' - 'kSVQoOVmFsaWRhdGVCYWNrdXASIC53YWxsZXQudjEuVmFsaWRhdGVCYWNrdXBSZXF1ZXN0GiEu' - 'd2FsbGV0LnYxLlZhbGlkYXRlQmFja3VwUmVzcG9uc2U='); + 'Cg1XYWxsZXRTZXJ2aWNlElgKD1NlbmRUcmFuc2FjdGlvbhIhLndhbGxldC52MS5TZW5kVHJhbn' + 'NhY3Rpb25SZXF1ZXN0GiIud2FsbGV0LnYxLlNlbmRUcmFuc2FjdGlvblJlc3BvbnNlEkkKCkdl' + 'dEJhbGFuY2USHC53YWxsZXQudjEuR2V0QmFsYW5jZVJlcXVlc3QaHS53YWxsZXQudjEuR2V0Qm' + 'FsYW5jZVJlc3BvbnNlElIKDUdldE5ld0FkZHJlc3MSHy53YWxsZXQudjEuR2V0TmV3QWRkcmVz' + 'c1JlcXVlc3QaIC53YWxsZXQudjEuR2V0TmV3QWRkcmVzc1Jlc3BvbnNlElsKEExpc3RUcmFuc2' + 'FjdGlvbnMSIi53YWxsZXQudjEuTGlzdFRyYW5zYWN0aW9uc1JlcXVlc3QaIy53YWxsZXQudjEu' + 'TGlzdFRyYW5zYWN0aW9uc1Jlc3BvbnNlEkwKC0xpc3RVbnNwZW50Eh0ud2FsbGV0LnYxLkxpc3' + 'RVbnNwZW50UmVxdWVzdBoeLndhbGxldC52MS5MaXN0VW5zcGVudFJlc3BvbnNlEmcKFExpc3RS' + 'ZWNlaXZlQWRkcmVzc2VzEiYud2FsbGV0LnYxLkxpc3RSZWNlaXZlQWRkcmVzc2VzUmVxdWVzdB' + 'onLndhbGxldC52MS5MaXN0UmVjZWl2ZUFkZHJlc3Nlc1Jlc3BvbnNlEmoKFUxpc3RTaWRlY2hh' + 'aW5EZXBvc2l0cxInLndhbGxldC52MS5MaXN0U2lkZWNoYWluRGVwb3NpdHNSZXF1ZXN0Gigud2' + 'FsbGV0LnYxLkxpc3RTaWRlY2hhaW5EZXBvc2l0c1Jlc3BvbnNlEm0KFkNyZWF0ZVNpZGVjaGFp' + 'bkRlcG9zaXQSKC53YWxsZXQudjEuQ3JlYXRlU2lkZWNoYWluRGVwb3NpdFJlcXVlc3QaKS53YW' + 'xsZXQudjEuQ3JlYXRlU2lkZWNoYWluRGVwb3NpdFJlc3BvbnNlEkwKC1NpZ25NZXNzYWdlEh0u' + 'd2FsbGV0LnYxLlNpZ25NZXNzYWdlUmVxdWVzdBoeLndhbGxldC52MS5TaWduTWVzc2FnZVJlc3' + 'BvbnNlElIKDVZlcmlmeU1lc3NhZ2USHy53YWxsZXQudjEuVmVyaWZ5TWVzc2FnZVJlcXVlc3Qa' + 'IC53YWxsZXQudjEuVmVyaWZ5TWVzc2FnZVJlc3BvbnNlEkMKCEdldFN0YXRzEhoud2FsbGV0Ln' + 'YxLkdldFN0YXRzUmVxdWVzdBobLndhbGxldC52MS5HZXRTdGF0c1Jlc3BvbnNlEkYKDFVubG9j' + 'a1dhbGxldBIeLndhbGxldC52MS5VbmxvY2tXYWxsZXRSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYn' + 'VmLkVtcHR5EjwKCkxvY2tXYWxsZXQSFi5nb29nbGUucHJvdG9idWYuRW1wdHkaFi5nb29nbGUu' + 'cHJvdG9idWYuRW1wdHkSQgoQSXNXYWxsZXRVbmxvY2tlZBIWLmdvb2dsZS5wcm90b2J1Zi5FbX' + 'B0eRoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRJPCgxDcmVhdGVDaGVxdWUSHi53YWxsZXQudjEu' + 'Q3JlYXRlQ2hlcXVlUmVxdWVzdBofLndhbGxldC52MS5DcmVhdGVDaGVxdWVSZXNwb25zZRJGCg' + 'lHZXRDaGVxdWUSGy53YWxsZXQudjEuR2V0Q2hlcXVlUmVxdWVzdBocLndhbGxldC52MS5HZXRD' + 'aGVxdWVSZXNwb25zZRJkChNHZXRDaGVxdWVQcml2YXRlS2V5EiUud2FsbGV0LnYxLkdldENoZX' + 'F1ZVByaXZhdGVLZXlSZXF1ZXN0GiYud2FsbGV0LnYxLkdldENoZXF1ZVByaXZhdGVLZXlSZXNw' + 'b25zZRJMCgtMaXN0Q2hlcXVlcxIdLndhbGxldC52MS5MaXN0Q2hlcXVlc1JlcXVlc3QaHi53YW' + 'xsZXQudjEuTGlzdENoZXF1ZXNSZXNwb25zZRJhChJDaGVja0NoZXF1ZUZ1bmRpbmcSJC53YWxs' + 'ZXQudjEuQ2hlY2tDaGVxdWVGdW5kaW5nUmVxdWVzdBolLndhbGxldC52MS5DaGVja0NoZXF1ZU' + 'Z1bmRpbmdSZXNwb25zZRJPCgxQcmV2aWV3U3dlZXASHi53YWxsZXQudjEuUHJldmlld1N3ZWVw' + 'UmVxdWVzdBofLndhbGxldC52MS5QcmV2aWV3U3dlZXBSZXNwb25zZRJMCgtTd2VlcENoZXF1ZR' + 'IdLndhbGxldC52MS5Td2VlcENoZXF1ZVJlcXVlc3QaHi53YWxsZXQudjEuU3dlZXBDaGVxdWVS' + 'ZXNwb25zZRJGCgxEZWxldGVDaGVxdWUSHi53YWxsZXQudjEuRGVsZXRlQ2hlcXVlUmVxdWVzdB' + 'oWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRJMCg9TZXRVVFhPTWV0YWRhdGESIS53YWxsZXQudjEu' + 'U2V0VVRYT01ldGFkYXRhUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRJYCg9HZXRVVF' + 'hPTWV0YWRhdGESIS53YWxsZXQudjEuR2V0VVRYT01ldGFkYXRhUmVxdWVzdBoiLndhbGxldC52' + 'MS5HZXRVVFhPTWV0YWRhdGFSZXNwb25zZRJeChhTZXRDb2luU2VsZWN0aW9uU3RyYXRlZ3kSKi' + '53YWxsZXQudjEuU2V0Q29pblNlbGVjdGlvblN0cmF0ZWd5UmVxdWVzdBoWLmdvb2dsZS5wcm90' + 'b2J1Zi5FbXB0eRJfChhHZXRDb2luU2VsZWN0aW9uU3RyYXRlZ3kSFi5nb29nbGUucHJvdG9idW' + 'YuRW1wdHkaKy53YWxsZXQudjEuR2V0Q29pblNlbGVjdGlvblN0cmF0ZWd5UmVzcG9uc2USagoV' + 'R2V0VHJhbnNhY3Rpb25EZXRhaWxzEicud2FsbGV0LnYxLkdldFRyYW5zYWN0aW9uRGV0YWlsc1' + 'JlcXVlc3QaKC53YWxsZXQudjEuR2V0VHJhbnNhY3Rpb25EZXRhaWxzUmVzcG9uc2USZAoTR2V0' + 'VVRYT0Rpc3RyaWJ1dGlvbhIlLndhbGxldC52MS5HZXRVVFhPRGlzdHJpYnV0aW9uUmVxdWVzdB' + 'omLndhbGxldC52MS5HZXRVVFhPRGlzdHJpYnV0aW9uUmVzcG9uc2USQAoHQnVtcEZlZRIZLndh' + 'bGxldC52MS5CdW1wRmVlUmVxdWVzdBoaLndhbGxldC52MS5CdW1wRmVlUmVzcG9uc2USTAoLU2' + 'VsZWN0Q29pbnMSHS53YWxsZXQudjEuU2VsZWN0Q29pbnNSZXF1ZXN0Gh4ud2FsbGV0LnYxLlNl' + 'bGVjdENvaW5zUmVzcG9uc2USRwoMQ3JlYXRlQmFja3VwEhYuZ29vZ2xlLnByb3RvYnVmLkVtcH' + 'R5Gh8ud2FsbGV0LnYxLkNyZWF0ZUJhY2t1cFJlc3BvbnNlEkgKDVJlc3RvcmVCYWNrdXASHy53' + 'YWxsZXQudjEuUmVzdG9yZUJhY2t1cFJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkSVQ' + 'oOVmFsaWRhdGVCYWNrdXASIC53YWxsZXQudjEuVmFsaWRhdGVCYWNrdXBSZXF1ZXN0GiEud2Fs' + 'bGV0LnYxLlZhbGlkYXRlQmFja3VwUmVzcG9uc2U='); diff --git a/sidechain_core/lib/gen/wallet/v1/wallet.pbserver.dart b/sidechain_core/lib/gen/wallet/v1/wallet.pbserver.dart index 4585814dd..dbe47ab64 100644 --- a/sidechain_core/lib/gen/wallet/v1/wallet.pbserver.dart +++ b/sidechain_core/lib/gen/wallet/v1/wallet.pbserver.dart @@ -22,7 +22,6 @@ import 'wallet.pbjson.dart'; export 'wallet.pb.dart'; abstract class WalletServiceBase extends $pb.GeneratedService { - $async.Future<$13.CreateBitcoinCoreWalletResponse> createBitcoinCoreWallet($pb.ServerContext ctx, $13.CreateBitcoinCoreWalletRequest request); $async.Future<$13.SendTransactionResponse> sendTransaction($pb.ServerContext ctx, $13.SendTransactionRequest request); $async.Future<$13.GetBalanceResponse> getBalance($pb.ServerContext ctx, $13.GetBalanceRequest request); $async.Future<$13.GetNewAddressResponse> getNewAddress($pb.ServerContext ctx, $13.GetNewAddressRequest request); @@ -59,7 +58,6 @@ abstract class WalletServiceBase extends $pb.GeneratedService { $pb.GeneratedMessage createRequest($core.String methodName) { switch (methodName) { - case 'CreateBitcoinCoreWallet': return $13.CreateBitcoinCoreWalletRequest(); case 'SendTransaction': return $13.SendTransactionRequest(); case 'GetBalance': return $13.GetBalanceRequest(); case 'GetNewAddress': return $13.GetNewAddressRequest(); @@ -99,7 +97,6 @@ abstract class WalletServiceBase extends $pb.GeneratedService { $async.Future<$pb.GeneratedMessage> handleCall($pb.ServerContext ctx, $core.String methodName, $pb.GeneratedMessage request) { switch (methodName) { - case 'CreateBitcoinCoreWallet': return this.createBitcoinCoreWallet(ctx, request as $13.CreateBitcoinCoreWalletRequest); case 'SendTransaction': return this.sendTransaction(ctx, request as $13.SendTransactionRequest); case 'GetBalance': return this.getBalance(ctx, request as $13.GetBalanceRequest); case 'GetNewAddress': return this.getNewAddress(ctx, request as $13.GetNewAddressRequest); From 9f53963fcf644bd82f8519cf36756c2ad75051fe Mon Sep 17 00:00:00 2001 From: octobocto Date: Fri, 25 Sep 2026 22:03:10 -0700 Subject: [PATCH 12/13] bitwindow: leave L1 derivation to the orchestrator --- .../server/api/wallet/integration_test.go | 270 -------- .../server/api/wallet/sign_message_test.go | 43 +- bitwindow/server/api/wallet/wallet.go | 383 +---------- .../server/engines/deniability_engine.go | 2 +- .../server/engines/deniability_engine_test.go | 5 +- .../engines/derivation_fallback_test.go | 76 --- .../server/engines/descriptor_checksum.go | 94 --- .../engines/descriptor_checksum_test.go | 46 -- bitwindow/server/engines/wallet_engine.go | 646 ++---------------- .../engines/wallet_engine_watchonly_test.go | 65 +- .../wallet_manager_integration_test.go | 3 +- bitwindow/server/tests/apitests/apitests.go | 30 +- 12 files changed, 133 insertions(+), 1530 deletions(-) delete mode 100644 bitwindow/server/api/wallet/integration_test.go delete mode 100644 bitwindow/server/engines/derivation_fallback_test.go delete mode 100644 bitwindow/server/engines/descriptor_checksum.go delete mode 100644 bitwindow/server/engines/descriptor_checksum_test.go diff --git a/bitwindow/server/api/wallet/integration_test.go b/bitwindow/server/api/wallet/integration_test.go deleted file mode 100644 index e92d73ff8..000000000 --- a/bitwindow/server/api/wallet/integration_test.go +++ /dev/null @@ -1,270 +0,0 @@ -package api_wallet - -import ( - "context" - "encoding/json" - "os" - "path/filepath" - "testing" - - "connectrpc.com/connect" - "github.com/LayerTwo-Labs/sidesail/bitwindow/server/engines" - "github.com/LayerTwo-Labs/sidesail/bitwindow/server/service" - "github.com/LayerTwo-Labs/sidesail/bitwindow/server/tests/mocks" - corepb "github.com/barebitcoin/btc-buf/gen/bitcoin/bitcoind/v1alpha" - corerpc "github.com/barebitcoin/btc-buf/gen/bitcoin/bitcoind/v1alpha/bitcoindv1alphaconnect" - "github.com/btcsuite/btcd/chaincfg" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -// TestDeriveAndCheckAddressesIntegration tests the actual implementation -// Run with: go test -v -run TestDeriveAndCheckAddressesIntegration -func TestDeriveAndCheckAddressesIntegration(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - ctx := context.Background() - - // Create temp directory for wallet.json - tempDir := t.TempDir() - walletFile := filepath.Join(tempDir, "wallet.json") - - // Your actual wallet data from wallet.json - walletData := map[string]interface{}{ - "version": 1, - "activeWalletId": "80CEBA2163224572BDEADD2D2181C51B", - "wallets": []map[string]interface{}{ - { - "version": 1, - "master": map[string]interface{}{ - "seed_hex": "0329e77e27d1e24336be53d25a897e92e67b5ec7e88eca7529b14e3ffd9168a247b6906469fb8a79ecb25ec077e033f6b567d5d9b0ae334f1e33457ae6bb1364", - }, - "l1": map[string]interface{}{ - "mnemonic": "hover drama license spread finish tray permit glad help minimum rough stove", - }, - "id": "80CEBA2163224572BDEADD2D2181C51B", - "name": "asd", - "wallet_type": "bitcoinCore", - }, - }, - } - - // Write wallet.json - data, err := json.Marshal(walletData) - require.NoError(t, err) - err = os.WriteFile(walletFile, data, 0644) - require.NoError(t, err) - - // Create mock bitcoind - mockBitcoind := mocks.NewMockBitcoinServiceClient(ctrl) - - // Create wallet engine with temp directory and bitcoind connector - walletEngine := engines.NewWalletEngine( - func(ctx context.Context) (corerpc.BitcoinServiceClient, error) { - return mockBitcoind, nil - }, - tempDir, - &chaincfg.SigNetParams, - ) - - // Mock GetBitcoinCoreWalletName to return expected wallet name - // The actual implementation uses first 8 chars of wallet ID - expectedCoreWalletName := "wallet_80CEBA21" - - // Mock all Bitcoin Core wallet management calls - mockBitcoind.EXPECT(). - ListWallets(gomock.Any(), gomock.Any()). - Return(&connect.Response[corepb.ListWalletsResponse]{ - Msg: &corepb.ListWalletsResponse{ - Wallets: []string{expectedCoreWalletName}, - }, - }, nil). - AnyTimes() - - mockBitcoind.EXPECT(). - CreateWallet(gomock.Any(), gomock.Any()). - Return(&connect.Response[corepb.CreateWalletResponse]{ - Msg: &corepb.CreateWalletResponse{ - Name: expectedCoreWalletName, - }, - }, nil). - AnyTimes() - - mockBitcoind.EXPECT(). - ImportDescriptors(gomock.Any(), gomock.Any()). - Return(&connect.Response[corepb.ImportDescriptorsResponse]{ - Msg: &corepb.ImportDescriptorsResponse{}, - }, nil). - AnyTimes() - - // Create server with real wallet manager - bitcoindService := service.New("test-bitcoind", func(ctx context.Context) (corerpc.BitcoinServiceClient, error) { - return mockBitcoind, nil - }) - - server := &Server{ - database: nil, // Not needed for address derivation - bitcoind: bitcoindService, - walletEngine: walletEngine, - walletDir: tempDir, - } - - t.Run("derive addresses with no transactions - all unused", func(t *testing.T) { - // Mock ListTransactions to return empty (no addresses used) - mockBitcoind.EXPECT(). - ListTransactions(gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, req *connect.Request[corepb.ListTransactionsRequest]) (*connect.Response[corepb.ListTransactionsResponse], error) { - // Verify we're querying the correct wallet - require.Equal(t, expectedCoreWalletName, req.Msg.Wallet) - - return &connect.Response[corepb.ListTransactionsResponse]{ - Msg: &corepb.ListTransactionsResponse{ - Transactions: []*corepb.GetTransactionResponse{}, - }, - }, nil - }) - - // Call the ACTUAL implementation - firstUnused, derivedAddresses, err := server.deriveAndCheckAddresses(ctx, "80CEBA2163224572BDEADD2D2181C51B") - - require.NoError(t, err) - require.NotEmpty(t, firstUnused, "Should return first unused address") - require.Len(t, derivedAddresses, 100, "Should derive 100 addresses") - - // First address should be the unused one - require.Equal(t, derivedAddresses[0].Address, firstUnused) - require.Equal(t, uint32(0), derivedAddresses[0].Index) - require.False(t, derivedAddresses[0].Used) - - // All should be unused - for _, addr := range derivedAddresses { - require.False(t, addr.Used, "All addresses should be unused") - } - - t.Logf("\n=== Test Results ===") - t.Logf("First unused address (index 0): %s", firstUnused) - t.Logf("Total addresses derived: %d", len(derivedAddresses)) - t.Logf("\nFirst 5 addresses:") - for i := 0; i < 5 && i < len(derivedAddresses); i++ { - t.Logf(" [%d] %s (used: %v)", derivedAddresses[i].Index, derivedAddresses[i].Address, derivedAddresses[i].Used) - } - }) - - t.Run("first 3 addresses used - returns index 3", func(t *testing.T) { - // First, we need to know what addresses will be derived - // So we call once to get them - mockBitcoind.EXPECT(). - ListTransactions(gomock.Any(), gomock.Any()). - Return(&connect.Response[corepb.ListTransactionsResponse]{ - Msg: &corepb.ListTransactionsResponse{ - Transactions: []*corepb.GetTransactionResponse{}, - }, - }, nil) - - _, allAddresses, err := server.deriveAndCheckAddresses(ctx, "80CEBA2163224572BDEADD2D2181C51B") - require.NoError(t, err) - - // Now mock with first 3 addresses marked as used - mockBitcoind.EXPECT(). - ListTransactions(gomock.Any(), gomock.Any()). - Return(&connect.Response[corepb.ListTransactionsResponse]{ - Msg: &corepb.ListTransactionsResponse{ - Transactions: []*corepb.GetTransactionResponse{ - { - Details: []*corepb.GetTransactionResponse_Details{ - {Address: allAddresses[0].Address, Amount: 0.1}, - }, - }, - { - Details: []*corepb.GetTransactionResponse_Details{ - {Address: allAddresses[1].Address, Amount: 0.2}, - }, - }, - { - Details: []*corepb.GetTransactionResponse_Details{ - {Address: allAddresses[2].Address, Amount: 0.3}, - }, - }, - }, - }, - }, nil) - - // Call again with mocked transactions - firstUnused, derivedAddresses, err := server.deriveAndCheckAddresses(ctx, "80CEBA2163224572BDEADD2D2181C51B") - - require.NoError(t, err) - require.NotEmpty(t, firstUnused) - - // Should return the 4th address (index 3) - require.Equal(t, allAddresses[3].Address, firstUnused) - require.Equal(t, derivedAddresses[3].Address, firstUnused) - - // Verify usage flags - require.True(t, derivedAddresses[0].Used, "Address 0 should be marked used") - require.True(t, derivedAddresses[1].Used, "Address 1 should be marked used") - require.True(t, derivedAddresses[2].Used, "Address 2 should be marked used") - require.False(t, derivedAddresses[3].Used, "Address 3 should be unused") - - t.Logf("\n=== Test Results ===") - t.Logf("Addresses 0-2 marked as used") - t.Logf("First unused address (index 3): %s", firstUnused) - t.Logf("\nAddress usage:") - for i := 0; i < 5 && i < len(derivedAddresses); i++ { - t.Logf(" [%d] %s (used: %v)", derivedAddresses[i].Index, derivedAddresses[i].Address, derivedAddresses[i].Used) - } - }) - - t.Run("address with zero balance is reused - returns next unused", func(t *testing.T) { - // Get all addresses first - mockBitcoind.EXPECT(). - ListTransactions(gomock.Any(), gomock.Any()). - Return(&connect.Response[corepb.ListTransactionsResponse]{ - Msg: &corepb.ListTransactionsResponse{ - Transactions: []*corepb.GetTransactionResponse{}, - }, - }, nil) - - _, allAddresses, err := server.deriveAndCheckAddresses(ctx, "80CEBA2163224572BDEADD2D2181C51B") - require.NoError(t, err) - - // Mock: address 0 received and spent (balance=0, but has transactions) - // Address 1 never used - mockBitcoind.EXPECT(). - ListTransactions(gomock.Any(), gomock.Any()). - Return(&connect.Response[corepb.ListTransactionsResponse]{ - Msg: &corepb.ListTransactionsResponse{ - Transactions: []*corepb.GetTransactionResponse{ - { - Details: []*corepb.GetTransactionResponse_Details{ - {Address: allAddresses[0].Address, Amount: 1.0}, // Received - }, - }, - { - Details: []*corepb.GetTransactionResponse_Details{ - {Address: allAddresses[0].Address, Amount: -1.0}, // Spent - }, - }, - }, - }, - }, nil) - - firstUnused, derivedAddresses, err := server.deriveAndCheckAddresses(ctx, "80CEBA2163224572BDEADD2D2181C51B") - - require.NoError(t, err) - - // Should NOT return address 0 (even though balance is 0, it has been used) - // Should return address 1 - require.NotEqual(t, allAddresses[0].Address, firstUnused, "Should not reuse emptied address") - require.Equal(t, allAddresses[1].Address, firstUnused, "Should return first truly unused address") - - require.True(t, derivedAddresses[0].Used, "Address 0 should be marked used (has transactions)") - require.False(t, derivedAddresses[1].Used, "Address 1 should be unused") - - t.Logf("\n=== Test Results ===") - t.Logf("Address 0 has transactions (received and spent) - marked as USED") - t.Logf("Address 1 has no transactions - returned as first unused") - t.Logf("First unused: %s", firstUnused) - t.Log("\nThis test proves we don't reuse emptied addresses!") - }) -} diff --git a/bitwindow/server/api/wallet/sign_message_test.go b/bitwindow/server/api/wallet/sign_message_test.go index b043917e2..78b14c28e 100644 --- a/bitwindow/server/api/wallet/sign_message_test.go +++ b/bitwindow/server/api/wallet/sign_message_test.go @@ -24,9 +24,10 @@ import ( const testSeedHex = "0329e77e27d1e24336be53d25a897e92e67b5ec7e88eca7529b14e3ffd9168a247b6906469fb8a79ecb25ec077e033f6b567d5d9b0ae334f1e33457ae6bb1364" -// signingWallet is a seed-only wallet at the given account override. -func signingWallet(accountIndex uint32, derivationPath string) *engines.WalletInfo { - wallet := &engines.WalletInfo{AccountIndex: accountIndex, DerivationPath: derivationPath} +// signingWallet is a seed-only wallet at the given account override and stored +// script type. +func signingWallet(accountIndex uint32, derivationPath, scriptType string) *engines.WalletInfo { + wallet := &engines.WalletInfo{AccountIndex: accountIndex, DerivationPath: derivationPath, ScriptType: scriptType} wallet.Master.SeedHex = testSeedHex return wallet } @@ -72,7 +73,7 @@ func TestDeriveMessageSigningPrivateKey(t *testing.T) { expected := deriveAddressKey(t, chainParams, 1, 0, index) privKey, err := deriveMessageSigningPrivateKey( - signingWallet(0, ""), chainParams, addressOf(t, expected, chainParams), + signingWallet(0, "", ""), chainParams, addressOf(t, expected, chainParams), ) require.NoError(t, err) @@ -85,13 +86,13 @@ func TestDeriveMessageSigningPrivateKey(t *testing.T) { // key here - signing it would be a lie. beyondGap := deriveAddressKey(t, chainParams, 1, 0, addressScanDepth) _, err := deriveMessageSigningPrivateKey( - signingWallet(0, ""), chainParams, addressOf(t, beyondGap, chainParams), + signingWallet(0, "", ""), chainParams, addressOf(t, beyondGap, chainParams), ) require.ErrorContains(t, err, "not one of the wallet") // Mainnet uses coin type 0, so the same index is a different address entirely mainnetAddr := addressOf(t, deriveAddressKey(t, &chaincfg.MainNetParams, 0, 0, 0), &chaincfg.MainNetParams) - _, err = deriveMessageSigningPrivateKey(signingWallet(0, ""), chainParams, mainnetAddr) + _, err = deriveMessageSigningPrivateKey(signingWallet(0, "", ""), chainParams, mainnetAddr) require.ErrorContains(t, err, "not one of the wallet") } @@ -105,8 +106,8 @@ func TestDeriveMessageSigningPrivateKeyHonorsAccount(t *testing.T) { address := addressOf(t, account5, chainParams) for _, wallet := range []*engines.WalletInfo{ - signingWallet(5, ""), - signingWallet(0, "m/84'/1'/5'"), + signingWallet(5, "", ""), + signingWallet(0, "m/84'/1'/5'", ""), } { privKey, err := deriveMessageSigningPrivateKey(wallet, chainParams, address) require.NoError(t, err) @@ -117,7 +118,7 @@ func TestDeriveMessageSigningPrivateKeyHonorsAccount(t *testing.T) { } // The default-account wallet does not own that address - _, err := deriveMessageSigningPrivateKey(signingWallet(0, ""), chainParams, address) + _, err := deriveMessageSigningPrivateKey(signingWallet(0, "", ""), chainParams, address) require.ErrorContains(t, err, "not one of the wallet") } @@ -147,8 +148,8 @@ func TestDeriveMessageSigningPrivateKeyHandlesTaproot(t *testing.T) { ) for _, wallet := range []*engines.WalletInfo{ - signingWallet(0, ""), // taproot comes alongside segwit off one seed - signingWallet(0, "m/86'/1'/0'"), // explicit m/86' path is its single kind + signingWallet(0, "", ""), // taproot comes alongside segwit off one seed + signingWallet(0, "m/86'/1'/0'", "taproot"), // a taproot wallet on an explicit m/86' path } { privKey, err := deriveMessageSigningPrivateKey(wallet, chainParams, taproot.EncodeAddress()) require.NoError(t, err) @@ -245,24 +246,25 @@ func pubKeyOf(t *testing.T, key *hdkeychain.ExtendedKey) *btcec.PublicKey { return pubKey } -// Electrum wallets can sit at m/44' or m/49', and the stored wallet does not say -// which, so those addresses must resolve too. +// A legacy or nested segwit wallet stores its script type, and signs for the +// addresses of that type. A default wallet does not own them. func TestDeriveMessageSigningPrivateKeyHandlesLegacyAndNested(t *testing.T) { t.Parallel() chainParams := &chaincfg.SigNetParams for _, tc := range []struct { - purpose uint32 - path string - address func(*testing.T, *btcec.PublicKey) string + purpose uint32 + path string + scriptType string + address func(*testing.T, *btcec.PublicKey) string }{ - {44, "m/44'/1'/0'", func(t *testing.T, pub *btcec.PublicKey) string { + {44, "m/44'/1'/0'", "legacy", func(t *testing.T, pub *btcec.PublicKey) string { addr, err := btcutil.NewAddressPubKeyHash(btcutil.Hash160(pub.SerializeCompressed()), chainParams) require.NoError(t, err) return addr.EncodeAddress() }}, - {49, "m/49'/1'/0'", func(t *testing.T, pub *btcec.PublicKey) string { + {49, "m/49'/1'/0'", "nested-segwit", func(t *testing.T, pub *btcec.PublicKey) string { witness, err := btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(pub.SerializeCompressed()), chainParams) require.NoError(t, err) redeem, err := txscript.PayToAddrScript(witness) @@ -278,10 +280,13 @@ func TestDeriveMessageSigningPrivateKeyHandlesLegacyAndNested(t *testing.T) { expected, err := key.ECPrivKey() require.NoError(t, err) - for _, wallet := range []*engines.WalletInfo{signingWallet(0, ""), signingWallet(0, tc.path)} { + for _, wallet := range []*engines.WalletInfo{signingWallet(0, "", tc.scriptType), signingWallet(0, tc.path, tc.scriptType)} { privKey, err := deriveMessageSigningPrivateKey(wallet, chainParams, address) require.NoError(t, err) require.Equal(t, expected.Serialize(), privKey.Serialize()) } + + _, err = deriveMessageSigningPrivateKey(signingWallet(0, "", ""), chainParams, address) + require.ErrorContains(t, err, "not one of the wallet") } } diff --git a/bitwindow/server/api/wallet/wallet.go b/bitwindow/server/api/wallet/wallet.go index 11d4e9310..96733ade0 100644 --- a/bitwindow/server/api/wallet/wallet.go +++ b/bitwindow/server/api/wallet/wallet.go @@ -33,11 +33,8 @@ import ( corepb "github.com/barebitcoin/btc-buf/gen/bitcoin/bitcoind/v1alpha" corerpc "github.com/barebitcoin/btc-buf/gen/bitcoin/bitcoind/v1alpha/bitcoindv1alphaconnect" "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/rs/zerolog" "github.com/samber/lo" @@ -234,84 +231,18 @@ func (s *Server) sendWithRequiredInputs( // GetNewAddress implements drivechainv1connect.DrivechainServiceHandler. func (s *Server) GetNewAddress(ctx context.Context, c *connect.Request[pb.GetNewAddressRequest]) (*connect.Response[pb.GetNewAddressResponse], error) { walletId := c.Msg.WalletId - walletType, err := s.walletEngine.GetWalletBackendType(ctx, walletId) - if err != nil { - return nil, fmt.Errorf("get wallet type: %w", err) - } - - addressType := c.Msg.AddressType - if addressType == pb.AddressType_ADDRESS_TYPE_UNSPECIFIED { - addressType = pb.AddressType_ADDRESS_TYPE_SEGWIT - } - - coreAddressType := "bech32" - if addressType == pb.AddressType_ADDRESS_TYPE_TAPROOT { - coreAddressType = "bech32m" - } - - // For segwit Bitcoin Core wallets, derive addresses and find the first - // unused one to prevent address reuse and gaps in the derivation path. - // Taproot addresses are served straight from Bitcoin Core (bech32m). - if walletType == engines.WalletTypeBitcoinCore && addressType == pb.AddressType_ADDRESS_TYPE_SEGWIT { - unusedAddress, derivedAddresses, err := s.deriveAndCheckAddresses(ctx, walletId) - if err != nil { - zerolog.Ctx(ctx).Warn().Err(err).Msg("derive addresses failed, will generate new") - } - if unusedAddress != "" { - zerolog.Ctx(ctx).Debug().Str("address", unusedAddress).Msg("using derived unused address") - - // Save the unused address to addressbook - err = addressbook.Create(ctx, s.database, &walletId, "", unusedAddress, addressbook.DirectionReceive) - if err != nil && !strings.Contains(err.Error(), addressbook.ErrUniqueAddress) { - zerolog.Ctx(ctx).Warn().Err(err).Msg("save address to addressbook") - } - - return connect.NewResponse(&pb.GetNewAddressResponse{ - Address: unusedAddress, - Index: 0, - }), nil - } - - // Also save any other derived addresses we found (for addressbook sync) - for _, addr := range derivedAddresses { - err = addressbook.Create(ctx, s.database, &walletId, "", addr.Address, addressbook.DirectionReceive) - if err != nil && !strings.Contains(err.Error(), addressbook.ErrUniqueAddress) { - zerolog.Ctx(ctx).Debug().Err(err).Str("address", addr.Address).Msg("save derived address") - } - } + var addressType orchpb.AddressType + switch c.Msg.AddressType { + case pb.AddressType_ADDRESS_TYPE_UNSPECIFIED: + addressType = orchpb.AddressType_ADDRESS_TYPE_UNSPECIFIED + case pb.AddressType_ADDRESS_TYPE_SEGWIT: + addressType = orchpb.AddressType_ADDRESS_TYPE_SEGWIT + case pb.AddressType_ADDRESS_TYPE_TAPROOT: + addressType = orchpb.AddressType_ADDRESS_TYPE_TAPROOT } - - var address string - switch walletType { - case engines.WalletTypeBitcoinCore: - // Watch-only Core wallets import a descriptor; full wallets use the - // seed-derived wallet. Both serve addresses from Bitcoin Core. - ensure := s.walletEngine.GetBitcoinCoreWalletName - watchOnly, err := s.walletEngine.IsWatchOnly(ctx, walletId) - if err != nil { - return nil, err - } - if watchOnly { - ensure = s.walletEngine.EnsureWatchOnlyWallet - } - address, err = s.getBitcoinCoreAddress(ctx, walletId, ensure, coreAddressType) - if err != nil { - return nil, err - } - - case engines.WalletTypeElectrum: - // Electrum path — orchestrator derives the address and serves chain - // data over Esplora. - if addressType == pb.AddressType_ADDRESS_TYPE_TAPROOT { - return nil, connect.NewError(connect.CodeUnimplemented, fmt.Errorf("taproot addresses are not supported for electrum wallets")) - } - address, err = s.walletEngine.GetElectrumReceiveAddress(ctx, walletId) - if err != nil { - return nil, err - } - - default: - return nil, fmt.Errorf("unknown wallet type: %s", walletType) + address, err := s.walletEngine.GetNewAddress(ctx, walletId, addressType) + if err != nil { + return nil, err } // Store all receiving addresses in the address book @@ -330,118 +261,20 @@ func (s *Server) GetNewAddress(ctx context.Context, c *connect.Request[pb.GetNew }), nil } -// DerivedAddress represents an address derived from a wallet seed -type DerivedAddress struct { - Address string - Index uint32 - Used bool -} - -// deriveAndCheckAddresses is the single source of truth for Bitcoin Core address derivation -// It derives addresses from the seed and checks which have been used -// Returns: (firstUnusedAddress, allDerivedAddresses, error) -func (s *Server) deriveAndCheckAddresses(ctx context.Context, walletId string) (string, []DerivedAddress, error) { - // Get wallet info to access seed - walletInfo, err := s.walletEngine.GetWalletInfo(ctx, walletId) - if err != nil { - return "", nil, fmt.Errorf("get wallet info: %w", err) - } - - if walletInfo.Master.SeedHex == "" { - return "", nil, fmt.Errorf("wallet has no seed") - } - - chainParams := s.walletEngine.GetChainParams() - - external, err := deriveExternalChainKey(walletInfo, chainParams, orchwallet.ScriptNativeSegwit) - if err != nil { - return "", nil, err - } - - walletName, err := s.walletEngine.GetBitcoinCoreWalletName(ctx, walletId) - if err != nil { - return "", nil, fmt.Errorf("get wallet name: %w", err) - } - - bitcoind, err := s.bitcoind.Get(ctx) - if err != nil { - return "", nil, fmt.Errorf("bitcoind: %w", err) - } - - // Get all transactions once to avoid repeated RPC calls - txResp, err := bitcoind.ListTransactions(ctx, connect.NewRequest(&corepb.ListTransactionsRequest{ - Wallet: walletName, - Count: 1000, // Check recent transactions - })) - if err != nil { - return "", nil, fmt.Errorf("list transactions: %w", err) - } - - // Build a map of used addresses for fast lookup - usedAddresses := make(map[string]bool) - for _, tx := range txResp.Msg.Transactions { - for _, detail := range tx.Details { - usedAddresses[detail.Address] = true - } - } - - var derivedAddresses []DerivedAddress - firstUnusedAddress := "" - - // Derive addresses and check usage - for i := uint32(0); i < addressScanDepth; i++ { - // Derive address at index i - addrKey, err := external.Derive(i) - if err != nil { - return "", nil, fmt.Errorf("derive address %d: %w", i, err) - } - - pubKey, err := addrKey.ECPubKey() - if err != nil { - return "", nil, fmt.Errorf("get public key %d: %w", i, err) - } - - // Create P2WPKH address - pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed()) - witnessAddr, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, chainParams) - if err != nil { - return "", nil, fmt.Errorf("create address %d: %w", i, err) - } - - address := witnessAddr.EncodeAddress() - used := usedAddresses[address] - - derivedAddresses = append(derivedAddresses, DerivedAddress{ - Address: address, - Index: i, - Used: used, - }) - - // Track first unused address - if firstUnusedAddress == "" && !used { - firstUnusedAddress = address - } - } - - return firstUnusedAddress, derivedAddresses, nil -} - -// syncCoreAddresses syncs derived addresses to the addressbook -// This ensures the addressbook stays in sync with wallet state +// syncCoreAddresses adds the receive addresses of each Bitcoin Core wallet to +// the addressbook. func (s *Server) syncCoreAddresses(ctx context.Context) error { wallets, err := s.walletEngine.GetAllWallets(ctx) if err != nil { return fmt.Errorf("get wallets: %w", err) } - // Sync addresses for each Bitcoin Core wallet for _, wallet := range wallets { if wallet.WalletType != engines.WalletTypeBitcoinCore { continue } - // Use the unified function to derive and check addresses - _, derivedAddresses, err := s.deriveAndCheckAddresses(ctx, wallet.ID) + addresses, err := s.walletEngine.DeriveReceiveAddresses(ctx, wallet.ID, addressScanDepth) if err != nil { zerolog.Ctx(ctx).Warn().Err(err). Str("wallet_id", wallet.ID). @@ -450,12 +283,11 @@ func (s *Server) syncCoreAddresses(ctx context.Context) error { continue } - // Add all derived addresses to addressbook - for _, addr := range derivedAddresses { - err := addressbook.Create(ctx, s.database, &wallet.ID, "", addr.Address, addressbook.DirectionReceive) + for _, address := range addresses { + err := addressbook.Create(ctx, s.database, &wallet.ID, "", address, addressbook.DirectionReceive) if err != nil && !strings.Contains(err.Error(), addressbook.ErrUniqueAddress) { zerolog.Ctx(ctx).Debug().Err(err). - Str("address", addr.Address). + Str("address", address). Str("wallet_id", wallet.ID). Msg("save address to addressbook") } @@ -487,34 +319,6 @@ func (s *Server) startAddressSyncLoop(ctx context.Context) { } } -// getBitcoinCoreAddress is a helper to get a new address from Bitcoin Core wallets -func (s *Server) getBitcoinCoreAddress( - ctx context.Context, - walletId string, - getWalletName func(context.Context, string) (string, error), - addressType string, -) (string, error) { - walletName, err := getWalletName(ctx, walletId) - if err != nil { - return "", fmt.Errorf("get wallet name: %w", err) - } - - bitcoind, err := s.bitcoind.Get(ctx) - if err != nil { - return "", err - } - - resp, err := bitcoind.GetNewAddress(ctx, connect.NewRequest(&corepb.GetNewAddressRequest{ - Wallet: walletName, - AddressType: addressType, - })) - if err != nil { - return "", fmt.Errorf("bitcoin core get new address: %w", err) - } - - return resp.Msg.Address, nil -} - // GetBalance implements drivechainv1connect.DrivechainServiceHandler. func (s *Server) GetBalance(ctx context.Context, c *connect.Request[pb.GetBalanceRequest]) (*connect.Response[pb.GetBalanceResponse], error) { walletId := c.Msg.WalletId @@ -855,153 +659,22 @@ func (s *Server) createWalletSidechainDeposit( return connect.NewResponse(&pb.CreateSidechainDepositResponse{Txid: txid}), nil } -// addressScanDepth bounds how far along the external chain the wallet derives -// receiving addresses, and so how far a signable address can sit. +// addressScanDepth bounds how far along a receive chain a signable address can +// sit, and how many receive addresses the addressbook sync adds. const addressScanDepth = 100 -// walletScriptKinds returns the script kinds a wallet's receive descriptors were -// imported with, mirroring engines.coreDescriptors. -func walletScriptKinds(wallet *engines.WalletInfo) ([]orchwallet.ScriptKind, error) { - // Core wallets import BIP84+BIP86, Electrum ones can be at m/44' or m/49', - // and the stored wallet does not say which — so try every standard purpose. - if strings.TrimSpace(wallet.DerivationPath) == "" { - return []orchwallet.ScriptKind{ - orchwallet.ScriptNativeSegwit, - orchwallet.ScriptTaproot, - orchwallet.ScriptNestedSegwit, - orchwallet.ScriptLegacy, - }, nil - } - - accountPath, err := orchwallet.ParseAccountPath(wallet.DerivationPath) - if err != nil { - return nil, fmt.Errorf("invalid derivation path: %w", err) - } - - kind, ok := orchwallet.PurposeToCoreKind(accountPath.Purpose) - if !ok { - return nil, fmt.Errorf("unsupported derivation purpose %d'", accountPath.Purpose) - } - - return []orchwallet.ScriptKind{kind}, nil -} - -// deriveExternalChainKey derives the external chain a wallet's addresses of this -// script kind come from, at the account path its descriptors were imported with. -func deriveExternalChainKey(wallet *engines.WalletInfo, chainParams *chaincfg.Params, kind orchwallet.ScriptKind) (*hdkeychain.ExtendedKey, error) { - seed, err := hex.DecodeString(wallet.Master.SeedHex) - if err != nil { - return nil, fmt.Errorf("decode seed: %w", err) - } - - masterKey, err := hdkeychain.NewMaster(seed, chainParams) - if err != nil { - return nil, fmt.Errorf("derive master key: %w", err) - } - - accountPath, err := orchwallet.ResolveAccountPath( - wallet.AccountIndex, wallet.DerivationPath, kind, chainParams, - ) - if err != nil { - return nil, fmt.Errorf("resolve account path: %w", err) - } - - account, err := engines.DeriveHardenedPath(masterKey, accountPath) - if err != nil { - return nil, fmt.Errorf("derive account: %w", err) - } - - external, err := account.Derive(0) - if err != nil { - return nil, fmt.Errorf("derive external chain: %w", err) - } - - return external, nil -} - -// receiveAddressForKind builds the receiving address a pubkey produces under kind. -func receiveAddressForKind(kind orchwallet.ScriptKind, pubKey *btcec.PublicKey, chainParams *chaincfg.Params) (btcutil.Address, error) { - pkHash := btcutil.Hash160(pubKey.SerializeCompressed()) - - if kind == orchwallet.ScriptNativeSegwit { - return btcutil.NewAddressWitnessPubKeyHash(pkHash, chainParams) - } - - if kind == orchwallet.ScriptTaproot { - return btcutil.NewAddressTaproot(schnorr.SerializePubKey(txscript.ComputeTaprootKeyNoScript(pubKey)), chainParams) - } - - if kind == orchwallet.ScriptLegacy { - return btcutil.NewAddressPubKeyHash(pkHash, chainParams) - } - - if kind == orchwallet.ScriptNestedSegwit { - witness, err := btcutil.NewAddressWitnessPubKeyHash(pkHash, chainParams) - if err != nil { - return nil, err - } - - redeem, err := txscript.PayToAddrScript(witness) - if err != nil { - return nil, err - } - - return btcutil.NewAddressScriptHash(redeem, chainParams) - } - - return nil, fmt.Errorf("unsupported script kind %s", kind) -} - // deriveMessageSigningPrivateKey returns the key behind address, so the // signature proves ownership of that address rather than some other one. func deriveMessageSigningPrivateKey(wallet *engines.WalletInfo, chainParams *chaincfg.Params, address string) (*btcec.PrivateKey, error) { - kinds, err := walletScriptKinds(wallet) - if err != nil { - return nil, err - } - - for _, kind := range kinds { - external, err := deriveExternalChainKey(wallet, chainParams, kind) - if err != nil { - return nil, err - } - - for i := uint32(0); i < addressScanDepth; i++ { - addrKey, err := external.Derive(i) - if err != nil { - return nil, fmt.Errorf("derive address %d: %w", i, err) - } - - pubKey, err := addrKey.ECPubKey() - if err != nil { - return nil, fmt.Errorf("get public key %d: %w", i, err) - } - - derived, err := receiveAddressForKind(kind, pubKey, chainParams) - if err != nil { - return nil, fmt.Errorf("create address %d: %w", i, err) - } - - if derived.EncodeAddress() != address { - continue - } - - privKey, err := addrKey.ECPrivKey() - if err != nil { - return nil, fmt.Errorf("get private key %d: %w", i, err) - } - - // A taproot address commits to the tweaked output key, so the - // untweaked internal key would sign for a key nobody can check. - if kind == orchwallet.ScriptTaproot { - privKey = txscript.TweakTaprootPrivKey(*privKey, []byte{}) - } - - return privKey, nil - } - } - - return nil, fmt.Errorf("address %s is not one of the wallet's first %d receiving addresses", address, addressScanDepth) + if wallet.Master.SeedHex == "" { + return nil, errors.New("wallet has no seed to sign with") + } + return orchwallet.SigningKey(&orchwallet.WalletData{ + Master: orchwallet.MasterWallet{SeedHex: wallet.Master.SeedHex}, + ScriptType: wallet.ScriptType, + AccountIndex: wallet.AccountIndex, + DerivationPath: wallet.DerivationPath, + }, chainParams, address, addressScanDepth) } // SignMessage implements walletv1connect.WalletServiceHandler. diff --git a/bitwindow/server/engines/deniability_engine.go b/bitwindow/server/engines/deniability_engine.go index 67b65f7a0..f12f07b6d 100644 --- a/bitwindow/server/engines/deniability_engine.go +++ b/bitwindow/server/engines/deniability_engine.go @@ -563,7 +563,7 @@ func (e *DeniabilityEngine) getNewAddress(ctx context.Context, walletType Wallet } return e.getBitcoinCoreNewAddress(ctx, walletId) case WalletTypeElectrum: - return e.walletEngine.GetElectrumReceiveAddress(ctx, walletId) + return e.walletEngine.GetNewAddress(ctx, walletId, orchpb.AddressType_ADDRESS_TYPE_UNSPECIFIED) default: return "", fmt.Errorf("unsupported wallet type for deniability: %s", walletType) } diff --git a/bitwindow/server/engines/deniability_engine_test.go b/bitwindow/server/engines/deniability_engine_test.go index 5c1027ebd..a975eea6c 100644 --- a/bitwindow/server/engines/deniability_engine_test.go +++ b/bitwindow/server/engines/deniability_engine_test.go @@ -58,9 +58,10 @@ func testDenialWalletEngine( dir, &chaincfg.SigNetParams, ) - if orchestrator != nil { - engine.SetOrchestratorClient(orchestrator) + if orchestrator == nil { + orchestrator = apitests.CoreWalletOrchestrator(t) } + engine.SetOrchestratorClient(orchestrator) return engine } diff --git a/bitwindow/server/engines/derivation_fallback_test.go b/bitwindow/server/engines/derivation_fallback_test.go deleted file mode 100644 index 6932ee226..000000000 --- a/bitwindow/server/engines/derivation_fallback_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package engines - -import ( - "encoding/hex" - "strings" - "testing" - - "github.com/btcsuite/btcd/btcutil/hdkeychain" - "github.com/btcsuite/btcd/chaincfg" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// abandonSeedHex is the BIP39 seed for the standard "abandon … about" mnemonic. -const abandonSeedHex = "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4" - -func mustMaster(t *testing.T) *hdkeychain.ExtendedKey { - t.Helper() - seed, err := hex.DecodeString(abandonSeedHex) - require.NoError(t, err) - master, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) - require.NoError(t, err) - return master -} - -// TestCoreDescriptorsDefault asserts the local fallback derivation imports the -// standard BIP84 + BIP86 descriptors at account 0 when no override is set. -func TestCoreDescriptorsDefault(t *testing.T) { - e := &WalletEngine{chainParams: &chaincfg.MainNetParams} - descs, err := e.coreDescriptors(mustMaster(t), 0, "", true) - require.NoError(t, err) - require.Len(t, descs, 4) - - assert.True(t, strings.HasPrefix(descs[0].desc, "wpkh(")) - assert.Contains(t, descs[0].desc, "/84'/0'/0']") - assert.False(t, descs[0].internal) - assert.True(t, descs[1].internal) - assert.True(t, strings.HasPrefix(descs[2].desc, "tr(")) - assert.Contains(t, descs[2].desc, "/86'/0'/0']") -} - -// TestCoreDescriptorsAccountIndex asserts an account index shifts both the -// BIP84 and BIP86 descriptor origins to that account. -func TestCoreDescriptorsAccountIndex(t *testing.T) { - e := &WalletEngine{chainParams: &chaincfg.MainNetParams} - descs, err := e.coreDescriptors(mustMaster(t), 1, "", true) - require.NoError(t, err) - require.Len(t, descs, 4) - for _, d := range descs { - if strings.HasPrefix(d.desc, "wpkh(") { - assert.Contains(t, d.desc, "/84'/0'/1']") - } else { - assert.Contains(t, d.desc, "/86'/0'/1']") - } - } -} - -// TestCoreDescriptorsExplicitPath asserts a full path override imports the -// single descriptor for that path's purpose (here taproot). -func TestCoreDescriptorsExplicitPath(t *testing.T) { - e := &WalletEngine{chainParams: &chaincfg.MainNetParams} - descs, err := e.coreDescriptors(mustMaster(t), 0, "m/86'/0'/2'", true) - require.NoError(t, err) - require.Len(t, descs, 2) - for _, d := range descs { - assert.True(t, strings.HasPrefix(d.desc, "tr(")) - assert.Contains(t, d.desc, "/86'/0'/2']") - } -} - -// TestCoreDescriptorsRejectsBadPath asserts malformed override paths are rejected. -func TestCoreDescriptorsRejectsBadPath(t *testing.T) { - e := &WalletEngine{chainParams: &chaincfg.MainNetParams} - _, err := e.coreDescriptors(mustMaster(t), 0, "m/84'/0'", true) - require.Error(t, err) -} diff --git a/bitwindow/server/engines/descriptor_checksum.go b/bitwindow/server/engines/descriptor_checksum.go deleted file mode 100644 index 2ad375a49..000000000 --- a/bitwindow/server/engines/descriptor_checksum.go +++ /dev/null @@ -1,94 +0,0 @@ -package engines - -import ( - "fmt" - "strings" -) - -// Descriptor checksum implementation based on Bitcoin Core's algorithm -// https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp - -const inputCharset = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " -const checksumCharset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" - -// polymod computes the descriptor checksum polymod -func polymod(c uint64, val uint64) uint64 { - c0 := c >> 35 - c = ((c & 0x7ffffffff) << 5) ^ val - if c0&1 != 0 { - c ^= 0xf5dee51989 - } - if c0&2 != 0 { - c ^= 0xa9fdca3312 - } - if c0&4 != 0 { - c ^= 0x1bab10e32d - } - if c0&8 != 0 { - c ^= 0x3706b1677a - } - if c0&16 != 0 { - c ^= 0x644d626ffd - } - return c -} - -// DescriptorChecksum computes the checksum for a descriptor string -// Implementation based on Bitcoin Core's algorithm: -// https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp -func DescriptorChecksum(desc string) (string, error) { - c := uint64(1) - cls := 0 - clscount := 0 - - for _, ch := range desc { - pos := strings.IndexRune(inputCharset, ch) - if pos == -1 { - return "", fmt.Errorf("invalid character in descriptor: %c", ch) - } - - // Process lower 5 bits - c = polymod(c, uint64(pos&31)) - - // Accumulate upper 3 bits - cls = cls*3 + (pos >> 5) - clscount++ - - // Every 3 characters, process the accumulated upper bits - if clscount == 3 { - c = polymod(c, uint64(cls)) - cls = 0 - clscount = 0 - } - } - - // Process any remaining upper bits - if clscount > 0 { - c = polymod(c, uint64(cls)) - } - - // Add 8 zeros - for i := 0; i < 8; i++ { - c = polymod(c, 0) - } - - // XOR with final constant - c ^= 1 - - // Convert to checksum characters - var checksum strings.Builder - for i := 0; i < 8; i++ { - checksum.WriteByte(checksumCharset[(c>>(5*(7-i)))&31]) - } - - return checksum.String(), nil -} - -// AddDescriptorChecksum adds a checksum to a descriptor string -func AddDescriptorChecksum(desc string) (string, error) { - checksum, err := DescriptorChecksum(desc) - if err != nil { - return "", err - } - return fmt.Sprintf("%s#%s", desc, checksum), nil -} diff --git a/bitwindow/server/engines/descriptor_checksum_test.go b/bitwindow/server/engines/descriptor_checksum_test.go deleted file mode 100644 index 35fa54649..000000000 --- a/bitwindow/server/engines/descriptor_checksum_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package engines - -import ( - "testing" -) - -func TestDescriptorChecksum(t *testing.T) { - tests := []struct { - desc string - expectedChecksum string - }{ - { - // From Bitcoin Core getdescriptorinfo - desc: "wpkh(tpubDDH1ndozCcuGXjVaXnB4NHUWbKfMTYfxH3wuU1GFvCXtEZZqsMY6NxBdgdaebExgDAVicckGNJDU8wVfRUWWMaov5jX4zPaDorqe75QwjAC/0/*)", - expectedChecksum: "v39n29tr", - }, - { - // Another example from Bitcoin Core - desc: "wpkh(tpubDDH1ndozCcuGXjVaXnB4NHUWbKfMTYfxH3wuU1GFvCXtEZZqsMY6NxBdgdaebExgDAVicckGNJDU8wVfRUWWMaov5jX4zPaDorqe75QwjAC/1/*)", - expectedChecksum: "a9qjhsmm", - }, - { - // Test with xprv - expected checksum from Bitcoin Core error message - desc: "wpkh(tprv8gayeDmk4FDbeGTne8WTxspQ2J9RJDV3hkM8BVDxVvjVQ5K5ExiWCTZmWXypiXyFhDMoCWKac4U8FMXkc5wvjFYQwiGYFdqmKAqdgPAL8pw/0/*)", - expectedChecksum: "4zyy5snq", - }, - { - // Another xprv - desc: "wpkh(tprv8gayeDmk4FDbeGTne8WTxspQ2J9RJDV3hkM8BVDxVvjVQ5K5ExiWCTZmWXypiXyFhDMoCWKac4U8FMXkc5wvjFYQwiGYFdqmKAqdgPAL8pw/1/*)", - expectedChecksum: "ykp9f9rc", - }, - } - - for _, tt := range tests { - t.Run(tt.desc[:20]+"...", func(t *testing.T) { - checksum, err := DescriptorChecksum(tt.desc) - if err != nil { - t.Fatalf("compute checksum: %v", err) - } - - if checksum != tt.expectedChecksum { - t.Errorf("checksum mismatch:\n got: %s\n expected: %s", checksum, tt.expectedChecksum) - } - }) - } -} diff --git a/bitwindow/server/engines/wallet_engine.go b/bitwindow/server/engines/wallet_engine.go index 25584561b..c5448d4b8 100644 --- a/bitwindow/server/engines/wallet_engine.go +++ b/bitwindow/server/engines/wallet_engine.go @@ -18,11 +18,8 @@ import ( "github.com/LayerTwo-Labs/sidesail/bitwindow/server/wallet" orchpb "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/gen/walletmanager/v1" orchrpc "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/gen/walletmanager/v1/walletmanagerv1connect" - orchwallet "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/wallet" corepb "github.com/barebitcoin/btc-buf/gen/bitcoin/bitcoind/v1alpha" corerpc "github.com/barebitcoin/btc-buf/gen/bitcoin/bitcoind/v1alpha/bitcoindv1alphaconnect" - "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" "github.com/rs/zerolog" "github.com/samber/lo" @@ -60,6 +57,8 @@ type WalletInfo struct { // DerivationPath is an explicit account-level path (m/purpose'/coin'/account') // overriding purpose/coin/account; empty = standard purposes at AccountIndex. DerivationPath string `json:"derivation_path,omitempty"` + // ScriptType is the stored address kind; empty means native segwit. + ScriptType string `json:"script_type,omitempty"` } // IsWatchOnly reports whether the wallet holds no signing key — it carries an @@ -613,247 +612,19 @@ func (e *WalletEngine) EnsureBitcoinCoreWallet(ctx context.Context, walletId str // ensureBitcoinCoreWalletLocked is the singleflight-wrapped body of // EnsureBitcoinCoreWallet. Don't call directly. func (e *WalletEngine) ensureBitcoinCoreWalletLocked(ctx context.Context, walletId string) (string, error) { - // Try orchestrator first - if e.orchClient != nil { - resp, err := e.orchClient.CreateBitcoinCoreWallet(ctx, connect.NewRequest(&orchpb.CreateBitcoinCoreWalletRequest{ - WalletId: walletId, - })) - if err == nil { - e.mu.Lock() - e.coreWallets[walletId] = resp.Msg.CoreWalletName - e.mu.Unlock() - return resp.Msg.CoreWalletName, nil - } - // If the orchestrator says "still warming up" (Unavailable), don't - // fall through to the local path — it would re-issue the same RPCs - // against bitcoind and fail the same way, defeating the orchestrator's - // 5s backoff and storming bitcoind during IBD/rescan. - if connect.CodeOf(err) == connect.CodeUnavailable || IsBitcoinCoreStartupError(err.Error()) { - return "", err - } - // Otherwise fall through to local fallback. - } - - e.mu.Lock() - defer e.mu.Unlock() - - // Check cache - if walletName, exists := e.coreWallets[walletId]; exists { - return walletName, nil - } - - // Get wallet info - wallet, err := e.GetWalletInfo(ctx, walletId) - if err != nil { - return "", err - } - - if wallet.WalletType != WalletTypeBitcoinCore { - return "", fmt.Errorf("wallet %s is not a Bitcoin Core wallet", walletId) - } - - // Generate wallet name from wallet ID - walletName := fmt.Sprintf("wallet_%s", walletId[:8]) - - // Get bitcoind client - bitcoindClient, err := e.bitcoindConnector(ctx) - if err != nil { - return "", fmt.Errorf("get bitcoind client: %w", err) - } - - // Check if wallet already exists in Bitcoin Core - listResp, err := bitcoindClient.ListWallets(ctx, connect.NewRequest(&emptypb.Empty{})) - if err != nil { - return "", fmt.Errorf("list Bitcoin Core wallets: %w", err) - } - - walletExists := lo.Contains(listResp.Msg.Wallets, walletName) - if !walletExists { - // Create wallet from seed - if err := e.CreateBitcoinCoreWalletFromSeed(ctx, walletName, wallet.Master.SeedHex, wallet.AccountIndex, wallet.DerivationPath); err != nil { - return "", fmt.Errorf("create Bitcoin Core wallet: %w", err) - } - zerolog.Ctx(ctx).Info(). - Str("wallet_id", walletId). - Str("wallet_name", walletName). - Msg("created Bitcoin Core wallet from seed") - } - - // Cache the mapping - e.coreWallets[walletId] = walletName - return walletName, nil -} - -// CreateBitcoinCoreWalletFromSeed creates a Bitcoin Core wallet and imports the -// seed. accountIndex/derivationPath are the optional account-level derivation -// override (0/"" = standard BIP84+BIP86 at account 0). -func (e *WalletEngine) CreateBitcoinCoreWalletFromSeed( - ctx context.Context, - walletName string, - seedHex string, - accountIndex uint32, - derivationPath string, -) error { - // Decode seed - seed, err := hex.DecodeString(seedHex) - if err != nil { - return fmt.Errorf("decode seed hex: %w", err) - } - - // Derive master key - masterKey, err := hdkeychain.NewMaster(seed, e.chainParams) - if err != nil { - return fmt.Errorf("derive master key: %w", err) - } - - descriptors, err := e.coreDescriptors(masterKey, accountIndex, derivationPath, true) - if err != nil { - return err - } - - // Get bitcoind client - bitcoindClient, err := e.bitcoindConnector(ctx) - if err != nil { - return fmt.Errorf("get bitcoind client: %w", err) - } - - // Create descriptor wallet in Bitcoin Core - _, err = bitcoindClient.CreateWallet(ctx, connect.NewRequest(&corepb.CreateWalletRequest{ - Name: walletName, - DisablePrivateKeys: false, - Blank: true, - Passphrase: "", - AvoidReuse: false, - })) - if err != nil { - // If wallet already exists on disk, load it instead - if strings.Contains(err.Error(), "Database already exists") { - zerolog.Ctx(ctx).Info(). - Str("wallet_name", walletName). - Msg("Bitcoin Core wallet already exists on disk, loading it") - - _, err = bitcoindClient.LoadWallet(ctx, connect.NewRequest(&corepb.LoadWalletRequest{ - Filename: walletName, - LoadOnStartup: true, - })) - if err != nil { - return fmt.Errorf("load Bitcoin Core wallet: %w", err) - } - } else { - return fmt.Errorf("create Bitcoin Core wallet: %w", err) - } - } - - var requests []*corepb.ImportDescriptorsRequest_Request - for _, d := range descriptors { - descriptorWithChecksum, err := AddDescriptorChecksum(d.desc) - if err != nil { - return fmt.Errorf("compute descriptor checksum: %w", err) - } - - requests = append(requests, &corepb.ImportDescriptorsRequest_Request{ - Descriptor_: descriptorWithChecksum, - Active: true, - Timestamp: nil, - Internal: d.internal, - RangeEnd: 999, - }) + if e.orchClient == nil { + return "", fmt.Errorf("orchestrator wallet client not connected") } - - resp, err := bitcoindClient.ImportDescriptors(ctx, connect.NewRequest(&corepb.ImportDescriptorsRequest{ - Wallet: walletName, - Requests: requests, + resp, err := e.orchClient.CreateBitcoinCoreWallet(ctx, connect.NewRequest(&orchpb.CreateBitcoinCoreWalletRequest{ + WalletId: walletId, })) if err != nil { - return fmt.Errorf("import descriptors: %w", err) - } - - // Check results - for i, result := range resp.Msg.Responses { - if !result.Success { - errMsg := "unknown error" - if result.Error != nil { - errMsg = result.Error.Message - } - return fmt.Errorf("descriptor %d import failed: %s", i, errMsg) - } - } - - return nil -} - -// coreDescriptor is one descriptor string and whether it's the internal (change) chain. -type coreDescriptor struct { - desc string - internal bool -} - -// coreDescriptors builds the receive+change descriptors for a Core wallet from -// the master key and the optional account-level derivation override. With no -// override it returns BIP84 + BIP86 at account 0; an account index shifts both; -// an explicit path returns the single descriptor for that path's purpose. This -// mirrors the orchestrator CoreBackend so the local fallback derives identically. -func (e *WalletEngine) coreDescriptors(masterKey *hdkeychain.ExtendedKey, accountIndex uint32, derivationPath string, withOrigin bool) ([]coreDescriptor, error) { - pubKey, err := masterKey.ECPubKey() - if err != nil { - return nil, fmt.Errorf("get master public key: %w", err) - } - fingerprint := hex.EncodeToString(btcutil.Hash160(pubKey.SerializeCompressed())[:4]) - - var kinds []orchwallet.ScriptKind - if strings.TrimSpace(derivationPath) != "" { - ap, err := orchwallet.ParseAccountPath(derivationPath) - if err != nil { - return nil, fmt.Errorf("invalid derivation path: %w", err) - } - kind, ok := orchwallet.PurposeToCoreKind(ap.Purpose) - if !ok { - return nil, fmt.Errorf("unsupported core descriptor purpose %d'", ap.Purpose) - } - kinds = []orchwallet.ScriptKind{kind} - } else { - kinds = []orchwallet.ScriptKind{orchwallet.ScriptNativeSegwit, orchwallet.ScriptTaproot} - } - - var out []coreDescriptor - for _, kind := range kinds { - ap, err := orchwallet.ResolveAccountPath(accountIndex, derivationPath, kind, e.chainParams) - if err != nil { - return nil, err - } - acct, err := DeriveHardenedPath(masterKey, ap) - if err != nil { - return nil, err - } - acctXprv := acct.String() - open, close, ok := orchwallet.CoreDescriptorWrapper(kind) - if !ok { - return nil, fmt.Errorf("unsupported core descriptor kind %s", kind) - } - origin := "" - if withOrigin { - origin = fmt.Sprintf("[%s/%s]", fingerprint, ap.Origin("'")) - } - out = append(out, - coreDescriptor{desc: fmt.Sprintf("%s%s%s/0/*%s", open, origin, acctXprv, close), internal: false}, - coreDescriptor{desc: fmt.Sprintf("%s%s%s/1/*%s", open, origin, acctXprv, close), internal: true}, - ) - } - return out, nil -} - -// DeriveHardenedPath derives the hardened account-level key for an AccountPath. -func DeriveHardenedPath(masterKey *hdkeychain.ExtendedKey, ap orchwallet.AccountPath) (*hdkeychain.ExtendedKey, error) { - const h = hdkeychain.HardenedKeyStart - cur := masterKey - for _, idx := range []uint32{ap.Purpose, ap.Coin, ap.Account} { - next, err := cur.Derive(h + idx) - if err != nil { - return nil, fmt.Errorf("derive %d': %w", idx, err) - } - cur = next + return "", err } - return cur, nil + e.mu.Lock() + e.coreWallets[walletId] = resp.Msg.CoreWalletName + e.mu.Unlock() + return resp.Msg.CoreWalletName, nil } // GetBitcoinCoreWalletName returns the Bitcoin Core wallet name for a walletId @@ -861,22 +632,38 @@ func (e *WalletEngine) GetBitcoinCoreWalletName(ctx context.Context, walletId st return e.EnsureBitcoinCoreWallet(ctx, walletId) } -// GetElectrumReceiveAddress returns a fresh receive address for an electrum -// wallet from the orchestrator, which derives it locally and serves chain -// data over Esplora (no Bitcoin Core). -func (e *WalletEngine) GetElectrumReceiveAddress(ctx context.Context, walletId string) (string, error) { +// GetNewAddress returns a receive address of the wallet. The orchestrator +// derives it for Core and electrum wallets alike. +func (e *WalletEngine) GetNewAddress(ctx context.Context, walletId string, addressType orchpb.AddressType) (string, error) { if e.orchClient == nil { return "", fmt.Errorf("orchestrator wallet client not connected") } resp, err := e.orchClient.GetNewAddress(ctx, connect.NewRequest(&orchpb.GetNewAddressRequest{ - WalletId: walletId, + WalletId: walletId, + AddressType: addressType, })) if err != nil { - return "", fmt.Errorf("electrum: get new address: %w", err) + return "", fmt.Errorf("get new address: %w", err) } return resp.Msg.Address, nil } +// DeriveReceiveAddresses returns the first count receive addresses of the +// wallet, as the orchestrator derives them. +func (e *WalletEngine) DeriveReceiveAddresses(ctx context.Context, walletId string, count int) ([]string, error) { + if e.orchClient == nil { + return nil, fmt.Errorf("orchestrator wallet client not connected") + } + resp, err := e.orchClient.DeriveAddresses(ctx, connect.NewRequest(&orchpb.DeriveAddressesRequest{ + WalletId: walletId, + Count: int32(count), + })) + if err != nil { + return nil, fmt.Errorf("derive addresses: %w", err) + } + return resp.Msg.Addresses, nil +} + // GetElectrumBalance returns the confirmed and pending balance (in sats) for // an electrum wallet from the orchestrator's Esplora-backed provider. func (e *WalletEngine) GetElectrumBalance(ctx context.Context, walletId string) (confirmed, pending uint64, err error) { @@ -1091,202 +878,7 @@ func (e *WalletEngine) EnsureWatchOnlyWallet(ctx context.Context, walletId strin return legacy, nil } - // Try orchestrator first (it handles full and watch-only Core wallets) - if e.orchClient != nil { - resp, err := e.orchClient.CreateBitcoinCoreWallet(ctx, connect.NewRequest(&orchpb.CreateBitcoinCoreWalletRequest{ - WalletId: walletId, - })) - if err == nil { - e.mu.Lock() - e.coreWallets[walletId] = resp.Msg.CoreWalletName - e.mu.Unlock() - return resp.Msg.CoreWalletName, nil - } - // A starting bitcoind fails the local path the same way, so propagate - // instead of storming it. A transport failure means the orchestrator is - // down, and the local path is exactly the fallback for that. - if IsBitcoinCoreStartupError(err.Error()) { - return "", err - } - // Otherwise fall through to local on error - } - - e.mu.Lock() - defer e.mu.Unlock() - - // Check cache - if walletName, exists := e.coreWallets[walletId]; exists { - return walletName, nil - } - - // Get wallet info - wallet, err := e.GetWalletInfo(ctx, walletId) - if err != nil { - return "", err - } - - if !wallet.IsWatchOnly() { - return "", fmt.Errorf("wallet %s is not a watch-only wallet", walletId) - } - - // Generate wallet name from wallet ID - walletName := fmt.Sprintf("wallet_%s", walletId[:8]) - - // Get bitcoind client - bitcoindClient, err := e.bitcoindConnector(ctx) - if err != nil { - return "", fmt.Errorf("get bitcoind client: %w", err) - } - - // Check if wallet already exists in Bitcoin Core - listResp, err := bitcoindClient.ListWallets(ctx, connect.NewRequest(&emptypb.Empty{})) - if err != nil { - return "", fmt.Errorf("list Bitcoin Core wallets: %w", err) - } - - walletExists := lo.Contains(listResp.Msg.Wallets, walletName) - if !walletExists { - // Create watch-only wallet - if err := e.createWatchOnlyWallet(ctx, walletName, wallet); err != nil { - return "", fmt.Errorf("create watch-only wallet: %w", err) - } - zerolog.Ctx(ctx).Info(). - Str("wallet_id", walletId). - Str("wallet_name", walletName). - Msg("created watch-only Bitcoin Core wallet") - } - - // Cache the mapping - e.coreWallets[walletId] = walletName - return walletName, nil -} - -// createWatchOnlyWallet creates a watch-only Bitcoin Core wallet -func (e *WalletEngine) createWatchOnlyWallet( - ctx context.Context, - walletName string, - wallet *WalletInfo, -) error { - // Get bitcoind client - bitcoindClient, err := e.bitcoindConnector(ctx) - if err != nil { - return fmt.Errorf("get bitcoind client: %w", err) - } - - // Create descriptor wallet in Bitcoin Core (watch-only) - _, err = bitcoindClient.CreateWallet(ctx, connect.NewRequest(&corepb.CreateWalletRequest{ - Name: walletName, - DisablePrivateKeys: true, - Blank: true, - Passphrase: "", - AvoidReuse: false, - })) - if err != nil { - // If wallet already exists on disk, load it instead - if strings.Contains(err.Error(), "Database already exists") { - zerolog.Ctx(ctx).Info(). - Str("wallet_name", walletName). - Msg("Bitcoin Core watch-only wallet already exists on disk, loading it") - - _, err = bitcoindClient.LoadWallet(ctx, connect.NewRequest(&corepb.LoadWalletRequest{ - Filename: walletName, - LoadOnStartup: false, - })) - if err != nil { - return fmt.Errorf("load Bitcoin Core wallet: %w", err) - } - } else { - return fmt.Errorf("create Bitcoin Core wallet: %w", err) - } - } - - // Import the descriptor or xpub - var descriptorToImport string - var isXpub bool - - switch { - case wallet.WatchOnly.Descriptor != "": - descriptorToImport = wallet.WatchOnly.Descriptor - isXpub = false - - case wallet.WatchOnly.Xpub != "": - xpub := wallet.WatchOnly.Xpub - descriptorToImport = fmt.Sprintf("wpkh(%s/0/*)#checksum", xpub) - isXpub = true - - default: - return fmt.Errorf("watch-only wallet requires either descriptor or xpub") - } - - if err := e.importDescriptorToWallet(ctx, bitcoindClient, walletName, descriptorToImport, isXpub); err != nil { - return fmt.Errorf("import descriptor: %w", err) - } - - return nil -} - -// importDescriptorToWallet imports a descriptor into a Bitcoin Core wallet -func (e *WalletEngine) importDescriptorToWallet( - ctx context.Context, - bitcoindClient corerpc.BitcoinServiceClient, - walletName string, - descriptor string, - isXpub bool, -) error { - descriptorsToImport := []string{descriptor} - - if isXpub { - // Add change descriptor for xpub-based wallets - xpubStart := strings.Index(descriptor, "wpkh(") + 5 - xpubEnd := strings.Index(descriptor, "/0/*)") - if xpubStart > 5 && xpubEnd > xpubStart { - xpub := descriptor[xpubStart:xpubEnd] - changeDescriptor := fmt.Sprintf("wpkh(%s/1/*)#checksum", xpub) - descriptorsToImport = append(descriptorsToImport, changeDescriptor) - } - } - - var requests []*corepb.ImportDescriptorsRequest_Request - for i, desc := range descriptorsToImport { - // Strip any existing (possibly placeholder) checksum and compute a real - // one — Bitcoin Core rejects descriptors imported without a valid checksum. - descWithChecksum, err := AddDescriptorChecksum(strings.Split(desc, "#")[0]) - if err != nil { - return fmt.Errorf("compute descriptor checksum: %w", err) - } - isInternal := i == 1 - - requests = append(requests, &corepb.ImportDescriptorsRequest_Request{ - Descriptor_: descWithChecksum, - Active: true, - Timestamp: nil, - Internal: isInternal, - Label: "", - RangeStart: 0, - RangeEnd: 1000, - }) - } - - resp, err := bitcoindClient.ImportDescriptors(ctx, connect.NewRequest(&corepb.ImportDescriptorsRequest{ - Wallet: walletName, - Requests: requests, - })) - if err != nil { - return fmt.Errorf("bitcoin core importdescriptors: %w", err) - } - - // Check results - for i, result := range resp.Msg.Responses { - if !result.Success { - errMsg := "unknown error" - if result.Error != nil { - errMsg = result.Error.Message - } - return fmt.Errorf("descriptor %d import failed: %s", i, errMsg) - } - } - - return nil + return e.EnsureBitcoinCoreWallet(ctx, walletId) } // ============================================================================ @@ -1310,32 +902,14 @@ func (e *WalletEngine) SyncWallets(ctx context.Context) error { log.Info().Msg("wallet sync: starting") - // Try orchestrator first - if e.orchClient != nil { - resp, err := e.orchClient.EnsureCoreWallets(ctx, connect.NewRequest(&orchpb.EnsureCoreWalletsRequest{})) - if err == nil { - log.Info().Int32("synced", resp.Msg.SyncedCount).Msg("wallet sync: completed via orchestrator") - return nil - } - log.Warn().Err(err).Msg("wallet sync: orchestrator failed, falling back to local") + if e.orchClient == nil { + return fmt.Errorf("orchestrator wallet client not connected") } - - wallets, err := e.loadAllWallets() + resp, err := e.orchClient.EnsureCoreWallets(ctx, connect.NewRequest(&orchpb.EnsureCoreWalletsRequest{})) if err != nil { - return fmt.Errorf("load wallets: %w", err) - } - - if len(wallets) == 0 { - log.Info().Msg("wallet sync: no wallets to sync") - return nil - } - - // Ensure Bitcoin Core wallets exist - if err := e.ensureBitcoinCoreWallets(ctx, wallets); err != nil { - log.Error().Err(err).Msg("wallet sync: failed") + return fmt.Errorf("wallet sync: %w", err) } - - log.Info().Msg("wallet sync: completed") + log.Info().Int32("synced", resp.Msg.SyncedCount).Msg("wallet sync: completed via orchestrator") return nil } @@ -1384,148 +958,6 @@ func (e *WalletEngine) GetAllWallets(ctx context.Context) ([]WalletInfo, error) return e.loadAllWallets() } -func (e *WalletEngine) ensureBitcoinCoreWallets(ctx context.Context, wallets []WalletInfo) error { - log := zerolog.Ctx(ctx) - - // Find all Bitcoin Core wallets - coreWallets := lo.Filter(wallets, func(w WalletInfo, _ int) bool { - return w.WalletType == WalletTypeBitcoinCore - }) - - if len(coreWallets) == 0 { - return nil - } - - // Get bitcoind client - bitcoindClient, err := e.bitcoindConnector(ctx) - if err != nil { - return fmt.Errorf("bitcoind not available: %w", err) - } - - // List existing wallets in Bitcoin Core - listResp, err := bitcoindClient.ListWallets(ctx, connect.NewRequest(&emptypb.Empty{})) - if err != nil { - return fmt.Errorf("list Bitcoin Core wallets: %w", err) - } - - // Check each wallet - for _, wallet := range coreWallets { - walletName := fmt.Sprintf("wallet_%s", wallet.ID[:8]) - walletExists := lo.Contains(listResp.Msg.Wallets, walletName) - - if !walletExists { - log.Info(). - Str("wallet_id", wallet.ID). - Str("wallet_name", walletName). - Msg("wallet sync: creating missing Bitcoin Core wallet") - - if err := e.createBitcoinCoreWalletForSync(ctx, bitcoindClient, walletName, &wallet); err != nil { - log.Error(). - Err(err). - Str("wallet_id", wallet.ID). - Msg("wallet sync: failed to create Bitcoin Core wallet") - continue - } - - log.Info(). - Str("wallet_id", wallet.ID). - Str("wallet_name", walletName). - Msg("wallet sync: created Bitcoin Core wallet") - } - } - - return nil -} - -func (e *WalletEngine) createBitcoinCoreWalletForSync( - ctx context.Context, - bitcoindClient corerpc.BitcoinServiceClient, - walletName string, - wallet *WalletInfo, -) error { - // Decode seed - seed, err := hex.DecodeString(wallet.Master.SeedHex) - if err != nil { - return fmt.Errorf("decode seed hex: %w", err) - } - - // Derive master key - masterKey, err := hdkeychain.NewMaster(seed, e.chainParams) - if err != nil { - return fmt.Errorf("derive master key: %w", err) - } - - descriptors, err := e.coreDescriptors(masterKey, wallet.AccountIndex, wallet.DerivationPath, false) - if err != nil { - return err - } - - // Create wallet - _, err = bitcoindClient.CreateWallet(ctx, connect.NewRequest(&corepb.CreateWalletRequest{ - Name: walletName, - DisablePrivateKeys: false, - Blank: true, - Passphrase: "", - AvoidReuse: false, - })) - if err != nil { - // If wallet already exists on disk, load it instead - if strings.Contains(err.Error(), "Database already exists") { - zerolog.Ctx(ctx).Info(). - Str("wallet_name", walletName). - Msg("Bitcoin Core wallet already exists on disk, loading it") - - _, err = bitcoindClient.LoadWallet(ctx, connect.NewRequest(&corepb.LoadWalletRequest{ - Filename: walletName, - LoadOnStartup: false, - })) - if err != nil { - return fmt.Errorf("load Bitcoin Core wallet: %w", err) - } - } else { - return fmt.Errorf("create Bitcoin Core wallet: %w", err) - } - } - - var requests []*corepb.ImportDescriptorsRequest_Request - for _, d := range descriptors { - descriptorWithChecksum, err := AddDescriptorChecksum(d.desc) - if err != nil { - return fmt.Errorf("compute descriptor checksum: %w", err) - } - - requests = append(requests, &corepb.ImportDescriptorsRequest_Request{ - Descriptor_: descriptorWithChecksum, - Active: true, - Timestamp: nil, - Internal: d.internal, - RangeStart: 0, - RangeEnd: 1000, - }) - } - - resp, err := bitcoindClient.ImportDescriptors(ctx, connect.NewRequest(&corepb.ImportDescriptorsRequest{ - Wallet: walletName, - Requests: requests, - })) - if err != nil { - return fmt.Errorf("import descriptors: %w", err) - } - - // Check results - for i, result := range resp.Msg.Responses { - if !result.Success { - errMsg := "unknown error" - if result.Error != nil { - errMsg = result.Error.Message - } - return fmt.Errorf("descriptor %d import failed: %s", i, errMsg) - } - } - - return nil -} - // HasOrchestratorClient reports whether the orchestrator's electrum surface is // reachable at all — false in dev setups that never dialled it. func (e *WalletEngine) HasOrchestratorClient() bool { diff --git a/bitwindow/server/engines/wallet_engine_watchonly_test.go b/bitwindow/server/engines/wallet_engine_watchonly_test.go index dac1a2c2b..826bc23d1 100644 --- a/bitwindow/server/engines/wallet_engine_watchonly_test.go +++ b/bitwindow/server/engines/wallet_engine_watchonly_test.go @@ -3,8 +3,6 @@ package engines import ( "context" "errors" - "os" - "path/filepath" "testing" "connectrpc.com/connect" @@ -28,68 +26,19 @@ func (s stubOrchWalletClient) CreateBitcoinCoreWallet(context.Context, *connect. return nil, s.err } -// TestEnsureWatchOnlyWalletTransientOrchestratorError asserts a warming-up -// orchestrator or a starting bitcoind propagates instead of falling through to -// the local path, which would mint a second Core wallet for the same walletId. +// TestEnsureWatchOnlyWalletTransientOrchestratorError asserts an orchestrator +// error reaches the caller. Only the orchestrator creates a Core wallet. func TestEnsureWatchOnlyWalletTransientOrchestratorError(t *testing.T) { - // A starting bitcoind fails the local path the same way, so it propagates. - t.Run("startup propagates", func(t *testing.T) { - orchErr := connect.NewError(connect.CodeInternal, errors.New("-28: Verifying blocks…")) + for _, orchErr := range []error{ + connect.NewError(connect.CodeInternal, errors.New("-28: Verifying blocks…")), + connect.NewError(connect.CodeUnavailable, errors.New("connection refused")), + } { e := NewWalletEngine(nil, t.TempDir(), &chaincfg.MainNetParams) e.SetOrchestratorClient(stubOrchWalletClient{err: orchErr}) _, err := e.EnsureWatchOnlyWallet(context.Background(), "deadbeefcafebabe") require.ErrorIs(t, err, orchErr) - }) - - // A down orchestrator is exactly what the local path is the fallback for. - t.Run("unavailable falls back to local", func(t *testing.T) { - orchErr := connect.NewError(connect.CodeUnavailable, errors.New("connection refused")) - e := NewWalletEngine(nil, t.TempDir(), &chaincfg.MainNetParams) - e.SetOrchestratorClient(stubOrchWalletClient{err: orchErr}) - - _, err := e.EnsureWatchOnlyWallet(context.Background(), "deadbeefcafebabe") - require.NotErrorIs(t, err, orchErr) - }) -} - -// TestEnsureWatchOnlyWalletLocalName asserts the local fallback names the Core -// wallet the same way the orchestrator does, so both resolve one walletId to -// one bitcoind wallet. -func TestEnsureWatchOnlyWalletLocalName(t *testing.T) { - walletDir := t.TempDir() - require.NoError(t, os.WriteFile(filepath.Join(walletDir, "wallet.json"), []byte(`{ - "version": 1, - "activeWalletId": "deadbeefcafebabe", - "wallets": [{ - "id": "deadbeefcafebabe", - "name": "watcher", - "wallet_type": "bitcoinCore", - "watch_only": {"xpub": "xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj"} - }] - }`), 0o600)) - - ctrl := gomock.NewController(t) - mockBitcoind := mocks.NewMockBitcoinServiceClient(ctrl) - mockBitcoind.EXPECT(). - ListWallets(gomock.Any(), gomock.Any()). - Return(&connect.Response[corepb.ListWalletsResponse]{ - Msg: &corepb.ListWalletsResponse{Wallets: []string{"wallet_deadbeef"}}, - }, nil). - AnyTimes() - // No wallet from before the rename, so the local name applies. - mockBitcoind.EXPECT(). - LoadWallet(gomock.Any(), gomock.Any()). - Return(nil, errors.New("Wallet file verification failed. Path does not exist.")). - AnyTimes() - - e := NewWalletEngine(func(context.Context) (corerpc.BitcoinServiceClient, error) { - return mockBitcoind, nil - }, walletDir, &chaincfg.MainNetParams) - - walletName, err := e.EnsureWatchOnlyWallet(context.Background(), "deadbeefcafebabe") - require.NoError(t, err) - require.Equal(t, "wallet_deadbeef", walletName) + } } // A wallet from before the rename holds the scan state, so it wins over a fresh diff --git a/bitwindow/server/engines/wallet_manager_integration_test.go b/bitwindow/server/engines/wallet_manager_integration_test.go index 02fc12b1e..b16f5e76e 100644 --- a/bitwindow/server/engines/wallet_manager_integration_test.go +++ b/bitwindow/server/engines/wallet_manager_integration_test.go @@ -8,6 +8,7 @@ import ( "testing" "connectrpc.com/connect" + orchwallet "github.com/LayerTwo-Labs/sidesail/sidechain-orchestrator/wallet" corepb "github.com/barebitcoin/btc-buf/gen/bitcoin/bitcoind/v1alpha" coreproxy "github.com/barebitcoin/btc-buf/server" "github.com/btcsuite/btcd/btcutil/hdkeychain" @@ -143,7 +144,7 @@ func TestDescriptorImportIntegration(t *testing.T) { // Don't use GetDescriptorInfo - it strips private keys! // Compute checksum ourselves to keep private keys intact - descriptorWithChecksum, err := AddDescriptorChecksum(d.desc) + descriptorWithChecksum, err := orchwallet.AddDescriptorChecksum(d.desc) if err != nil { t.Fatalf("compute checksum: %v", err) } diff --git a/bitwindow/server/tests/apitests/apitests.go b/bitwindow/server/tests/apitests/apitests.go index 96399a08e..d755ddac6 100644 --- a/bitwindow/server/tests/apitests/apitests.go +++ b/bitwindow/server/tests/apitests/apitests.go @@ -49,11 +49,15 @@ type configg struct { type ServerOpt func(opt *configg) -func (o *configg) populate(_ *testing.T, ctrl *gomock.Controller, options ...ServerOpt) { +func (o *configg) populate(t *testing.T, ctrl *gomock.Controller, options ...ServerOpt) { for _, option := range options { option(o) } + if o.walletType == "bitcoinCore" && o.orchestrator == nil { + o.orchestrator = CoreWalletOrchestrator(t) + } + if o.enforcer == nil { o.enforcer = mocks.NewMockValidatorServiceClient(ctrl) } @@ -297,6 +301,30 @@ func defaultBitcoindMock(ctrl *gomock.Controller) bitcoindv1alphaconnect.Bitcoin return mock } +// CoreWalletOrchestrator returns an orchestrator client that names the Bitcoin +// Core wallet of a wallet as the orchestrator does. Every other call answers +// as unimplemented. +func CoreWalletOrchestrator(t *testing.T) orchrpc.WalletManagerServiceClient { + t.Helper() + mux := http.NewServeMux() + mux.Handle(orchrpc.NewWalletManagerServiceHandler(coreWalletManager{})) + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + return orchrpc.NewWalletManagerServiceClient(server.Client(), server.URL) +} + +type coreWalletManager struct { + orchrpc.UnimplementedWalletManagerServiceHandler +} + +func (coreWalletManager) CreateBitcoinCoreWallet( + _ context.Context, req *connect.Request[orchpb.CreateBitcoinCoreWalletRequest], +) (*connect.Response[orchpb.CreateBitcoinCoreWalletResponse], error) { + return connect.NewResponse(&orchpb.CreateBitcoinCoreWalletResponse{ + CoreWalletName: "wallet_" + req.Msg.WalletId[:8], + }), nil +} + // ExpectCoreWalletSetup allows the calls the wallet engine makes in the // background when it brings a Bitcoin Core wallet up. It asserts nothing: every // argument matches, and zero calls pass. Its only job is to stop an unrelated From 78f044421e3b588b4bf6ab0ddbfe28127a00fb5e Mon Sep 17 00:00:00 2001 From: octobocto Date: Fri, 25 Sep 2026 22:07:05 -0700 Subject: [PATCH 13/13] orchestrator: drop exports the Core fallback used --- sidechain-orchestrator/wallet/core_backend.go | 17 --------------- sidechain-orchestrator/wallet/derivation.go | 21 ------------------- .../wallet/derivation_test.go | 18 ---------------- 3 files changed, 56 deletions(-) diff --git a/sidechain-orchestrator/wallet/core_backend.go b/sidechain-orchestrator/wallet/core_backend.go index 847fb7bbb..47474de0c 100644 --- a/sidechain-orchestrator/wallet/core_backend.go +++ b/sidechain-orchestrator/wallet/core_backend.go @@ -1707,23 +1707,6 @@ func (p *CoreBackend) createCoreWallet(ctx context.Context, walletName string, w return p.createAndImport(ctx, walletName, w.IsWatchOnly(), imports) } -// coreDescriptorWrapper returns the open/close fragments wrapping the key -// expression for a single-sig kind's Core descriptor. -func coreDescriptorWrapper(kind ScriptKind) (open, close string, ok bool) { - switch kind { - case ScriptLegacy: - return "pkh(", ")", true - case ScriptNestedSegwit: - return "sh(wpkh(", "))", true - case ScriptNativeSegwit: - return "wpkh(", ")", true - case ScriptTaproot: - return "tr(", ")", true - default: - return "", "", false - } -} - // purposeToCoreKind maps a BIP purpose to the single-sig kind Core imports for it. func purposeToCoreKind(purpose uint32) (ScriptKind, bool) { switch purpose { diff --git a/sidechain-orchestrator/wallet/derivation.go b/sidechain-orchestrator/wallet/derivation.go index 8958f7b7c..9edf6127b 100644 --- a/sidechain-orchestrator/wallet/derivation.go +++ b/sidechain-orchestrator/wallet/derivation.go @@ -269,27 +269,6 @@ func ResolveCreateDerivationPath(accountIndex uint32, derivationPath string) (ac return accountIndex, "", nil } -// ResolveAccountPath resolves the account path for a stored override: an -// explicit derivationPath takes precedence; otherwise the standard path for the -// kind and network at accountIndex. Mirrors accountPathFor for callers outside -// this package (e.g. the bitwindow Core fallback) so both derive identically. -func ResolveAccountPath(accountIndex uint32, derivationPath string, kind ScriptKind, net *chaincfg.Params) (AccountPath, error) { - return accountPathFor(&WalletData{AccountIndex: accountIndex, DerivationPath: derivationPath}, kind, net) -} - -// CoreDescriptorWrapper returns the open/close fragments wrapping the key -// expression for a single-sig kind's Core descriptor. Exported for the -// bitwindow Core fallback path. -func CoreDescriptorWrapper(kind ScriptKind) (open, close string, ok bool) { - return coreDescriptorWrapper(kind) -} - -// PurposeToCoreKind maps a BIP purpose to the single-sig kind Core imports for -// it. Exported for the bitwindow Core fallback path. -func PurposeToCoreKind(purpose uint32) (ScriptKind, bool) { - return purposeToCoreKind(purpose) -} - // accountPathFor resolves the account path a wallet derives from for a given // script kind: its explicit DerivationPath override if present, else the // standard path for the kind and network at the wallet's AccountIndex. diff --git a/sidechain-orchestrator/wallet/derivation_test.go b/sidechain-orchestrator/wallet/derivation_test.go index d785f9424..65a6da6aa 100644 --- a/sidechain-orchestrator/wallet/derivation_test.go +++ b/sidechain-orchestrator/wallet/derivation_test.go @@ -230,24 +230,6 @@ func TestAStandardPathGivesOneAddressOnEveryBackend(t *testing.T) { } } -func TestCoreDescriptorWrapper(t *testing.T) { - cases := []struct { - kind ScriptKind - open, close string - }{ - {ScriptLegacy, "pkh(", ")"}, - {ScriptNestedSegwit, "sh(wpkh(", "))"}, - {ScriptNativeSegwit, "wpkh(", ")"}, - {ScriptTaproot, "tr(", ")"}, - } - for _, c := range cases { - open, close, ok := coreDescriptorWrapper(c.kind) - require.True(t, ok, c.kind) - assert.Equal(t, c.open, open, c.kind) - assert.Equal(t, c.close, close, c.kind) - } -} - // A custom account path — any depth or purpose — derives the key it names, so a // wallet created at one scans the addresses the user asked for. func TestCustomDerivationPathDerivesThatPath(t *testing.T) {