package btcwallet import ( "bytes" "context" "encoding/hex" "fmt" "strings" "sync" "time" "github.com/ark-network/ark/common" "github.com/ark-network/ark/common/bitcointree" "github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/ports" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btclog" "github.com/btcsuite/btcwallet/chain" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/walletdb" _ "github.com/btcsuite/btcwallet/walletdb/bdb" "github.com/btcsuite/btcwallet/wtxmgr" "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/lightninglabs/neutrino" "github.com/lightningnetwork/lnd/blockcache" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/btcwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" log "github.com/sirupsen/logrus" "github.com/vulpemventures/go-bip39" ) type WalletOption func(*service) error type WalletConfig struct { Datadir string Network common.Network } func (c WalletConfig) chainParams() *chaincfg.Params { mutinyNetSigNetParams := chaincfg.CustomSignetParams(common.MutinyNetChallenge, nil) mutinyNetSigNetParams.TargetTimePerBlock = common.MutinyNetBlockTime switch c.Network.Name { case common.Bitcoin.Name: return &chaincfg.MainNetParams case common.BitcoinTestNet.Name: return &chaincfg.TestNet3Params case common.BitcoinRegTest.Name: return &chaincfg.RegressionNetParams case common.BitcoinSigNet.Name: return &mutinyNetSigNetParams default: return &chaincfg.MainNetParams } } type accountName string const ( mainAccount accountName = "main" connectorAccount accountName = "connector" aspKeyAccount accountName = "aspkey" // https://github.com/bitcoin/bitcoin/blob/439e58c4d8194ca37f70346727d31f52e69592ec/src/policy/policy.cpp#L23C8-L23C11 // biggest input size to compute the maximum dust amount biggestInputSize = 148 + 182 // = 330 vbytes ) var ( ErrWalletNotLoaded = fmt.Errorf("wallet not loaded, create or unlock it first") p2wpkhKeyScope = waddrmgr.KeyScopeBIP0084 p2trKeyScope = waddrmgr.KeyScopeBIP0086 outputLockDuration = time.Minute ) // add additional chain API not supported by the chain.Interface type type extraChainAPI interface { getTx(txid string) (*wire.MsgTx, error) getTxStatus(txid string) (isConfirmed bool, blocktime int64, err error) broadcast(txHex string) error } type service struct { wallet *btcwallet.BtcWallet cfg WalletConfig chainSource chain.Interface scanner chain.Interface extraAPI extraChainAPI feeEstimator chainfee.Estimator watchedScriptsLock sync.RWMutex watchedScripts map[string]struct{} aspTaprootAddr waddrmgr.ManagedPubKeyAddress } // WithNeutrino creates a start a neutrino node using the provided service datadir func WithNeutrino(initialPeer string, esploraURL string) WalletOption { return func(s *service) error { if s.cfg.Network.Name == common.BitcoinRegTest.Name && len(initialPeer) == 0 { return fmt.Errorf("initial neutrino peer required for regtest network, set NEUTRINO_PEER env var") } db, err := createOrOpenWalletDB(s.cfg.Datadir + "/neutrino.db") if err != nil { return err } netParams := s.cfg.chainParams() config := neutrino.Config{ DataDir: s.cfg.Datadir, ChainParams: *netParams, Database: db, } if len(initialPeer) > 0 { config.AddPeers = []string{initialPeer} } neutrino.UseLogger(logger("neutrino")) btcwallet.UseLogger(logger("btcwallet")) neutrinoSvc, err := neutrino.NewChainService(config) if err != nil { return err } if err := neutrinoSvc.Start(); err != nil { return err } // wait for neutrino to sync for !neutrinoSvc.IsCurrent() { time.Sleep(1 * time.Second) } chainSrc := chain.NewNeutrinoClient(netParams, neutrinoSvc) scanner := chain.NewNeutrinoClient(netParams, neutrinoSvc) esploraClient := &esploraClient{url: esploraURL} estimator, err := chainfee.NewWebAPIEstimator(esploraClient, true, 5*time.Minute, 20*time.Minute) if err != nil { return err } if err := withExtraAPI(esploraClient)(s); err != nil { return err } if err := withFeeEstimator(estimator)(s); err != nil { return err } if err := withChainSource(chainSrc)(s); err != nil { return err } return withScanner(scanner)(s) } } func WithPollingBitcoind(host, user, pass string) WalletOption { return func(s *service) error { netParams := s.cfg.chainParams() // Create a new bitcoind configuration bitcoindConfig := &chain.BitcoindConfig{ ChainParams: netParams, Host: host, User: user, Pass: pass, PollingConfig: &chain.PollingConfig{ BlockPollingInterval: 10 * time.Second, TxPollingInterval: 5 * time.Second, TxPollingIntervalJitter: 0.1, RPCBatchSize: 20, RPCBatchInterval: 1 * time.Second, }, } chain.UseLogger(logger("chain")) // Create the BitcoindConn first bitcoindConn, err := chain.NewBitcoindConn(bitcoindConfig) if err != nil { return fmt.Errorf("failed to create bitcoind connection: %w", err) } // Start the bitcoind connection if err := bitcoindConn.Start(); err != nil { return fmt.Errorf("failed to start bitcoind connection: %w", err) } // Now create the BitcoindClient using the connection chainClient := bitcoindConn.NewBitcoindClient() // Start the chain client if err := chainClient.Start(); err != nil { bitcoindConn.Stop() return fmt.Errorf("failed to start bitcoind client: %w", err) } // wait for bitcoind to sync for !chainClient.IsCurrent() { time.Sleep(1 * time.Second) } estimator, err := chainfee.NewBitcoindEstimator( rpcclient.ConnConfig{ Host: bitcoindConfig.Host, User: bitcoindConfig.User, Pass: bitcoindConfig.Pass, }, "CONSERVATIVE", chainfee.AbsoluteFeePerKwFloor, ) if err != nil { return fmt.Errorf("failed to create bitcoind fee estimator: %w", err) } if err := withExtraAPI(&bitcoindRPCClient{chainClient})(s); err != nil { return err } if err := withFeeEstimator(estimator)(s); err != nil { return err } // Set up the wallet as chain source and scanner if err := withChainSource(chainClient)(s); err != nil { chainClient.Stop() bitcoindConn.Stop() return fmt.Errorf("failed to set chain source: %w", err) } if err := withScanner(chainClient)(s); err != nil { chainClient.Stop() bitcoindConn.Stop() return fmt.Errorf("failed to set scanner: %w", err) } return nil } } // NewService creates the wallet service, an option must be set to configure the chain source. func NewService(cfg WalletConfig, options ...WalletOption) (ports.WalletService, error) { wallet.UseLogger(logger("wallet")) svc := &service{ cfg: cfg, watchedScriptsLock: sync.RWMutex{}, watchedScripts: make(map[string]struct{}), } for _, option := range options { if err := option(svc); err != nil { return nil, err } } return svc, nil } func (s *service) Close() { if s.walletLoaded() { if err := s.wallet.Stop(); err != nil { log.WithError(err).Warn("failed to gracefully stop the wallet, forcing shutdown") } } } func (s *service) GenSeed(_ context.Context) (string, error) { entropy, err := bip39.NewEntropy(256) if err != nil { return "", err } return bip39.NewMnemonic(entropy) } func (s *service) Create(_ context.Context, seed, password string) error { return s.create(seed, password, 0) } func (s *service) Restore(_ context.Context, seed, password string) error { return s.create(seed, password, 100) } func (s *service) Unlock(_ context.Context, password string) error { if !s.walletLoaded() { pwd := []byte(password) opt := btcwallet.LoaderWithLocalWalletDB(s.cfg.Datadir, false, time.Minute) config := btcwallet.Config{ LogDir: s.cfg.Datadir, PrivatePass: pwd, PublicPass: pwd, Birthday: time.Now(), RecoveryWindow: 0, NetParams: s.cfg.chainParams(), LoaderOptions: []btcwallet.LoaderOption{opt}, CoinSelectionStrategy: wallet.CoinSelectionLargest, ChainSource: s.chainSource, } blockCache := blockcache.NewBlockCache(2 * 1024 * 1024 * 1024) wallet, err := btcwallet.New(config, blockCache) if err != nil { return fmt.Errorf("failed to setup wallet loader: %s", err) } if err := wallet.Start(); err != nil { return fmt.Errorf("failed to start wallet: %s", err) } for { if !wallet.InternalWallet().ChainSynced() { log.Debugf("waiting sync: current height %d", wallet.InternalWallet().Manager.SyncedTo().Height) time.Sleep(3 * time.Second) continue } break } log.Debugf("chain synced") addrs, err := wallet.ListAddresses(string(aspKeyAccount), false) if err != nil { return err } for info, addrs := range addrs { if info.AccountName != string(aspKeyAccount) { continue } for _, addr := range addrs { if addr.Internal { continue } splittedPath := strings.Split(addr.DerivationPath, "/") last := splittedPath[len(splittedPath)-1] if last == "0" { decoded, err := btcutil.DecodeAddress(addr.Address, s.cfg.chainParams()) if err != nil { return err } infos, err := wallet.AddressInfo(decoded) if err != nil { return err } managedPubkeyAddr, ok := infos.(waddrmgr.ManagedPubKeyAddress) if !ok { return fmt.Errorf("failed to cast address to managed pubkey address") } s.aspTaprootAddr = managedPubkeyAddr break } } } s.wallet = wallet return nil } return s.wallet.InternalWallet().Unlock([]byte(password), nil) } func (s *service) Lock(_ context.Context, _ string) error { if !s.walletLoaded() { return ErrWalletNotLoaded } s.wallet.InternalWallet().Lock() return nil } func (s *service) BroadcastTransaction(ctx context.Context, txHex string) (string, error) { if err := s.extraAPI.broadcast(txHex); err != nil { return "", err } var tx wire.MsgTx if err := tx.Deserialize(hex.NewDecoder(strings.NewReader(txHex))); err != nil { return "", err } if err := s.wallet.PublishTransaction(&tx, ""); err != nil { return "", err } return tx.TxHash().String(), nil } func (s *service) ConnectorsAccountBalance(ctx context.Context) (uint64, uint64, error) { amount, err := s.getBalance(connectorAccount) if err != nil { return 0, 0, err } return amount, 0, nil } func (s *service) MainAccountBalance(ctx context.Context) (uint64, uint64, error) { amount, err := s.getBalance(mainAccount) if err != nil { return 0, 0, err } return amount, 0, nil } func (s *service) DeriveAddresses(ctx context.Context, num int) ([]string, error) { addresses := make([]string, 0, num) for i := 0; i < num; i++ { addr, err := s.deriveNextAddress(mainAccount) if err != nil { return nil, err } addresses = append(addresses, addr.EncodeAddress()) } if len(addresses) == 0 { return nil, fmt.Errorf("no addresses derived") } return addresses, nil } func (s *service) DeriveConnectorAddress(ctx context.Context) (string, error) { addr, err := s.deriveNextAddress(connectorAccount) if err != nil { return "", err } return addr.EncodeAddress(), nil } func (s *service) GetPubkey(ctx context.Context) (*secp256k1.PublicKey, error) { return s.aspTaprootAddr.PubKey(), nil } func (s *service) GetForfeitAddress(ctx context.Context) (string, error) { addrs, err := s.wallet.ListAddresses(string(mainAccount), false) if err != nil { return "", err } if len(addrs) == 0 { addr, err := s.deriveNextAddress(mainAccount) if err != nil { return "", err } return addr.EncodeAddress(), nil } for info, addrs := range addrs { if info.AccountName != string(mainAccount) { continue } for _, addr := range addrs { if addr.Internal { continue } splittedPath := strings.Split(addr.DerivationPath, "/") last := splittedPath[len(splittedPath)-1] if last == "0" { return addr.Address, nil } } } return "", fmt.Errorf("forfeit address not found") } func (s *service) ListConnectorUtxos(ctx context.Context, connectorAddress string) ([]ports.TxInput, error) { w := s.wallet.InternalWallet() addr, err := btcutil.DecodeAddress(connectorAddress, w.ChainParams()) if err != nil { return nil, err } script, err := txscript.PayToAddrScript(addr) if err != nil { return nil, err } connectorAccountNumber, err := w.AccountNumber(p2wpkhKeyScope, string(connectorAccount)) if err != nil { return nil, err } utxos, err := w.UnspentOutputs(wallet.OutputSelectionPolicy{ Account: connectorAccountNumber, RequiredConfirmations: 0, }) if err != nil { return nil, err } txInputs := make([]ports.TxInput, 0, len(utxos)) for _, utxo := range utxos { if !bytes.Equal(utxo.Output.PkScript, script) { continue } txInputs = append(txInputs, transactionOutputTxInput{utxo}) } return txInputs, nil } func (s *service) LockConnectorUtxos(ctx context.Context, utxos []ports.TxOutpoint) error { w := s.wallet.InternalWallet() for _, utxo := range utxos { id, _ := chainhash.NewHashFromStr(utxo.GetTxid()) if _, err := w.LeaseOutput( wtxmgr.LockID(id[:]), wire.OutPoint{ Hash: *id, Index: utxo.GetIndex(), }, outputLockDuration, ); err != nil { return err } } return nil } func (s *service) SelectUtxos(ctx context.Context, _ string, amount uint64) ([]ports.TxInput, uint64, error) { w := s.wallet.InternalWallet() mainAccountNumber, err := w.AccountNumber(p2wpkhKeyScope, string(mainAccount)) if err != nil { return nil, 0, err } utxos, err := w.UnspentOutputs(wallet.OutputSelectionPolicy{ Account: mainAccountNumber, RequiredConfirmations: 0, // allow uncomfirmed utxos }) if err != nil { return nil, 0, err } coins := make([]wallet.Coin, 0, len(utxos)) for _, utxo := range utxos { coins = append(coins, wallet.Coin{ OutPoint: *wire.NewOutPoint(&utxo.OutPoint.Hash, utxo.OutPoint.Index), TxOut: utxo.Output, }) } arranged, err := wallet.CoinSelectionLargest.ArrangeCoins( coins, btcutil.Amount(0), // unused by CoinSelectionLargest strategy ) if err != nil { return nil, 0, err } selectedAmount := uint64(0) selectedUtxos := make([]ports.TxInput, 0, len(arranged)) for _, coin := range arranged { if selectedAmount >= amount { break } selectedAmount += uint64(coin.Value) selectedUtxos = append(selectedUtxos, coinTxInput{coin}) } if selectedAmount < amount { return nil, 0, fmt.Errorf("insufficient funds to select %d, only %d available", amount, selectedAmount) } for _, utxo := range selectedUtxos { if _, err := w.LeaseOutput( wtxmgr.LockID(utxo.(coinTxInput).Hash), wire.OutPoint{ Hash: utxo.(coinTxInput).Hash, Index: utxo.(coinTxInput).Index, }, outputLockDuration, ); err != nil { return nil, 0, err } } return selectedUtxos, selectedAmount - amount, nil } func (s *service) SignTransaction(ctx context.Context, partialTx string, extractRawTx bool) (string, error) { ptx, err := psbt.NewFromRawBytes( strings.NewReader(partialTx), true, ) if err != nil { return "", err } signedInputs, err := s.signPsbt(ptx, nil) if err != nil { return "", err } if extractRawTx { // verify that all inputs are signed if len(signedInputs) != len(ptx.Inputs) { return "", fmt.Errorf("not all inputs are signed, unable to finalize the psbt") } for i, in := range ptx.Inputs { isTaproot := txscript.IsPayToTaproot(in.WitnessUtxo.PkScript) if isTaproot && len(in.TaprootLeafScript) > 0 { closure, err := bitcointree.DecodeClosure(in.TaprootLeafScript[0].Script) if err != nil { return "", err } witness := make(wire.TxWitness, 4) castClosure, isTaprootMultisig := closure.(*bitcointree.MultisigClosure) if isTaprootMultisig { ownerPubkey := schnorr.SerializePubKey(castClosure.Pubkey) aspKey := schnorr.SerializePubKey(castClosure.AspPubkey) for _, sig := range in.TaprootScriptSpendSig { if bytes.Equal(sig.XOnlyPubKey, ownerPubkey) { witness[0] = sig.Signature } if bytes.Equal(sig.XOnlyPubKey, aspKey) { witness[1] = sig.Signature } } witness[2] = in.TaprootLeafScript[0].Script witness[3] = in.TaprootLeafScript[0].ControlBlock for idw, w := range witness { if w == nil { return "", fmt.Errorf("missing witness element %d, cannot finalize taproot mutlisig input %d", idw, i) } } var witnessBuf bytes.Buffer if err := psbt.WriteTxWitness(&witnessBuf, witness); err != nil { return "", err } ptx.Inputs[i].FinalScriptWitness = witnessBuf.Bytes() continue } } if err := psbt.Finalize(ptx, i); err != nil { return "", fmt.Errorf("failed to finalize input %d: %w", i, err) } } extracted, err := psbt.Extract(ptx) if err != nil { return "", err } var buf bytes.Buffer if err := extracted.Serialize(&buf); err != nil { return "", err } return hex.EncodeToString(buf.Bytes()), nil } return ptx.B64Encode() } func (s *service) SignTransactionTapscript(ctx context.Context, partialTx string, inputIndexes []int) (string, error) { partial, err := psbt.NewFromRawBytes( strings.NewReader(partialTx), true, ) if err != nil { return "", err } if len(inputIndexes) == 0 { inputIndexes = make([]int, len(partial.Inputs)) for i := range partial.Inputs { inputIndexes[i] = i } } signedInputs, err := s.signPsbt(partial, inputIndexes) if err != nil { return "", err } for _, index := range inputIndexes { hasBeenSigned := false for _, signedIndex := range signedInputs { if signedIndex == uint32(index) { hasBeenSigned = true break } } if !hasBeenSigned { return "", fmt.Errorf("input %d has not been signed", index) } } return partial.B64Encode() } func (s *service) Status(ctx context.Context) (ports.WalletStatus, error) { if !s.walletLoaded() { return status{ initialized: s.walletInitialized(), }, nil } w := s.wallet.InternalWallet() return status{ true, !w.Manager.IsLocked(), w.ChainSynced(), }, nil } func (s *service) WaitForSync(ctx context.Context, txid string) error { w := s.wallet.InternalWallet() txhash, err := chainhash.NewHashFromStr(txid) if err != nil { return err } ticker := time.NewTicker(5 * time.Second) for { select { case <-ctx.Done(): return ctx.Err() case <-ticker.C: _, err := w.GetTransaction(*txhash) if err != nil { if strings.Contains(err.Error(), wallet.ErrNoTx.Error()) { continue } return err } else { ticker.Stop() return nil } } } } func (s *service) MinRelayFeeRate(ctx context.Context) chainfee.SatPerKVByte { return s.feeEstimator.RelayFeePerKW().FeePerKVByte() } func (s *service) MinRelayFee(ctx context.Context, vbytes uint64) (uint64, error) { fee := s.feeEstimator.RelayFeePerKW().FeeForVByte(lntypes.VByte(vbytes)) return uint64(fee.ToUnit(btcutil.AmountSatoshi)), nil } func (s *service) EstimateFees(ctx context.Context, partialTx string) (uint64, error) { feeRate, err := s.feeEstimator.EstimateFeePerKW(1) if err != nil { return 0, err } partial, err := psbt.NewFromRawBytes( strings.NewReader(partialTx), true, ) if err != nil { return 0, err } weightEstimator := &input.TxWeightEstimator{} for _, input := range partial.Inputs { if input.WitnessUtxo == nil { return 0, fmt.Errorf("missing witness utxo for input") } script, err := txscript.ParsePkScript(input.WitnessUtxo.PkScript) if err != nil { return 0, err } switch script.Class() { case txscript.PubKeyHashTy: weightEstimator.AddP2PKHInput() case txscript.WitnessV0PubKeyHashTy: weightEstimator.AddP2WKHInput() case txscript.WitnessV1TaprootTy: if len(input.TaprootLeafScript) > 0 { leaf := input.TaprootLeafScript[0] ctrlBlock, err := txscript.ParseControlBlock(leaf.ControlBlock) if err != nil { return 0, err } weightEstimator.AddTapscriptInput(64*2, &waddrmgr.Tapscript{ RevealedScript: leaf.Script, ControlBlock: ctrlBlock, }) } else { weightEstimator.AddTaprootKeySpendInput(txscript.SigHashDefault) } default: return 0, fmt.Errorf("unsupported script type: %v", script.Class()) } } for _, output := range partial.UnsignedTx.TxOut { script, err := txscript.ParsePkScript(output.PkScript) if err != nil { return 0, err } switch script.Class() { case txscript.PubKeyHashTy: weightEstimator.AddP2PKHOutput() case txscript.WitnessV0PubKeyHashTy: weightEstimator.AddP2WKHOutput() case txscript.ScriptHashTy: weightEstimator.AddP2SHOutput() case txscript.WitnessV0ScriptHashTy: weightEstimator.AddP2WSHOutput() case txscript.WitnessV1TaprootTy: weightEstimator.AddP2TROutput() default: return 0, fmt.Errorf("unsupported script type: %v", script.Class()) } } fee := feeRate.FeeForVByte(lntypes.VByte(weightEstimator.VSize())) return uint64(fee.ToUnit(btcutil.AmountSatoshi)), nil } func (s *service) WatchScripts(ctx context.Context, scripts []string) error { addresses := make([]btcutil.Address, 0, len(scripts)) for _, script := range scripts { scriptBytes, err := hex.DecodeString(script) if err != nil { return err } addr, err := fromOutputScript(scriptBytes, s.cfg.chainParams()) if err != nil { return err } addresses = append(addresses, addr) } if err := s.scanner.NotifyReceived(addresses); err != nil { if err := s.UnwatchScripts(ctx, scripts); err != nil { return fmt.Errorf("error while unwatching scripts: %w", err) } return err } s.watchedScriptsLock.Lock() defer s.watchedScriptsLock.Unlock() for _, script := range scripts { s.watchedScripts[script] = struct{}{} } return nil } func (s *service) UnwatchScripts(ctx context.Context, scripts []string) error { s.watchedScriptsLock.Lock() defer s.watchedScriptsLock.Unlock() for _, script := range scripts { delete(s.watchedScripts, script) } return nil } func (s *service) GetNotificationChannel( ctx context.Context, ) <-chan map[string][]ports.VtxoWithValue { ch := make(chan map[string][]ports.VtxoWithValue) go func() { const maxCacheSize = 100 sentTxs := make(map[chainhash.Hash]struct{}) cache := func(hash chainhash.Hash) { if len(sentTxs) > maxCacheSize { sentTxs = make(map[chainhash.Hash]struct{}) } sentTxs[hash] = struct{}{} } for n := range s.scanner.Notifications() { switch m := n.(type) { case chain.RelevantTx: if _, sent := sentTxs[m.TxRecord.Hash]; sent { continue } notification := s.castNotification(m.TxRecord) cache(m.TxRecord.Hash) ch <- notification case chain.FilteredBlockConnected: for _, tx := range m.RelevantTxs { if _, sent := sentTxs[tx.Hash]; sent { continue } notification := s.castNotification(tx) cache(tx.Hash) ch <- notification } } } }() return ch } func (s *service) IsTransactionConfirmed( ctx context.Context, txid string, ) (isConfirmed bool, blocktime int64, err error) { return s.extraAPI.getTxStatus(txid) } func (s *service) GetDustAmount( ctx context.Context, ) (uint64, error) { return s.MinRelayFee(ctx, biggestInputSize) } func (s *service) GetTransaction(ctx context.Context, txid string) (string, error) { tx, err := s.extraAPI.getTx(txid) if err != nil { return "", err } if tx == nil { return "", fmt.Errorf("transaction not found") } var buf bytes.Buffer if err := tx.Serialize(&buf); err != nil { return "", err } return hex.EncodeToString(buf.Bytes()), nil } func (s *service) castNotification(tx *wtxmgr.TxRecord) map[string][]ports.VtxoWithValue { vtxos := make(map[string][]ports.VtxoWithValue) s.watchedScriptsLock.RLock() defer s.watchedScriptsLock.RUnlock() for outputIndex, txout := range tx.MsgTx.TxOut { script := hex.EncodeToString(txout.PkScript) if _, ok := s.watchedScripts[script]; !ok { continue } if len(vtxos[script]) <= 0 { vtxos[script] = make([]ports.VtxoWithValue, 0) } vtxos[script] = append(vtxos[script], ports.VtxoWithValue{ VtxoKey: domain.VtxoKey{ Txid: tx.Hash.String(), VOut: uint32(outputIndex), }, Value: uint64(txout.Value), }) } return vtxos } func (s *service) create(mnemonic, password string, addrGap uint32) error { if len(mnemonic) <= 0 { return fmt.Errorf("missing hd seed") } if len(password) <= 0 { return fmt.Errorf("missing password") } pwd := []byte(password) seed := bip39.NewSeed(mnemonic, password) opt := btcwallet.LoaderWithLocalWalletDB(s.cfg.Datadir, false, time.Minute) config := btcwallet.Config{ LogDir: s.cfg.Datadir, PrivatePass: pwd, PublicPass: pwd, Birthday: time.Now(), RecoveryWindow: addrGap, HdSeed: seed, NetParams: s.cfg.chainParams(), LoaderOptions: []btcwallet.LoaderOption{opt}, CoinSelectionStrategy: wallet.CoinSelectionLargest, ChainSource: s.chainSource, } blockCache := blockcache.NewBlockCache(2 * 1024 * 1024 * 1024) wallet, err := btcwallet.New(config, blockCache) if err != nil { return fmt.Errorf("failed to setup wallet loader: %s", err) } if err := wallet.Start(); err != nil { return fmt.Errorf("failed to start wallet: %s", err) } if err := s.initWallet(wallet); err != nil { return err } for { if !wallet.InternalWallet().ChainSynced() { log.Debugf("waiting sync: current height %d", wallet.InternalWallet().Manager.SyncedTo().Height) time.Sleep(3 * time.Second) continue } break } log.Debugf("chain synced") if addrGap > 0 { // TODO: fix rescan if err := wallet.InternalWallet().Rescan(nil, nil); err != nil { return err } } wallet.InternalWallet().Lock() s.wallet = wallet return nil } func (s *service) initWallet(wallet *btcwallet.BtcWallet) error { w := wallet.InternalWallet() walletAccounts, err := w.Accounts(p2wpkhKeyScope) if err != nil { return fmt.Errorf("failed to list wallet accounts: %s", err) } var mainAccountNumber, connectorAccountNumber, aspKeyAccountNumber uint32 if walletAccounts != nil { for _, account := range walletAccounts.Accounts { switch account.AccountName { case string(mainAccount): mainAccountNumber = account.AccountNumber case string(connectorAccount): connectorAccountNumber = account.AccountNumber case string(aspKeyAccount): aspKeyAccountNumber = account.AccountNumber default: continue } } } if mainAccountNumber == 0 && connectorAccountNumber == 0 && aspKeyAccountNumber == 0 { log.Debug("creating default accounts for ark wallet...") mainAccountNumber, err = w.NextAccount(p2wpkhKeyScope, string(mainAccount)) if err != nil { return fmt.Errorf("failed to create %s: %s", mainAccount, err) } connectorAccountNumber, err = w.NextAccount(p2wpkhKeyScope, string(connectorAccount)) if err != nil { return fmt.Errorf("failed to create %s: %s", connectorAccount, err) } aspKeyAccountNumber, err = w.NextAccount(p2trKeyScope, string(aspKeyAccount)) if err != nil { return fmt.Errorf("failed to create %s: %s", aspKeyAccount, err) } } log.Debugf("main account number: %d", mainAccountNumber) log.Debugf("connector account number: %d", connectorAccountNumber) log.Debugf("asp key account number: %d", aspKeyAccountNumber) addrs, err := wallet.ListAddresses(string(aspKeyAccount), false) if err != nil { return err } if len(addrs) == 0 { aspKeyAddr, err := wallet.NewAddress(lnwallet.TaprootPubkey, false, string(aspKeyAccount)) if err != nil { return err } addrInfos, err := wallet.AddressInfo(aspKeyAddr) if err != nil { return err } managedAddr, ok := addrInfos.(waddrmgr.ManagedPubKeyAddress) if !ok { return fmt.Errorf("failed to cast address to managed pubkey address") } s.aspTaprootAddr = managedAddr } else { for info, addrs := range addrs { if info.AccountName != string(aspKeyAccount) { continue } for _, addr := range addrs { if addr.Internal { continue } splittedPath := strings.Split(addr.DerivationPath, "/") last := splittedPath[len(splittedPath)-1] if last == "0" { decoded, err := btcutil.DecodeAddress(addr.Address, s.cfg.chainParams()) if err != nil { return err } infos, err := wallet.AddressInfo(decoded) if err != nil { return err } managedPubkeyAddr, ok := infos.(waddrmgr.ManagedPubKeyAddress) if !ok { return fmt.Errorf("failed to cast address to managed pubkey address") } s.aspTaprootAddr = managedPubkeyAddr break } } } } return nil } func (s *service) getBalance(account accountName) (uint64, error) { if !s.walletLoaded() { return 0, ErrWalletNotLoaded } balance, err := s.wallet.ConfirmedBalance(0, string(account)) if err != nil { return 0, err } return uint64(balance), nil } // this only supports deriving segwit v0 accounts func (s *service) deriveNextAddress(account accountName) (btcutil.Address, error) { if !s.walletLoaded() { return nil, ErrWalletNotLoaded } return s.wallet.NewAddress(lnwallet.WitnessPubKey, false, string(account)) } func (s *service) walletLoaded() bool { return s.wallet != nil } func (s *service) walletInitialized() bool { opts := []btcwallet.LoaderOption{btcwallet.LoaderWithLocalWalletDB(s.cfg.Datadir, false, time.Minute)} loader, err := btcwallet.NewWalletLoader( s.cfg.chainParams(), 0, opts..., ) if err != nil { return false } exist, _ := loader.WalletExists() return exist } func withChainSource(chainSource chain.Interface) WalletOption { return func(s *service) error { if s.chainSource != nil { return fmt.Errorf("chain source already set") } if err := chainSource.Start(); err != nil { return fmt.Errorf("failed to start chain source: %s", err) } s.chainSource = chainSource return nil } } func withScanner(chainSource chain.Interface) WalletOption { return func(s *service) error { if s.scanner != nil { return fmt.Errorf("scanner already set") } if err := chainSource.Start(); err != nil { return fmt.Errorf("failed to start scanner: %s", err) } s.scanner = chainSource return nil } } func withExtraAPI(api extraChainAPI) WalletOption { return func(s *service) error { if s.extraAPI != nil { return fmt.Errorf("extra chain API already set") } s.extraAPI = api return nil } } func withFeeEstimator(estimator chainfee.Estimator) WalletOption { return func(s *service) error { if s.feeEstimator != nil { return fmt.Errorf("fee estimator already set") } if err := estimator.Start(); err != nil { return fmt.Errorf("failed to start fee estimator: %s", err) } s.feeEstimator = estimator return nil } } func createOrOpenWalletDB(path string) (walletdb.DB, error) { db, err := walletdb.Open("bdb", path, true, 60*time.Second) if err == nil { return db, nil } if err != walletdb.ErrDbDoesNotExist { return nil, err } return walletdb.Create("bdb", path, true, 60*time.Second) } // status implements ports.WalletStatus interface type status struct { initialized bool unlocked bool synced bool } func (s status) IsInitialized() bool { return s.initialized } func (s status) IsUnlocked() bool { return s.unlocked } func (s status) IsSynced() bool { return s.synced } func fromOutputScript(script []byte, netParams *chaincfg.Params) (btcutil.Address, error) { return btcutil.NewAddressTaproot(script[2:], netParams) } func logger(subsystem string) btclog.Logger { return btclog.NewBackend(log.StandardLogger().Writer()).Logger(subsystem) }